initial commit

This commit is contained in:
Eric Chiang
2016-07-25 13:00:28 -07:00
commit cab271f304
1438 changed files with 335968 additions and 0 deletions

209
vendor/google.golang.org/appengine/search/doc.go generated vendored Normal file
View File

@@ -0,0 +1,209 @@
// 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 provides a client for App Engine's search service.
Basic Operations
Indexes contain documents. Each index is identified by its name: a
human-readable ASCII string.
Within an index, documents are associated with an ID, which is also
a human-readable ASCII string. A document's contents are a mapping from
case-sensitive field names to values. Valid types for field values are:
- string,
- search.Atom,
- search.HTML,
- time.Time (stored with millisecond precision),
- float64 (value between -2,147,483,647 and 2,147,483,647 inclusive),
- appengine.GeoPoint.
The Get and Put methods on an Index load and save a document.
A document's contents are typically represented by a struct pointer.
Example code:
type Doc struct {
Author string
Comment string
Creation time.Time
}
index, err := search.Open("comments")
if err != nil {
return err
}
newID, err := index.Put(ctx, "", &Doc{
Author: "gopher",
Comment: "the truth of the matter",
Creation: time.Now(),
})
if err != nil {
return err
}
A single document can be retrieved by its ID. Pass a destination struct
to Get to hold the resulting document.
var doc Doc
err := index.Get(ctx, id, &doc)
if err != nil {
return err
}
Search and Listing Documents
Indexes have two methods for retrieving multiple documents at once: Search and
List.
Searching an index for a query will result in an iterator. As with an iterator
from package datastore, pass a destination struct to Next to decode the next
result. Next will return Done when the iterator is exhausted.
for t := index.Search(ctx, "Comment:truth", nil); ; {
var doc Doc
id, err := t.Next(&doc)
if err == search.Done {
break
}
if err != nil {
return err
}
fmt.Fprintf(w, "%s -> %#v\n", id, doc)
}
Search takes a string query to determine which documents to return. The query
can be simple, such as a single word to match, or complex. The query
language is described at
https://cloud.google.com/appengine/docs/go/search/query_strings
Search also takes an optional SearchOptions struct which gives much more
control over how results are calculated and returned.
Call List to iterate over all documents in an index.
for t := index.List(ctx, nil); ; {
var doc Doc
id, err := t.Next(&doc)
if err == search.Done {
break
}
if err != nil {
return err
}
fmt.Fprintf(w, "%s -> %#v\n", id, doc)
}
Fields and Facets
A document's contents can be represented by a variety of types. These are
typically struct pointers, but they can also be represented by any type
implementing the FieldLoadSaver interface. The FieldLoadSaver allows metadata
to be set for the document with the DocumentMetadata type. Struct pointers are
more strongly typed and are easier to use; FieldLoadSavers are more flexible.
A document's contents can be expressed in two ways: fields and facets.
Fields are the most common way of providing content for documents. Fields can
store data in multiple types and can be matched in searches using query
strings.
Facets provide a way to attach categorical information to a document. The only
valid types for facets are search.Atom and float64. Facets allow search
results to contain summaries of the categories matched in a search, and to
restrict searches to only match against specific categories.
By default, for struct pointers, all of the struct fields are used as document
fields, and the field name used is the same as on the struct (and hence must
start with an upper case letter). Struct fields may have a
`search:"name,options"` tag. The name must start with a letter and be
composed only of word characters. A "-" tag name means that the field will be
ignored. If options is "facet" then the struct field will be used as a
document facet. If options is "" then the comma may be omitted. There are no
other recognized options.
Example code:
// A and B are renamed to a and b.
// A, C and I are facets.
// D's tag is equivalent to having no tag at all (E).
// F and G are ignored entirely by the search package.
// I has tag information for both the search and json packages.
type TaggedStruct struct {
A float64 `search:"a,facet"`
B float64 `search:"b"`
C float64 `search:",facet"`
D float64 `search:""`
E float64
F float64 `search:"-"`
G float64 `search:"-,facet"`
I float64 `search:",facet" json:"i"`
}
The FieldLoadSaver Interface
A document's contents can also be represented by any type that implements the
FieldLoadSaver interface. This type may be a struct pointer, but it
does not have to be. The search package will call Load when loading the
document's contents, and Save when saving them. In addition to a slice of
Fields, the Load and Save methods also use the DocumentMetadata type to
provide additional information about a document (such as its Rank, or set of
Facets). Possible uses for this interface include deriving non-stored fields,
verifying fields or setting specific languages for string and HTML fields.
Example code:
type CustomFieldsExample struct {
// Item's title and which language it is in.
Title string
Lang string
// Mass, in grams.
Mass int
}
func (x *CustomFieldsExample) Load(fields []search.Field, meta *search.DocumentMetadata) error {
// Load the title field, failing if any other field is found.
for _, f := range fields {
if f.Name != "title" {
return fmt.Errorf("unknown field %q", f.Name)
}
s, ok := f.Value.(string)
if !ok {
return fmt.Errorf("unsupported type %T for field %q", f.Value, f.Name)
}
x.Title = s
x.Lang = f.Language
}
// Load the mass facet, failing if any other facet is found.
for _, f := range meta.Facets {
if f.Name != "mass" {
return fmt.Errorf("unknown facet %q", f.Name)
}
m, ok := f.Value.(float64)
if !ok {
return fmt.Errorf("unsupported type %T for facet %q", f.Value, f.Name)
}
x.Mass = int(m)
}
return nil
}
func (x *CustomFieldsExample) Save() ([]search.Field, *search.DocumentMetadata, error) {
fields := []search.Field{
{Name: "title", Value: x.Title, Language: x.Lang},
}
meta := &search.DocumentMetadata{
Facets: {
{Name: "mass", Value: float64(x.Mass)},
},
}
return fields, meta, nil
}
*/
package search

