├── .github └── dependabot.yml ├── types.go ├── go.mod ├── params.go ├── README.md ├── parse.go ├── limit.go ├── LICENSE ├── operator.go ├── sort.go ├── allow.go ├── reflect.go ├── rsql.go ├── go.sum ├── lexer.go ├── rsql_test.go └── filter.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | 10 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | // Strings : 4 | type Strings []string 5 | 6 | // IndexOf : 7 | func (slice Strings) IndexOf(search string) (idx int) { 8 | idx = -1 9 | length := len(slice) 10 | for i := 0; i < length; i++ { 11 | if slice[i] == search { 12 | idx = i 13 | break 14 | } 15 | } 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/si3nloong/go-rsql 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/kr/pretty v0.1.0 // indirect 7 | github.com/stretchr/testify v1.4.1-0.20190904163530-85f2b59c4459 8 | github.com/timtadh/data-structures v0.5.3 // indirect 9 | github.com/timtadh/lexmachine v0.2.3-0.20191122170559-2474ad5d8313 10 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /params.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | // Params : 4 | type Params struct { 5 | Selects []string 6 | Filters Filters 7 | Sorts []Sort 8 | Limit uint 9 | Offset uint 10 | Cursor string 11 | } 12 | 13 | type Filters []Filter 14 | 15 | func (fs Filters) Lookup(key string) (val interface{}, ok bool) { 16 | for _, f := range fs { 17 | if key == f.Name { 18 | val = f.Value 19 | ok = true 20 | return 21 | } 22 | } 23 | ok = false 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go RSQL 2 | 3 | ## 🔨 Installation 4 | 5 | ```console 6 | go get github.com/si3nloong/go-rsql 7 | ``` 8 | 9 | ```go 10 | 11 | type QueryParams struct { 12 | Name string `rsql:"n,filter,sort,allow=eq|gt|gte"` 13 | Status string `rsql:"status,filter"` 14 | PtrStr *string `rsql:"text,filter"` 15 | No int `rsql:"no,column=No2,filter"` 16 | } 17 | 18 | func main() { 19 | p := MustNew(i) 20 | 21 | params, err := p.ParseQuery(`filter=status=eq="111";no=gt=1991;text==null&sort=status,-no`) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | log.Println(params.Filters) 27 | log.Println(params.Sorts) 28 | } 29 | ``` 30 | 31 | ## 📄 License 32 | 33 | [MIT](https://github.com/si3nloong/go-rsql/blob/master/LICENSE) 34 | 35 | Copyright (c) 2020-present, SianLoong Lee -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | func parseRawQuery(m map[string]string, query string) (err error) { 9 | for query != "" { 10 | key := query 11 | if i := strings.IndexAny(key, "&"); i >= 0 { 12 | key, query = key[:i], key[i+1:] 13 | } else { 14 | query = "" 15 | } 16 | if key == "" { 17 | continue 18 | } 19 | value := "" 20 | if i := strings.Index(key, "="); i >= 0 { 21 | key, value = key[:i], key[i+1:] 22 | } 23 | key, err1 := url.QueryUnescape(key) 24 | if err1 != nil { 25 | if err == nil { 26 | err = err1 27 | } 28 | continue 29 | } 30 | value, err1 = url.QueryUnescape(value) 31 | if err1 != nil { 32 | if err == nil { 33 | err = err1 34 | } 35 | continue 36 | } 37 | // m[key] = append(m[key], value) 38 | m[key] = value 39 | } 40 | return err 41 | } 42 | -------------------------------------------------------------------------------- /limit.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func (p *RSQL) parseLimit(values map[string]string, params *Params) error { 9 | val, ok := values[p.LimitTag] 10 | params.Limit = p.DefaultLimit 11 | delete(values, p.LimitTag) 12 | if !ok || len(val) < 1 { 13 | return nil 14 | } 15 | 16 | u64, err := strconv.ParseUint(val, 10, 64) 17 | if err != nil { 18 | return err 19 | } 20 | if u64 > uint64(maxUint) { 21 | return fmt.Errorf("rsql: overflow unsigned integer, %d", u64) 22 | } 23 | params.Limit = uint(u64) 24 | return nil 25 | } 26 | 27 | func (p *RSQL) parseOffset(values map[string]string, params *Params) error { 28 | val, ok := values[p.PageTag] 29 | delete(values, p.PageTag) 30 | if !ok || len(val) < 1 { 31 | return nil 32 | } 33 | 34 | u64, err := strconv.ParseUint(val, 10, 64) 35 | if err != nil { 36 | return err 37 | } 38 | if u64 > uint64(maxUint) { 39 | return fmt.Errorf("rsql: overflow unsigned integer, %d", u64) 40 | } 41 | if u64 > 0 { 42 | params.Offset = uint(u64-1) * (params.Limit) 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SianLoong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /operator.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | // Expr : 4 | type Expr int 5 | 6 | // types : 7 | const ( 8 | Equal Expr = iota 9 | NotEqual 10 | LesserThan 11 | LesserOrEqual 12 | GreaterThan 13 | GreaterOrEqual 14 | In 15 | NotIn 16 | Like 17 | NotLike 18 | ) 19 | 20 | func (e Expr) String() string { 21 | switch e { 22 | case Equal: 23 | return "eq" 24 | case NotEqual: 25 | return "ne" 26 | case GreaterThan: 27 | return "gt" 28 | case GreaterOrEqual: 29 | return "gte" 30 | case LesserThan: 31 | return "lt" 32 | case LesserOrEqual: 33 | return "lte" 34 | case In: 35 | return "in" 36 | case NotIn: 37 | return "notIn" 38 | case Like: 39 | return "like" 40 | case NotLike: 41 | return "notLike" 42 | default: 43 | return "unknown" 44 | } 45 | } 46 | 47 | var operators = map[string]Expr{ 48 | "==": Equal, 49 | "=eq=": Equal, 50 | "!=": NotEqual, 51 | "=ne=": NotEqual, 52 | ">": GreaterThan, 53 | "=gt=": GreaterThan, 54 | ">=": GreaterOrEqual, 55 | "=gte=": GreaterOrEqual, 56 | "<": LesserThan, 57 | "=lt=": LesserThan, 58 | "<=": LesserOrEqual, 59 | "=lte=": LesserOrEqual, 60 | "=like=": Like, 61 | "=nlike=": NotLike, 62 | "=in=": In, 63 | "=nin=": NotIn, 64 | } 65 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | type Direction int 11 | 12 | const ( 13 | Asc Direction = iota 14 | Desc 15 | ) 16 | 17 | // Sort : 18 | type Sort struct { 19 | Field string 20 | Direction Direction 21 | } 22 | 23 | func (p *RSQL) parseSort(values map[string]string, params *Params) error { 24 | val, ok := values[p.SortTag] 25 | delete(values, p.SortTag) 26 | if !ok || len(val) < 1 { 27 | return nil 28 | } 29 | 30 | paths := strings.Split(val, ",") 31 | for _, v := range paths { 32 | v = strings.TrimSpace(v) 33 | if len(v) == 0 { 34 | return errors.New("rsql: invalid sort") 35 | } 36 | 37 | v, err := url.QueryUnescape(v) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | dir := Asc 43 | desc := v[0] == '-' 44 | if desc { 45 | v = v[1:] 46 | dir = Desc 47 | } 48 | 49 | f, ok := p.codec.Names[v] 50 | if !ok { 51 | return fmt.Errorf("rsql: invalid field %q to sort", v) 52 | } 53 | 54 | if _, ok := f.Tag.Lookup("sort"); !ok { 55 | return fmt.Errorf("rsql: field %q is not allow to sort", v) 56 | } 57 | 58 | name := f.Name 59 | if v, ok := f.Tag.Lookup("column"); ok { 60 | name = v 61 | } 62 | 63 | params.Sorts = append(params.Sorts, Sort{ 64 | Field: name, 65 | Direction: dir, 66 | }) 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /allow.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "reflect" 5 | "time" 6 | ) 7 | 8 | var ( 9 | allowNums = []string{ 10 | "eq", "ne", 11 | "gt", "gte", "lt", "lte", 12 | "in", "notIn", 13 | "between", 14 | } 15 | 16 | allowOperators = map[interface{}][]string{ 17 | reflect.String: {"eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "like", "notLike"}, 18 | reflect.Bool: {"eq", "ne"}, 19 | reflect.Int: allowNums, 20 | reflect.Int8: allowNums, 21 | reflect.Int16: allowNums, 22 | reflect.Int32: allowNums, 23 | reflect.Int64: allowNums, 24 | reflect.Uint: allowNums, 25 | reflect.Uint8: allowNums, 26 | reflect.Uint16: allowNums, 27 | reflect.Uint32: allowNums, 28 | reflect.Uint64: allowNums, 29 | reflect.Float32: allowNums, 30 | reflect.Float64: allowNums, 31 | reflect.TypeOf([]byte{}): {"eq", "ne", "like", "notLike"}, 32 | reflect.TypeOf(time.Time{}): {"eq", "ne", "gt", "gte", "lt", "lte"}, 33 | } 34 | ) 35 | 36 | func getAllows(t reflect.Type) []string { 37 | t = indirect(t) 38 | v, ok := allowOperators[t] 39 | if ok { 40 | return v 41 | } 42 | return allowOperators[t.Kind()] 43 | } 44 | 45 | func indirect(t reflect.Type) reflect.Type { 46 | for t.Kind() == reflect.Ptr { 47 | t = t.Elem() 48 | } 49 | return t 50 | } 51 | -------------------------------------------------------------------------------- /reflect.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | // StructField : 9 | type StructField struct { 10 | Name string 11 | Tag *StructTag 12 | Type reflect.Type 13 | } 14 | 15 | // StructTag : 16 | type StructTag struct { 17 | name string 18 | values map[string]string 19 | } 20 | 21 | // Struct : 22 | type Struct struct { 23 | Fields []*StructField 24 | Names map[string]*StructField 25 | } 26 | 27 | // NewTag : 28 | func NewTag(name string, tag reflect.StructTag) *StructTag { 29 | paths := strings.Split(tag.Get(name), ",") 30 | t := new(StructTag) 31 | t.name = paths[0] 32 | t.values = make(map[string]string) 33 | for _, v := range paths[1:] { 34 | p := strings.SplitN(v, "=", 2) 35 | t.values[p[0]] = "" 36 | if len(p) > 1 { 37 | t.values[p[0]] = p[1] 38 | } 39 | } 40 | return t 41 | } 42 | 43 | func (t StructTag) Lookup(key string) (value string, ok bool) { 44 | value, ok = t.values[key] 45 | return 46 | } 47 | 48 | func getCodec(t reflect.Type) *Struct { 49 | fields := make([]*StructField, 0) 50 | codec := new(Struct) 51 | for i := 0; i < t.NumField(); i++ { 52 | fv := t.Field(i) 53 | 54 | tag := NewTag("rsql", fv.Tag) 55 | f := new(StructField) 56 | f.Name = fv.Name 57 | f.Tag = tag 58 | if tag.name != "" { 59 | f.Name = tag.name 60 | } 61 | f.Type = fv.Type 62 | 63 | fields = append(fields, f) 64 | } 65 | 66 | codec.Fields = fields 67 | codec.Names = make(map[string]*StructField) 68 | for _, f := range fields { 69 | codec.Names[f.Name] = f 70 | } 71 | return codec 72 | } 73 | -------------------------------------------------------------------------------- /rsql.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/timtadh/lexmachine" 7 | ) 8 | 9 | const ( 10 | defaultLimit = uint(50) 11 | // defaultMaxLimit = uint(100) 12 | maxUint = ^uint(0) 13 | ) 14 | 15 | // FormatFunc : 16 | type FormatFunc func(string) string 17 | 18 | // RSQL : 19 | type RSQL struct { 20 | SelectTag string 21 | FilterTag string 22 | SortTag string 23 | LimitTag string 24 | PageTag string 25 | codec *Struct 26 | lexer *lexmachine.Lexer 27 | FormatColumn FormatFunc 28 | DefaultLimit uint 29 | MaxLimit uint 30 | } 31 | 32 | // New : 33 | func New(src interface{}) (*RSQL, error) { 34 | v := reflect.ValueOf(src) 35 | codec := getCodec(v.Type()) 36 | lexer := lexmachine.NewLexer() 37 | dl := newDefaultTokenLexer() 38 | dl.addActions(lexer) 39 | 40 | p := new(RSQL) 41 | p.lexer = lexer 42 | p.FilterTag = "filter" 43 | p.SortTag = "sort" 44 | p.LimitTag = "limit" 45 | p.PageTag = "page" 46 | p.DefaultLimit = defaultLimit 47 | p.codec = codec 48 | return p, nil 49 | } 50 | 51 | // MustNew : 52 | func MustNew(src interface{}) *RSQL { 53 | p, err := New(src) 54 | if err != nil { 55 | panic(err) 56 | } 57 | return p 58 | } 59 | 60 | // ParseQuery : 61 | func (p *RSQL) ParseQuery(query string) (*Params, error) { 62 | return p.ParseQueryBytes([]byte(query)) 63 | } 64 | 65 | // ParseQueryBytes : 66 | func (p *RSQL) ParseQueryBytes(query []byte) (*Params, error) { 67 | values := make(map[string]string) 68 | if err := parseRawQuery(values, string(query)); err != nil { 69 | return nil, err 70 | } 71 | 72 | var ( 73 | params = new(Params) 74 | // errs = make(Errors, 0) 75 | ) 76 | 77 | if err := p.parseFilter(values, params); err != nil { 78 | return nil, err 79 | } 80 | if err := p.parseSort(values, params); err != nil { 81 | return nil, err 82 | } 83 | if err := p.parseLimit(values, params); err != nil { 84 | return nil, err 85 | } 86 | if err := p.parseOffset(values, params); err != nil { 87 | return nil, err 88 | } 89 | 90 | return params, nil 91 | } 92 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 4 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.4.1-0.20190904163530-85f2b59c4459 h1:QxRTTI3zEzHS/l6/nGubGnZYZ/7C/HeWOKT36oFSrgM= 12 | github.com/stretchr/testify v1.4.1-0.20190904163530-85f2b59c4459/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 13 | github.com/timtadh/data-structures v0.5.2/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU= 14 | github.com/timtadh/data-structures v0.5.3 h1:F2tEjoG9qWIyUjbvXVgJqEOGJPMIiYn7U5W5mE+i/vQ= 15 | github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU= 16 | github.com/timtadh/getopt v1.0.0/go.mod h1:L3EL6YN2G0eIAhYBo9b7SB9d/kEQmdnwthIlMJfj210= 17 | github.com/timtadh/lexmachine v0.2.3-0.20191122170559-2474ad5d8313 h1:bwwHP6SOlxj+5F6+eVoXSK7p0OJpE+0Zghx4BcdON8A= 18 | github.com/timtadh/lexmachine v0.2.3-0.20191122170559-2474ad5d8313/go.mod h1:An87z74QsP8eXNi0u+/7uJp24zHN0jSlvLZl/TXM1KE= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 23 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 24 | -------------------------------------------------------------------------------- /lexer.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | 7 | "github.com/timtadh/lexmachine" 8 | "github.com/timtadh/lexmachine/machines" 9 | ) 10 | 11 | // types : 12 | const ( 13 | Expression = iota 14 | String 15 | Or 16 | And 17 | Numeric 18 | Text 19 | Group 20 | Whitespace 21 | ) 22 | 23 | type Token struct { 24 | Type int 25 | Value string 26 | Lexeme []byte 27 | TC int 28 | StartLine int 29 | StartColumn int 30 | EndLine int 31 | EndColumn int 32 | } 33 | 34 | type defaultTokenLexer struct { 35 | ids map[string]int 36 | lexer *lexmachine.Lexer 37 | } 38 | 39 | func newDefaultTokenLexer() *defaultTokenLexer { 40 | return &defaultTokenLexer{ 41 | ids: map[string]int{ 42 | "whitespace": Whitespace, 43 | "grouping": Group, 44 | "string": String, 45 | "text": Text, 46 | "numeric": Numeric, 47 | "and": And, 48 | "or": Or, 49 | "operator": Expression, 50 | }, 51 | } 52 | } 53 | 54 | func (l *defaultTokenLexer) addActions(lexer *lexmachine.Lexer) { 55 | b := new(bytes.Buffer) 56 | b.WriteByte('(') 57 | for k := range operators { 58 | b.WriteString(escapeStr(k)) 59 | b.WriteByte('|') 60 | } 61 | b.Truncate(b.Len() - 1) 62 | b.WriteByte(')') 63 | 64 | lexer.Add([]byte(`\s`), l.token("whitespace")) 65 | lexer.Add([]byte(`\(|\)`), l.token("grouping")) 66 | lexer.Add([]byte(`\"(\\.|[^\"])*\"`), l.token("string")) 67 | lexer.Add([]byte(`\'(\\.|[^\'])*\'`), l.token("string")) 68 | lexer.Add([]byte(`(\,|or)`), l.token("or")) 69 | lexer.Add([]byte(`(\;|and)`), l.token("and")) 70 | lexer.Add([]byte(`(\-)?([0-9]*\.[0-9]+|[0-9]+)`), l.token("numeric")) 71 | lexer.Add([]byte(`[a-zA-Z0-9\_\.\%]+`), l.token("text")) 72 | lexer.Add(b.Bytes(), l.token("operator")) 73 | l.lexer = lexer 74 | } 75 | 76 | func (l *defaultTokenLexer) token(name string) lexmachine.Action { 77 | return func(s *lexmachine.Scanner, m *machines.Match) (interface{}, error) { 78 | return &Token{ 79 | Type: l.ids[name], 80 | Value: string(m.Bytes), 81 | Lexeme: m.Bytes, 82 | TC: m.TC, 83 | StartLine: m.StartLine, 84 | StartColumn: m.StartColumn, 85 | EndLine: m.EndLine, 86 | EndColumn: m.EndColumn, 87 | }, nil 88 | } 89 | } 90 | 91 | func escapeStr(str string) string { 92 | length := len(str) 93 | blr := new(strings.Builder) 94 | for i := 0; i < length; i++ { 95 | if (str[i] >= 'a' && str[i] <= 'z') || 96 | (str[i] >= 'A' && str[i] <= 'Z') || 97 | (str[i] >= '0' && str[i] <= '9') { 98 | blr.WriteByte(str[i]) 99 | } else { 100 | blr.WriteByte('\\') 101 | blr.WriteByte(str[i]) 102 | } 103 | } 104 | return blr.String() 105 | } 106 | -------------------------------------------------------------------------------- /rsql_test.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type CustomInt int 11 | 12 | // TestRSQL : 13 | func TestRSQL(t *testing.T) { 14 | { 15 | var i struct { 16 | Name string `rsql:"n,filter,sort,allow=eq|gt|gte"` 17 | Status string `rsql:"status,filter,sort"` 18 | PtrStr *string `rsql:"text,filter,sort"` 19 | No int `rsql:"no,filter,sort,column=No2"` 20 | Int CustomInt `rsql:"int,filter"` 21 | SubmittedAt time.Time `rsql:"submittedAt,filter"` 22 | CreatedAt time.Time `rsql:"createdAt,sort"` 23 | } 24 | 25 | p := MustNew(i) 26 | param, err := p.ParseQuery(`filter=int>10;status=eq="111";no=gt=1991;text==null&sort=status,-no&limit=100&page=2`) 27 | require.NoError(t, err) 28 | require.NotNil(t, param) 29 | require.True(t, len(param.Filters) > 0) 30 | require.True(t, len(param.Sorts) > 0) 31 | require.Equal(t, uint(100), param.Limit) 32 | 33 | param, err = p.ParseQuery(`filter=(int>10;status=eq="111";no=gt=1991;text==null)&sort=status,-no&limit=100`) 34 | require.NoError(t, err) 35 | require.NotNil(t, param) 36 | 37 | param, err = p.ParseQuery(`filter=(status=="APPROVED";submittedAt>="2019-12-22T16:00:00Z";submittedAt<="2019-12-31T15:59:59Z")&sort=-createdAt&limit=10`) 38 | require.NoError(t, err) 39 | require.NotNil(t, param) 40 | // log.Println("Filters :", param.Filters) 41 | 42 | param, err = p.ParseQuery(`filter=(submittedAt>='2019-12-22T16:00:00Z';submittedAt<='2019-12-31T15:59:59Z';status=="APPROVED")&limit=100`) 43 | require.NoError(t, err) 44 | require.NotNil(t, param) 45 | // log.Println("Filters :", param.Filters) 46 | } 47 | 48 | { 49 | var i struct { 50 | Flag bool `rsql:"flag,filter"` 51 | Status string `rsql:"status,filter,sort,allow=eq|gt|gte"` 52 | SubmittedAt time.Time `rsql:"submittedAt,filter,sort"` 53 | } 54 | p := MustNew(i) 55 | param, err := p.ParseQuery(`filter=status=eq="approved";flag=ne=false;submittedAt>="2019-12-22T16:00:00Z";submittedAt<="2019-12-31T15:59:59Z"&sort=status&limit=10`) 56 | require.NoError(t, err) 57 | require.NotNil(t, param) 58 | } 59 | 60 | { 61 | var i struct { 62 | Title string `rsql:"title,filter"` 63 | Audience string `rsql:"audience,filter"` 64 | Status string `rsql:"status,filter,sort,allow=eq|gt|gte"` 65 | ScheduleDateTime time.Time `rsql:"scheduleDateTime,filter,sort"` 66 | } 67 | p := MustNew(i) 68 | param, err := p.ParseQuery(`filter=(audience=="CUSTOMIZED";status=="PENDING";scheduleDateTime>='2020-01-14T16:00:00Z';scheduleDateTime<='2020-01-20T15:59:59Z';title=like="testing%25")`) 69 | require.NoError(t, err) 70 | require.NotNil(t, param) 71 | } 72 | 73 | { 74 | var i struct { 75 | Name string `rsql:"name,sort"` 76 | Status string `rsql:"status,filter,sort,allow=eq|gt|gte"` 77 | ScheduleDateTime time.Time `rsql:"scheduleDateTime,filter,sort"` 78 | } 79 | 80 | p := MustNew(i) 81 | query := `filter=&sort=name,-status&limit=10&page=2` 82 | param, err := p.ParseQuery(query) 83 | require.NoError(t, err) 84 | require.Equal(t, uint(10), param.Offset) 85 | 86 | /* 87 | actions.Find(). 88 | From("table"). 89 | Where( 90 | expr.Equal("key", "value"), 91 | ). 92 | OrderBy( 93 | expr.Asc("F1"), 94 | expr.Asc("F2"), 95 | expr.Desc("Status"), 96 | ). 97 | Limit(10). 98 | Offset(2 * 10) 99 | */ 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package rsql 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/timtadh/lexmachine" 14 | ) 15 | 16 | var ( 17 | typeOfTime = reflect.TypeOf(time.Time{}) 18 | typeOfByte = reflect.TypeOf([]byte(nil)) 19 | ) 20 | 21 | // Filter : 22 | type Filter struct { 23 | Name string 24 | Operator Expr 25 | Value interface{} 26 | } 27 | 28 | func (p *RSQL) parseFilter(values map[string]string, params *Params) error { 29 | val, ok := values[p.FilterTag] 30 | if !ok || len(val) < 1 { 31 | return nil 32 | } 33 | 34 | scan, err := p.lexer.Scanner([]byte(val)) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | loop: 40 | for { 41 | tkn1, err := nextToken(scan) 42 | if err != nil { 43 | if err == io.EOF { 44 | break loop 45 | } 46 | return err 47 | } 48 | 49 | switch tkn1.Value { 50 | case "(", ")": 51 | continue 52 | } 53 | 54 | f, ok := p.codec.Names[tkn1.Value] 55 | if !ok { 56 | return fmt.Errorf("rsql: invalid field %q to filter", tkn1.Value) 57 | } 58 | 59 | if _, ok := f.Tag.Lookup("filter"); !ok { 60 | return fmt.Errorf("rsql: field %q is not allow to filter", tkn1.Value) 61 | } 62 | 63 | name := tkn1.Value 64 | if v, ok := f.Tag.Lookup("column"); ok { 65 | name = v 66 | } 67 | 68 | allows := getAllows(f.Type) 69 | if v, ok := f.Tag.Lookup("allow"); ok { 70 | allows = strings.Split(v, "|") 71 | } 72 | 73 | tkn2, err := nextToken(scan) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | op := operators[tkn2.Value] 79 | if Strings(allows).IndexOf(op.String()) < 0 { 80 | return fmt.Errorf("rsql: operator %s is not allow for field %q", op, tkn1.Value) 81 | } 82 | 83 | tkn3, err := nextToken(scan) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | v := reflect.New(f.Type).Elem() 89 | tkn3.Value = strings.Trim(tkn3.Value, `"'`) 90 | value, err := convertValue(v, tkn3.Value) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | params.Filters = append(params.Filters, Filter{ 96 | Name: name, 97 | Operator: op, 98 | Value: value, 99 | }) 100 | 101 | tkn, err := nextToken(scan) 102 | if err != nil { 103 | if err == io.EOF { 104 | break loop 105 | } 106 | return err 107 | } 108 | 109 | switch tkn.Value { 110 | case ";", ",": 111 | case "(", ")": 112 | default: 113 | return errors.New("unexpected char") 114 | } 115 | } 116 | 117 | // for _, f := range params.Filters { 118 | // log.Println("Each :", f, reflect.TypeOf(f.Value), reflect.TypeOf(f)) 119 | // } 120 | return nil 121 | } 122 | 123 | func nextToken(scan *lexmachine.Scanner) (*Token, error) { 124 | it, err, eof := scan.Next() 125 | if eof { 126 | return nil, io.EOF 127 | } 128 | if err != nil { 129 | return nil, err 130 | } 131 | return it.(*Token), nil 132 | } 133 | 134 | func convertValue(v reflect.Value, value string) (interface{}, error) { 135 | value = strings.TrimSpace(value) 136 | 137 | switch v.Type() { 138 | case typeOfTime: 139 | t, err := time.Parse(time.RFC3339, value) 140 | if err != nil { 141 | return nil, err 142 | } 143 | v.Set(reflect.ValueOf(t)) 144 | 145 | case typeOfByte: 146 | x, err := base64.StdEncoding.DecodeString(value) 147 | if err != nil { 148 | return nil, err 149 | } 150 | v.SetBytes(x) 151 | 152 | default: 153 | switch v.Kind() { 154 | case reflect.String: 155 | v.SetString(value) 156 | 157 | case reflect.Bool: 158 | x, err := strconv.ParseBool(value) 159 | if err != nil { 160 | return nil, err 161 | } 162 | v.SetBool(x) 163 | 164 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 165 | x, err := strconv.ParseInt(value, 10, 64) 166 | if err != nil { 167 | return nil, err 168 | } 169 | if v.OverflowInt(x) { 170 | return nil, errors.New("int overflow") 171 | } 172 | v.SetInt(x) 173 | 174 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 175 | x, err := strconv.ParseUint(value, 10, 64) 176 | if err != nil { 177 | return nil, err 178 | } 179 | if v.OverflowUint(x) { 180 | return nil, errors.New("unsigned int overflow") 181 | } 182 | v.SetUint(x) 183 | 184 | case reflect.Float32, reflect.Float64: 185 | x, err := strconv.ParseFloat(value, 64) 186 | if err != nil { 187 | return nil, err 188 | } 189 | if v.OverflowFloat(x) { 190 | return nil, errors.New("float overflow") 191 | } 192 | v.SetFloat(x) 193 | 194 | case reflect.Ptr: 195 | if value == "null" { 196 | zero := reflect.Zero(v.Type()) 197 | return zero.Interface(), nil 198 | } 199 | return convertValue(v.Elem(), value) 200 | 201 | default: 202 | return nil, fmt.Errorf("unsupported data type %v", v.Type()) 203 | } 204 | } 205 | 206 | return v.Interface(), nil 207 | } 208 | --------------------------------------------------------------------------------