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/entity"
"local/dndex/storage/operator" "local/dndex/storage/operator"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
@ -45,40 +46,54 @@ func TestBoltDBCount(t *testing.T) {
} }
for name, filter := range map[string]struct { for name, filter := range map[string]struct {
filter interface{} filter interface{}
match bool matchOne bool
matchMany bool
}{ }{
"one.Query": { "one.Query": {
filter: ones[0].Query(), filter: ones[0].Query(),
match: true, matchOne: true,
}, },
"title:title": { "title:title": {
filter: map[string]interface{}{entity.Title: ones[1].Title}, filter: map[string]interface{}{entity.Title: ones[1].Title},
match: true, matchOne: true,
}, },
"title:title, text:text": { "title:title, text:text": {
filter: map[string]interface{}{entity.Title: ones[2].Title, entity.Text: ones[2].Text}, filter: map[string]interface{}{entity.Title: ones[2].Title, entity.Text: ones[2].Text},
match: true, matchOne: true,
}, },
"title:title, text:gibberish": { "title:title, text:gibberish": {
filter: map[string]interface{}{entity.Title: ones[3].Title, entity.Text: ones[2].Text}, filter: map[string]interface{}{entity.Title: ones[3].Title, entity.Text: ones[2].Text},
match: false,
}, },
"name:$in[gibberish]": { "name:$in[gibberish]": {
filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name + ones[1].Name}), filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name + ones[1].Name}),
match: false,
}, },
"name:$in[name]": { "name:$in[name]": {
filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name}), filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name}),
match: true, matchOne: true,
}, },
"name:$regex[gibberish]": { "name:$regex[gibberish]": {
filter: operator.Regex{Key: entity.Name, Value: ones[3].Name + ones[4].Name}, filter: operator.Regex{Key: entity.Name, Value: ones[3].Name + ones[4].Name},
match: false,
}, },
"name:$regex[name]": { "name:$regex[name]": {
filter: operator.Regex{Key: entity.Name, Value: ones[3].Name}, filter: operator.Regex{Key: entity.Name, Value: ones[3].Name},
match: true, 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 f := filter
@ -87,10 +102,12 @@ func TestBoltDBCount(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if f.match && n != 1 { if f.matchOne && n != 1 {
t.Fatalf("%v results for %+v, want match=%v", n, f, f.match) t.Fatalf("%v results for %+v, want matchOne=%v", n, f, f.matchOne)
} else if !f.match && n != 0 { } else if f.matchMany && n < 2 {
t.Fatalf("%v results for %+v, want match=%v", n, f, f.match) 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 ( import (
"context" "context"
"errors"
"fmt" "fmt"
"local/dndex/config" "local/dndex/config"
"local/dndex/storage/driver" "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) { func (g Graph) ListCaseInsensitive(ctx context.Context, namespace string, from ...string) ([]entity.One, error) {
if len(from) == 0 { filter := operator.CaseInsensitives{Key: entity.Name, Values: from}
return g.List(ctx, namespace)
}
pattern := strings.Join(from, "|")
pattern = "^(?i)(" + pattern + ")"
filter := operator.Regex{Key: entity.Name, Value: pattern}
return g.find(ctx, namespace, filter) 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 { if ones, err := g.ListCaseInsensitive(ctx, namespace, one.Name); err != nil {
return err return err
} else if len(ones) > 0 { } 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) return g.driver.Insert(ctx, namespace, one)
} }

View File

@ -38,6 +38,7 @@ func TestIntegration(t *testing.T) {
randomOne(), randomOne(),
} }
ones[0].Connections = map[string]entity.One{ones[2].Name: entity.One{Name: ones[2].Name, Relationship: ":("}} 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() { cleanFill := func() {
clean() clean()
for i := range ones { for i := range ones {
@ -255,7 +256,7 @@ func TestIntegration(t *testing.T) {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -282,7 +283,7 @@ func TestIntegration(t *testing.T) {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -2,6 +2,7 @@ package operator
import ( import (
"fmt" "fmt"
"strings"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
) )
@ -15,6 +16,36 @@ func (re Regex) MarshalBSON() ([]byte, error) {
return filterMarshal("$regex", re.Key, re.Value) 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 { type FilterIn struct {
Key string Key string
Values []interface{} 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{}) return json.NewEncoder(w).Encode(entity.One{})
} }
if len(ones) > 1 { 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] 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()}) 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 return err
} }