mirror of
https://github.com/hanzoai/search-go.git
synced 2026-10-30 18:22:58 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers, and the test and example pointer helpers went once new(expr) could say them; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
416 lines
11 KiB
Go
416 lines
11 KiB
Go
package meilisearch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
)
|
|
|
|
type (
|
|
Hit map[string]json.RawMessage // Hit is a map of key and value raw buffer
|
|
Hits []Hit // Hits is an alias for a slice of Hit.
|
|
)
|
|
|
|
// Deprecated: Decode decodes a single Hit into the provided struct.
|
|
//
|
|
// Please use DecodeInto for better performance without intermediate marshaling.
|
|
func (h Hit) Decode(vPtr any) error {
|
|
if vPtr == nil || reflect.ValueOf(vPtr).Kind() != reflect.Pointer {
|
|
return errors.New("vPtr must be a non-nil pointer")
|
|
}
|
|
|
|
raw, err := json.Marshal(h)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal hit: %w", err)
|
|
}
|
|
|
|
return json.Unmarshal(raw, vPtr)
|
|
}
|
|
|
|
// DecodeInto decodes a single Hit into the provided struct without intermediate marshaling.
|
|
func (h Hit) DecodeInto(out any) error {
|
|
if out == nil {
|
|
return errors.New("out must be a non-nil pointer")
|
|
}
|
|
rv := reflect.ValueOf(out)
|
|
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
|
return errors.New("out must be a non-nil pointer")
|
|
}
|
|
rv = rv.Elem()
|
|
|
|
switch rv.Kind() {
|
|
case reflect.Struct:
|
|
// existing struct path (keep your current implementation) ...
|
|
ti := getTypeInfo(rv.Type())
|
|
for key, raw := range h {
|
|
if len(raw) == 0 {
|
|
continue
|
|
}
|
|
idx, ok := ti.byNameIndex[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
f := ti.fields[idx]
|
|
fv, ok := fieldByIndexPathAlloc(rv, f.indexPath)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if isJSONNull(raw) {
|
|
if fv.CanSet() {
|
|
fv.Set(reflect.Zero(fv.Type()))
|
|
}
|
|
continue
|
|
}
|
|
if f.hasString {
|
|
if err := unmarshalSingleField(rv.Addr().Interface(), f.jsonName, raw); err != nil {
|
|
return fmt.Errorf("decode field %q: %w", key, err)
|
|
}
|
|
continue
|
|
}
|
|
if !fv.CanAddr() {
|
|
continue
|
|
}
|
|
if err := json.Unmarshal(raw, fv.Addr().Interface()); err != nil {
|
|
return fmt.Errorf("decode field %q: %w", key, err)
|
|
}
|
|
}
|
|
return nil
|
|
|
|
case reflect.Map:
|
|
// NEW: map[string]T support
|
|
if rv.Type().Key().Kind() != reflect.String {
|
|
return fmt.Errorf("map key must be string, got %s", rv.Type().Key())
|
|
}
|
|
if rv.IsNil() {
|
|
rv.Set(reflect.MakeMapWithSize(rv.Type(), len(h)))
|
|
}
|
|
elemT := rv.Type().Elem()
|
|
for k, raw := range h {
|
|
// For null values, set zero (nil for pointer/slice/map/interface, 0 for numbers/bool)
|
|
if isJSONNull(raw) {
|
|
rv.SetMapIndex(reflect.ValueOf(k), reflect.Zero(elemT))
|
|
continue
|
|
}
|
|
elemV := reflect.New(elemT) // *elemT
|
|
// If elemT is interface{}, this is *interface{} and is fine.
|
|
if err := json.Unmarshal(raw, elemV.Interface()); err != nil {
|
|
return fmt.Errorf("decode map value for key %q: %w", k, err)
|
|
}
|
|
// Store the concrete elem (dereference)
|
|
rv.SetMapIndex(reflect.ValueOf(k), elemV.Elem())
|
|
}
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("out must point to a struct or map, got %T", out)
|
|
}
|
|
}
|
|
|
|
// DecodeWith decodes a Hit into the provided struct using the provided marshal and unmarshal functions.
|
|
func (h Hit) DecodeWith(vPtr any, marshal JSONMarshal, unmarshal JSONUnmarshal) error {
|
|
if vPtr == nil || reflect.ValueOf(vPtr).Kind() != reflect.Pointer {
|
|
return errors.New("vPtr must be a non-nil pointer")
|
|
}
|
|
|
|
raw, err := marshal(h)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal hit: %w", err)
|
|
}
|
|
|
|
return unmarshal(raw, vPtr)
|
|
}
|
|
|
|
func (h Hits) Len() int {
|
|
return len(h)
|
|
}
|
|
|
|
// Deprecated: Decode decodes the Hits into the provided target slice.
|
|
//
|
|
// Please use DecodeInto for better performance without intermediate marshaling.
|
|
func (h Hits) Decode(vSlicePtr any) error {
|
|
v := reflect.ValueOf(vSlicePtr)
|
|
|
|
if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Slice {
|
|
return fmt.Errorf("v must be a pointer to a slice, got %T", vSlicePtr)
|
|
}
|
|
|
|
raw := []Hit(h)
|
|
buf, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal hits: %w", err)
|
|
}
|
|
|
|
return json.Unmarshal(buf, vSlicePtr)
|
|
}
|
|
|
|
// DecodeWith decodes a Hits into the provided struct using the provided marshal and unmarshal functions.
|
|
func (h Hits) DecodeWith(vSlicePtr any, marshal JSONMarshal, unmarshal JSONUnmarshal) error {
|
|
v := reflect.ValueOf(vSlicePtr)
|
|
|
|
if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Slice {
|
|
return errors.New("v must be a pointer to a slice")
|
|
}
|
|
|
|
raw := []Hit(h)
|
|
buf, err := marshal(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal hit: %w", err)
|
|
}
|
|
|
|
return unmarshal(buf, vSlicePtr)
|
|
}
|
|
|
|
// DecodeInto decodes hs into the provided slice pointer without re-marshal.
|
|
// vSlicePtr must be a non-nil pointer to a slice whose element type is a struct or *struct.
|
|
// Example:
|
|
//
|
|
// var out []exampleBookForTest
|
|
// if err := hits.DecodeInto(&out); err != nil { ... }
|
|
//
|
|
// var outPtr []*exampleBookForTest
|
|
// if err := hits.DecodeInto(&outPtr); err != nil { ... }
|
|
func (h Hits) DecodeInto(vSlicePtr any) error {
|
|
if vSlicePtr == nil {
|
|
return fmt.Errorf("vSlicePtr must be a non-nil pointer to a slice")
|
|
}
|
|
rv := reflect.ValueOf(vSlicePtr)
|
|
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
|
return fmt.Errorf("vSlicePtr must be a non-nil pointer, got %T", vSlicePtr)
|
|
}
|
|
sv := rv.Elem()
|
|
if sv.Kind() != reflect.Slice {
|
|
return fmt.Errorf("vSlicePtr must point to a slice, got %s", sv.Kind())
|
|
}
|
|
|
|
elemType := sv.Type().Elem()
|
|
out := reflect.MakeSlice(sv.Type(), 0, len(h))
|
|
|
|
switch elemType.Kind() {
|
|
case reflect.Struct:
|
|
for i := range h {
|
|
elemPtr := reflect.New(elemType)
|
|
if err := h[i].DecodeInto(elemPtr.Interface()); err != nil {
|
|
return fmt.Errorf("decode hits[%d]: %w", i, err)
|
|
}
|
|
out = reflect.Append(out, elemPtr.Elem())
|
|
}
|
|
|
|
case reflect.Pointer:
|
|
et := elemType.Elem()
|
|
switch et.Kind() {
|
|
case reflect.Struct:
|
|
for i := range h {
|
|
elemPtr := reflect.New(et)
|
|
if err := h[i].DecodeInto(elemPtr.Interface()); err != nil {
|
|
return fmt.Errorf("decode hits[%d]: %w", i, err)
|
|
}
|
|
out = reflect.Append(out, elemPtr.Convert(elemType))
|
|
}
|
|
case reflect.Map:
|
|
// *** FIX 1: use et (map type), not elemType (pointer type)
|
|
if et.Key().Kind() != reflect.String {
|
|
return fmt.Errorf("slice element must be map with string key, got %s", et)
|
|
}
|
|
for i := range h {
|
|
// allocate *map[string]V and initialize the map
|
|
mPtr := reflect.New(et) // *map[string]V
|
|
mPtr.Elem().Set(reflect.MakeMap(et)) // init
|
|
// decode into *map
|
|
if err := h[i].DecodeInto(mPtr.Interface()); err != nil {
|
|
return fmt.Errorf("decode hits[%d]: %w", i, err)
|
|
}
|
|
// append the pointer as required by [] *map[string]V
|
|
out = reflect.Append(out, mPtr.Convert(elemType))
|
|
}
|
|
default:
|
|
return fmt.Errorf("slice element must be struct, *struct, or *map[string]T, got %s", elemType)
|
|
}
|
|
|
|
case reflect.Map:
|
|
if elemType.Key().Kind() != reflect.String {
|
|
return fmt.Errorf("slice element must be map with string key, got %s", elemType)
|
|
}
|
|
for i := range h {
|
|
// allocate *map[string]V and initialize
|
|
mPtr := reflect.New(elemType) // *map[string]V
|
|
mPtr.Elem().Set(reflect.MakeMap(elemType)) // init
|
|
// decode into *map
|
|
if err := h[i].DecodeInto(mPtr.Interface()); err != nil {
|
|
return fmt.Errorf("decode hits[%d]: %w", i, err)
|
|
}
|
|
// append the map value (dereferenced)
|
|
out = reflect.Append(out, mPtr.Elem())
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("slice element must be struct, *struct, map[string]T, or *map[string]T, got %s", elemType)
|
|
}
|
|
|
|
sv.Set(out)
|
|
return nil
|
|
}
|
|
|
|
type fieldMeta struct {
|
|
jsonName string
|
|
indexPath []int
|
|
hasString bool
|
|
}
|
|
|
|
type typeInfo struct {
|
|
fields []fieldMeta
|
|
byNameIndex map[string]int // json name -> index in fields
|
|
}
|
|
|
|
var (
|
|
typeInfoCache sync.Map // reflect.Type -> *typeInfo
|
|
)
|
|
|
|
func getTypeInfo(t reflect.Type) *typeInfo {
|
|
if v, ok := typeInfoCache.Load(t); ok {
|
|
return v.(*typeInfo)
|
|
}
|
|
f := collectFields(t, nil)
|
|
ti := &typeInfo{
|
|
fields: f,
|
|
byNameIndex: make(map[string]int, len(f)),
|
|
}
|
|
for i := range f {
|
|
ti.byNameIndex[f[i].jsonName] = i
|
|
}
|
|
typeInfoCache.Store(t, ti)
|
|
return ti
|
|
}
|
|
|
|
func collectFields(t reflect.Type, prefix []int) []fieldMeta {
|
|
var out []fieldMeta
|
|
|
|
// Walk struct fields, including anonymous/embedded.
|
|
for i := 0; i < t.NumField(); i++ {
|
|
sf := t.Field(i)
|
|
// Skip unexported (PkgPath != "", except for embedded anonymous exported types)
|
|
if sf.PkgPath != "" && !sf.Anonymous {
|
|
continue
|
|
}
|
|
tag := sf.Tag.Get("json")
|
|
if tag == "-" {
|
|
continue
|
|
}
|
|
name := sf.Name
|
|
hasString := false
|
|
if tag != "" {
|
|
// split first token (name) and options
|
|
if c := indexByte(tag, ','); c >= 0 {
|
|
nameToken := tag[:c]
|
|
if nameToken != "" {
|
|
name = nameToken
|
|
}
|
|
if hasJSONTagOption(tag[c+1:], "string") {
|
|
hasString = true
|
|
}
|
|
} else {
|
|
// tag without options
|
|
name = tag
|
|
}
|
|
}
|
|
idx := append(append([]int(nil), prefix...), i)
|
|
|
|
// Inline embedded struct or *struct (pointer-embedded) when not renamed.
|
|
if sf.Anonymous && name == sf.Name {
|
|
u := sf.Type
|
|
if u.Kind() == reflect.Pointer {
|
|
u = u.Elem()
|
|
}
|
|
if u.Kind() == reflect.Struct {
|
|
out = append(out, collectFields(u, idx)...)
|
|
continue
|
|
}
|
|
}
|
|
|
|
out = append(out, fieldMeta{
|
|
jsonName: name,
|
|
indexPath: idx,
|
|
hasString: hasString,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// small helper: faster than strings.IndexByte here, avoids import
|
|
func indexByte(s string, c byte) int {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == c {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// hasJSONTagOption reports whether the comma-separated tag options contain opt.
|
|
func hasJSONTagOption(opts, opt string) bool {
|
|
// opts like: "omitempty,string"
|
|
start := 0
|
|
for i := 0; i <= len(opts); i++ {
|
|
if i == len(opts) || opts[i] == ',' {
|
|
if opts[start:i] == opt {
|
|
return true
|
|
}
|
|
start = i + 1
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// fieldByIndexPathAlloc walks from the addressable struct value rv to the field
|
|
// indicated by indexPath, allocating intermediate *struct fields if needed.
|
|
// Returns the leaf field value and whether it is usable (addressable/settable).
|
|
func fieldByIndexPathAlloc(rv reflect.Value, indexPath []int) (reflect.Value, bool) {
|
|
cur := rv
|
|
for _, idx := range indexPath {
|
|
if cur.Kind() == reflect.Pointer {
|
|
if cur.IsNil() {
|
|
if !cur.CanSet() {
|
|
return reflect.Value{}, false
|
|
}
|
|
if cur.Type().Elem().Kind() != reflect.Struct {
|
|
return reflect.Value{}, false
|
|
}
|
|
cur.Set(reflect.New(cur.Type().Elem()))
|
|
}
|
|
cur = cur.Elem()
|
|
}
|
|
if cur.Kind() != reflect.Struct {
|
|
return reflect.Value{}, false
|
|
}
|
|
|
|
if idx < 0 || idx >= cur.NumField() {
|
|
return reflect.Value{}, false
|
|
}
|
|
|
|
cur = cur.Field(idx)
|
|
}
|
|
if cur.Kind() == reflect.Pointer && cur.IsNil() {
|
|
if cur.CanSet() && cur.Type().Elem().Kind() == reflect.Struct {
|
|
cur.Set(reflect.New(cur.Type().Elem()))
|
|
}
|
|
}
|
|
return cur, cur.CanAddr() || cur.CanSet()
|
|
}
|
|
|
|
// unmarshalSingleField unmarshals a single field into dst (pointer to struct)
|
|
// by constructing a minimal {"name": raw} object and delegating to encoding/json.
|
|
// This preserves stdlib behaviors such as json:",string".
|
|
func unmarshalSingleField(dst any, name string, raw []byte) error {
|
|
tmp := map[string]json.RawMessage{name: json.RawMessage(raw)}
|
|
b, err := json.Marshal(tmp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(b, dst)
|
|
}
|
|
|
|
func isJSONNull(b []byte) bool {
|
|
return len(b) == 4 && b[0] == 'n' && b[1] == 'u' && b[2] == 'l' && b[3] == 'l'
|
|
}
|