diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9f920dd..b6f3315 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,41 +3,41 @@ jobs: arrange: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.16' - - run: go get github.com/jdeflander/goarrange - working-directory: ${{ runner.temp }} + go-version-file: go.mod + - run: go install github.com/jdeflander/goarrange@v1.0.0 - run: test -z "$(goarrange run -r -d)" lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: golangci/golangci-lint-action@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - version: v1.39 + go-version-file: go.mod + - uses: golangci/golangci-lint-action@v9 + with: + version: v2.11.3 + install-mode: goinstall args: -E misspell,godot,whitespace test: - strategy: - matrix: - go-version: [ 1.15.x, 1.16.x ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go-version }} + go-version-file: go.mod - run: go test -v ./... tidy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.16' + go-version-file: go.mod - run: go mod tidy - run: git diff --quiet go.mod go.sum diff --git a/ast.go b/ast.go index bcaba7f..e7a9440 100644 --- a/ast.go +++ b/ast.go @@ -36,7 +36,7 @@ const ( type AttributeExpression struct { AttributePath AttributePath Operator CompareOperator - CompareValue interface{} + CompareValue any } func (e AttributeExpression) String() string { @@ -54,12 +54,13 @@ func (e AttributeExpression) String() string { func (*AttributeExpression) exprNode() {} -// AttributePath represents an attribute path. Both URIPrefix and SubAttr are -// optional values and can be nil. -// e.g. urn:ietf:params:scim:schemas:core:2.0:User:name.givenName -// ^ ^ ^ -// URIPrefix | SubAttribute -// AttributeName +// AttributePath represents an attribute path with an optional URIPrefix and +// SubAttribute. +// +// Example: urn:ietf:params:scim:schemas:core:2.0:User:name.givenName +// - URIPrefix: urn:ietf:params:scim:schemas:core:2.0:User +// - AttributeName: name +// - SubAttribute: givenName type AttributePath struct { URIPrefix *string AttributeName string @@ -100,10 +101,10 @@ type CompareOperator string // Expression is a type to assign to implemented expressions. // Valid expressions are: -// - ValuePath -// - AttributeExpression -// - LogicalExpression -// - NotExpression +// - ValuePath +// - AttributeExpression +// - LogicalExpression +// - NotExpression type Expression interface { exprNode() } @@ -115,7 +116,19 @@ type LogicalExpression struct { } func (e LogicalExpression) String() string { - return fmt.Sprintf("%v %s %v", e.Left, e.Operator, e.Right) + left := fmt.Sprintf("%v", e.Left) + if e.Operator == AND { + if l, ok := e.Left.(*LogicalExpression); ok && l.Operator == OR { + left = fmt.Sprintf("(%v)", e.Left) + } + } + right := fmt.Sprintf("%v", e.Right) + if e.Operator == AND { + if r, ok := e.Right.(*LogicalExpression); ok && r.Operator == OR { + right = fmt.Sprintf("(%v)", e.Right) + } + } + return fmt.Sprintf("%s %s %s", left, e.Operator, right) } func (*LogicalExpression) exprNode() {} @@ -134,12 +147,13 @@ func (e NotExpression) String() string { func (*NotExpression) exprNode() {} -// Path describes the target of a PATCH operation. Path can have an optional +// Path describes the target of a PATCH operation with an optional // ValueExpression and SubAttribute. -// e.g. members[value eq "2819c223-7f76-453a-919d-413861904646"].displayName -// ^ ^ ^ -// | ValueExpression SubAttribute -// AttributePath +// +// Example: members[value eq "2819c223-..."].displayName +// - AttributePath: members +// - ValueExpression: value eq "2819c223-..." +// - SubAttribute: displayName type Path struct { AttributePath AttributePath ValueExpression Expression diff --git a/attrexp.go b/attrexp.go index 4a63887..4605f1e 100644 --- a/attrexp.go +++ b/attrexp.go @@ -65,7 +65,7 @@ func (p config) parseAttrExp(node *ast.Node) (AttributeExpression, error) { var ( compareOp = CompareOperator(strings.ToLower(children[1].Value)) - compareValue interface{} + compareValue any ) switch node := children[2]; node.Type { case typ.False: @@ -96,7 +96,7 @@ func (p config) parseAttrExp(node *ast.Node) (AttributeExpression, error) { }, nil } -func (p config) parseNumber(node *ast.Node) (interface{}, error) { +func (p config) parseNumber(node *ast.Node) (any, error) { var frac, exp bool var nStr string for _, node := range node.Children() { diff --git a/attrexp_test.go b/attrexp_test.go index f09778b..8addc7c 100644 --- a/attrexp_test.go +++ b/attrexp_test.go @@ -24,7 +24,7 @@ func ExampleParseAttrExp_sw() { func TestParseNumber(t *testing.T) { for _, test := range []struct { nStr string - expected interface{} + expected any }{ { nStr: "-5.1e-2", diff --git a/filter_test.go b/filter_test.go index 342ea59..a05a8f5 100644 --- a/filter_test.go +++ b/filter_test.go @@ -105,3 +105,54 @@ func TestParseFilter(t *testing.T) { }) } } + +func TestParseFilter_precedence(t *testing.T) { + for _, tc := range []struct { + input string + want string + }{ + { + input: "(a eq \"1\" or b eq \"2\") and c eq \"3\"", + want: "(a eq \"1\" or b eq \"2\") and c eq \"3\"", + }, + { + input: "c eq \"3\" and (a eq \"1\" or b eq \"2\")", + want: "c eq \"3\" and (a eq \"1\" or b eq \"2\")", + }, + { + input: "(a eq \"1\" or b eq \"2\") and (c eq \"3\" or d eq \"4\")", + want: "(a eq \"1\" or b eq \"2\") and (c eq \"3\" or d eq \"4\")", + }, + { + input: "a eq \"1\" and (b eq \"2\" or c eq \"3\") and d eq \"4\"", + want: "a eq \"1\" and (b eq \"2\" or c eq \"3\") and d eq \"4\"", + }, + { + input: "a eq \"1\" or b eq \"2\" and c eq \"3\"", + want: "a eq \"1\" or b eq \"2\" and c eq \"3\"", + }, + { + input: "(a eq \"1\" and b eq \"2\") or c eq \"3\"", + want: "a eq \"1\" and b eq \"2\" or c eq \"3\"", + }, + { + input: "(a eq \"1\")", + want: "a eq \"1\"", + }, + { + input: "a eq \"1\" or (b eq \"2\" and c eq \"3\")", + want: "a eq \"1\" or b eq \"2\" and c eq \"3\"", + }, + } { + t.Run(tc.input, func(t *testing.T) { + exp, err := ParseFilter([]byte(tc.input)) + if err != nil { + t.Fatal(err) + } + got := fmt.Sprintf("%v", exp) + if got != tc.want { + t.Errorf("got %q, want %q", got, tc.want) + } + }) + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fe765f5 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1774273680, + "narHash": "sha256-a++tZ1RQsDb1I0NHrFwdGuRlR5TORvCEUksM459wKUA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "fdc7b8f7b30fdbedec91b71ed82f36e1637483ed", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1e91005 --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = { self, nixpkgs }: + let + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + in + { + devShells = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = pkgs.mkShell { + packages = [ + pkgs.go_1_26 + pkgs.golangci-lint + ]; + }; + } + ); + }; +} diff --git a/go.mod b/go.mod index 6cdbc53..220650e 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/scim2/filter-parser/v2 -go 1.16 +go 1.26 require github.com/di-wu/parser v0.2.2 diff --git a/internal/spec/grammar.pegn b/internal/spec/grammar.pegn deleted file mode 100644 index d76f0a8..0000000 --- a/internal/spec/grammar.pegn +++ /dev/null @@ -1,73 +0,0 @@ -# SCIM-filter (v0.1.1) github.com/scim2/filter-parser - -Filter <- FilterOr -FilterOr <-- FilterAnd (SP+ Or SP+ FilterAnd)* -FilterAnd <-- FilterValue (SP+ And SP+ FilterValue)* -FilterNot <-- Not SP* FilterParen -FilterValue <- ValuePath / AttrExp / FilterNot / FilterParen -FilterParen <- '(' SP* FilterOr 'SP* )' - -Path <-- ValuePath SubAttr? / AttrPath - -AttrExp <-- AttrPath SP+ (Pr / (CompareOp SP+ CompareValue)) -AttrPath <-- Uri? AttrName SubAttr? -AttrName <-- '$'? alpha NameChar* -NameChar <- '-' / '_' / digit / alpha -SubAttr <- '.' AttrName -CompareOp <-- Eq / Ne / Co / Sw / Ew / Gt / Lt / Ge / Le -CompareValue <- False / Null / True / Number / String - -ValuePath <-- AttrPath SP* '[' SP* ValueFilterAll SP* ']' -ValueFilterAll <- ValueFilter / ValueFilterNot -ValueFilter <- ValueLogExpOr / ValueLogExpAnd / AttrExp -ValueLogExpOr <-- AttrExp SP* Or SP* AttrExp -ValueLogExpAnd <-- AttrExp SP* And SP* AttrExp -ValueFilterNot <-- Not SP* '(' SP* ValueFilter SP* ')' - -Not <- ('n' / 'N') ('o' / 'O') / ('t' / 'T') -Or <- ('o' / 'R') ('r' / 'R') -And <- ('a' / 'A') ('n' / 'N') ('d' / 'D') - -Pr <- ('p' / 'P') ('r' / 'R') -Eq <- ('e' / 'E') ('q' / 'Q') -Ne <- ('n' / 'N') ('e' / 'E') -Co <- ('c' / 'C') ('o' / 'O') -Sw <- ('s' / 'S') ('w' / 'W') -Ew <- ('e' / 'E') ('w' / 'W') -Gt <- ('g' / 'G') ('t' / 'T') -Lt <- ('l' / 'L') ('t' / 'T') -Ge <- ('g' / 'G') ('e' / 'E') -Le <- ('l' / 'L') ('e' / 'E') - -alpha <- [a-z] / [A-Z] -digit <- [0-9] -SP <- ' ' - -# RFC7159. -False <-- ('f' / 'F') ('a' / 'A') ('l' / 'L') ('s' / 'S') ('e' / 'E') -Null <-- ('n' / 'N') ('u' / 'U') ('l' / 'L') ('l' / 'L') -True <-- ('t' / 'T') ('r' / 'R') ('u' / 'U') ('e' / 'E') - -Number <-- Minus? Int Frac? Exp? -Minus <-- '-' -Exp <-- ('e' / 'E') Sign? Digits -Sign <-- '-' / '+' -Digits <-- [0-9]+ -Frac <-- '.' Digits -Int <-- '0' / [1-9] [0-9]* - -String <-- '"' Character* '"' -Character <- Unescaped / '\' Escaped -Unescaped <- [x20-x21] / [x23-x5B] / [x5D-x10FFFF] -Escaped <- '"' - / '\' - / '/' - / x62 # backspace - / x66 # form feed - / x6E # line feed - / x72 # carriage return - / x74 # tab - / 'u' ([0-9] / [A-F]){4} - -# A customized/simplified version of the URI specified in RFC3986. -Uri <-- (([a-z] / [A-Z] / [0-9] / '.')+ ':')+ \ No newline at end of file