Make names case insensitive for API

master
breel 2020-07-25 13:58:02 -06:00
parent 34075cf19a
commit ee4f23a9f4
6 changed files with 75 additions and 32 deletions

@ -1 +1 @@
Subproject commit c98901f929a0c46f6bbf72d8e251c6ad485d81b5
Subproject commit 6749d9fda2d1022ab24f36dde889dc51629df983

View File

@ -7,6 +7,7 @@ import (
"local/dndex/storage/entity"
"local/dndex/storage/operator"
"os"
"strings"
"testing"
"time"
@ -45,40 +46,54 @@ func TestBoltDBCount(t *testing.T) {
}
for name, filter := range map[string]struct {
filter interface{}
match bool
filter interface{}
matchOne bool
matchMany bool
}{
"one.Query": {
filter: ones[0].Query(),
match: true,
filter: ones[0].Query(),
matchOne: true,
},
"title:title": {
filter: map[string]interface{}{entity.Title: ones[1].Title},
match: true,
filter: map[string]interface{}{entity.Title: ones[1].Title},
matchOne: true,
},
"title:title, text:text": {
filter: map[string]interface{}{entity.Title: ones[2].Title, entity.Text: ones[2].Text},
match: true,
filter: map[string]interface{}{entity.Title: ones[2].Title, entity.Text: ones[2].Text},
matchOne: true,
},
"title:title, text:gibberish": {
filter: map[string]interface{}{entity.Title: ones[3].Title, entity.Text: ones[2].Text},
match: false,
},
"name:$in[gibberish]": {
filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name + ones[1].Name}),
match: false,
},
"name:$in[name]": {
filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name}),
match: true,
filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name}),
matchOne: true,
},
"name:$regex[gibberish]": {
filter: operator.Regex{Key: entity.Name, Value: ones[3].Name + ones[4].Name},
match: false,
},
"name:$regex[name]": {
filter: operator.Regex{Key: entity.Name, Value: ones[3].Name},
match: true,
filter: operator.Regex{Key: entity.Name, Value: ones[3].Name},
matchOne: true,
},
"name:caseInsensitive[]": {
filter: operator.CaseInsensitive{Key: entity.Name, Value: ""},
matchMany: true,
},
"name:caseInsensitive[NAME]": {
filter: operator.CaseInsensitive{Key: entity.Name, Value: strings.ToUpper(ones[3].Name)},
matchOne: true,
},
"name:caseInsensitives[[]{}]": {
filter: operator.CaseInsensitives{Key: entity.Name, Values: []string{}},
matchMany: true,
},
"name:caseInsensitives[[]{NAME}]": {
filter: operator.CaseInsensitives{Key: entity.Name, Values: []string{strings.ToUpper(ones[3].Name)}},
matchOne: true,
},
} {
f := filter
@ -87,10 +102,12 @@ func TestBoltDBCount(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if f.match && n != 1 {
t.Fatalf("%v results for %+v, want match=%v", n, f, f.match)
} else if !f.match && n != 0 {
t.Fatalf("%v results for %+v, want match=%v", n, f, f.match)
if f.matchOne && n != 1 {
t.Fatalf("%v results for %+v, want matchOne=%v", n, f, f.matchOne)
} else if f.matchMany && n < 2 {
t.Fatalf("%v results for %+v, want matchMany=%v", n, f, f.matchMany)
} else if !f.matchOne && !f.matchMany && n != 0 {
t.Fatalf("%v results for %+v, want match=%v", n, f, f.matchOne || f.matchMany)
}
})
}

View File

@ -2,7 +2,6 @@ package storage
import (
"context"
"errors"
"fmt"
"local/dndex/config"
"local/dndex/storage/driver"
@ -36,12 +35,7 @@ func (g Graph) Search(ctx context.Context, namespace string, nameContains string
}
func (g Graph) ListCaseInsensitive(ctx context.Context, namespace string, from ...string) ([]entity.One, error) {
if len(from) == 0 {
return g.List(ctx, namespace)
}
pattern := strings.Join(from, "|")
pattern = "^(?i)(" + pattern + ")"
filter := operator.Regex{Key: entity.Name, Value: pattern}
filter := operator.CaseInsensitives{Key: entity.Name, Values: from}
return g.find(ctx, namespace, filter)
}
@ -74,7 +68,7 @@ func (g Graph) Insert(ctx context.Context, namespace string, one entity.One) err
if ones, err := g.ListCaseInsensitive(ctx, namespace, one.Name); err != nil {
return err
} else if len(ones) > 0 {
return errors.New("collision on primary key when case insensitive")
return fmt.Errorf("collision on primary key when case insensitive: cannot create %q because %+v exists", one.Name, ones)
}
return g.driver.Insert(ctx, namespace, one)
}

View File

@ -38,6 +38,7 @@ func TestIntegration(t *testing.T) {
randomOne(),
}
ones[0].Connections = map[string]entity.One{ones[2].Name: entity.One{Name: ones[2].Name, Relationship: ":("}}
ones[1].Name = ones[0].Name[1 : len(ones[0].Name)-1]
cleanFill := func() {
clean()
for i := range ones {
@ -255,7 +256,7 @@ func TestIntegration(t *testing.T) {
t.Fatal(err)
}
err = graph.Delete(ctx, "col", operator.Regex{Key: entity.Name, Value: "^(?i)" + one.Name})
err = graph.Delete(ctx, "col", operator.CaseInsensitive{Key: entity.Name, Value: one.Name})
if err != nil {
t.Fatal(err)
}
@ -282,7 +283,7 @@ func TestIntegration(t *testing.T) {
t.Fatal(err)
}
err = graph.Delete(ctx, "col", operator.Regex{Key: entity.Name, Value: "^(?i)" + one.Name})
err = graph.Delete(ctx, "col", operator.CaseInsensitive{Key: entity.Name, Value: one.Name})
if err != nil {
t.Fatal(err)
}

View File

@ -2,6 +2,7 @@ package operator
import (
"fmt"
"strings"
"go.mongodb.org/mongo-driver/bson"
)
@ -15,6 +16,36 @@ func (re Regex) MarshalBSON() ([]byte, error) {
return filterMarshal("$regex", re.Key, re.Value)
}
type CaseInsensitives struct {
Key string
Values []string
}
func (cis CaseInsensitives) MarshalBSON() ([]byte, error) {
values := cis.Values
if len(cis.Values) == 0 {
values = []string{".*"}
}
ci := CaseInsensitive{
Key: cis.Key,
Value: fmt.Sprintf("^(%s)$", strings.Join(values, "|")),
}
return bson.Marshal(ci)
}
type CaseInsensitive struct {
Key string
Value string
}
func (ci CaseInsensitive) MarshalBSON() ([]byte, error) {
value := ci.Value
if value == "" {
value = "^$"
}
return bson.Marshal(Regex{Key: ci.Key, Value: "(?i)" + ci.Value})
}
type FilterIn struct {
Key string
Values []interface{}

View File

@ -58,7 +58,7 @@ func whoGet(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Re
return json.NewEncoder(w).Encode(entity.One{})
}
if len(ones) > 1 {
return errors.New("more than one result found matching " + id)
return fmt.Errorf("more than one result found matching %q: %+v", id, ones)
}
one := ones[0]
@ -151,7 +151,7 @@ func whoDelete(namespace string, g storage.Graph, w http.ResponseWriter, r *http
return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
if err := g.Delete(r.Context(), namespace, entity.One{Name: id}); err != nil {
if err := g.Delete(r.Context(), namespace, operator.CaseInsensitive{Key: entity.Name, Value: id}); err != nil {
return err
}