82
vendor/google.golang.org/appengine/search/field.go generated vendored Normal file
View File

@@ -0,0 +1,82 @@
// Copyright 2014 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
// Field is a name/value pair. A search index's document can be loaded and
// saved as a sequence of Fields.
type Field struct {
// Name is the field name. A valid field name matches /[A-Za-z][A-Za-z0-9_]*/.
Name string
// Value is the field value. The valid types are:
// - string,
// - search.Atom,
// - search.HTML,
// - time.Time (stored with millisecond precision),
// - float64,
// - GeoPoint.
Value interface{}
// Language is a two-letter ISO 639-1 code for the field's language,
// defaulting to "en" if nothing is specified. It may only be specified for
// fields of type string and search.HTML.
Language string
// Derived marks fields that were calculated as a result of a
// FieldExpression provided to Search. This field is ignored when saving a
// document.
Derived bool
}
// Facet is a name/value pair which is used to add categorical information to a
// document.
type Facet struct {
// Name is the facet name. A valid facet name matches /[A-Za-z][A-Za-z0-9_]*/.
// A facet name cannot be longer than 500 characters.
Name string
// Value is the facet value.
//
// When being used in documents (for example, in
// DocumentMetadata.Facets), the valid types are:
// - search.Atom,
// - float64.
//
// When being used in SearchOptions.Refinements or being returned
// in FacetResult, the valid types are:
// - search.Atom,
// - search.Range.
Value interface{}
}
// DocumentMetadata is a struct containing information describing a given document.
type DocumentMetadata struct {
// Rank is an integer specifying the order the document will be returned in
// search results. If zero, the rank will be set to the number of seconds since
// 2011-01-01 00:00:00 UTC when being Put into an index.
Rank int
// Facets is the set of facets for this document.
Facets []Facet
}
// FieldLoadSaver can be converted from and to a slice of Fields
// with additional document metadata.
type FieldLoadSaver interface {
Load([]Field, *DocumentMetadata) error
Save() ([]Field, *DocumentMetadata, error)
}
// FieldList converts a []Field to implement FieldLoadSaver.
type FieldList []Field
// Load loads all of the provided fields into l.
// It does not first reset *l to an empty slice.
func (l *FieldList) Load(f []Field, _ *DocumentMetadata) error {
*l = append(*l, f...)
return nil
}
// Save returns all of l's fields as a slice of Fields.
func (l *FieldList) Save() ([]Field, *DocumentMetadata, error) {
return *l, nil, nil
}
var _ FieldLoadSaver = (*FieldList)(nil)

