From 00c630965911682d9955a665f203144c88343ff5 Mon Sep 17 00:00:00 2001 From: Anton Zadvorny Date: Sun, 12 May 2024 07:04:35 +0300 Subject: [PATCH] Extract rql package --- .golangci.yml | 42 ++++++++++++++++++++++++ go.mod | 14 ++++++++ go.sum | 18 +++++++++++ parser.go | 42 ++++++++++++++++++++++++ parser_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 .golangci.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 parser.go create mode 100644 parser_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..1744d86 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,42 @@ +run: + timeout: 5m + +linters-settings: + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - paramTypeCombine + - unnamedResult + +linters: + enable: + - dupl + - exportloopref + - gochecknoinits + - gocritic + - gocyclo + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - stylecheck + - typecheck + - unconvert + - unused + fast: false + disable-all: true diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..19e0f42 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module go.pkg.cx/rql + +go 1.21 + +require ( + github.com/alecthomas/participle/v2 v2.1.1 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a9b729c --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..71c364d --- /dev/null +++ b/parser.go @@ -0,0 +1,42 @@ +package rql + +import ( + "github.com/alecthomas/participle/v2" + "github.com/alecthomas/participle/v2/lexer" +) + +var ( + rqlLexer = lexer.MustSimple([]lexer.SimpleRule{ + {Name: "whitespace", Pattern: `\s+`}, + {Name: "String", Pattern: `"[^"]*"`}, + {Name: "Ident", Pattern: `[a-zA-Z_][a-zA-Z_0-9]*`}, + {Name: "Punct", Pattern: `[-[!@#$%^&*()+_={};':",.<>?/]|]`}, + }) + + rqlParser = participle.MustBuild[Query]( + participle.Lexer(rqlLexer), + participle.Unquote("String"), + participle.UseLookahead(2), + ) +) + +func Parse(query string) (*Query, error) { + return rqlParser.ParseString("", query) +} + +type Operator string + +type Values struct { + Single *string `parser:"@String"` + List []string `parser:"| ( '[' @String (',' @String)* ']' )"` +} + +type Predicate struct { + Key string `parser:"@Ident '.'"` + Operator Operator `parser:"@('eq'|'lt'|'lte'|'gt'|'gte'|'like'|'in')"` + Values Values `parser:"'=' @@"` +} + +type Query struct { + Predicates []*Predicate `parser:"@@*"` +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..6dc6df8 --- /dev/null +++ b/parser_test.go @@ -0,0 +1,87 @@ +package rql + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + one := "1" + john := "John" + + cases := []struct { + name string + query string + predicates []*Predicate + errMessage string + }{ + { + name: "Valid Query", + query: `id.eq="1"`, + predicates: []*Predicate{{Key: "id", Operator: "eq", Values: Values{Single: &one}}}, + errMessage: "", + }, + { + name: "Multiple Predicates", + query: `id.eq="1" name.eq="John"`, + predicates: []*Predicate{{Key: "id", Operator: "eq", Values: Values{Single: &one}}, {Key: "name", Operator: "eq", Values: Values{Single: &john}}}, + errMessage: "", + }, + { + name: "List Values", + query: `id.in=["1", "2", "3"]`, + predicates: []*Predicate{ + { + Key: "id", + Operator: "in", + Values: Values{ + List: []string{"1", "2", "3"}, + }, + }, + }, + errMessage: "", + }, + { + name: "Empty Query", + query: "", + predicates: nil, + errMessage: "", + }, + { + name: "Whitespace in Query", + query: ` id . eq = "1" `, + predicates: []*Predicate{{Key: "id", Operator: "eq", Values: Values{Single: &one}}}, + errMessage: "", + }, + { + name: "Missing Key", + query: `.eq="1"`, + predicates: nil, + errMessage: `unexpected token "."`, + }, + { + name: "Invalid Operator", + query: `id.invalid="1"`, + predicates: nil, + errMessage: `unexpected token "invalid"`, + }, + { + name: "Invalid Query", + query: "invalid_query", + predicates: nil, + errMessage: `unexpected token ""`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result, err := Parse(tc.query) + + assert.Equal(t, tc.predicates, result.Predicates) + if tc.errMessage != "" { + assert.ErrorContains(t, err, tc.errMessage) + } + }) + } +}