From ee90b26dfa490efb89a856e71284b785e0c96f36 Mon Sep 17 00:00:00 2001 From: AaronS Date: Sun, 22 Jun 2025 21:20:21 -0700 Subject: [PATCH 1/6] * added support for Go modules * added support for SQL Still need to adjust readme --- CHANGELOG.md | 3 + README.md | 54 +++++++++++---- go.mod | 3 + rsql.go | 103 +++++++++++++++++++++++++++ rsql_test.go | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 342 insertions(+), 12 deletions(-) create mode 100644 go.mod 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..a9b2136 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -go-rsql -======= +# go-rsql + +## overview -# 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. @@ -11,9 +11,10 @@ so RSQL also provides a friendlier syntax for logical operators and some compari 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). +Currently, mongodb and SQL is supported out of the box. It is very easy to create a new parser if needed. + +## basic usage (mongodb) -# basic usage ```go package main @@ -38,9 +39,35 @@ func main(){ } ``` +## basic usage (SQL) + +```go +package main -# supported operators -The library supports the following basic operators by default: +import ( + +"github.com/rbicker/go-rsql" +"log" +) + +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` + 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 + +go-rsql supports the following basic operators by default: | Basic Operator | Description | |----------------|---------------------| @@ -53,7 +80,7 @@ The library supports the following basic operators by default: | =in= | In | | =out= | Not in | -The following table lists two joining operators: +go-rsql supports two joining operators: | Composite Operator | Description | |--------------------|---------------------| @@ -61,9 +88,10 @@ The following table lists two joining operators: | , | Logical OR | -# advanced usage +## advanced usage + +### custom operators -## custom operators The library makes it easy to define custom operators: ```go package main @@ -126,7 +154,8 @@ func main(){ } ``` -## transform keys +### 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 @@ -156,7 +185,8 @@ func main() { } ``` -## define allowed or forbidden keys +### define allowed or forbidden keys + ```go package main 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..6fc00bf 100644 --- a/rsql.go +++ b/rsql.go @@ -140,6 +140,109 @@ 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 { + // operators + var operators = []Operator{ + { + "==", + func(key, value string) string { + return fmt.Sprintf(`%s = %s`, key, value) + }, + }, + { + "!=", + func(key, value string) string { + return fmt.Sprintf(`%s <> %s`, key, value) + }, + }, + { + "=gt=", + func(key, value string) string { + return fmt.Sprintf(`%s > %s`, key, value) + }, + }, + { + "=ge=", + func(key, value string) string { + return fmt.Sprintf(`%s >= %s`, key, value) + }, + }, + { + "=lt=", + func(key, value string) string { + return fmt.Sprintf(`%s < %s`, key, value) + }, + }, + { + "=le=", + func(key, value string) string { + return fmt.Sprintf(`%s <= %s`, key, value) + }, + }, + { + "=in=", + func(key, value string) string { + // remove parentheses + value = value[1 : len(value)-1] + return fmt.Sprintf(`%s IN (%s)`, key, value) + }, + }, + { + "=out=", + func(key, value string) string { + // remove parentheses + value = value[1 : len(value)-1] + return fmt.Sprintf(`%s NOT IN (%s)`, key, value) + }, + }, + } + 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 { 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 From 8dabe6cb06c7145712365afd4d7b7b10c03a561e Mon Sep 17 00:00:00 2001 From: AaronS Date: Sun, 22 Jun 2025 21:45:43 -0700 Subject: [PATCH 2/6] finished the sql parser example --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a9b2136..11dc522 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,12 @@ func main(){ package main import ( + "log" -"github.com/rbicker/go-rsql" -"log" + "github.com/rbicker/go-rsql" ) -func main(){ +func main() { parser, err := rsql.NewParser(rsql.SQL()) if err != nil { log.Fatalf("error while creating parser: %s", err) @@ -61,7 +61,21 @@ func main(){ log.Fatalf("error while parsing: %s", err) } log.Println(res) - // { "$or": [ { "status": "A" }, { "qty": { "$lt": 30 } } ] } + // status = "A" OR qty < 30 + + // example use + // The 1=1 allows us to add or not add more conditions. It shouldn't affect + // query run times. + qry := "SELECT * FROM books WHERE 1=1" + // 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 != "" { + // The parentheses may not be necessary but they help ensure the order + // of operations. + qry += " AND (" + res + ")" + } + + log.Println(qry) } ``` From ec4f2e3ad5cb6b0fd50f84448b8b91060afc1078 Mon Sep 17 00:00:00 2001 From: AaronS Date: Mon, 23 Jun 2025 17:44:28 -0700 Subject: [PATCH 3/6] simplified example --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 11dc522..a46147a 100644 --- a/README.md +++ b/README.md @@ -64,18 +64,15 @@ func main() { // status = "A" OR qty < 30 // example use - // The 1=1 allows us to add or not add more conditions. It shouldn't affect - // query run times. - qry := "SELECT * FROM books WHERE 1=1" + 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 != "" { - // The parentheses may not be necessary but they help ensure the order - // of operations. - qry += " AND (" + res + ")" + qry += " WHERE " + res } log.Println(qry) + // SELECT * FROM books WHERE status = "A" OR qty < 30 } ``` From 0a1b2e6c8e4d5f1e082a6f64d7a3f4b28640ffe7 Mon Sep 17 00:00:00 2001 From: AaronS Date: Tue, 24 Jun 2025 10:01:15 -0700 Subject: [PATCH 4/6] adjustments for prepared statements --- rsql.go | 135 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 50 deletions(-) diff --git a/rsql.go b/rsql.go index 6fc00bf..550ac22 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) (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) (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) (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) (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) (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) (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) (string, []any) { + return fmt.Sprintf(`{ "%s": { "$lte": %s } }`, key, value), []any{value} }, }, { "=in=", - func(key, value string) string { + func(key, value string) (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) (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} }, }, } @@ -143,58 +145,83 @@ 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 { - // operators + paramIndex := 0 + var operators = []Operator{ { "==", - func(key, value string) string { - return fmt.Sprintf(`%s = %s`, key, value) + func(key, value string) (string, []any) { + paramIndex++ + return fmt.Sprintf(`%s = $%d`, key, paramIndex), []any{value} }, }, { "!=", - func(key, value string) string { - return fmt.Sprintf(`%s <> %s`, key, value) + func(key, value string) (string, []any) { + paramIndex++ + return fmt.Sprintf(`%s <> $%d`, key, paramIndex), []any{value} }, }, { "=gt=", - func(key, value string) string { - return fmt.Sprintf(`%s > %s`, key, value) + func(key, value string) (string, []any) { + paramIndex++ + return fmt.Sprintf(`%s > $%d`, key, paramIndex), []any{value} }, }, { "=ge=", - func(key, value string) string { - return fmt.Sprintf(`%s >= %s`, key, value) + func(key, value string) (string, []any) { + paramIndex++ + return fmt.Sprintf(`%s >= $%d`, key, paramIndex), []any{value} }, }, { "=lt=", - func(key, value string) string { - return fmt.Sprintf(`%s < %s`, key, value) + func(key, value string) (string, []any) { + paramIndex++ + return fmt.Sprintf(`%s < $%d`, key, paramIndex), []any{value} }, }, { "=le=", - func(key, value string) string { - return fmt.Sprintf(`%s <= %s`, key, value) + func(key, value string) (string, []any) { + paramIndex++ + return fmt.Sprintf(`%s <= $%d`, key, paramIndex), []any{value} }, }, { "=in=", - func(key, value string) string { + func(key, value string) (string, []any) { // remove parentheses value = value[1 : len(value)-1] - return fmt.Sprintf(`%s IN (%s)`, key, value) + // 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) string { + func(key, value string) (string, []any) { // remove parentheses value = value[1 : len(value)-1] - return fmt.Sprintf(`%s NOT IN (%s)`, key, value) + // 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 }, }, } @@ -297,13 +324,17 @@ 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 + // 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 @@ -313,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 { @@ -322,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 { @@ -331,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 { @@ -352,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 { @@ -360,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) + 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) } @@ -383,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 From c3f9d9f5d7e5d337f019f0f81748712b7d14c42d Mon Sep 17 00:00:00 2001 From: AaronS Date: Tue, 24 Jun 2025 10:06:24 -0700 Subject: [PATCH 5/6] adjusting example --- README.md | 452 +++++++++++++++++++++++++++--------------------------- 1 file changed, 229 insertions(+), 223 deletions(-) diff --git a/README.md b/README.md index a46147a..f58fd7a 100644 --- a/README.md +++ b/README.md @@ -1,224 +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, 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` - res, err := parser.Process(s) - if err != nil { - log.Fatalf("error while parsing: %s", err) - } - log.Println(res) - // status = "A" OR qty < 30 - - // 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 = "A" OR qty < 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 -} +# 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 From de6c971604b1b558fe971fe6c9a86ab3a5fd9ea4 Mon Sep 17 00:00:00 2001 From: AaronS Date: Tue, 24 Jun 2025 10:47:37 -0700 Subject: [PATCH 6/6] adjusting index to support custom operators --- rsql.go | 72 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/rsql.go b/rsql.go index 550ac22..4fd6a01 100644 --- a/rsql.go +++ b/rsql.go @@ -26,7 +26,7 @@ var reOperator = regexp.MustCompile(`([!=])[^=()]*=`) // (MongoDB) or prepared statements (SQL). type Operator struct { Operator string - Formatter func(key, value string) (string, []any) + Formatter func(key, value string, paramIndex *int) (string, []any) } // Parser represents a RSQL parser. @@ -66,43 +66,43 @@ func Mongo() func(parser *Parser) error { var operators = []Operator{ { "==", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { return fmt.Sprintf(`{ "%s": %s }`, key, value), []any{value} }, }, { "!=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { return fmt.Sprintf(`{ "%s": { "$ne": %s } }`, key, value), []any{value} }, }, { "=gt=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { return fmt.Sprintf(`{ "%s": { "$gt": %s } }`, key, value), []any{value} }, }, { "=ge=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { return fmt.Sprintf(`{ "%s": { "$gte": %s } }`, key, value), []any{value} }, }, { "=lt=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { return fmt.Sprintf(`{ "%s": { "$lt": %s } }`, key, value), []any{value} }, }, { "=le=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { return fmt.Sprintf(`{ "%s": { "$lte": %s } }`, key, value), []any{value} }, }, { "=in=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { // remove parentheses value = value[1 : len(value)-1] return fmt.Sprintf(`{ "%s": { "$in": %s } }`, key, value), []any{value} @@ -110,7 +110,7 @@ func Mongo() func(parser *Parser) error { }, { "=out=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { // remove parentheses value = value[1 : len(value)-1] return fmt.Sprintf(`{ "%s": { "$nin": %s } }`, key, value), []any{value} @@ -145,54 +145,52 @@ 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 { - paramIndex := 0 - var operators = []Operator{ { "==", - func(key, value string) (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} }, }, { "!=", - func(key, value string) (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) (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} }, }, { "=ge=", - func(key, value string) (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} }, }, { "=lt=", - func(key, value string) (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} }, }, { "=le=", - func(key, value string) (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} }, }, { "=in=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { // remove parentheses value = value[1 : len(value)-1] // Split values and create placeholders @@ -201,15 +199,15 @@ func SQL() func(parser *Parser) error { placeholders := make([]string, len(values)) for i, v := range values { vals[i] = v - paramIndex++ - placeholders[i] = fmt.Sprintf("$%d", paramIndex) + *paramIndex++ + placeholders[i] = fmt.Sprintf("$%d", *paramIndex) } return fmt.Sprintf(`%s IN (%s)`, key, strings.Join(placeholders, ", ")), vals }, }, { "=out=", - func(key, value string) (string, []any) { + func(key, value string, paramIndex *int) (string, []any) { // remove parentheses value = value[1 : len(value)-1] // Split values and create placeholders @@ -218,8 +216,8 @@ func SQL() func(parser *Parser) error { placeholders := make([]string, len(values)) for i, v := range values { vals[i] = v - paramIndex++ - placeholders[i] = fmt.Sprintf("$%d", paramIndex) + *paramIndex++ + placeholders[i] = fmt.Sprintf("$%d", *paramIndex) } return fmt.Sprintf(`%s NOT IN (%s)`, key, strings.Join(placeholders, ", ")), vals }, @@ -328,6 +326,8 @@ func containsString(ss []string, s string) bool { 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{} @@ -403,7 +403,7 @@ func (parser *Parser) Process(s string, options ...func(*ProcessOptions) error) var opValues []any for _, op := range parser.operators { if operator == op.Operator { - res, opValues = op.Formatter(key, value) + res, opValues = op.Formatter(key, value, ¶mIndex) values = append(values, opValues...) break }