1112
vendor/google.golang.org/appengine/search/search.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,991 @@
// Copyright 2012 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 (
"errors"
"fmt"
"reflect"
"strings"
"testing"
"time"
"github.com/golang/protobuf/proto"
"google.golang.org/appengine"
"google.golang.org/appengine/internal/aetesting"
pb "google.golang.org/appengine/internal/search"
)
type TestDoc struct {
String string
Atom Atom
HTML HTML
Float float64
Location appengine.GeoPoint
Time time.Time
}
type FieldListWithMeta struct {
Fields FieldList
Meta *DocumentMetadata
}
func (f *FieldListWithMeta) Load(fields []Field, meta *DocumentMetadata) error {
f.Meta = meta
return f.Fields.Load(fields, nil)
}
func (f *FieldListWithMeta) Save() ([]Field, *DocumentMetadata, error) {
fields, _, err := f.Fields.Save()
return fields, f.Meta, err
}
// Assert that FieldListWithMeta satisfies FieldLoadSaver
var _ FieldLoadSaver = &FieldListWithMeta{}
var (
float = 3.14159
floatOut = "3.14159e+00"
latitude = 37.3894
longitude = 122.0819
testGeo = appengine.GeoPoint{latitude, longitude}
testString = "foo<b>bar"
testTime = time.Unix(1337324400, 0)
testTimeOut = "1337324400000"
searchMeta = &DocumentMetadata{
Rank: 42,
}
searchDoc = TestDoc{
String: testString,
Atom: Atom(testString),
HTML: HTML(testString),
Float: float,
Location: testGeo,
Time: testTime,
}
searchFields = FieldList{
Field{Name: "String", Value: testString},
Field{Name: "Atom", Value: Atom(testString)},
Field{Name: "HTML", Value: HTML(testString)},
Field{Name: "Float", Value: float},
Field{Name: "Location", Value: testGeo},
Field{Name: "Time", Value: testTime},
}
// searchFieldsWithLang is a copy of the searchFields with the Language field
// set on text/HTML Fields.
searchFieldsWithLang = FieldList{}
protoFields = []*pb.Field{
newStringValueField("String", testString, pb.FieldValue_TEXT),
newStringValueField("Atom", testString, pb.FieldValue_ATOM),
newStringValueField("HTML", testString, pb.FieldValue_HTML),
newStringValueField("Float", floatOut, pb.FieldValue_NUMBER),
{
Name: proto.String("Location"),
Value: &pb.FieldValue{
Geo: &pb.FieldValue_Geo{
Lat: proto.Float64(latitude),
Lng: proto.Float64(longitude),
},
Type: pb.FieldValue_GEO.Enum(),
},
},
newStringValueField("Time", testTimeOut, pb.FieldValue_DATE),
}
)
func init() {
for _, f := range searchFields {
if f.Name == "String" || f.Name == "HTML" {
f.Language = "en"
}
searchFieldsWithLang = append(searchFieldsWithLang, f)
}
}
func newStringValueField(name, value string, valueType pb.FieldValue_ContentType) *pb.Field {
return &pb.Field{
Name: proto.String(name),
Value: &pb.FieldValue{
StringValue: proto.String(value),
Type: valueType.Enum(),
},
}
}
func newFacet(name, value string, valueType pb.FacetValue_ContentType) *pb.Facet {
return &pb.Facet{
Name: proto.String(name),
Value: &pb.FacetValue{
StringValue: proto.String(value),
Type: valueType.Enum(),
},
}
}
func TestValidIndexNameOrDocID(t *testing.T) {
testCases := []struct {
s string
want bool
}{
{"", true},
{"!", false},
{"$", true},
{"!bad", false},
{"good!", true},
{"alsoGood", true},
{"has spaces", false},
{"is_inva\xffid_UTF-8", false},
{"is_non-ASCïI", false},
{"underscores_are_ok", true},
}
for _, tc := range testCases {
if got := validIndexNameOrDocID(tc.s); got != tc.want {
t.Errorf("%q: got %v, want %v", tc.s, got, tc.want)
}
}
}
func TestLoadDoc(t *testing.T) {
got, want := TestDoc{}, searchDoc
if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
t.Fatalf("loadDoc: %v", err)
}
if got != want {
t.Errorf("loadDoc: got %v, wanted %v", got, want)
}
}
func TestSaveDoc(t *testing.T) {
got, err := saveDoc(&searchDoc)
if err != nil {
t.Fatalf("saveDoc: %v", err)
}
want := protoFields
if !reflect.DeepEqual(got.Field, want) {
t.Errorf("\ngot %v\nwant %v", got, want)
}
}
func TestLoadFieldList(t *testing.T) {
var got FieldList
want := searchFieldsWithLang
if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
t.Fatalf("loadDoc: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("\ngot %v\nwant %v", got, want)
}
}
func TestLangFields(t *testing.T) {
fl := &FieldList{
{Name: "Foo", Value: "I am English", Language: "en"},
{Name: "Bar", Value: "私は日本人だ", Language: "jp"},
}
var got FieldList
doc, err := saveDoc(fl)
if err != nil {
t.Fatalf("saveDoc: %v", err)
}
if err := loadDoc(&got, doc, nil); err != nil {
t.Fatalf("loadDoc: %v", err)
}
if want := fl; !reflect.DeepEqual(&got, want) {
t.Errorf("got %v\nwant %v", got, want)
}
}
func TestSaveFieldList(t *testing.T) {
got, err := saveDoc(&searchFields)
if err != nil {
t.Fatalf("saveDoc: %v", err)
}
want := protoFields
if !reflect.DeepEqual(got.Field, want) {
t.Errorf("\ngot %v\nwant %v", got, want)
}
}
func TestLoadFieldAndExprList(t *testing.T) {
var got, want FieldList
for i, f := range searchFieldsWithLang {
f.Derived = (i >= 2) // First 2 elements are "fields", next are "expressions".
want = append(want, f)
}
doc, expr := &pb.Document{Field: protoFields[:2]}, protoFields[2:]
if err := loadDoc(&got, doc, expr); err != nil {
t.Fatalf("loadDoc: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v\nwant %v", got, want)
}
}
func TestLoadMeta(t *testing.T) {
var got FieldListWithMeta
want := FieldListWithMeta{
Meta: searchMeta,
Fields: searchFieldsWithLang,
}
doc := &pb.Document{
Field: protoFields,
OrderId: proto.Int32(42),
}
if err := loadDoc(&got, doc, nil); err != nil {
t.Fatalf("loadDoc: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("\ngot %v\nwant %v", got, want)
}
}
func TestSaveMeta(t *testing.T) {
got, err := saveDoc(&FieldListWithMeta{
Meta: searchMeta,
Fields: searchFields,
})
if err != nil {
t.Fatalf("saveDoc: %v", err)
}
want := &pb.Document{
Field: protoFields,
OrderId: proto.Int32(42),
}
if !proto.Equal(got, want) {
t.Errorf("\ngot %v\nwant %v", got, want)
}
}
func TestLoadSaveWithStruct(t *testing.T) {
type gopher struct {
Name string
Info string `search:"about"`
Legs float64 `search:",facet"`
Fuzz Atom `search:"Fur,facet"`
}
doc := gopher{"Gopher", "Likes slide rules.", 4, Atom("furry")}
pb := &pb.Document{
Field: []*pb.Field{
newStringValueField("Name", "Gopher", pb.FieldValue_TEXT),
newStringValueField("about", "Likes slide rules.", pb.FieldValue_TEXT),
},
Facet: []*pb.Facet{
newFacet("Legs", "4e+00", pb.FacetValue_NUMBER),
newFacet("Fur", "furry", pb.FacetValue_ATOM),
},
}
var gotDoc gopher
if err := loadDoc(&gotDoc, pb, nil); err != nil {
t.Fatalf("loadDoc: %v", err)
}
if !reflect.DeepEqual(gotDoc, doc) {
t.Errorf("loading doc\ngot %v\nwant %v", gotDoc, doc)
}
gotPB, err := saveDoc(&doc)
if err != nil {
t.Fatalf("saveDoc: %v", err)
}
gotPB.OrderId = nil // Don't test: it's time dependent.
if !proto.Equal(gotPB, pb) {
t.Errorf("saving doc\ngot %v\nwant %v", gotPB, pb)
}
}
func TestValidFieldNames(t *testing.T) {
testCases := []struct {
name string
valid bool
}{
{"Normal", true},
{"Also_OK_123", true},
{"Not so great", false},
{"lower_case", true},
{"Exclaim!", false},
{"Hello세상아 안녕", false},
{"", false},
{"Hεllo", false},
{strings.Repeat("A", 500), true},
{strings.Repeat("A", 501), false},
}
for _, tc := range testCases {
_, err := saveDoc(&FieldList{
Field{Name: tc.name, Value: "val"},
})
if err != nil && !strings.Contains(err.Error(), "invalid field name") {
t.Errorf("unexpected err %q for field name %q", err, tc.name)
}
if (err == nil) != tc.valid {
t.Errorf("field %q: expected valid %t, received err %v", tc.name, tc.valid, err)
}
}
}
func TestValidLangs(t *testing.T) {
testCases := []struct {
field Field
valid bool
}{
{Field{Name: "Foo", Value: "String", Language: ""}, true},
{Field{Name: "Foo", Value: "String", Language: "en"}, true},
{Field{Name: "Foo", Value: "String", Language: "aussie"}, false},
{Field{Name: "Foo", Value: "String", Language: "12"}, false},
{Field{Name: "Foo", Value: HTML("String"), Language: "en"}, true},
{Field{Name: "Foo", Value: Atom("String"), Language: "en"}, false},
{Field{Name: "Foo", Value: 42, Language: "en"}, false},
}
for _, tt := range testCases {
_, err := saveDoc(&FieldList{tt.field})
if err == nil != tt.valid {
t.Errorf("Field %v, got error %v, wanted valid %t", tt.field, err, tt.valid)
}
}
}
func TestDuplicateFields(t *testing.T) {
testCases := []struct {
desc string
fields FieldList
errMsg string // Non-empty if we expect an error
}{
{
desc: "multi string",
fields: FieldList{{Name: "FieldA", Value: "val1"}, {Name: "FieldA", Value: "val2"}, {Name: "FieldA", Value: "val3"}},
},
{
desc: "multi atom",
fields: FieldList{{Name: "FieldA", Value: Atom("val1")}, {Name: "FieldA", Value: Atom("val2")}, {Name: "FieldA", Value: Atom("val3")}},
},
{
desc: "mixed",
fields: FieldList{{Name: "FieldA", Value: testString}, {Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: float}},
},
{
desc: "multi time",
fields: FieldList{{Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: testTime}},
errMsg: `duplicate time field "FieldA"`,
},
{
desc: "multi num",
fields: FieldList{{Name: "FieldA", Value: float}, {Name: "FieldA", Value: float}},
errMsg: `duplicate numeric field "FieldA"`,
},
}
for _, tc := range testCases {
_, err := saveDoc(&tc.fields)
if (err == nil) != (tc.errMsg == "") || (err != nil && !strings.Contains(err.Error(), tc.errMsg)) {
t.Errorf("%s: got err %v, wanted %q", tc.desc, err, tc.errMsg)
}
}
}
func TestLoadErrFieldMismatch(t *testing.T) {
testCases := []struct {
desc string
dst interface{}
src []*pb.Field
err error
}{
{
desc: "missing",
dst: &struct{ One string }{},
src: []*pb.Field{newStringValueField("Two", "woop!", pb.FieldValue_TEXT)},
err: &ErrFieldMismatch{
FieldName: "Two",
Reason: "no such struct field",
},
},
{
desc: "wrong type",
dst: &struct{ Num float64 }{},
src: []*pb.Field{newStringValueField("Num", "woop!", pb.FieldValue_TEXT)},
err: &ErrFieldMismatch{
FieldName: "Num",
Reason: "type mismatch: float64 for string data",
},
},
{
desc: "unsettable",
dst: &struct{ lower string }{},
src: []*pb.Field{newStringValueField("lower", "woop!", pb.FieldValue_TEXT)},
err: &ErrFieldMismatch{
FieldName: "lower",
Reason: "cannot set struct field",
},
},
}
for _, tc := range testCases {
err := loadDoc(tc.dst, &pb.Document{Field: tc.src}, nil)
if !reflect.DeepEqual(err, tc.err) {
t.Errorf("%s, got err %v, wanted %v", tc.desc, err, tc.err)
}
}
}
func TestLimit(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, res *pb.SearchResponse) error {
limit := 20 // Default per page.
if req.Params.Limit != nil {
limit = int(*req.Params.Limit)
}
res.Status = &pb.RequestStatus{Code: pb.SearchServiceError_OK.Enum()}
res.MatchedCount = proto.Int64(int64(limit))
for i := 0; i < limit; i++ {
res.Result = append(res.Result, &pb.SearchResult{Document: &pb.Document{}})
res.Cursor = proto.String("moreresults")
}
return nil
})
const maxDocs = 500 // Limit maximum number of docs.
testCases := []struct {
limit, want int
}{
{limit: 0, want: maxDocs},
{limit: 42, want: 42},
{limit: 100, want: 100},
{limit: 1000, want: maxDocs},
}
for _, tt := range testCases {
it := index.Search(c, "gopher", &SearchOptions{Limit: tt.limit, IDsOnly: true})
count := 0
for ; count < maxDocs; count++ {
_, err := it.Next(nil)
if err == Done {
break
}
if err != nil {
t.Fatalf("err after %d: %v", count, err)
}
}
if count != tt.want {
t.Errorf("got %d results, expected %d", count, tt.want)
}
}
}
func TestPut(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
expectedIn := &pb.IndexDocumentRequest{
Params: &pb.IndexDocumentParams{
Document: []*pb.Document{
{Field: protoFields, OrderId: proto.Int32(42)},
},
IndexSpec: &pb.IndexSpec{
Name: proto.String("Doc"),
},
},
}
if !proto.Equal(in, expectedIn) {
return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn)
}
*out = pb.IndexDocumentResponse{
Status: []*pb.RequestStatus{
{Code: pb.SearchServiceError_OK.Enum()},
},
DocId: []string{
"doc_id",
},
}
return nil
})
id, err := index.Put(c, "", &FieldListWithMeta{
Meta: searchMeta,
Fields: searchFields,
})
if err != nil {
t.Fatal(err)
}
if want := "doc_id"; id != want {
t.Errorf("Got doc ID %q, want %q", id, want)
}
}
func TestPutAutoOrderID(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
if len(in.Params.GetDocument()) < 1 {
return fmt.Errorf("expected at least one Document, got %v", in)
}
got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds())
if d := got - want; -5 > d || d > 5 {
return fmt.Errorf("got OrderId %d, want near %d", got, want)
}
*out = pb.IndexDocumentResponse{
Status: []*pb.RequestStatus{
{Code: pb.SearchServiceError_OK.Enum()},
},
DocId: []string{
"doc_id",
},
}
return nil
})
if _, err := index.Put(c, "", &searchFields); err != nil {
t.Fatal(err)
}
}
func TestPutBadStatus(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(_ *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
*out = pb.IndexDocumentResponse{
Status: []*pb.RequestStatus{
{
Code: pb.SearchServiceError_INVALID_REQUEST.Enum(),
ErrorDetail: proto.String("insufficient gophers"),
},
},
}
return nil
})
wantErr := "search: INVALID_REQUEST: insufficient gophers"
if _, err := index.Put(c, "", &searchFields); err == nil || err.Error() != wantErr {
t.Fatalf("Put: got %v error, want %q", err, wantErr)
}
}
func TestSortOptions(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
noErr := errors.New("") // Sentinel err to return to prevent sending request.
testCases := []struct {
desc string
sort *SortOptions
wantSort []*pb.SortSpec
wantScorer *pb.ScorerSpec
wantErr string
}{
{
desc: "No SortOptions",
},
{
desc: "Basic",
sort: &SortOptions{
Expressions: []SortExpression{
{Expr: "dog"},
{Expr: "cat", Reverse: true},
{Expr: "gopher", Default: "blue"},
{Expr: "fish", Default: 2.0},
},
Limit: 42,
Scorer: MatchScorer,
},
wantSort: []*pb.SortSpec{
{SortExpression: proto.String("dog")},
{SortExpression: proto.String("cat"), SortDescending: proto.Bool(false)},
{SortExpression: proto.String("gopher"), DefaultValueText: proto.String("blue")},
{SortExpression: proto.String("fish"), DefaultValueNumeric: proto.Float64(2)},
},
wantScorer: &pb.ScorerSpec{
Limit: proto.Int32(42),
Scorer: pb.ScorerSpec_MATCH_SCORER.Enum(),
},
},
{
desc: "Bad expression default",
sort: &SortOptions{
Expressions: []SortExpression{
{Expr: "dog", Default: true},
},
},
wantErr: `search: invalid Default type bool for expression "dog"`,
},
{
desc: "RescoringMatchScorer",
sort: &SortOptions{Scorer: RescoringMatchScorer},
wantScorer: &pb.ScorerSpec{Scorer: pb.ScorerSpec_RESCORING_MATCH_SCORER.Enum()},
},
}
for _, tt := range testCases {
c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
params := req.Params
if !reflect.DeepEqual(params.SortSpec, tt.wantSort) {
t.Errorf("%s: params.SortSpec=%v; want %v", tt.desc, params.SortSpec, tt.wantSort)
}
if !reflect.DeepEqual(params.ScorerSpec, tt.wantScorer) {
t.Errorf("%s: params.ScorerSpec=%v; want %v", tt.desc, params.ScorerSpec, tt.wantScorer)
}
return noErr // Always return some error to prevent response parsing.
})
it := index.Search(c, "gopher", &SearchOptions{Sort: tt.sort})
_, err := it.Next(nil)
if err == nil {
t.Fatalf("%s: err==nil; should not happen", tt.desc)
}
if err.Error() != tt.wantErr {
t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
}
}
}
func TestFieldSpec(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
errFoo := errors.New("foo") // sentinel error when there isn't one.
testCases := []struct {
desc string
opts *SearchOptions
want *pb.FieldSpec
}{
{
desc: "No options",
want: &pb.FieldSpec{},
},
{
desc: "Fields",
opts: &SearchOptions{
Fields: []string{"one", "two"},
},
want: &pb.FieldSpec{
Name: []string{"one", "two"},
},
},
{
desc: "Expressions",
opts: &SearchOptions{
Expressions: []FieldExpression{
{Name: "one", Expr: "price * quantity"},
{Name: "two", Expr: "min(daily_use, 10) * rate"},
},
},
want: &pb.FieldSpec{
Expression: []*pb.FieldSpec_Expression{
{Name: proto.String("one"), Expression: proto.String("price * quantity")},
{Name: proto.String("two"), Expression: proto.String("min(daily_use, 10) * rate")},
},
},
},
}
for _, tt := range testCases {
c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
params := req.Params
if !reflect.DeepEqual(params.FieldSpec, tt.want) {
t.Errorf("%s: params.FieldSpec=%v; want %v", tt.desc, params.FieldSpec, tt.want)
}
return errFoo // Always return some error to prevent response parsing.
})
it := index.Search(c, "gopher", tt.opts)
if _, err := it.Next(nil); err != errFoo {
t.Fatalf("%s: got error %v; want %v", tt.desc, err, errFoo)
}
}
}
func TestBasicSearchOpts(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
noErr := errors.New("") // Sentinel err to return to prevent sending request.
testCases := []struct {
desc string
facetOpts []FacetSearchOption
cursor Cursor
offset int
want *pb.SearchParams
wantErr string
}{
{
desc: "No options",
want: &pb.SearchParams{},
},
{
desc: "Default auto discovery",
facetOpts: []FacetSearchOption{
AutoFacetDiscovery(0, 0),
},
want: &pb.SearchParams{
AutoDiscoverFacetCount: proto.Int32(10),
},
},
{
desc: "Auto discovery",
facetOpts: []FacetSearchOption{
AutoFacetDiscovery(7, 12),
},
want: &pb.SearchParams{
AutoDiscoverFacetCount: proto.Int32(7),
FacetAutoDetectParam: &pb.FacetAutoDetectParam{
ValueLimit: proto.Int32(12),
},
},
},
{
desc: "Param Depth",
facetOpts: []FacetSearchOption{
AutoFacetDiscovery(7, 12),
},
want: &pb.SearchParams{
AutoDiscoverFacetCount: proto.Int32(7),
FacetAutoDetectParam: &pb.FacetAutoDetectParam{
ValueLimit: proto.Int32(12),
},
},
},
{
desc: "Doc depth",
facetOpts: []FacetSearchOption{
FacetDocumentDepth(123),
},
want: &pb.SearchParams{
FacetDepth: proto.Int32(123),
},
},
{
desc: "Facet discovery",
facetOpts: []FacetSearchOption{
FacetDiscovery("colour"),
FacetDiscovery("size", Atom("M"), Atom("L")),
FacetDiscovery("price", LessThan(7), Range{7, 14}, AtLeast(14)),
},
want: &pb.SearchParams{
IncludeFacet: []*pb.FacetRequest{
{Name: proto.String("colour")},
{Name: proto.String("size"), Params: &pb.FacetRequestParam{
ValueConstraint: []string{"M", "L"},
}},
{Name: proto.String("price"), Params: &pb.FacetRequestParam{
Range: []*pb.FacetRange{
{End: proto.String("7e+00")},
{Start: proto.String("7e+00"), End: proto.String("1.4e+01")},
{Start: proto.String("1.4e+01")},
},
}},
},
},
},
{
desc: "Facet discovery - bad value",
facetOpts: []FacetSearchOption{
FacetDiscovery("colour", true),
},
wantErr: "bad FacetSearchOption: unsupported value type bool",
},
{
desc: "Facet discovery - mix value types",
facetOpts: []FacetSearchOption{
FacetDiscovery("colour", Atom("blue"), AtLeast(7)),
},
wantErr: "bad FacetSearchOption: values must all be Atom, or must all be Range",
},
{
desc: "Facet discovery - invalid range",
facetOpts: []FacetSearchOption{
FacetDiscovery("colour", Range{negInf, posInf}),
},
wantErr: "bad FacetSearchOption: invalid range: either Start or End must be finite",
},
{
desc: "Cursor",
cursor: Cursor("mycursor"),
want: &pb.SearchParams{
Cursor: proto.String("mycursor"),
},
},
{
desc: "Offset",
offset: 121,
want: &pb.SearchParams{
Offset: proto.Int32(121),
},
},
{
desc: "Cursor and Offset set",
cursor: Cursor("mycursor"),
offset: 121,
wantErr: "at most one of Cursor and Offset may be specified",
},
}
for _, tt := range testCases {
c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
if tt.want == nil {
t.Errorf("%s: expected call to fail", tt.desc)
return nil
}
// Set default fields.
tt.want.Query = proto.String("gopher")
tt.want.IndexSpec = &pb.IndexSpec{Name: proto.String("Doc")}
tt.want.CursorType = pb.SearchParams_PER_RESULT.Enum()
tt.want.FieldSpec = &pb.FieldSpec{}
if got := req.Params; !reflect.DeepEqual(got, tt.want) {
t.Errorf("%s: params=%v; want %v", tt.desc, got, tt.want)
}
return noErr // Always return some error to prevent response parsing.
})
it := index.Search(c, "gopher", &SearchOptions{
Facets: tt.facetOpts,
Cursor: tt.cursor,
Offset: tt.offset,
})
_, err := it.Next(nil)
if err == nil {
t.Fatalf("%s: err==nil; should not happen", tt.desc)
}
if err.Error() != tt.wantErr {
t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
}
}
}
func TestFacetRefinements(t *testing.T) {
index, err := Open("Doc")
if err != nil {
t.Fatalf("err from Open: %v", err)
}
noErr := errors.New("") // Sentinel err to return to prevent sending request.
testCases := []struct {
desc string
refine []Facet
want []*pb.FacetRefinement
wantErr string
}{
{
desc: "No refinements",
},
{
desc: "Basic",
refine: []Facet{
{Name: "fur", Value: Atom("fluffy")},
{Name: "age", Value: LessThan(123)},
{Name: "age", Value: AtLeast(0)},
{Name: "legs", Value: Range{Start: 3, End: 5}},
},
want: []*pb.FacetRefinement{
{Name: proto.String("fur"), Value: proto.String("fluffy")},
{Name: proto.String("age"), Range: &pb.FacetRefinement_Range{End: proto.String("1.23e+02")}},
{Name: proto.String("age"), Range: &pb.FacetRefinement_Range{Start: proto.String("0e+00")}},
{Name: proto.String("legs"), Range: &pb.FacetRefinement_Range{Start: proto.String("3e+00"), End: proto.String("5e+00")}},
},
},
{
desc: "Infinite range",
refine: []Facet{
{Name: "age", Value: Range{Start: negInf, End: posInf}},
},
wantErr: `search: refinement for facet "age": either Start or End must be finite`,
},
{
desc: "Bad End value in range",
refine: []Facet{
{Name: "age", Value: LessThan(2147483648)},
},
wantErr: `search: refinement for facet "age": invalid value for End`,
},
{
desc: "Bad Start value in range",
refine: []Facet{
{Name: "age", Value: AtLeast(-2147483649)},
},
wantErr: `search: refinement for facet "age": invalid value for Start`,
},
{
desc: "Unknown value type",
refine: []Facet{
{Name: "age", Value: "you can't use strings!"},
},
wantErr: `search: unsupported refinement for facet "age" of type string`,
},
}
for _, tt := range testCases {
c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
if got := req.Params.FacetRefinement; !reflect.DeepEqual(got, tt.want) {
t.Errorf("%s: params.FacetRefinement=%v; want %v", tt.desc, got, tt.want)
}
return noErr // Always return some error to prevent response parsing.
})
it := index.Search(c, "gopher", &SearchOptions{Refinements: tt.refine})
_, err := it.Next(nil)
if err == nil {
t.Fatalf("%s: err==nil; should not happen", tt.desc)
}
if err.Error() != tt.wantErr {
t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
}
}
}
func TestNamespaceResetting(t *testing.T) {
namec := make(chan *string, 1)
c0 := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(req *pb.IndexDocumentRequest, res *pb.IndexDocumentResponse) error {
namec <- req.Params.IndexSpec.Namespace
return fmt.Errorf("RPC error")
})
// Check that wrapping c0 in a namespace twice works correctly.
c1, err := appengine.Namespace(c0, "A")
if err != nil {
t.Fatalf("appengine.Namespace: %v", err)
}
c2, err := appengine.Namespace(c1, "") // should act as the original context
if err != nil {
t.Fatalf("appengine.Namespace: %v", err)
}
i := (&Index{})
i.Put(c0, "something", &searchDoc)
if ns := <-namec; ns != nil {
t.Errorf(`Put with c0: ns = %q, want nil`, *ns)
}
i.Put(c1, "something", &searchDoc)
if ns := <-namec; ns == nil {
t.Error(`Put with c1: ns = nil, want "A"`)
} else if *ns != "A" {
t.Errorf(`Put with c1: ns = %q, want "A"`, *ns)
}
i.Put(c2, "something", &searchDoc)
if ns := <-namec; ns != nil {
t.Errorf(`Put with c2: ns = %q, want nil`, *ns)
}
}

