forked from mystiq/dex
252 lines
6.1 KiB
Go
252 lines
6.1 KiB
Go
|
// Copyright 2015 Google Inc. All rights reserved.
|
||
|
// Use of this source code is governed by the Apache 2.0
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package search
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
// ErrFieldMismatch is returned when a field is to be loaded into a different
|
||
|
// than the one it was stored from, or when a field is missing or unexported in
|
||
|
// the destination struct.
|
||
|
type ErrFieldMismatch struct {
|
||
|
FieldName string
|
||
|
Reason string
|
||
|
}
|
||
|
|
||
|
func (e *ErrFieldMismatch) Error() string {
|
||
|
return fmt.Sprintf("search: cannot load field %q: %s", e.FieldName, e.Reason)
|
||
|
}
|
||
|
|
||
|
// ErrFacetMismatch is returned when a facet is to be loaded into a different
|
||
|
// type than the one it was stored from, or when a field is missing or
|
||
|
// unexported in the destination struct. StructType is the type of the struct
|
||
|
// pointed to by the destination argument passed to Iterator.Next.
|
||
|
type ErrFacetMismatch struct {
|
||
|
StructType reflect.Type
|
||
|
FacetName string
|
||
|
Reason string
|
||
|
}
|
||
|
|
||
|
func (e *ErrFacetMismatch) Error() string {
|
||
|
return fmt.Sprintf("search: cannot load facet %q into a %q: %s", e.FacetName, e.StructType, e.Reason)
|
||
|
}
|
||
|
|
||
|
// structCodec defines how to convert a given struct to/from a search document.
|
||
|
type structCodec struct {
|
||
|
// byIndex returns the struct tag for the i'th struct field.
|
||
|
byIndex []structTag
|
||
|
|
||
|
// fieldByName returns the index of the struct field for the given field name.
|
||
|
fieldByName map[string]int
|
||
|
|
||
|
// facetByName returns the index of the struct field for the given facet name,
|
||
|
facetByName map[string]int
|
||
|
}
|
||
|
|
||
|
// structTag holds a structured version of each struct field's parsed tag.
|
||
|
type structTag struct {
|
||
|
name string
|
||
|
facet bool
|
||
|
ignore bool
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
codecsMu sync.RWMutex
|
||
|
codecs = map[reflect.Type]*structCodec{}
|
||
|
)
|
||
|
|
||
|
func loadCodec(t reflect.Type) (*structCodec, error) {
|
||
|
codecsMu.RLock()
|
||
|
codec, ok := codecs[t]
|
||
|
codecsMu.RUnlock()
|
||
|
if ok {
|
||
|
return codec, nil
|
||
|
}
|
||
|
|
||
|
codecsMu.Lock()
|
||
|
defer codecsMu.Unlock()
|
||
|
if codec, ok := codecs[t]; ok {
|
||
|
return codec, nil
|
||
|
}
|
||
|
|
||
|
codec = &structCodec{
|
||
|
fieldByName: make(map[string]int),
|
||
|
facetByName: make(map[string]int),
|
||
|
}
|
||
|
|
||
|
for i, I := 0, t.NumField(); i < I; i++ {
|
||
|
f := t.Field(i)
|
||
|
name, opts := f.Tag.Get("search"), ""
|
||
|
if i := strings.Index(name, ","); i != -1 {
|
||
|
name, opts = name[:i], name[i+1:]
|
||
|
}
|
||
|
ignore := false
|
||
|
if name == "-" {
|
||
|
ignore = true
|
||
|
} else if name == "" {
|
||
|
name = f.Name
|
||
|
} else if !validFieldName(name) {
|
||
|
return nil, fmt.Errorf("search: struct tag has invalid field name: %q", name)
|
||
|
}
|
||
|
facet := opts == "facet"
|
||
|
codec.byIndex = append(codec.byIndex, structTag{name: name, facet: facet, ignore: ignore})
|
||
|
if facet {
|
||
|
codec.facetByName[name] = i
|
||
|
} else {
|
||
|
codec.fieldByName[name] = i
|
||
|
}
|
||
|
}
|
||
|
|
||
|
codecs[t] = codec
|
||
|
return codec, nil
|
||
|
}
|
||
|
|
||
|
// structFLS adapts a struct to be a FieldLoadSaver.
|
||
|
type structFLS struct {
|
||
|
v reflect.Value
|
||
|
codec *structCodec
|
||
|
}
|
||
|
|
||
|
func (s structFLS) Load(fields []Field, meta *DocumentMetadata) error {
|
||
|
var err error
|
||
|
for _, field := range fields {
|
||
|
i, ok := s.codec.fieldByName[field.Name]
|
||
|
if !ok {
|
||
|
// Note the error, but keep going.
|
||
|
err = &ErrFieldMismatch{
|
||
|
FieldName: field.Name,
|
||
|
Reason: "no such struct field",
|
||
|
}
|
||
|
continue
|
||
|
|
||
|
}
|
||
|
f := s.v.Field(i)
|
||
|
if !f.CanSet() {
|
||
|
// Note the error, but keep going.
|
||
|
err = &ErrFieldMismatch{
|
||
|
FieldName: field.Name,
|
||
|
Reason: "cannot set struct field",
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
v := reflect.ValueOf(field.Value)
|
||
|
if ft, vt := f.Type(), v.Type(); ft != vt {
|
||
|
err = &ErrFieldMismatch{
|
||
|
FieldName: field.Name,
|
||
|
Reason: fmt.Sprintf("type mismatch: %v for %v data", ft, vt),
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
f.Set(v)
|
||
|
}
|
||
|
if meta == nil {
|
||
|
return err
|
||
|
}
|
||
|
for _, facet := range meta.Facets {
|
||
|
i, ok := s.codec.facetByName[facet.Name]
|
||
|
if !ok {
|
||
|
// Note the error, but keep going.
|
||
|
if err == nil {
|
||
|
err = &ErrFacetMismatch{
|
||
|
StructType: s.v.Type(),
|
||
|
FacetName: facet.Name,
|
||
|
Reason: "no matching field found",
|
||
|
}
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
f := s.v.Field(i)
|
||
|
if !f.CanSet() {
|
||
|
// Note the error, but keep going.
|
||
|
if err == nil {
|
||
|
err = &ErrFacetMismatch{
|
||
|
StructType: s.v.Type(),
|
||
|
FacetName: facet.Name,
|
||
|
Reason: "unable to set unexported field of struct",
|
||
|
}
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
v := reflect.ValueOf(facet.Value)
|
||
|
if ft, vt := f.Type(), v.Type(); ft != vt {
|
||
|
if err == nil {
|
||
|
err = &ErrFacetMismatch{
|
||
|
StructType: s.v.Type(),
|
||
|
FacetName: facet.Name,
|
||
|
Reason: fmt.Sprintf("type mismatch: %v for %d data", ft, vt),
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
f.Set(v)
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func (s structFLS) Save() ([]Field, *DocumentMetadata, error) {
|
||
|
fields := make([]Field, 0, len(s.codec.fieldByName))
|
||
|
var facets []Facet
|
||
|
for i, tag := range s.codec.byIndex {
|
||
|
if tag.ignore {
|
||
|
continue
|
||
|
}
|
||
|
f := s.v.Field(i)
|
||
|
if !f.CanSet() {
|
||
|
continue
|
||
|
}
|
||
|
if tag.facet {
|
||
|
facets = append(facets, Facet{Name: tag.name, Value: f.Interface()})
|
||
|
} else {
|
||
|
fields = append(fields, Field{Name: tag.name, Value: f.Interface()})
|
||
|
}
|
||
|
}
|
||
|
return fields, &DocumentMetadata{Facets: facets}, nil
|
||
|
}
|
||
|
|
||
|
// newStructFLS returns a FieldLoadSaver for the struct pointer p.
|
||
|
func newStructFLS(p interface{}) (FieldLoadSaver, error) {
|
||
|
v := reflect.ValueOf(p)
|
||
|
if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
|
||
|
return nil, ErrInvalidDocumentType
|
||
|
}
|
||
|
codec, err := loadCodec(v.Elem().Type())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return structFLS{v.Elem(), codec}, nil
|
||
|
}
|
||
|
|
||
|
func loadStructWithMeta(dst interface{}, f []Field, meta *DocumentMetadata) error {
|
||
|
x, err := newStructFLS(dst)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return x.Load(f, meta)
|
||
|
}
|
||
|
|
||
|
func saveStructWithMeta(src interface{}) ([]Field, *DocumentMetadata, error) {
|
||
|
x, err := newStructFLS(src)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
return x.Save()
|
||
|
}
|
||
|
|
||
|
// LoadStruct loads the fields from f to dst. dst must be a struct pointer.
|
||
|
func LoadStruct(dst interface{}, f []Field) error {
|
||
|
return loadStructWithMeta(dst, f, nil)
|
||
|
}
|
||
|
|
||
|
// SaveStruct returns the fields from src as a slice of Field.
|
||
|
// src must be a struct pointer.
|
||
|
func SaveStruct(src interface{}) ([]Field, error) {
|
||
|
f, _, err := saveStructWithMeta(src)
|
||
|
return f, err
|
||
|
}
|