diff --git a/CHANGELOG.md b/CHANGELOG.md index 71f5c75..014be9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +* added support for Go modules +* added support for SQL ## [0.4.0] - 2021-08-01 ### Changed diff --git a/README.md b/README.md index d628192..f58fd7a 100644 --- a/README.md +++ b/README.md @@ -1,183 +1,230 @@ -go-rsql -======= - -# overview -RSQL is a query language for parametrized filtering of entries in APIs. -It is based on FIQL (Feed Item Query Language) – an URI-friendly syntax for expressing filters across the entries in an Atom Feed. -FIQL is great for use in URI; there are no unsafe characters, so URL encoding is not required. -On the other side, FIQL’s syntax is not very intuitive and URL encoding isn’t always that big deal, -so RSQL also provides a friendlier syntax for logical operators and some comparison operators. - -This is a small RSQL helper library, written in golang. -It can be used to parse a RSQL string and turn it into a database query string. - -Currently, only mongodb is supported out of the box (however it is very easy to extend the parser if needed). - -# basic usage -```go -package main - -import ( - -"github.com/rbicker/go-rsql" -"log" -) - -func main(){ - parser, err := rsql.NewParser(rsql.Mongo()) - if err != nil { - log.Fatalf("error while creating parser: %s", err) - } - s := `status=="A",qty=lt=30` - res, err := parser.Process(s) - if err != nil { - log.Fatalf("error while parsing: %s", err) - } - log.Println(res) - // { "$or": [ { "status": "A" }, { "qty": { "$lt": 30 } } ] } -} -``` - - -# supported operators -The library supports the following basic operators by default: - -| Basic Operator | Description | -|----------------|---------------------| -| == | Equal To | -| != | Not Equal To | -| =gt= | Greater Than | -| =ge= | Greater Or Equal To | -| =lt= | Less Than | -| =le= | Less Or Equal To | -| =in= | In | -| =out= | Not in | - -The following table lists two joining operators: - -| Composite Operator | Description | -|--------------------|---------------------| -| ; | Logical AND | -| , | Logical OR | - - -# advanced usage - -## custom operators -The library makes it easy to define custom operators: -```go -package main - -import ( - -"fmt" -"github.com/rbicker/go-rsql" -"log" -) - -func main(){ - // create custom operators for "exists"- and "all"-operations - customOperators := []rsql.Operator{ - { - Operator: "=ex=", - Formatter: func (key, value string) string { - return fmt.Sprintf(`{ "%s": { "$exists": %s } }`, key, value) - }, - }, - { - Operator: "=all=", - Formatter: func(key, value string) string { - return fmt.Sprintf(`{ "%s": { "$all": [ %s ] } }`, key, value[1:len(value)-1]) - }, - }, - } - // create parser with default mongo operators - // plus the two custom operators - var opts []func(*rsql.Parser) error - opts = append(opts, rsql.Mongo()) - opts = append(opts, rsql.WithOperators(customOperators...)) - parser, err := rsql.NewParser(opts...) - if err != nil { - log.Fatalf("error while creating parser: %s", err) - } - // parse string with some default operators - res, err := parser.Process(`(a==1;b==2),c=gt=5`) - if err != nil { - log.Fatalf("error while parsing: %s", err) - } - log.Println(res) - // { "$or": [ { "$and": [ { "a": 1 }, { "b": 2 } ] }, { "c": { "$gt": 5 } } ] } - - // use custom operator =ex= - res, err = parser.Process(`a=ex=true`) - if err != nil { - log.Fatalf("error while parsing: %s", err) - } - log.Println(res) - // { "a": { "$exists": true } } - - // use custom list operator =all= - res, err = parser.Process(`tags=all=('waterproof','rechargeable')`) - if err != nil { - log.Fatalf("error while parsing: %s", err) - } - log.Println(res) - // { "tags": { "$all": [ 'waterproof','rechargeable' ] } } -} -``` - -## transform keys -If your database key naming scheme is different from the one used in your rsql statements, you can add functions to transform your keys. - -```go -package main - -import ( - "github.com/rbicker/go-rsql" - "log" - "strings" -) - -func main() { - transformer := func(s string) string { - return strings.ToUpper(s) - } - parser, err := rsql.NewParser(rsql.Mongo(), rsql.WithKeyTransformers(transformer)) - if err != nil { - log.Fatalf("error while creating parser: %s", err) - } - s := `status=="a",qty=lt=30` - res, err := parser.Process(s) - if err != nil { - log.Fatalf("error while parsing: %s", err) - } - log.Println(res) - // { "$or": [ { "STATUS": "a" }, { "QTY": { "$lt": 30 } } ] } -} -``` - -## define allowed or forbidden keys -```go -package main - -import ( - "github.com/rbicker/go-rsql" - "log" -) - -func main() { - parser, err := rsql.NewParser(rsql.Mongo()) - if err != nil { - log.Fatalf("error while creating parser: %s", err) - } - s := `status=="a",qty=lt=30` - _, err = parser.Process(s, rsql.SetAllowedKeys([]string{"status, qty"})) - // -> ok - _, err = parser.Process(s, rsql.SetAllowedKeys([]string{"status"})) - // -> error - _, err = parser.Process(s, rsql.SetForbiddenKeys([]string{"status"})) - // -> error - _, err = parser.Process(s, rsql.SetAllowedKeys([]string{"age"})) - // -> ok -} +# go-rsql + +## overview + +RSQL is a query language for parametrized filtering of entries in APIs. +It is based on FIQL (Feed Item Query Language) – an URI-friendly syntax for expressing filters across the entries in an Atom Feed. +FIQL is great for use in URI; there are no unsafe characters, so URL encoding is not required. +On the other side, FIQL’s syntax is not very intuitive and URL encoding isn’t always that big deal, +so RSQL also provides a friendlier syntax for logical operators and some comparison operators. + +This is a small RSQL helper library, written in golang. +It can be used to parse a RSQL string and turn it into a database query string. + +Currently, mongodb and SQL is supported out of the box. It is very easy to create a new parser if needed. + +## basic usage (mongodb) + +```go +package main + +import ( + +"github.com/rbicker/go-rsql" +"log" +) + +func main(){ + parser, err := rsql.NewParser(rsql.Mongo()) + if err != nil { + log.Fatalf("error while creating parser: %s", err) + } + s := `status=="A",qty=lt=30` + res, err := parser.Process(s) + if err != nil { + log.Fatalf("error while parsing: %s", err) + } + log.Println(res) + // { "$or": [ { "status": "A" }, { "qty": { "$lt": 30 } } ] } +} +``` + +## basic usage (SQL) + +```go +package main + +import ( + "log" + + "github.com/rbicker/go-rsql" +) + +func main() { + parser, err := rsql.NewParser(rsql.SQL()) + if err != nil { + log.Fatalf("error while creating parser: %s", err) + } + s := `status==A,qty=lt=30` + + var args []any + + res, args, err := parser.Process(s) + if err != nil { + log.Fatalf("error while parsing: %s", err) + } + log.Println(res) + // status = $1 OR qty < $2 + + // example use + qry := "SELECT * FROM books" + // This example is simplified. but in a real world example you may not + // have a url query string and res will be empty. + if res != "" { + qry += " WHERE " + res + } + + log.Println(qry) + // SELECT * FROM books WHERE status = $1 OR qty < $2 + + log.Println(args) + // [A 30] +} +``` + +## supported operators + +go-rsql supports the following basic operators by default: + +| Basic Operator | Description | +|----------------|---------------------| +| == | Equal To | +| != | Not Equal To | +| =gt= | Greater Than | +| =ge= | Greater Or Equal To | +| =lt= | Less Than | +| =le= | Less Or Equal To | +| =in= | In | +| =out= | Not in | + +go-rsql supports two joining operators: + +| Composite Operator | Description | +|--------------------|---------------------| +| ; | Logical AND | +| , | Logical OR | + + +## advanced usage + +### custom operators + +The library makes it easy to define custom operators: +```go +package main + +import ( + +"fmt" +"github.com/rbicker/go-rsql" +"log" +) + +func main(){ + // create custom operators for "exists"- and "all"-operations + customOperators := []rsql.Operator{ + { + Operator: "=ex=", + Formatter: func (key, value string) string { + return fmt.Sprintf(`{ "%s": { "$exists": %s } }`, key, value) + }, + }, + { + Operator: "=all=", + Formatter: func(key, value string) string { + return fmt.Sprintf(`{ "%s": { "$all": [ %s ] } }`, key, value[1:len(value)-1]) + }, + }, + } + // create parser with default mongo operators + // plus the two custom operators + var opts []func(*rsql.Parser) error + opts = append(opts, rsql.Mongo()) + opts = append(opts, rsql.WithOperators(customOperators...)) + parser, err := rsql.NewParser(opts...) + if err != nil { + log.Fatalf("error while creating parser: %s", err) + } + // parse string with some default operators + res, err := parser.Process(`(a==1;b==2),c=gt=5`) + if err != nil { + log.Fatalf("error while parsing: %s", err) + } + log.Println(res) + // { "$or": [ { "$and": [ { "a": 1 }, { "b": 2 } ] }, { "c": { "$gt": 5 } } ] } + + // use custom operator =ex= + res, err = parser.Process(`a=ex=true`) + if err != nil { + log.Fatalf("error while parsing: %s", err) + } + log.Println(res) + // { "a": { "$exists": true } } + + // use custom list operator =all= + res, err = parser.Process(`tags=all=('waterproof','rechargeable')`) + if err != nil { + log.Fatalf("error while parsing: %s", err) + } + log.Println(res) + // { "tags": { "$all": [ 'waterproof','rechargeable' ] } } +} +``` + +### transform keys + +If your database key naming scheme is different from the one used in your rsql statements, you can add functions to transform your keys. + +```go +package main + +import ( + "github.com/rbicker/go-rsql" + "log" + "strings" +) + +func main() { + transformer := func(s string) string { + return strings.ToUpper(s) + } + parser, err := rsql.NewParser(rsql.Mongo(), rsql.WithKeyTransformers(transformer)) + if err != nil { + log.Fatalf("error while creating parser: %s", err) + } + s := `status=="a",qty=lt=30` + res, err := parser.Process(s) + if err != nil { + log.Fatalf("error while parsing: %s", err) + } + log.Println(res) + // { "$or": [ { "STATUS": "a" }, { "QTY": { "$lt": 30 } } ] } +} +``` + +### define allowed or forbidden keys + +```go +package main + +import ( + "github.com/rbicker/go-rsql" + "log" +) + +func main() { + parser, err := rsql.NewParser(rsql.Mongo()) + if err != nil { + log.Fatalf("error while creating parser: %s", err) + } + s := `status=="a",qty=lt=30` + _, err = parser.Process(s, rsql.SetAllowedKeys([]string{"status, qty"})) + // -> ok + _, err = parser.Process(s, rsql.SetAllowedKeys([]string{"status"})) + // -> error + _, err = parser.Process(s, rsql.SetForbiddenKeys([]string{"status"})) + // -> error + _, err = parser.Process(s, rsql.SetAllowedKeys([]string{"age"})) + // -> ok +} ``` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e8cd861 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/rbicker/go-rsql + +go 1.24 \ No newline at end of file diff --git a/rsql.go b/rsql.go index 93ff61f..4fd6a01 100644 --- a/rsql.go +++ b/rsql.go @@ -21,12 +21,12 @@ var specialEncode = map[string]string{ var reOperator = regexp.MustCompile(`([!=])[^=()]*=`) // Operator represents a query Operator. -// It defines the Operator itself, the mongodb representation -// of the Operator and if it is a list Operator or not. -// Operators must match regex reOperator: `(!|=)[^=()]*=` +// It defines the Operator itself and a formatter function that returns both the +// formatted string and any values. Values can be used for parameterized queries +// (MongoDB) or prepared statements (SQL). type Operator struct { Operator string - Formatter func(key, value string) string + Formatter func(key, value string, paramIndex *int) (string, []any) } // Parser represents a RSQL parser. @@ -58,60 +58,62 @@ func NewParser(options ...func(*Parser) error) (*Parser, error) { } // Mongo adds the default mongo operators to the parser +// TODO adjust the returned strings for parameterized queries +// values are already returned. func Mongo() func(parser *Parser) error { return func(parser *Parser) error { // operators var operators = []Operator{ { "==", - func(key, value string) string { - return fmt.Sprintf(`{ "%s": %s }`, key, value) + func(key, value string, paramIndex *int) (string, []any) { + return fmt.Sprintf(`{ "%s": %s }`, key, value), []any{value} }, }, { "!=", - func(key, value string) string { - return fmt.Sprintf(`{ "%s": { "$ne": %s } }`, key, value) + func(key, value string, paramIndex *int) (string, []any) { + return fmt.Sprintf(`{ "%s": { "$ne": %s } }`, key, value), []any{value} }, }, { "=gt=", - func(key, value string) string { - return fmt.Sprintf(`{ "%s": { "$gt": %s } }`, key, value) + func(key, value string, paramIndex *int) (string, []any) { + return fmt.Sprintf(`{ "%s": { "$gt": %s } }`, key, value), []any{value} }, }, { "=ge=", - func(key, value string) string { - return fmt.Sprintf(`{ "%s": { "$gte": %s } }`, key, value) + func(key, value string, paramIndex *int) (string, []any) { + return fmt.Sprintf(`{ "%s": { "$gte": %s } }`, key, value), []any{value} }, }, { "=lt=", - func(key, value string) string { - return fmt.Sprintf(`{ "%s": { "$lt": %s } }`, key, value) + func(key, value string, paramIndex *int) (string, []any) { + return fmt.Sprintf(`{ "%s": { "$lt": %s } }`, key, value), []any{value} }, }, { "=le=", - func(key, value string) string { - return fmt.Sprintf(`{ "%s": { "$lte": %s } }`, key, value) + func(key, value string, paramIndex *int) (string, []any) { + return fmt.Sprintf(`{ "%s": { "$lte": %s } }`, key, value), []any{value} }, }, { "=in=", - func(key, value string) string { + func(key, value string, paramIndex *int) (string, []any) { // remove parentheses value = value[1 : len(value)-1] - return fmt.Sprintf(`{ "%s": { "$in": %s } }`, key, value) + return fmt.Sprintf(`{ "%s": { "$in": %s } }`, key, value), []any{value} }, }, { "=out=", - func(key, value string) string { + func(key, value string, paramIndex *int) (string, []any) { // remove parentheses value = value[1 : len(value)-1] - return fmt.Sprintf(`{ "%s": { "$nin": %s } }`, key, value) + return fmt.Sprintf(`{ "%s": { "$nin": %s } }`, key, value), []any{value} }, }, } @@ -140,6 +142,132 @@ func Mongo() func(parser *Parser) error { } } +// SQL adds the default SQL operators to the parser +func SQL() func(parser *Parser) error { + return func(parser *Parser) error { + var operators = []Operator{ + { + "==", + func(key, value string, paramIndex *int) (string, []any) { + *paramIndex++ + return fmt.Sprintf(`%s = $%d`, key, *paramIndex), []any{value} + }, + }, + { + "!=", + func(key, value string, paramIndex *int) (string, []any) { + *paramIndex++ + return fmt.Sprintf(`%s <> $%d`, key, *paramIndex), []any{value} + }, + }, + { + "=gt=", + func(key, value string, paramIndex *int) (string, []any) { + *paramIndex++ + return fmt.Sprintf(`%s > $%d`, key, *paramIndex), []any{value} + }, + }, + { + "=ge=", + func(key, value string, paramIndex *int) (string, []any) { + *paramIndex++ + return fmt.Sprintf(`%s >= $%d`, key, *paramIndex), []any{value} + }, + }, + { + "=lt=", + func(key, value string, paramIndex *int) (string, []any) { + *paramIndex++ + return fmt.Sprintf(`%s < $%d`, key, *paramIndex), []any{value} + }, + }, + { + "=le=", + func(key, value string, paramIndex *int) (string, []any) { + *paramIndex++ + return fmt.Sprintf(`%s <= $%d`, key, *paramIndex), []any{value} + }, + }, + { + "=in=", + func(key, value string, paramIndex *int) (string, []any) { + // remove parentheses + value = value[1 : len(value)-1] + // Split values and create placeholders + values := strings.Split(value, ",") + vals := make([]any, len(values)) + placeholders := make([]string, len(values)) + for i, v := range values { + vals[i] = v + *paramIndex++ + placeholders[i] = fmt.Sprintf("$%d", *paramIndex) + } + return fmt.Sprintf(`%s IN (%s)`, key, strings.Join(placeholders, ", ")), vals + }, + }, + { + "=out=", + func(key, value string, paramIndex *int) (string, []any) { + // remove parentheses + value = value[1 : len(value)-1] + // Split values and create placeholders + values := strings.Split(value, ",") + vals := make([]any, len(values)) + placeholders := make([]string, len(values)) + for i, v := range values { + vals[i] = v + *paramIndex++ + placeholders[i] = fmt.Sprintf("$%d", *paramIndex) + } + return fmt.Sprintf(`%s NOT IN (%s)`, key, strings.Join(placeholders, ", ")), vals + }, + }, + } + parser.operators = append(parser.operators, operators...) + // AND formatter + parser.andFormatter = func(ss []string) string { + if len(ss) == 0 { + return "" + } + if len(ss) == 1 { + return ss[0] + } + + // Add parentheses around expressions that contain AND or OR + for i, s := range ss { + if strings.Contains(s, " AND ") || strings.Contains(s, " OR ") { + if !strings.HasPrefix(s, "(") || !strings.HasSuffix(s, ")") { + ss[i] = "(" + s + ")" + } + } + } + + return strings.Join(ss, " AND ") + } + // OR formatter + parser.orFormatter = func(ss []string) string { + if len(ss) == 0 { + return "" + } + if len(ss) == 1 { + return ss[0] + } + + // Add parentheses around expressions that contain AND or OR + for i, s := range ss { + if strings.Contains(s, " AND ") || strings.Contains(s, " OR ") { + if !strings.HasPrefix(s, "(") || !strings.HasSuffix(s, ")") { + ss[i] = "(" + s + ")" + } + } + } + + return strings.Join(ss, " OR ") + } + return nil + } +} + // WithOperator adds custom operators to the parser func WithOperators(operators ...Operator) func(parser *Parser) error { return func(parser *Parser) error { @@ -194,13 +322,19 @@ func containsString(ss []string, s string) bool { } // Process takes the given string and processes it using parser's operators. -func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) (string, error) { +// It returns the processed string and a slice of values for prepared statements. +func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) (string, []any, error) { + // Initialize values slice + var values []any + // paramIndex has to be here and not in the parsers to support custom formatters. + paramIndex := 0 + // set process options opts := ProcessOptions{} for _, op := range options { err := op(&opts) if err != nil { - return "", fmt.Errorf("setting process option failed: %w", err) + return "", nil, fmt.Errorf("setting process option failed: %w", err) } } // regex to match identifier within operation, before the equal or expression mark @@ -210,7 +344,7 @@ func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) // get ORs locations, err := findORs(s, -1) if err != nil { - return "", fmt.Errorf("unable to find ORs: %w", err) + return "", nil, fmt.Errorf("unable to find ORs: %w", err) } var ors []string for _, loc := range locations { @@ -219,7 +353,7 @@ func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) // handle ANDs locs, err := findANDs(content, -1) if err != nil { - return "", fmt.Errorf("unable to find ANDs: %w", err) + return "", nil, fmt.Errorf("unable to find ANDs: %w", err) } var ands []string for _, l := range locs { @@ -228,16 +362,18 @@ func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) // handle parentheses parentheses, err := findOuterParentheses(content, -1) if err != nil { - return "", fmt.Errorf("unable to find parentheses: %w", err) + return "", nil, fmt.Errorf("unable to find parentheses: %w", err) } for _, p := range parentheses { start, end := p[0], p[1] content := content[start+1 : end] // handle nested - replacement, err := parser.Process(content) + replacement, nestedValues, err := parser.Process(content) if err != nil { - return "", err + return "", nil, err } + // Add nested values to our values slice + values = append(values, nestedValues...) ands = append(ands, replacement) } if len(parentheses) > 0 { @@ -249,7 +385,7 @@ func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) key := reKey.FindString(content) value := reValue.FindString(content) if operator == "" || key == "" || value == "" { - return "", fmt.Errorf("incomplete operation '%s'", content) + return "", nil, fmt.Errorf("incomplete operation '%s'", content) } // run key transformers for _, t := range parser.keyTransformers { @@ -257,21 +393,23 @@ func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) } // check if key is allowed if containsString(opts.forbiddenKeys, key) { - return "", fmt.Errorf("given key '%s' is not allowed", key) + return "", nil, fmt.Errorf("given key '%s' is not allowed", key) } if len(opts.allowedKeys) > 0 && !containsString(opts.allowedKeys, key) { - return "", fmt.Errorf("given key '%s' is not allowed", key) + return "", nil, fmt.Errorf("given key '%s' is not allowed", key) } // parse operation var res string + var opValues []any for _, op := range parser.operators { if operator == op.Operator { - res = op.Formatter(key, value) + res, opValues = op.Formatter(key, value, ¶mIndex) + values = append(values, opValues...) break } } if res == "" { - return "", fmt.Errorf("unknown operator '%s' in '%s'", operator, content) + return "", nil, fmt.Errorf("unknown operator '%s' in '%s'", operator, content) } ands = append(ands, res) } @@ -280,7 +418,7 @@ func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) ors = append(ors, replacement) } // replace OR-block and return - return parser.orFormatter(ors), nil + return parser.orFormatter(ors), values, nil } // encodeSpecial encodes all the special strings diff --git a/rsql_test.go b/rsql_test.go index 8af9530..64110e6 100644 --- a/rsql_test.go +++ b/rsql_test.go @@ -389,6 +389,197 @@ func TestParser_ProcessMongo(t *testing.T) { } } +func TestParser_ProcessSQL(t *testing.T) { + tests := []struct { + name string + s string + options []func(*ProcessOptions) error + customOperators []Operator + keyTransformers []func(s string) string + want string + wantErr bool + }{ + { + name: "empty", + s: "", + want: "", + }, + { + name: "==", + s: "a==1", + want: `a = 1`, + }, + { + name: "!=", + s: "a!=1", + want: `a <> 1`, + }, + { + name: "=gt=", + s: "a=gt=1", + want: `a > 1`, + }, + { + name: "=ge=", + s: "a=ge=1", + want: `a >= 1`, + }, + { + name: "=lt=", + s: "a=lt=1", + want: `a < 1`, + }, + { + name: "=le=", + s: "a=le=1", + want: `a <= 1`, + }, + { + name: "=in=", + s: "a=in=(1,2,3)", + want: `a IN (1,2,3)`, + }, + { + name: "=out=", + s: "a=out=(1,2,3)", + want: `a NOT IN (1,2,3)`, + }, + { + name: "(a==1)", + s: "(a==1)", + want: `a = 1`, + }, + { + name: "a==1;b==2", + s: "a==1;b==2", + want: `a = 1 AND b = 2`, + }, + { + name: "a==1,b==2", + s: "a==1,b==2", + want: `a = 1 OR b = 2`, + }, + { + name: "a==1;b==2,c==1", + s: "a==1;b==2,c==1", + want: `(a = 1 AND b = 2) OR c = 1`, + }, + { + name: "a==1,b==2;c==1", + s: "a==1,b==2;c==1", + want: `a = 1 OR (b = 2 AND c = 1)`, + }, + { + name: "(a==1;b==2),c=gt=5", + s: "(a==1;b==2),c=gt=5", + want: `(a = 1 AND b = 2) OR c > 5`, + }, + { + name: "c==1,(a==1;b==2)", + s: "c==1,(a==1;b==2)", + want: `c = 1 OR (a = 1 AND b = 2)`, + }, + { + name: "a==1;(b==1,c==2)", + s: "a==1;(b==1,c==2)", + want: `a = 1 AND (b = 1 OR c = 2)`, + }, + { + name: "(a==1,b==1);(c==1,d==2)", + s: "(a==1,b==1);(c==1,d==2)", + want: `(a = 1 OR b = 1) AND (c = 1 OR d = 2)`, + }, + { + name: "custom operator: =like=", + s: "a=like=c*", + customOperators: []Operator{ + { + Operator: "=like=", + Formatter: func(key, value string) string { + value = strings.ReplaceAll(value, "*", "%") + return fmt.Sprintf(`%s LIKE '%s'`, key, value) + }, + }, + }, + want: `a LIKE 'c%'`, + }, + { + name: "all keys allowed", + s: "a==1", + options: []func(*ProcessOptions) error{}, + wantErr: false, + want: `a = 1`, + }, + { + name: "key allowed", + s: "a==1", + options: []func(*ProcessOptions) error{ + SetAllowedKeys([]string{"a"}), + }, + wantErr: false, + want: `a = 1`, + }, + { + name: "key not allowed", + s: "a==1", + options: []func(*ProcessOptions) error{ + SetAllowedKeys([]string{"b"}), + }, + wantErr: true, + want: "", + }, + { + name: "key forbidden", + s: "a==1", + options: []func(*ProcessOptions) error{ + SetForbiddenKeys([]string{"a"}), + }, + wantErr: true, + want: "", + }, + { + name: "key not forbidden", + s: "a==1", + options: []func(*ProcessOptions) error{ + SetForbiddenKeys([]string{"b"}), + }, + wantErr: false, + want: `a = 1`, + }, + { + name: "uppercase key transformer", + s: "a==1", + keyTransformers: []func(s string) string{ + func(s string) string { + return strings.ToUpper(s) + }, + }, + wantErr: false, + want: `A = 1`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var opts []func(*Parser) error + opts = append(opts, SQL()) + opts = append(opts, WithOperators(tt.customOperators...)) + opts = append(opts, WithKeyTransformers(tt.keyTransformers...)) + parser, err := NewParser(opts...) + if err != nil { + t.Fatalf("error while creating parser: %s", err) + } + got, err := parser.Process(tt.s, tt.options...) + if (err != nil) != tt.wantErr { + t.Errorf("Process() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Process() got = %v, want %v", got, tt.want) + } + }) + } +} + func Test_findParts(t *testing.T) { tests := []struct { name string