251
vendor/google.golang.org/appengine/search/struct.go generated vendored Normal file
View File

@@ -0,0 +1,251 @@
// 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
}

View File

@@ -0,0 +1,213 @@
// 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 (
"reflect"
"testing"
)
func TestLoadingStruct(t *testing.T) {
testCases := []struct {
desc string
fields []Field
meta *DocumentMetadata
want interface{}
wantErr bool
}{
{
desc: "Basic struct",
fields: []Field{
{Name: "Name", Value: "Gopher"},
{Name: "Legs", Value: float64(4)},
},
want: &struct {
Name string
Legs float64
}{"Gopher", 4},
},
{
desc: "Struct with tags",
fields: []Field{
{Name: "Name", Value: "Gopher"},
{Name: "about", Value: "Likes slide rules."},
},
meta: &DocumentMetadata{Facets: []Facet{
{Name: "Legs", Value: float64(4)},
{Name: "Fur", Value: Atom("furry")},
}},
want: &struct {
Name string
Info string `search:"about"`
Legs float64 `search:",facet"`
Fuzz Atom `search:"Fur,facet"`
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
},
{
desc: "Bad field from tag",
want: &struct {
AlphaBeta string `search:"αβ"`
}{},
wantErr: true,
},
{
desc: "Ignore missing field",
fields: []Field{
{Name: "Meaning", Value: float64(42)},
},
want: &struct{}{},
wantErr: true,
},
{
desc: "Ignore unsettable field",
fields: []Field{
{Name: "meaning", Value: float64(42)},
},
want: &struct{ meaning float64 }{}, // field not populated.
wantErr: true,
},
{
desc: "Error on missing facet",
meta: &DocumentMetadata{Facets: []Facet{
{Name: "Set", Value: Atom("yes")},
{Name: "Missing", Value: Atom("no")},
}},
want: &struct {
Set Atom `search:",facet"`
}{Atom("yes")},
wantErr: true,
},
{
desc: "Error on unsettable facet",
meta: &DocumentMetadata{Facets: []Facet{
{Name: "Set", Value: Atom("yes")},
{Name: "unset", Value: Atom("no")},
}},
want: &struct {
Set Atom `search:",facet"`
}{Atom("yes")},
wantErr: true,
},
{
desc: "Error setting ignored field",
fields: []Field{
{Name: "Set", Value: "yes"},
{Name: "Ignored", Value: "no"},
},
want: &struct {
Set string
Ignored string `search:"-"`
}{Set: "yes"},
wantErr: true,
},
{
desc: "Error setting ignored facet",
meta: &DocumentMetadata{Facets: []Facet{
{Name: "Set", Value: Atom("yes")},
{Name: "Ignored", Value: Atom("no")},
}},
want: &struct {
Set Atom `search:",facet"`
Ignored Atom `search:"-,facet"`
}{Set: Atom("yes")},
wantErr: true,
},
}
for _, tt := range testCases {
// Make a pointer to an empty version of what want points to.
dst := reflect.New(reflect.TypeOf(tt.want).Elem()).Interface()
err := loadStructWithMeta(dst, tt.fields, tt.meta)
if err != nil != tt.wantErr {
t.Errorf("%s: got err %v; want err %t", tt.desc, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(dst, tt.want) {
t.Errorf("%s: doesn't match\ngot: %v\nwant: %v", tt.desc, dst, tt.want)
}
}
}
func TestSavingStruct(t *testing.T) {
testCases := []struct {
desc string
doc interface{}
wantFields []Field
wantFacets []Facet
}{
{
desc: "Basic struct",
doc: &struct {
Name string
Legs float64
}{"Gopher", 4},
wantFields: []Field{
{Name: "Name", Value: "Gopher"},
{Name: "Legs", Value: float64(4)},
},
},
{
desc: "Struct with tags",
doc: &struct {
Name string
Info string `search:"about"`
Legs float64 `search:",facet"`
Fuzz Atom `search:"Fur,facet"`
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
wantFields: []Field{
{Name: "Name", Value: "Gopher"},
{Name: "about", Value: "Likes slide rules."},
},
wantFacets: []Facet{
{Name: "Legs", Value: float64(4)},
{Name: "Fur", Value: Atom("furry")},
},
},
{
desc: "Ignore unexported struct fields",
doc: &struct {
Name string
info string
Legs float64 `search:",facet"`
fuzz Atom `search:",facet"`
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
wantFields: []Field{
{Name: "Name", Value: "Gopher"},
},
wantFacets: []Facet{
{Name: "Legs", Value: float64(4)},
},
},
{
desc: "Ignore fields marked -",
doc: &struct {
Name string
Info string `search:"-"`
Legs float64 `search:",facet"`
Fuzz Atom `search:"-,facet"`
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
wantFields: []Field{
{Name: "Name", Value: "Gopher"},
},
wantFacets: []Facet{
{Name: "Legs", Value: float64(4)},
},
},
}
for _, tt := range testCases {
fields, meta, err := saveStructWithMeta(tt.doc)
if err != nil {
t.Errorf("%s: got err %v; want nil", tt.desc, err)
continue
}
if !reflect.DeepEqual(fields, tt.wantFields) {
t.Errorf("%s: fields don't match\ngot: %v\nwant: %v", tt.desc, fields, tt.wantFields)
}
if facets := meta.Facets; !reflect.DeepEqual(facets, tt.wantFacets) {
t.Errorf("%s: facets don't match\ngot: %v\nwant: %v", tt.desc, facets, tt.wantFacets)
}
}
}