From 586157a8580edd247b283da6fa7dfceb0e5f6891 Mon Sep 17 00:00:00 2001 From: Den Go Date: Wed, 30 Sep 2020 23:45:54 +0300 Subject: [PATCH] added filter list with query parser --- filter_test.go | 51 +++++++++++++++ go.mod | 3 - go.sum | 26 -------- main.go | 174 +++++++++++++++++++++++++++++++------------------ main_test.go | 66 ++++++++++++++++++- utils_test.go | 12 ++++ 6 files changed, 238 insertions(+), 94 deletions(-) create mode 100644 utils_test.go diff --git a/filter_test.go b/filter_test.go index b6933cc..f7fdd87 100644 --- a/filter_test.go +++ b/filter_test.go @@ -1 +1,52 @@ package rqp + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Where(t *testing.T) { + t.Run("ErrUnknownMethod", func(t *testing.T) { + filter := Filter{ + Key: "id[not]", + Name: "id", + Method: NOT, + Or: true, + } + _, err := filter.Where() + assert.Equal(t, err, ErrUnknownMethod) + + filter = Filter{ + Key: "id[fake]", + Name: "id", + Method: "fake", + Or: true, + } + _, err = filter.Where() + assert.Equal(t, err, ErrUnknownMethod) + }) +} + +func Test_Args(t *testing.T) { + t.Run("ErrUnknownMethod", func(t *testing.T) { + filter := Filter{ + Key: "id[not]", + Name: "id", + Method: NOT, + Or: true, + Value: "id", + } + _, err := filter.Args() + assert.Equal(t, err, ErrUnknownMethod) + + filter = Filter{ + Key: "id[fake]", + Name: "id", + Method: "fake", + Or: true, + } + _, err = filter.Args() + assert.Equal(t, err, ErrUnknownMethod) + }) +} diff --git a/go.mod b/go.mod index 4bee335..9ed494b 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,6 @@ module github.com/timsolov/rest-query-parser go 1.13 require ( - github.com/josharian/impl v0.0.0-20191119165012-6b9658ad00c7 // indirect github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.5.1 - golang.org/x/mod v0.3.0 // indirect - golang.org/x/tools v0.0.0-20200515220128-d3bf790afa53 // indirect ) diff --git a/go.sum b/go.sum index a90d810..4516e6a 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,12 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/josharian/impl v0.0.0-20191119165012-6b9658ad00c7 h1:dhk1N6iuFDo1Lcew31sAQxrh8GVWaw0xu0MWNnMc6ao= -github.com/josharian/impl v0.0.0-20191119165012-6b9658ad00c7/go.mod h1:t4Tr0tn92eq5ISef4cS5plFAMYAqZlAXtgUcKE6y8nw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200515220128-d3bf790afa53 h1:vmsb6v0zUdmUlXfwKaYrHPPRCV0lHq/IwNIf0ASGjyQ= -golang.org/x/tools v0.0.0-20200515220128-d3bf790afa53/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/main.go b/main.go index 6192696..0fb7f7b 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ type Query struct { Offset int Limit int Sorts []Sort - Filters []*Filter + Filters [][]*Filter delimiterIN string delimiterOR string @@ -219,9 +219,11 @@ func (q *Query) AddSortBy(by string, desc bool) *Query { // HaveFilter returns true if request contains some filter func (q *Query) HaveFilter(name string) bool { - for _, v := range q.Filters { - if v.Name == name { - return true + for _, filters := range q.Filters { + for _, v := range filters { + if v.Name == name { + return true + } } } @@ -230,10 +232,12 @@ func (q *Query) HaveFilter(name string) bool { // AddFilter adds a filter to Query func (q *Query) AddFilter(name string, m Method, value interface{}) *Query { - q.Filters = append(q.Filters, &Filter{ - Name: name, - Method: m, - Value: value, + q.Filters = append(q.Filters, []*Filter{ + { + Name: name, + Method: m, + Value: value, + }, }) return q } @@ -241,16 +245,18 @@ func (q *Query) AddFilter(name string, m Method, value interface{}) *Query { // RemoveFilter removes the filter by name func (q *Query) RemoveFilter(name string) error { - for i, v := range q.Filters { - if v.Name == name { - // safe remove element from slice - if i < len(q.Filters)-1 { - copy(q.Filters[i:], q.Filters[i+1:]) - } - q.Filters[len(q.Filters)-1] = nil - q.Filters = q.Filters[:len(q.Filters)-1] + for key, filters := range q.Filters { + for i, v := range filters { + if v.Name == name { + // safe remove element from slice + if i < len(q.Filters[key])-1 { + copy(q.Filters[key][i:], q.Filters[key][i+1:]) + } + q.Filters[key][len(q.Filters[key])-1] = nil + q.Filters[key] = q.Filters[key][:len(q.Filters[key])-1] - return nil + return nil + } } } @@ -289,9 +295,11 @@ func (q *Query) RemoveValidation(NameAndOrTags string) error { // GetFilter returns filter by name func (q *Query) GetFilter(name string) (*Filter, error) { - for _, v := range q.Filters { - if v.Name == name { - return v, nil + for _, filters := range q.Filters { + for _, v := range filters { + if v.Name == name { + return v, nil + } } } @@ -313,9 +321,11 @@ type Replacer map[string]string func (q *Query) ReplaceNames(r Replacer) { for name, newname := range r { - for i, v := range q.Filters { - if v.Name == name { - q.Filters[i].Name = newname + for key, filters := range q.Filters { + for i, v := range filters { + if v.Name == name { + q.Filters[key][i].Name = newname + } } } for i, v := range q.Fields { @@ -340,55 +350,74 @@ func (q *Query) Where() string { return "" } + var whereList []string + var where string - var OR bool = false + var OR bool - for i := 0; i < len(q.Filters); i++ { - filter := q.Filters[i] + for key := range q.Filters { + where = "" + OR = false + if len(q.Filters[key]) == 0 { + continue + } + for i := 0; i < len(q.Filters[key]); i++ { + filter := q.Filters[key][i] - prefix := "" - suffix := "" + prefix := "" + suffix := "" - if filter.Or && !OR { - if i == 0 { - prefix = "(" + if filter.Or && !OR { + if i == 0 { + prefix = "(" + } else { + prefix = " AND (" + } + OR = true + } else if filter.Or && OR { + prefix = " OR " + // if last element of next element not OR method + if i+1 == len(q.Filters[key]) || (i+1 < len(q.Filters[key]) && !q.Filters[key][i+1].Or) { + suffix = ")" + OR = false + } } else { - prefix = " AND (" - } - OR = true - } else if filter.Or && OR { - prefix = " OR " - // if last element of next element not OR method - if i+1 == len(q.Filters) || (i+1 < len(q.Filters) && !q.Filters[i+1].Or) { - suffix = ")" - OR = false + if i > 0 { + prefix = " AND " + } } - } else { - if i > 0 { - prefix = " AND " + + if a, err := filter.Where(); err == nil { + where += fmt.Sprintf("%s%s%s", prefix, a, suffix) + } else { + continue } } - - if a, err := filter.Where(); err == nil { - where += fmt.Sprintf("%s%s%s", prefix, a, suffix) - } else { - continue + if where != "" { + whereList = append(whereList, where) } + } + if len(whereList) == 0 { + return "" } - return where + return strings.Join(whereList, " AND ") } // WHERE returns list of filters for WHERE SQL statement with `WHERE` word // return example: `WHERE id > 0 AND email LIKE 'some@email.com'` func (q *Query) WHERE() string { - if len(q.Filters) == 0 { return "" } - return " WHERE " + q.Where() + where := q.Where() + if where == "" { + return "" + } + + return " WHERE " + where } // Args returns slice of arguments for WHERE statement @@ -400,14 +429,20 @@ func (q *Query) Args() []interface{} { return args } - for i := 0; i < len(q.Filters); i++ { - filter := q.Filters[i] - - if a, err := filter.Args(); err == nil { - args = append(args, a...) - } else { + for key := range q.Filters { + if len(q.Filters[key]) == 0 { continue } + + for i := 0; i < len(q.Filters[key]); i++ { + filter := q.Filters[key][i] + + if a, err := filter.Args(); err == nil { + args = append(args, a...) + } else { + continue + } + } } return args @@ -503,11 +538,14 @@ func (q *Query) Parse() (err error) { err = q.parseSort(values, q.validations[low]) delete(requiredNames, low) default: - if len(values) != 1 { + if len(values) == 0 { return errors.Wrap(ErrBadFormat, key) } - if err = q.parseFilter(key, values[0]); err != nil { - return err + + for i := 0; i < len(values); i++ { + if err = q.parseFilter(key, values[i]); err != nil { + return err + } } } @@ -576,6 +614,8 @@ func (q *Query) parseFilter(key, value string) error { return errors.Wrap(ErrEmptyValue, key) } + filters := make([]*Filter, 0) + if strings.Contains(value, q.delimiterOR) { // OR multiple filter parts := strings.Split(value, q.delimiterOR) for i, v := range parts { @@ -609,7 +649,7 @@ func (q *Query) parseFilter(key, value string) error { // set OR filter.Or = true - q.Filters = append(q.Filters, filter) + filters = append(filters, filter) } } else { // Single filter filter, err := newFilter(key, value, q.delimiterIN, q.validations) @@ -623,20 +663,26 @@ func (q *Query) parseFilter(key, value string) error { return errors.Wrap(err, key) } - q.Filters = append(q.Filters, filter) + filters = append(filters, filter) } + q.Filters = append(q.Filters, filters) + return nil } // clean the filters slice func (q *Query) cleanFilters() { if len(q.Filters) > 0 { - for i := range q.Filters { - q.Filters[i] = nil + for key := range q.Filters { + for i := range q.Filters[key] { + q.Filters[key][i] = nil + } + q.Filters[key] = nil } q.Filters = nil } + q.Filters = make([][]*Filter, 0) } func (q *Query) parseSort(value []string, validate ValidationFunc) error { diff --git a/main_test.go b/main_test.go index 0c2ba96..7982f29 100644 --- a/main_test.go +++ b/main_test.go @@ -9,6 +9,57 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSetDelimiterOR(t *testing.T) { + q := New() + q.SetDelimiterOR("!") + assert.Equal(t, q.delimiterOR, "!") +} + +func TestSelect(t *testing.T) { + q := New() + assert.Equal(t, q.Select(), "*") + + q.AddField("test1") + q.AddField("test2") + assert.Equal(t, q.Select(), "test1, test2") +} + +func TestSELECT(t *testing.T) { + q := New() + assert.Equal(t, q.SELECT(), "SELECT *") + + q.AddField("test1") + q.AddField("test2") + assert.Equal(t, q.SELECT(), "SELECT test1, test2") +} + +func TestOrder(t *testing.T) { + q := New() + assert.Equal(t, q.Order(), "") +} + +func TestHaveSortBy(t *testing.T) { + q := New() + assert.Equal(t, q.HaveSortBy("fake"), false) +} + +func TestRemoveFilter(t *testing.T) { + q := New() + q.AddFilter("id", ILIKE, "id") + q.AddFilter("test", ILIKE, "test") + q.AddFilter("test2", ILIKE, "test2") + assert.NoError(t, q.RemoveFilter("test")) +} + +func TestGetFilter(t *testing.T) { + q := New() + q.AddFilter("id", ILIKE, "id") + q.AddFilter("test", ILIKE, "test") + q.AddFilter("test2", ILIKE, "test2") + _, err := q.GetFilter("test") + assert.NoError(t, err) +} + func TestFields(t *testing.T) { // mockValidation := func(value interface{}) error { return nil } @@ -178,7 +229,7 @@ func TestWhere(t *testing.T) { {url: "?u[eq]=1,2", expected: "", err: "u[eq]: method are not allowed"}, {url: "?u[gt]=1", expected: "", err: "u[gt]: method are not allowed"}, {url: "?id[in]=1,2", expected: " WHERE id IN (?, ?)"}, - {url: "?id[eq]=1&id[eq]=4", err: "id[eq]: bad format"}, + {url: "?id[eq]=1&id[eq]=4", expected: " WHERE id = ? AND id = ?"}, {url: "?id[gte]=1&id[lte]=4", expected: " WHERE id >= ? AND id <= ?", expected2: " WHERE id <= ? AND id >= ?"}, {url: "?id[gte]=1|id[lte]=4", expected: " WHERE (id >= ? OR id <= ?)", expected2: " WHERE (id <= ? OR id >= ?)"}, {url: "?u[not]=NULL", expected: " WHERE u IS NOT NULL"}, @@ -255,6 +306,19 @@ func TestWhere2(t *testing.T) { assert.EqualError(t, q.Parse(), "u[like]: empty value") } +func TestWhere3(t *testing.T) { + q := NewQV(nil, Validations{ + "test1": nil, + "test2": nil, + }) + URL, err := url.Parse("?test1[eq]=test10|test2[eq]=test20&test1[eq]=test11|test2[eq]=test21") + assert.NoError(t, err) + assert.NoError(t, q.SetUrlQuery(URL.Query()).Error) + assert.NoError(t, q.Parse()) + where := q.Where() + assert.Equal(t, where, "(test1 = ? OR test2 = ?) AND (test1 = ? OR test2 = ?)") +} + func TestArgs(t *testing.T) { q := New() q.SetDelimiterIN("!") diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..b5ec70f --- /dev/null +++ b/utils_test.go @@ -0,0 +1,12 @@ +package rqp + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_stringInSlice(t *testing.T) { + t.Run("RETURN FALSE", func(t *testing.T) { + assert.Equal(t, false, stringInSlice("", nil)) + }) +}