From ee4f23a9f4086bbb387f76fece904b7d184dec0c Mon Sep 17 00:00:00 2001 From: breel Date: Sat, 25 Jul 2020 13:58:02 -0600 Subject: [PATCH] Make names case insensitive for API --- public/vue/dndex-ui | 2 +- storage/driver/boltdb_test.go | 55 +++++++++++++++++++++++------------ storage/graph.go | 10 ++----- storage/graph_test.go | 5 ++-- storage/operator/filter.go | 31 ++++++++++++++++++++ view/who.go | 4 +-- 6 files changed, 75 insertions(+), 32 deletions(-) diff --git a/public/vue/dndex-ui b/public/vue/dndex-ui index c98901f..6749d9f 160000 --- a/public/vue/dndex-ui +++ b/public/vue/dndex-ui @@ -1 +1 @@ -Subproject commit c98901f929a0c46f6bbf72d8e251c6ad485d81b5 +Subproject commit 6749d9fda2d1022ab24f36dde889dc51629df983 diff --git a/storage/driver/boltdb_test.go b/storage/driver/boltdb_test.go index 2fd7baf..4038252 100644 --- a/storage/driver/boltdb_test.go +++ b/storage/driver/boltdb_test.go @@ -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) } }) } diff --git a/storage/graph.go b/storage/graph.go index e93be2b..609841c 100644 --- a/storage/graph.go +++ b/storage/graph.go @@ -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) } diff --git a/storage/graph_test.go b/storage/graph_test.go index d749d22..0f02009 100644 --- a/storage/graph_test.go +++ b/storage/graph_test.go @@ -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) } diff --git a/storage/operator/filter.go b/storage/operator/filter.go index 0286d3e..8c22e93 100644 --- a/storage/operator/filter.go +++ b/storage/operator/filter.go @@ -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{} diff --git a/view/who.go b/view/who.go index 9b6184d..ef23a7e 100644 --- a/view/who.go +++ b/view/who.go @@ -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 }