├── .github └── workflows │ └── pull_request.yml ├── LICENSE ├── README.md ├── ast.go ├── attrexp.go ├── attrexp_test.go ├── attrpath.go ├── attrpath_test.go ├── config.go ├── errors.go ├── filter.go ├── filter_test.go ├── go.mod ├── go.sum ├── internal ├── grammar │ ├── attrexp.go │ ├── classes.go │ ├── filter.go │ ├── filter_test.go │ ├── number.go │ ├── number_test.go │ ├── path.go │ ├── path_test.go │ ├── string.go │ ├── string_test.go │ ├── tokens.go │ ├── uri.go │ ├── uri_test.go │ ├── valuepath.go │ ├── values.go │ └── values_test.go ├── spec │ ├── grammar.abnf │ └── grammar.pegn └── types │ └── types.go ├── path.go ├── path_test.go ├── valuepath.go └── valuepath_test.go /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | jobs: 3 | arrange: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v2 7 | - uses: actions/setup-go@v2 8 | with: 9 | go-version: '1.16' 10 | - run: go get github.com/jdeflander/goarrange 11 | working-directory: ${{ runner.temp }} 12 | - run: test -z "$(goarrange run -r -d)" 13 | 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: golangci/golangci-lint-action@v2 19 | with: 20 | version: v1.39 21 | args: -E misspell,godot,whitespace 22 | 23 | test: 24 | strategy: 25 | matrix: 26 | go-version: [ 1.15.x, 1.16.x ] 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: actions/setup-go@v2 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | - run: go test -v ./... 34 | 35 | tidy: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions/setup-go@v2 40 | with: 41 | go-version: '1.16' 42 | - run: go mod tidy 43 | - run: git diff --quiet go.mod go.sum 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Quint Daenen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/scim2/filter-parser)](https://goreportcard.com/report/github.com/scim2/filter-parser) 2 | [![GoDoc](https://godoc.org/github.com/scim2/filter-parser?status.svg)](https://godoc.org/github.com/scim2/filter-parser) 3 | 4 | # Query Filter Parser for SCIM v2.0 5 | 6 | [RFC7644: Section-3.4.2.2](https://tools.ietf.org/html/rfc7644#section-3.4.2.2) 7 | 8 | ## Implemented Operators 9 | 10 | ### Attribute Operators 11 | 12 | - [x] eq, ne, co, sw, ew, gt, ge, lt, le 13 | - [x] pr 14 | 15 | ### Logical Operators 16 | 17 | - [x] and, or 18 | - [x] not 19 | - [x] precedence 20 | 21 | ### Grouping Operators 22 | 23 | - [x] ( ) 24 | - [x] [ ] 25 | 26 | ## Case Sensitivity 27 | 28 | Attribute names and attribute operators used in filters are case insensitive. 29 | For example, the following two expressions will evaluate to the same logical value: 30 | 31 | ``` 32 | filter=userName Eq "john" 33 | filter=Username eq "john" 34 | ``` 35 | 36 | ## Expressions Requirements 37 | 38 | Each expression MUST contain an attribute name followed by an attribute operator and optional value. 39 | 40 | Multiple expressions MAY be combined using logical operators. 41 | 42 | Expressions MAY be grouped together using round brackets "(" and ")". 43 | 44 | Filters MUST be evaluated using the following order of operations, in order of precedence: 45 | 46 | 1. Grouping operators 47 | 2. Attribute operators 48 | 3. Logical operators - where "not" takes precedence over "and", 49 | which takes precedence over "or" 50 | -------------------------------------------------------------------------------- /ast.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | // PR is an abbreviation for 'present'. 9 | PR CompareOperator = "pr" 10 | // EQ is an abbreviation for 'equals'. 11 | EQ CompareOperator = "eq" 12 | // NE is an abbreviation for 'not equals'. 13 | NE CompareOperator = "ne" 14 | // CO is an abbreviation for 'contains'. 15 | CO CompareOperator = "co" 16 | // SW is an abbreviation for 'starts with'. 17 | SW CompareOperator = "sw" 18 | // EW an abbreviation for 'ends with'. 19 | EW CompareOperator = "ew" 20 | // GT is an abbreviation for 'greater than'. 21 | GT CompareOperator = "gt" 22 | // LT is an abbreviation for 'less than'. 23 | LT CompareOperator = "lt" 24 | // GE is an abbreviation for 'greater or equal than'. 25 | GE CompareOperator = "ge" 26 | // LE is an abbreviation for 'less or equal than'. 27 | LE CompareOperator = "le" 28 | 29 | // AND is the logical operation and (&&). 30 | AND LogicalOperator = "and" 31 | // OR is the logical operation or (||). 32 | OR LogicalOperator = "or" 33 | ) 34 | 35 | // AttributeExpression represents an attribute expression/filter. 36 | type AttributeExpression struct { 37 | AttributePath AttributePath 38 | Operator CompareOperator 39 | CompareValue interface{} 40 | } 41 | 42 | func (e AttributeExpression) String() string { 43 | s := fmt.Sprintf("%v %s", e.AttributePath, e.Operator) 44 | if e.CompareValue != nil { 45 | switch e.CompareValue.(type) { 46 | case string: 47 | s += fmt.Sprintf(" %q", e.CompareValue) 48 | default: 49 | s += fmt.Sprintf(" %v", e.CompareValue) 50 | } 51 | } 52 | return s 53 | } 54 | 55 | func (*AttributeExpression) exprNode() {} 56 | 57 | // AttributePath represents an attribute path. Both URIPrefix and SubAttr are 58 | // optional values and can be nil. 59 | // e.g. urn:ietf:params:scim:schemas:core:2.0:User:name.givenName 60 | // ^ ^ ^ 61 | // URIPrefix | SubAttribute 62 | // AttributeName 63 | type AttributePath struct { 64 | URIPrefix *string 65 | AttributeName string 66 | SubAttribute *string 67 | } 68 | 69 | func (p AttributePath) String() string { 70 | s := p.AttributeName 71 | if p.URIPrefix != nil { 72 | s = fmt.Sprintf("%s:%s", p.URI(), s) 73 | } 74 | if p.SubAttribute != nil { 75 | s = fmt.Sprintf("%s.%s", s, p.SubAttributeName()) 76 | } 77 | return s 78 | } 79 | 80 | // SubAttributeName returns the sub attribute name if present. 81 | // Returns an empty string otherwise. 82 | func (p *AttributePath) SubAttributeName() string { 83 | if p.SubAttribute != nil { 84 | return *p.SubAttribute 85 | } 86 | return "" 87 | } 88 | 89 | // URI returns the URI if present. 90 | // Returns an empty string otherwise. 91 | func (p *AttributePath) URI() string { 92 | if p.URIPrefix != nil { 93 | return *p.URIPrefix 94 | } 95 | return "" 96 | } 97 | 98 | // CompareOperator represents a compare operation. 99 | type CompareOperator string 100 | 101 | // Expression is a type to assign to implemented expressions. 102 | // Valid expressions are: 103 | // - ValuePath 104 | // - AttributeExpression 105 | // - LogicalExpression 106 | // - NotExpression 107 | type Expression interface { 108 | exprNode() 109 | } 110 | 111 | // LogicalExpression represents an 'and' / 'or' node. 112 | type LogicalExpression struct { 113 | Left, Right Expression 114 | Operator LogicalOperator 115 | } 116 | 117 | func (e LogicalExpression) String() string { 118 | return fmt.Sprintf("%v %s %v", e.Left, e.Operator, e.Right) 119 | } 120 | 121 | func (*LogicalExpression) exprNode() {} 122 | 123 | // LogicalOperator represents a logical operation such as 'and' / 'or'. 124 | type LogicalOperator string 125 | 126 | // NotExpression represents an 'not' node. 127 | type NotExpression struct { 128 | Expression Expression 129 | } 130 | 131 | func (e NotExpression) String() string { 132 | return fmt.Sprintf("not(%v)", e.Expression) 133 | } 134 | 135 | func (*NotExpression) exprNode() {} 136 | 137 | // Path describes the target of a PATCH operation. Path can have an optional 138 | // ValueExpression and SubAttribute. 139 | // e.g. members[value eq "2819c223-7f76-453a-919d-413861904646"].displayName 140 | // ^ ^ ^ 141 | // | ValueExpression SubAttribute 142 | // AttributePath 143 | type Path struct { 144 | AttributePath AttributePath 145 | ValueExpression Expression 146 | SubAttribute *string 147 | } 148 | 149 | func (p Path) String() string { 150 | s := p.AttributePath.String() 151 | if p.ValueExpression != nil { 152 | s += fmt.Sprintf("[%s]", p.ValueExpression) 153 | } 154 | if p.SubAttribute != nil { 155 | s += fmt.Sprintf(".%s", *p.SubAttribute) 156 | } 157 | return s 158 | } 159 | 160 | // SubAttributeName returns the sub attribute name if present. 161 | // Returns an empty string otherwise. 162 | func (p *Path) SubAttributeName() string { 163 | if p.SubAttribute != nil { 164 | return *p.SubAttribute 165 | } 166 | return "" 167 | } 168 | 169 | // ValuePath represents a filter on a attribute path. 170 | type ValuePath struct { 171 | AttributePath AttributePath 172 | ValueFilter Expression 173 | } 174 | 175 | func (e ValuePath) String() string { 176 | return fmt.Sprintf("%v[%v]", e.AttributePath, e.ValueFilter) 177 | } 178 | 179 | func (*ValuePath) exprNode() {} 180 | -------------------------------------------------------------------------------- /attrexp.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/di-wu/parser" 7 | "github.com/di-wu/parser/ast" 8 | "github.com/scim2/filter-parser/v2/internal/grammar" 9 | "github.com/scim2/filter-parser/v2/internal/types" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // ParseAttrExp parses the given raw data as an AttributeExpression. 15 | func ParseAttrExp(raw []byte) (AttributeExpression, error) { 16 | return parseAttrExp(raw, config{}) 17 | } 18 | 19 | // ParseAttrExpNumber parses the given raw data as an AttributeExpression with json.Number. 20 | func ParseAttrExpNumber(raw []byte) (AttributeExpression, error) { 21 | return parseAttrExp(raw, config{useNumber: true}) 22 | } 23 | 24 | func parseAttrExp(raw []byte, c config) (AttributeExpression, error) { 25 | p, err := ast.New(raw) 26 | if err != nil { 27 | return AttributeExpression{}, err 28 | } 29 | node, err := grammar.AttrExp(p) 30 | if err != nil { 31 | return AttributeExpression{}, err 32 | } 33 | if _, err := p.Expect(parser.EOD); err != nil { 34 | return AttributeExpression{}, err 35 | } 36 | return c.parseAttrExp(node) 37 | } 38 | 39 | func (p config) parseAttrExp(node *ast.Node) (AttributeExpression, error) { 40 | if node.Type != typ.AttrExp { 41 | return AttributeExpression{}, invalidTypeError(typ.AttrExp, node.Type) 42 | } 43 | 44 | children := node.Children() 45 | if len(children) == 0 { 46 | return AttributeExpression{}, invalidLengthError(typ.AttrExp, 1, 0) 47 | } 48 | 49 | // AttrPath 'pr' 50 | attrPath, err := parseAttrPath(children[0]) 51 | if err != nil { 52 | return AttributeExpression{}, err 53 | } 54 | 55 | if len(children) == 1 { 56 | return AttributeExpression{ 57 | AttributePath: attrPath, 58 | Operator: PR, 59 | }, nil 60 | } 61 | 62 | if l := len(children); l != 3 { 63 | return AttributeExpression{}, invalidLengthError(typ.AttrExp, 3, l) 64 | } 65 | 66 | var ( 67 | compareOp = CompareOperator(strings.ToLower(children[1].Value)) 68 | compareValue interface{} 69 | ) 70 | switch node := children[2]; node.Type { 71 | case typ.False: 72 | compareValue = false 73 | case typ.Null: 74 | compareValue = nil 75 | case typ.True: 76 | compareValue = true 77 | case typ.Number: 78 | value, err := p.parseNumber(node) 79 | if err != nil { 80 | return AttributeExpression{}, err 81 | } 82 | compareValue = value 83 | case typ.String: 84 | str := node.Value 85 | str = strings.TrimPrefix(str, "\"") 86 | str = strings.TrimSuffix(str, "\"") 87 | compareValue = str 88 | default: 89 | return AttributeExpression{}, invalidChildTypeError(typ.AttrExp, node.Type) 90 | } 91 | 92 | return AttributeExpression{ 93 | AttributePath: attrPath, 94 | Operator: compareOp, 95 | CompareValue: compareValue, 96 | }, nil 97 | } 98 | 99 | func (p config) parseNumber(node *ast.Node) (interface{}, error) { 100 | var frac, exp bool 101 | var nStr string 102 | for _, node := range node.Children() { 103 | switch t := node.Type; t { 104 | case typ.Minus: 105 | nStr = "-" 106 | case typ.Int: 107 | nStr += node.Value 108 | case typ.Frac: 109 | frac = true 110 | children := node.Children() 111 | if l := len(children); l != 1 { 112 | return AttributeExpression{}, invalidLengthError(typ.Frac, 1, l) 113 | } 114 | nStr += fmt.Sprintf(".%s", children[0].Value) 115 | case typ.Exp: 116 | exp = true 117 | nStr += "e" 118 | for _, node := range node.Children() { 119 | switch t := node.Type; t { 120 | case typ.Sign, typ.Digits: 121 | nStr += node.Value 122 | default: 123 | return AttributeExpression{}, invalidChildTypeError(typ.Number, node.Type) 124 | } 125 | } 126 | default: 127 | return AttributeExpression{}, invalidChildTypeError(typ.Number, node.Type) 128 | } 129 | } 130 | 131 | if p.useNumber { 132 | return json.Number(nStr), nil 133 | } 134 | 135 | f, err := strconv.ParseFloat(nStr, 64) 136 | if err != nil { 137 | return AttributeExpression{}, &internalError{ 138 | Message: err.Error(), 139 | } 140 | } 141 | 142 | // Integers can not contain fractional or exponent parts. 143 | // More info: https://tools.ietf.org/html/rfc7643#section-2.3.4 144 | if !frac && !exp { 145 | return int(f), nil 146 | } 147 | return f, err 148 | } 149 | -------------------------------------------------------------------------------- /attrexp_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/di-wu/parser/ast" 7 | "github.com/scim2/filter-parser/v2/internal/grammar" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func ExampleParseAttrExp_pr() { 13 | fmt.Println(ParseAttrExp([]byte("userName pr"))) 14 | // Output: 15 | // userName pr 16 | } 17 | 18 | func ExampleParseAttrExp_sw() { 19 | fmt.Println(ParseAttrExp([]byte("userName sw \"J\""))) 20 | // Output: 21 | // userName sw "J" 22 | } 23 | 24 | func TestParseNumber(t *testing.T) { 25 | for _, test := range []struct { 26 | nStr string 27 | expected interface{} 28 | }{ 29 | { 30 | nStr: "-5.1e-2", 31 | expected: -0.051, 32 | }, 33 | { 34 | nStr: "-5.1e2", 35 | expected: float64(-510), 36 | }, 37 | { 38 | nStr: "-510", 39 | expected: -510, 40 | }, 41 | } { 42 | t.Run(test.nStr, func(t *testing.T) { 43 | p, _ := ast.New([]byte(test.nStr)) 44 | n, err := grammar.Number(p) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | { // Empty config. 49 | i, err := config{}.parseNumber(n) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | if i != test.expected { 54 | t.Error(test.expected, i) 55 | } 56 | } 57 | { // Config with useNumber = true. 58 | d := json.NewDecoder(strings.NewReader(test.nStr)) 59 | d.UseNumber() 60 | var number json.Number 61 | if err := d.Decode(&number); err != nil { 62 | t.Error(err) 63 | } 64 | 65 | i, err := config{ 66 | useNumber: true, 67 | }.parseNumber(n) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | if i != json.Number(test.nStr) { 72 | t.Error(test.nStr, i) 73 | } 74 | 75 | // Check if equal to json.Decode. 76 | if i != number { 77 | t.Error(number, i) 78 | } 79 | } 80 | { // Config with useNumber = true. 81 | d := json.NewDecoder(strings.NewReader(test.nStr)) 82 | d.UseNumber() 83 | var number json.Number 84 | if err := d.Decode(&number); err != nil { 85 | t.Error(err) 86 | } 87 | 88 | i, err := config{ 89 | useNumber: true, 90 | }.parseNumber(n) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | if i != json.Number(test.nStr) { 95 | t.Error(test.nStr, i) 96 | } 97 | 98 | // Check if equal to json.Decode. 99 | if i != number { 100 | t.Error(number, i) 101 | } 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /attrpath.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/scim2/filter-parser/v2/internal/grammar" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | "strings" 9 | ) 10 | 11 | // ParseAttrPath parses the given raw data as an AttributePath. 12 | func ParseAttrPath(raw []byte) (AttributePath, error) { 13 | p, err := ast.New(raw) 14 | if err != nil { 15 | return AttributePath{}, err 16 | } 17 | node, err := grammar.AttrPath(p) 18 | if err != nil { 19 | return AttributePath{}, err 20 | } 21 | if _, err := p.Expect(parser.EOD); err != nil { 22 | return AttributePath{}, err 23 | } 24 | return parseAttrPath(node) 25 | } 26 | 27 | func parseAttrPath(node *ast.Node) (AttributePath, error) { 28 | if node.Type != typ.AttrPath { 29 | return AttributePath{}, invalidTypeError(typ.AttrPath, node.Type) 30 | } 31 | 32 | // Indicates whether we encountered an attribute name. 33 | // These are the minimum requirements of an attribute path. 34 | var valid bool 35 | 36 | var attrPath AttributePath 37 | for _, node := range node.Children() { 38 | switch t := node.Type; t { 39 | case typ.URI: 40 | uri := node.Value 41 | uri = strings.TrimSuffix(uri, ":") 42 | attrPath.URIPrefix = &uri 43 | case typ.AttrName: 44 | name := node.Value 45 | if attrPath.AttributeName == "" { 46 | attrPath.AttributeName = name 47 | 48 | valid = true 49 | } else { 50 | attrPath.SubAttribute = &name 51 | } 52 | default: 53 | return AttributePath{}, invalidChildTypeError(typ.AttrPath, t) 54 | } 55 | } 56 | 57 | if !valid { 58 | return AttributePath{}, missingValueError(typ.AttrPath, typ.AttrName) 59 | } 60 | return attrPath, nil 61 | } 62 | -------------------------------------------------------------------------------- /attrpath_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "fmt" 4 | 5 | func ExampleParseAttrPath() { 6 | fmt.Println(ParseAttrPath([]byte("urn:ietf:params:scim:schemas:core:2.0:User:name.familyName"))) 7 | // Output: 8 | // urn:ietf:params:scim:schemas:core:2.0:User:name.familyName 9 | } 10 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | // config represents the internal config of the parser functions. 4 | type config struct { 5 | // useNumber indicates that json.Number needs to be returned instead of int/float64 values. 6 | useNumber bool 7 | } 8 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "github.com/scim2/filter-parser/v2/internal/types" 6 | ) 7 | 8 | func invalidChildTypeError(parentTyp, invalidType int) error { 9 | return &internalError{ 10 | Message: fmt.Sprintf( 11 | "invalid child type for %s (%03d): %s (%03d)", 12 | typ.Stringer[parentTyp], parentTyp, 13 | typ.Stringer[invalidType], invalidType, 14 | ), 15 | } 16 | } 17 | 18 | func invalidLengthError(parentTyp int, len, actual int) error { 19 | return &internalError{ 20 | Message: fmt.Sprintf( 21 | "length was not equal to %d for %s (%03d), got %d elements", 22 | len, typ.Stringer[parentTyp], parentTyp, actual, 23 | ), 24 | } 25 | } 26 | 27 | func invalidTypeError(expected, actual int) error { 28 | return &internalError{ 29 | Message: fmt.Sprintf( 30 | "invalid type: expected %s (%03d), actual %s (%03d)", 31 | typ.Stringer[expected], expected, 32 | typ.Stringer[actual], actual, 33 | ), 34 | } 35 | } 36 | 37 | func missingValueError(parentTyp int, valueType int) error { 38 | return &internalError{ 39 | Message: fmt.Sprintf( 40 | "missing a required value for %s (%03d): %s (%03d)", 41 | typ.Stringer[parentTyp], parentTyp, 42 | typ.Stringer[valueType], valueType, 43 | ), 44 | } 45 | } 46 | 47 | // internalError represents an internal error. If this error should NEVER occur. 48 | // If you get this error, please open an issue! 49 | type internalError struct { 50 | Message string 51 | } 52 | 53 | func (e *internalError) Error() string { 54 | return fmt.Sprintf("internal error: %s", e.Message) 55 | } 56 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/scim2/filter-parser/v2/internal/grammar" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | // ParseFilter parses the given raw data as an Expression. 11 | func ParseFilter(raw []byte) (Expression, error) { 12 | return parseFilter(raw, config{}) 13 | } 14 | 15 | // ParseFilterNumber parses the given raw data as an Expression with json.Number. 16 | func ParseFilterNumber(raw []byte) (Expression, error) { 17 | return parseFilter(raw, config{useNumber: true}) 18 | } 19 | 20 | func parseFilter(raw []byte, c config) (Expression, error) { 21 | p, err := ast.New(raw) 22 | if err != nil { 23 | return nil, err 24 | } 25 | node, err := grammar.Filter(p) 26 | if err != nil { 27 | return nil, err 28 | } 29 | if _, err := p.Expect(parser.EOD); err != nil { 30 | return nil, err 31 | } 32 | return c.parseFilterOr(node) 33 | } 34 | 35 | func (p config) parseFilterAnd(node *ast.Node) (Expression, error) { 36 | if node.Type != typ.FilterAnd { 37 | return nil, invalidTypeError(typ.FilterAnd, node.Type) 38 | } 39 | 40 | children := node.Children() 41 | if len(children) == 0 { 42 | return nil, invalidLengthError(typ.FilterAnd, 1, 0) 43 | } 44 | 45 | if len(children) == 1 { 46 | return p.parseFilterValue(children[0]) 47 | } 48 | 49 | var and LogicalExpression 50 | for _, node := range children { 51 | exp, err := p.parseFilterValue(node) 52 | if err != nil { 53 | return nil, err 54 | } 55 | switch { 56 | case and.Left == nil: 57 | and.Left = exp 58 | case and.Right == nil: 59 | and.Right = exp 60 | and.Operator = AND 61 | default: 62 | and = LogicalExpression{ 63 | Left: &LogicalExpression{ 64 | Left: and.Left, 65 | Right: and.Right, 66 | Operator: AND, 67 | }, 68 | Right: exp, 69 | Operator: AND, 70 | } 71 | } 72 | } 73 | return &and, nil 74 | } 75 | 76 | func (p config) parseFilterOr(node *ast.Node) (Expression, error) { 77 | if node.Type != typ.FilterOr { 78 | return nil, invalidTypeError(typ.FilterOr, node.Type) 79 | } 80 | 81 | children := node.Children() 82 | if len(children) == 0 { 83 | return nil, invalidLengthError(typ.FilterOr, 1, 0) 84 | } 85 | 86 | if len(children) == 1 { 87 | return p.parseFilterAnd(children[0]) 88 | } 89 | 90 | var or LogicalExpression 91 | for _, node := range children { 92 | exp, err := p.parseFilterAnd(node) 93 | if err != nil { 94 | return nil, err 95 | } 96 | switch { 97 | case or.Left == nil: 98 | or.Left = exp 99 | case or.Right == nil: 100 | or.Right = exp 101 | or.Operator = OR 102 | default: 103 | or = LogicalExpression{ 104 | Left: &LogicalExpression{ 105 | Left: or.Left, 106 | Right: or.Right, 107 | Operator: OR, 108 | }, 109 | Right: exp, 110 | Operator: OR, 111 | } 112 | } 113 | } 114 | return &or, nil 115 | } 116 | 117 | func (p config) parseFilterValue(node *ast.Node) (Expression, error) { 118 | switch t := node.Type; t { 119 | case typ.ValuePath: 120 | valuePath, err := p.parseValuePath(node) 121 | if err != nil { 122 | return nil, err 123 | } 124 | return &valuePath, nil 125 | case typ.AttrExp: 126 | attrExp, err := p.parseAttrExp(node) 127 | if err != nil { 128 | return nil, err 129 | } 130 | return &attrExp, nil 131 | case typ.FilterNot: 132 | children := node.Children() 133 | if l := len(children); l != 1 { 134 | return nil, invalidLengthError(typ.FilterNot, 1, l) 135 | } 136 | 137 | exp, err := p.parseFilterOr(children[0]) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return &NotExpression{ 142 | Expression: exp, 143 | }, nil 144 | case typ.FilterOr: 145 | return p.parseFilterOr(node) 146 | default: 147 | return nil, invalidChildTypeError(typ.FilterAnd, t) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func ExampleParseFilter_and() { 9 | fmt.Println(ParseFilter([]byte("title pr and userType eq \"Employee\""))) 10 | // Output: 11 | // title pr and userType eq "Employee" 12 | } 13 | 14 | func ExampleParseFilter_attrExp() { 15 | fmt.Println(ParseFilter([]byte("schemas eq \"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\""))) 16 | // Output: 17 | // schemas eq "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" 18 | } 19 | 20 | func ExampleParseFilter_caseInsensitivity() { 21 | fmt.Println(ParseFilter([]byte("NAME PR AND NOT (FIRST EQ \"test\") AND ANOTHER NE \"test\""))) 22 | // Output: 23 | // NAME pr and not(FIRST eq "test") and ANOTHER ne "test" 24 | } 25 | 26 | func ExampleParseFilter_not() { 27 | fmt.Println(ParseFilter([]byte("not (emails co \"example.com\" or emails.value co \"example.org\")"))) 28 | // Output: 29 | // not(emails co "example.com" or emails.value co "example.org") 30 | } 31 | 32 | func ExampleParseFilter_or() { 33 | fmt.Println(ParseFilter([]byte("title pr or userType eq \"Intern\""))) 34 | // Output: 35 | // title pr or userType eq "Intern" 36 | } 37 | 38 | func ExampleParseFilter_parentheses() { 39 | fmt.Println(ParseFilter([]byte("(emails.type eq \"work\")"))) 40 | // Output: 41 | // emails.type eq "work" 42 | } 43 | 44 | func ExampleParseFilter_valuePath() { 45 | fmt.Println(ParseFilter([]byte("emails[type eq \"work\" and value co \"@example.com\"]"))) 46 | // Output: 47 | // emails[type eq "work" and value co "@example.com"] 48 | } 49 | 50 | func Example_walk() { 51 | expression, _ := ParseFilter([]byte("emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]")) 52 | var walk func(e Expression) error 53 | walk = func(e Expression) error { 54 | switch v := e.(type) { 55 | case *LogicalExpression: 56 | _ = walk(v.Left) 57 | _ = walk(v.Right) 58 | case *ValuePath: 59 | _ = walk(v.ValueFilter) 60 | case *AttributeExpression: 61 | fmt.Printf("%s %s %q\n", v.AttributePath, v.Operator, v.CompareValue) 62 | default: 63 | // etc... 64 | } 65 | return nil 66 | } 67 | _ = walk(expression) 68 | // Output: 69 | // type eq "work" 70 | // value co "@example.com" 71 | // type eq "xmpp" 72 | // value co "@foo.com" 73 | } 74 | 75 | func TestParseFilter(t *testing.T) { 76 | for _, example := range []string{ 77 | "userName eq \"bjensen\"", 78 | "userName Eq \"bjensen\"", 79 | "name.familyName co \"O'Malley\"", 80 | "userName sw \"J\"", 81 | "urn:ietf:params:scim:schemas:core:2.0:User:userName sw \"J\"", 82 | "title pr", 83 | "meta.lastModified gt \"2011-05-13T04:42:34Z\"", 84 | "meta.lastModified ge \"2011-05-13T04:42:34Z\"", 85 | "meta.lastModified lt \"2011-05-13T04:42:34Z\"", 86 | "meta.lastModified le \"2011-05-13T04:42:34Z\"", 87 | "title pr and userType eq \"Employee\"", 88 | "title pr or userType eq \"Intern\"", 89 | "schemas eq \"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\"", 90 | "userType eq \"Employee\" and (emails co \"example.com\" or emails.value co \"example.org\")", 91 | "userType ne \"Employee\" and not (emails co \"example.com\" or emails.value co \"example.org\")", 92 | "userType eq \"Employee\" and (emails.type eq \"work\")", 93 | "userType eq \"Employee\" and emails[type eq \"work\" and value co \"@example.com\"]", 94 | "emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]", 95 | 96 | "name pr and userName pr and title pr", 97 | "name pr and not (first eq \"test\") and another ne \"test\"", 98 | "NAME PR AND NOT (FIRST EQ \"test\") AND ANOTHER NE \"test\"", 99 | "name pr or userName pr or title pr", 100 | } { 101 | t.Run(example, func(t *testing.T) { 102 | if _, err := ParseFilter([]byte(example)); err != nil { 103 | t.Error(err) 104 | } 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scim2/filter-parser/v2 2 | 3 | go 1.16 4 | 5 | require github.com/di-wu/parser v0.2.2 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/di-wu/parser v0.2.2 h1:I9oHJ8spBXOeL7Wps0ffkFFFiXJf/pk7NX9lcAMqRMU= 2 | github.com/di-wu/parser v0.2.2/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= 3 | -------------------------------------------------------------------------------- /internal/grammar/attrexp.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/di-wu/parser/op" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | func AttrExp(p *ast.Parser) (*ast.Node, error) { 11 | return p.Expect(ast.Capture{ 12 | Type: typ.AttrExp, 13 | TypeStrings: typ.Stringer, 14 | Value: op.And{ 15 | AttrPath, 16 | op.MinOne(SP), 17 | op.Or{ 18 | parser.CheckStringCI("pr"), 19 | op.And{ 20 | CompareOp, 21 | op.MinOne(SP), 22 | CompareValue, 23 | }, 24 | }, 25 | }, 26 | }) 27 | } 28 | 29 | func AttrName(p *ast.Parser) (*ast.Node, error) { 30 | return p.Expect(ast.Capture{ 31 | Type: typ.AttrName, 32 | TypeStrings: typ.Stringer, 33 | Value: op.And{ 34 | op.Optional('$'), 35 | Alpha, 36 | op.MinZero(NameChar), 37 | }, 38 | }) 39 | } 40 | 41 | func AttrPath(p *ast.Parser) (*ast.Node, error) { 42 | return p.Expect(ast.Capture{ 43 | Type: typ.AttrPath, 44 | TypeStrings: typ.Stringer, 45 | Value: op.And{ 46 | op.Optional(URI), 47 | AttrName, 48 | op.Optional(SubAttr), 49 | }, 50 | }) 51 | } 52 | 53 | func CompareOp(p *ast.Parser) (*ast.Node, error) { 54 | return p.Expect(ast.Capture{ 55 | Type: typ.CompareOp, 56 | TypeStrings: typ.Stringer, 57 | Value: op.Or{ 58 | parser.CheckStringCI("eq"), 59 | parser.CheckStringCI("ne"), 60 | parser.CheckStringCI("co"), 61 | parser.CheckStringCI("sw"), 62 | parser.CheckStringCI("ew"), 63 | parser.CheckStringCI("gt"), 64 | parser.CheckStringCI("lt"), 65 | parser.CheckStringCI("ge"), 66 | parser.CheckStringCI("le"), 67 | }, 68 | }) 69 | } 70 | 71 | func CompareValue(p *ast.Parser) (*ast.Node, error) { 72 | return p.Expect(op.Or{False, Null, True, Number, String}) 73 | } 74 | 75 | func NameChar(p *ast.Parser) (*ast.Node, error) { 76 | return p.Expect(op.Or{'-', '_', Digit, Alpha}) 77 | } 78 | 79 | func SubAttr(p *ast.Parser) (*ast.Node, error) { 80 | return p.Expect(op.And{'.', AttrName}) 81 | } 82 | -------------------------------------------------------------------------------- /internal/grammar/classes.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/op" 6 | ) 7 | 8 | func Alpha(p *parser.Parser) (*parser.Cursor, bool) { 9 | return p.Check(op.Or{ 10 | parser.CheckRuneRange('A', 'Z'), 11 | parser.CheckRuneRange('a', 'z'), 12 | }) 13 | } 14 | 15 | func Digit(p *parser.Parser) (*parser.Cursor, bool) { 16 | return p.Check(parser.CheckRuneRange('0', '9')) 17 | } 18 | -------------------------------------------------------------------------------- /internal/grammar/filter.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/di-wu/parser/op" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | func Filter(p *ast.Parser) (*ast.Node, error) { 11 | return FilterOr(p) 12 | } 13 | 14 | func FilterAnd(p *ast.Parser) (*ast.Node, error) { 15 | return p.Expect(ast.Capture{ 16 | Type: typ.FilterAnd, 17 | TypeStrings: typ.Stringer, 18 | Value: op.And{ 19 | FilterValue, 20 | op.MinZero(op.And{ 21 | op.MinOne(SP), 22 | parser.CheckStringCI("and"), 23 | op.MinOne(SP), 24 | FilterValue, 25 | }), 26 | }, 27 | }) 28 | } 29 | 30 | func FilterNot(p *ast.Parser) (*ast.Node, error) { 31 | return p.Expect(ast.Capture{ 32 | Type: typ.FilterNot, 33 | TypeStrings: typ.Stringer, 34 | Value: op.And{ 35 | parser.CheckStringCI("not"), 36 | op.MinZero(SP), 37 | FilterParentheses, 38 | }, 39 | }) 40 | } 41 | 42 | func FilterOr(p *ast.Parser) (*ast.Node, error) { 43 | return p.Expect(ast.Capture{ 44 | Type: typ.FilterOr, 45 | TypeStrings: typ.Stringer, 46 | Value: op.And{ 47 | FilterAnd, 48 | op.MinZero(op.And{ 49 | op.MinOne(SP), 50 | parser.CheckStringCI("or"), 51 | op.MinOne(SP), 52 | FilterAnd, 53 | }), 54 | }, 55 | }) 56 | } 57 | 58 | func FilterParentheses(p *ast.Parser) (*ast.Node, error) { 59 | return p.Expect(op.And{ 60 | '(', 61 | op.MinZero(SP), 62 | FilterOr, 63 | op.MinZero(SP), 64 | ')', 65 | }) 66 | } 67 | 68 | func FilterValue(p *ast.Parser) (*ast.Node, error) { 69 | return p.Expect(op.Or{ 70 | ValuePath, 71 | AttrExp, 72 | FilterNot, 73 | FilterParentheses, 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /internal/grammar/filter_test.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "fmt" 5 | "github.com/di-wu/parser/ast" 6 | ) 7 | 8 | func ExampleFilterAnd() { 9 | p := func(s string) { 10 | p, _ := ast.New([]byte(s)) 11 | fmt.Println(Filter(p)) 12 | } 13 | p("title pr and userType eq \"Employee\"") 14 | p("userType eq \"Employee\" and emails[type eq \"work\" and value co \"@example.com\"]") 15 | // Output: 16 | // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","title"]]]]],["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Employee\""]]]]]]] 17 | // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Employee\""]]],["ValuePath",[["AttrPath",[["AttrName","emails"]]],["ValueLogExpAnd",[["AttrExp",[["AttrPath",[["AttrName","type"]]],["CompareOp","eq"],["String","\"work\""]]],["AttrExp",[["AttrPath",[["AttrName","value"]]],["CompareOp","co"],["String","\"@example.com\""]]]]]]]]]]] 18 | } 19 | 20 | func ExampleFilterNot() { 21 | p := func(s string) { 22 | p, _ := ast.New([]byte(s)) 23 | fmt.Println(Filter(p)) 24 | } 25 | p("userType ne \"Employee\" and not (emails co \"example.com\" or emails.value co \"example.org\")") 26 | // Output: 27 | // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","ne"],["String","\"Employee\""]]],["FilterNot",[["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"]]],["CompareOp","co"],["String","\"example.com\""]]]]],["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"],["AttrName","value"]]],["CompareOp","co"],["String","\"example.org\""]]]]]]]]]]]]] 28 | } 29 | 30 | func ExampleFilterOr() { 31 | p := func(s string) { 32 | p, _ := ast.New([]byte(s)) 33 | fmt.Println(Filter(p)) 34 | } 35 | p("title pr or userType eq \"Intern\"") 36 | p("emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]") 37 | // Output: 38 | // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","title"]]]]]]],["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Intern\""]]]]]]] 39 | // ["FilterOr",[["FilterAnd",[["ValuePath",[["AttrPath",[["AttrName","emails"]]],["ValueLogExpAnd",[["AttrExp",[["AttrPath",[["AttrName","type"]]],["CompareOp","eq"],["String","\"work\""]]],["AttrExp",[["AttrPath",[["AttrName","value"]]],["CompareOp","co"],["String","\"@example.com\""]]]]]]]]],["FilterAnd",[["ValuePath",[["AttrPath",[["AttrName","ims"]]],["ValueLogExpAnd",[["AttrExp",[["AttrPath",[["AttrName","type"]]],["CompareOp","eq"],["String","\"xmpp\""]]],["AttrExp",[["AttrPath",[["AttrName","value"]]],["CompareOp","co"],["String","\"@foo.com\""]]]]]]]]]]] 40 | } 41 | 42 | func ExampleFilterParentheses() { 43 | p := func(s string) { 44 | p, _ := ast.New([]byte(s)) 45 | fmt.Println(Filter(p)) 46 | } 47 | p("userType eq \"Employee\" and (emails.type eq \"work\")") 48 | p("userType eq \"Employee\" and (emails co \"example.com\" or emails.value co \"example.org\")") 49 | // Output: 50 | // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Employee\""]]],["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"],["AttrName","type"]]],["CompareOp","eq"],["String","\"work\""]]]]]]]]]]] 51 | // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Employee\""]]],["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"]]],["CompareOp","co"],["String","\"example.com\""]]]]],["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"],["AttrName","value"]]],["CompareOp","co"],["String","\"example.org\""]]]]]]]]]]] 52 | } 53 | -------------------------------------------------------------------------------- /internal/grammar/number.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/di-wu/parser/op" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | func Digits(p *ast.Parser) (*ast.Node, error) { 11 | return p.Expect( 12 | ast.Capture{ 13 | Type: typ.Digits, 14 | TypeStrings: typ.Stringer, 15 | Value: op.MinOne( 16 | parser.CheckRuneRange('0', '9'), 17 | ), 18 | }, 19 | ) 20 | } 21 | 22 | func Exp(p *ast.Parser) (*ast.Node, error) { 23 | return p.Expect( 24 | ast.Capture{ 25 | Type: typ.Exp, 26 | TypeStrings: typ.Stringer, 27 | Value: op.And{ 28 | op.Or{ 29 | "e", 30 | "E", 31 | }, 32 | op.Optional( 33 | Sign, 34 | ), 35 | Digits, 36 | }, 37 | }, 38 | ) 39 | } 40 | 41 | func Frac(p *ast.Parser) (*ast.Node, error) { 42 | return p.Expect( 43 | ast.Capture{ 44 | Type: typ.Frac, 45 | TypeStrings: typ.Stringer, 46 | Value: op.And{ 47 | ".", 48 | Digits, 49 | }, 50 | }, 51 | ) 52 | } 53 | 54 | func Int(p *ast.Parser) (*ast.Node, error) { 55 | return p.Expect( 56 | ast.Capture{ 57 | Type: typ.Int, 58 | TypeStrings: typ.Stringer, 59 | Value: op.Or{ 60 | "0", 61 | op.And{ 62 | parser.CheckRuneRange('1', '9'), 63 | op.MinZero( 64 | parser.CheckRuneRange('0', '9'), 65 | ), 66 | }, 67 | }, 68 | }, 69 | ) 70 | } 71 | 72 | func Minus(p *ast.Parser) (*ast.Node, error) { 73 | return p.Expect( 74 | ast.Capture{ 75 | Type: typ.Minus, 76 | TypeStrings: typ.Stringer, 77 | Value: "-", 78 | }, 79 | ) 80 | } 81 | 82 | func Number(p *ast.Parser) (*ast.Node, error) { 83 | return p.Expect( 84 | ast.Capture{ 85 | Type: typ.Number, 86 | TypeStrings: typ.Stringer, 87 | Value: op.And{ 88 | op.Optional( 89 | Minus, 90 | ), 91 | Int, 92 | op.Optional( 93 | Frac, 94 | ), 95 | op.Optional( 96 | Exp, 97 | ), 98 | }, 99 | }, 100 | ) 101 | } 102 | 103 | func Sign(p *ast.Parser) (*ast.Node, error) { 104 | return p.Expect( 105 | ast.Capture{ 106 | Type: typ.Sign, 107 | TypeStrings: typ.Stringer, 108 | Value: op.Or{ 109 | "-", 110 | "+", 111 | }, 112 | }, 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /internal/grammar/number_test.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "fmt" 5 | "github.com/di-wu/parser/ast" 6 | ) 7 | 8 | func ExampleNumber() { 9 | p, _ := ast.New([]byte("-10.0e+01")) 10 | fmt.Println(Number(p)) 11 | // Output: 12 | // ["Number",[["Minus","-"],["Int","10"],["Frac",[["Digits","0"]]],["Exp",[["Sign","+"],["Digits","01"]]]]] 13 | } 14 | -------------------------------------------------------------------------------- /internal/grammar/path.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser/ast" 5 | "github.com/di-wu/parser/op" 6 | "github.com/scim2/filter-parser/v2/internal/types" 7 | ) 8 | 9 | func Path(p *ast.Parser) (*ast.Node, error) { 10 | return p.Expect(ast.Capture{ 11 | Type: typ.Path, 12 | TypeStrings: typ.Stringer, 13 | Value: op.Or{ 14 | op.And{ 15 | ValuePath, 16 | op.Optional(SubAttr), 17 | }, 18 | AttrPath, 19 | }, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /internal/grammar/path_test.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "fmt" 5 | "github.com/di-wu/parser/ast" 6 | ) 7 | 8 | func ExamplePath() { 9 | p, _ := ast.New([]byte("members[value eq \"2819c223-7f76-453a-919d-413861904646\"].displayName")) 10 | fmt.Println(Path(p)) 11 | // Output: 12 | // ["Path",[["ValuePath",[["AttrPath",[["AttrName","members"]]],["AttrExp",[["AttrPath",[["AttrName","value"]]],["CompareOp","eq"],["String","\"2819c223-7f76-453a-919d-413861904646\""]]]]],["AttrName","displayName"]]] 13 | } 14 | -------------------------------------------------------------------------------- /internal/grammar/string.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/di-wu/parser/op" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | func Character(p *ast.Parser) (*ast.Node, error) { 11 | return p.Expect( 12 | op.Or{ 13 | Unescaped, 14 | op.And{ 15 | "\\", 16 | Escaped, 17 | }, 18 | }, 19 | ) 20 | } 21 | 22 | func Escaped(p *ast.Parser) (*ast.Node, error) { 23 | return p.Expect( 24 | op.Or{ 25 | "\"", 26 | "\\", 27 | "/", 28 | 0x0062, 29 | 0x0066, 30 | 0x006E, 31 | 0x0072, 32 | 0x0074, 33 | op.And{ 34 | "u", 35 | op.Repeat(4, 36 | op.Or{ 37 | parser.CheckRuneRange('0', '9'), 38 | parser.CheckRuneRange('A', 'F'), 39 | }, 40 | ), 41 | }, 42 | }, 43 | ) 44 | } 45 | 46 | func String(p *ast.Parser) (*ast.Node, error) { 47 | return p.Expect( 48 | ast.Capture{ 49 | Type: typ.String, 50 | TypeStrings: typ.Stringer, 51 | Value: op.And{ 52 | "\"", 53 | op.MinZero( 54 | Character, 55 | ), 56 | "\"", 57 | }, 58 | }, 59 | ) 60 | } 61 | 62 | func Unescaped(p *ast.Parser) (*ast.Node, error) { 63 | return p.Expect( 64 | op.Or{ 65 | parser.CheckRuneRange(0x0020, 0x0021), 66 | parser.CheckRuneRange(0x0023, 0x005B), 67 | parser.CheckRuneRange(0x005D, 0x0010FFFF), 68 | }, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /internal/grammar/string_test.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "fmt" 5 | "github.com/di-wu/parser/ast" 6 | ) 7 | 8 | func ExampleString() { 9 | p, _ := ast.New([]byte("\"2819c223-7f76-453a-919d-413861904646\"")) 10 | fmt.Println(String(p)) 11 | // Output: 12 | // ["String","\"2819c223-7f76-453a-919d-413861904646\""] 13 | } 14 | 15 | func ExampleString_complex() { 16 | p, _ := ast.New([]byte("\"W/\\\"990-6468886345120203448\\\"\"")) 17 | fmt.Println(String(p)) 18 | // Output: 19 | // ["String","\"W/\\\"990-6468886345120203448\\\"\""] 20 | } 21 | -------------------------------------------------------------------------------- /internal/grammar/tokens.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | const ( 4 | SP = ' ' 5 | ) 6 | -------------------------------------------------------------------------------- /internal/grammar/uri.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/di-wu/parser/op" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | func URI(p *ast.Parser) (*ast.Node, error) { 11 | return p.Expect(ast.Capture{ 12 | Type: typ.URI, 13 | TypeStrings: typ.Stringer, 14 | Value: op.MinOne(op.And{ 15 | op.MinOne(op.Or{ 16 | parser.CheckRuneRange('a', 'z'), 17 | parser.CheckRuneRange('A', 'Z'), 18 | parser.CheckRuneRange('0', '9'), 19 | '.', 20 | }), 21 | ":", 22 | }), 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /internal/grammar/uri_test.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "fmt" 5 | "github.com/di-wu/parser/ast" 6 | ) 7 | 8 | func ExampleURI() { 9 | p, _ := ast.New([]byte("urn:ietf:params:scim:schemas:core:2.0:User:userName")) 10 | fmt.Println(URI(p)) 11 | // Output: 12 | // ["URI","urn:ietf:params:scim:schemas:core:2.0:User:"] 13 | } 14 | -------------------------------------------------------------------------------- /internal/grammar/valuepath.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/di-wu/parser/op" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | func ValueFilter(p *ast.Parser) (*ast.Node, error) { 11 | return p.Expect(op.Or{ 12 | ValueLogExpOr, 13 | ValueLogExpAnd, 14 | AttrExp, 15 | }) 16 | } 17 | 18 | func ValueFilterAll(p *ast.Parser) (*ast.Node, error) { 19 | return p.Expect(op.Or{ 20 | ValueFilter, 21 | ValueFilterNot, 22 | }) 23 | } 24 | 25 | func ValueFilterNot(p *ast.Parser) (*ast.Node, error) { 26 | return p.Expect(ast.Capture{ 27 | Type: typ.ValueFilterNot, 28 | TypeStrings: typ.Stringer, 29 | Value: op.And{ 30 | parser.CheckStringCI("not"), 31 | op.MinZero(SP), 32 | '(', 33 | op.MinZero(SP), 34 | ValueFilter, 35 | op.MinZero(SP), 36 | ')', 37 | }, 38 | }) 39 | } 40 | 41 | func ValueLogExpAnd(p *ast.Parser) (*ast.Node, error) { 42 | return p.Expect(ast.Capture{ 43 | Type: typ.ValueLogExpAnd, 44 | TypeStrings: typ.Stringer, 45 | Value: op.And{ 46 | AttrExp, 47 | op.MinZero(SP), 48 | parser.CheckStringCI("and"), 49 | op.MinZero(SP), 50 | AttrExp, 51 | }, 52 | }) 53 | } 54 | 55 | func ValueLogExpOr(p *ast.Parser) (*ast.Node, error) { 56 | return p.Expect(ast.Capture{ 57 | Type: typ.ValueLogExpOr, 58 | TypeStrings: typ.Stringer, 59 | Value: op.And{ 60 | AttrExp, 61 | op.MinZero(SP), 62 | parser.CheckStringCI("or"), 63 | op.MinZero(SP), 64 | AttrExp, 65 | }, 66 | }) 67 | } 68 | 69 | func ValuePath(p *ast.Parser) (*ast.Node, error) { 70 | return p.Expect(ast.Capture{ 71 | Type: typ.ValuePath, 72 | TypeStrings: typ.Stringer, 73 | Value: op.And{ 74 | AttrPath, 75 | op.MinZero(SP), 76 | '[', 77 | op.MinZero(SP), 78 | ValueFilterAll, 79 | op.MinZero(SP), 80 | ']', 81 | }, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /internal/grammar/values.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/scim2/filter-parser/v2/internal/types" 7 | ) 8 | 9 | // A boolean has no case sensitivity or uniqueness. 10 | // More info: https://tools.ietf.org/html/rfc7643#section-2.3.2 11 | 12 | func False(p *ast.Parser) (*ast.Node, error) { 13 | return p.Expect( 14 | ast.Capture{ 15 | Type: typ.False, 16 | TypeStrings: typ.Stringer, 17 | Value: parser.CheckStringCI("false"), 18 | }, 19 | ) 20 | } 21 | 22 | func Null(p *ast.Parser) (*ast.Node, error) { 23 | return p.Expect( 24 | ast.Capture{ 25 | Type: typ.Null, 26 | TypeStrings: typ.Stringer, 27 | Value: parser.CheckStringCI("null"), 28 | }, 29 | ) 30 | } 31 | 32 | func True(p *ast.Parser) (*ast.Node, error) { 33 | return p.Expect( 34 | ast.Capture{ 35 | Type: typ.True, 36 | TypeStrings: typ.Stringer, 37 | Value: parser.CheckStringCI("true"), 38 | }, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /internal/grammar/values_test.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | import ( 4 | "fmt" 5 | "github.com/di-wu/parser/ast" 6 | ) 7 | 8 | func ExampleFalse() { 9 | p, _ := ast.New([]byte("FaLSe")) 10 | fmt.Println(False(p)) 11 | // Output: 12 | // ["False","FaLSe"] 13 | } 14 | 15 | func ExampleTrue() { 16 | p, _ := ast.New([]byte("TRue")) 17 | fmt.Println(True(p)) 18 | // Output: 19 | // ["True","TRue"] 20 | } 21 | -------------------------------------------------------------------------------- /internal/spec/grammar.abnf: -------------------------------------------------------------------------------- 1 | ; RFC: https://tools.ietf.org/html/rfc7644#section-3.4.2.2 2 | ; Contains Errata: #4690 3 | FILTER = attrExp / logExp / valuePath / *1"not" "(" FILTER ")" 4 | valuePath = attrPath "[" valFilter "]" 5 | valFilter = attrExp / valLogExp / *1"not" "(" valFilter ")" 6 | valLogExp = attrExp SP ("and" / "or") SP attrExp 7 | attrExp = (attrPath SP "pr") / 8 | (attrPath SP compareOp SP compValue) 9 | logExp = FILTER SP ("and" / "or") SP FILTER 10 | compValue = false / null / true / number / string 11 | ; Rules from JSON (RFC 7159). 12 | compareOp = "eq" / "ne" / "co" / "sw" / "ew" / "gt" / "lt" / "ge" / "le" 13 | attrPath = [URI ":"] ATTRNAME *1subAttr 14 | ; URI is SCIM "schema" URI. 15 | ATTRNAME = ALPHA *(nameChar) 16 | nameChar = "-" / "_" / DIGIT / ALPHA 17 | subAttr = "." ATTRNAME 18 | 19 | ; RFC: https://tools.ietf.org/html/rfc7644#section-3.5.2 20 | PATH = attrPath / valuePath [subAttr] 21 | -------------------------------------------------------------------------------- /internal/spec/grammar.pegn: -------------------------------------------------------------------------------- 1 | # SCIM-filter (v0.1.1) github.com/scim2/filter-parser 2 | 3 | Filter <- FilterOr 4 | FilterOr <-- FilterAnd (SP+ Or SP+ FilterAnd)* 5 | FilterAnd <-- FilterValue (SP+ And SP+ FilterValue)* 6 | FilterNot <-- Not SP* FilterParen 7 | FilterValue <- ValuePath / AttrExp / FilterNot / FilterParen 8 | FilterParen <- '(' SP* FilterOr 'SP* )' 9 | 10 | Path <-- ValuePath SubAttr? / AttrPath 11 | 12 | AttrExp <-- AttrPath SP+ (Pr / (CompareOp SP+ CompareValue)) 13 | AttrPath <-- Uri? AttrName SubAttr? 14 | AttrName <-- '$'? alpha NameChar* 15 | NameChar <- '-' / '_' / digit / alpha 16 | SubAttr <- '.' AttrName 17 | CompareOp <-- Eq / Ne / Co / Sw / Ew / Gt / Lt / Ge / Le 18 | CompareValue <- False / Null / True / Number / String 19 | 20 | ValuePath <-- AttrPath SP* '[' SP* ValueFilterAll SP* ']' 21 | ValueFilterAll <- ValueFilter / ValueFilterNot 22 | ValueFilter <- ValueLogExpOr / ValueLogExpAnd / AttrExp 23 | ValueLogExpOr <-- AttrExp SP* Or SP* AttrExp 24 | ValueLogExpAnd <-- AttrExp SP* And SP* AttrExp 25 | ValueFilterNot <-- Not SP* '(' SP* ValueFilter SP* ')' 26 | 27 | Not <- ('n' / 'N') ('o' / 'O') / ('t' / 'T') 28 | Or <- ('o' / 'R') ('r' / 'R') 29 | And <- ('a' / 'A') ('n' / 'N') ('d' / 'D') 30 | 31 | Pr <- ('p' / 'P') ('r' / 'R') 32 | Eq <- ('e' / 'E') ('q' / 'Q') 33 | Ne <- ('n' / 'N') ('e' / 'E') 34 | Co <- ('c' / 'C') ('o' / 'O') 35 | Sw <- ('s' / 'S') ('w' / 'W') 36 | Ew <- ('e' / 'E') ('w' / 'W') 37 | Gt <- ('g' / 'G') ('t' / 'T') 38 | Lt <- ('l' / 'L') ('t' / 'T') 39 | Ge <- ('g' / 'G') ('e' / 'E') 40 | Le <- ('l' / 'L') ('e' / 'E') 41 | 42 | alpha <- [a-z] / [A-Z] 43 | digit <- [0-9] 44 | SP <- ' ' 45 | 46 | # RFC7159. 47 | False <-- ('f' / 'F') ('a' / 'A') ('l' / 'L') ('s' / 'S') ('e' / 'E') 48 | Null <-- ('n' / 'N') ('u' / 'U') ('l' / 'L') ('l' / 'L') 49 | True <-- ('t' / 'T') ('r' / 'R') ('u' / 'U') ('e' / 'E') 50 | 51 | Number <-- Minus? Int Frac? Exp? 52 | Minus <-- '-' 53 | Exp <-- ('e' / 'E') Sign? Digits 54 | Sign <-- '-' / '+' 55 | Digits <-- [0-9]+ 56 | Frac <-- '.' Digits 57 | Int <-- '0' / [1-9] [0-9]* 58 | 59 | String <-- '"' Character* '"' 60 | Character <- Unescaped / '\' Escaped 61 | Unescaped <- [x20-x21] / [x23-x5B] / [x5D-x10FFFF] 62 | Escaped <- '"' 63 | / '\' 64 | / '/' 65 | / x62 # backspace 66 | / x66 # form feed 67 | / x6E # line feed 68 | / x72 # carriage return 69 | / x74 # tab 70 | / 'u' ([0-9] / [A-F]){4} 71 | 72 | # A customized/simplified version of the URI specified in RFC3986. 73 | Uri <-- (([a-z] / [A-Z] / [0-9] / '.')+ ':')+ -------------------------------------------------------------------------------- /internal/types/types.go: -------------------------------------------------------------------------------- 1 | package typ 2 | 3 | const ( 4 | Unknown = iota 5 | 6 | FilterOr 7 | FilterAnd 8 | FilterNot 9 | 10 | Path 11 | 12 | AttrExp 13 | AttrPath 14 | AttrName 15 | CompareOp 16 | 17 | ValuePath 18 | ValueLogExpOr 19 | ValueLogExpAnd 20 | ValueFilterNot 21 | 22 | False 23 | Null 24 | True 25 | 26 | Number 27 | Minus 28 | Exp 29 | Sign 30 | Digits 31 | Frac 32 | Int 33 | 34 | String 35 | 36 | URI 37 | ) 38 | 39 | var Stringer = []string{ 40 | "Unknown", 41 | 42 | "FilterOr", 43 | "FilterAnd", 44 | "FilterNot", 45 | 46 | "Path", 47 | 48 | "AttrExp", 49 | "AttrPath", 50 | "AttrName", 51 | "CompareOp", 52 | 53 | "ValuePath", 54 | "ValueLogExpOr", 55 | "ValueLogExpAnd", 56 | "ValueFilterNot", 57 | 58 | "False", 59 | "Null", 60 | "True", 61 | 62 | "Number", 63 | "Minus", 64 | "Exp", 65 | "Sign", 66 | "Digits", 67 | "Frac", 68 | "Int", 69 | 70 | "String", 71 | 72 | "URI", 73 | } 74 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/scim2/filter-parser/v2/internal/grammar" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | // ParsePath parses the given raw data as an Path. 11 | func ParsePath(raw []byte) (Path, error) { 12 | return parsePath(raw, config{}) 13 | } 14 | 15 | // ParsePathNumber parses the given raw data as an Path with json.Number. 16 | func ParsePathNumber(raw []byte) (Path, error) { 17 | return parsePath(raw, config{useNumber: true}) 18 | } 19 | 20 | func parsePath(raw []byte, c config) (Path, error) { 21 | p, err := ast.New(raw) 22 | if err != nil { 23 | return Path{}, err 24 | } 25 | node, err := grammar.Path(p) 26 | if err != nil { 27 | return Path{}, err 28 | } 29 | if _, err := p.Expect(parser.EOD); err != nil { 30 | return Path{}, err 31 | } 32 | return c.parsePath(node) 33 | } 34 | 35 | func (p config) parsePath(node *ast.Node) (Path, error) { 36 | children := node.Children() 37 | if len(children) == 0 { 38 | return Path{}, invalidLengthError(typ.Path, 1, 0) 39 | } 40 | 41 | // AttrPath 42 | if node.Type == typ.AttrPath { 43 | attrPath, err := parseAttrPath(node) 44 | if err != nil { 45 | return Path{}, err 46 | } 47 | return Path{ 48 | AttributePath: attrPath, 49 | }, nil 50 | } 51 | 52 | if node.Type != typ.Path { 53 | return Path{}, invalidTypeError(typ.Path, node.Type) 54 | } 55 | 56 | // ValuePath SubAttr? 57 | valuePath, err := p.parseValuePath(children[0]) 58 | if err != nil { 59 | return Path{}, err 60 | } 61 | 62 | var subAttr *string 63 | if len(children) == 2 { 64 | node := children[1] 65 | if node.Type != typ.AttrName { 66 | return Path{}, invalidTypeError(typ.AttrName, node.Type) 67 | } 68 | value := node.Value 69 | subAttr = &value 70 | } 71 | 72 | return Path{ 73 | AttributePath: valuePath.AttributePath, 74 | ValueExpression: valuePath.ValueFilter, 75 | SubAttribute: subAttr, 76 | }, nil 77 | } 78 | -------------------------------------------------------------------------------- /path_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func ExampleParsePath_attrPath() { 9 | fmt.Println(ParsePath([]byte("members"))) 10 | fmt.Println(ParsePath([]byte("name.familyName"))) 11 | // Output: 12 | // members 13 | // name.familyName 14 | } 15 | 16 | func ExampleParsePath_valuePath() { 17 | fmt.Println(ParsePath([]byte("members[value eq \"2819c223-7f76-453a-919d-413861904646\"]"))) 18 | fmt.Println(ParsePath([]byte("members[value eq \"2819c223-7f76-453a-919d-413861904646\"].displayName"))) 19 | // Output: 20 | // members[value eq "2819c223-7f76-453a-919d-413861904646"] 21 | // members[value eq "2819c223-7f76-453a-919d-413861904646"].displayName 22 | } 23 | 24 | func TestParsePath(t *testing.T) { 25 | for _, example := range []string{ 26 | "members", 27 | "name.familyName", 28 | "addresses[type eq \"work\"]", 29 | "members[value eq \"2819c223-7f76-453a-919d-413861904646\"]", 30 | "members[value eq \"2819c223-7f76-453a-919d-413861904646\"].displayName", 31 | } { 32 | t.Run(example, func(t *testing.T) { 33 | if _, err := ParsePath([]byte(example)); err != nil { 34 | t.Error(err) 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /valuepath.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/di-wu/parser" 5 | "github.com/di-wu/parser/ast" 6 | "github.com/scim2/filter-parser/v2/internal/grammar" 7 | "github.com/scim2/filter-parser/v2/internal/types" 8 | ) 9 | 10 | // ParseValuePath parses the given raw data as an ValuePath. 11 | func ParseValuePath(raw []byte) (ValuePath, error) { 12 | return parseValuePath(raw, config{}) 13 | } 14 | 15 | // ParseValuePathNumber parses the given raw data as an ValuePath with json.Number. 16 | func ParseValuePathNumber(raw []byte) (ValuePath, error) { 17 | return parseValuePath(raw, config{useNumber: true}) 18 | } 19 | 20 | func parseValuePath(raw []byte, c config) (ValuePath, error) { 21 | p, err := ast.New(raw) 22 | if err != nil { 23 | return ValuePath{}, err 24 | } 25 | node, err := grammar.ValuePath(p) 26 | if err != nil { 27 | return ValuePath{}, err 28 | } 29 | if _, err := p.Expect(parser.EOD); err != nil { 30 | return ValuePath{}, err 31 | } 32 | return c.parseValuePath(node) 33 | } 34 | 35 | func (p config) parseValueFilter(node *ast.Node) (Expression, error) { 36 | switch t := node.Type; t { 37 | case typ.ValueLogExpOr, typ.ValueLogExpAnd: 38 | children := node.Children() 39 | if l := len(children); l != 2 { 40 | return nil, invalidLengthError(node.Type, 2, l) 41 | } 42 | 43 | left, err := p.parseAttrExp(children[0]) 44 | if err != nil { 45 | return nil, err 46 | } 47 | right, err := p.parseAttrExp(children[1]) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | var operator LogicalOperator 53 | if node.Type == typ.ValueLogExpOr { 54 | operator = OR 55 | } else { 56 | operator = AND 57 | } 58 | 59 | return &LogicalExpression{ 60 | Left: &left, 61 | Right: &right, 62 | Operator: operator, 63 | }, nil 64 | case typ.AttrExp: 65 | attrExp, err := p.parseAttrExp(node) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &attrExp, nil 70 | case typ.ValueFilterNot: 71 | children := node.Children() 72 | if l := len(children); l != 1 { 73 | return nil, invalidLengthError(typ.ValueFilterNot, 1, l) 74 | } 75 | 76 | valueFilter, err := p.parseValueFilter(children[0]) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return &NotExpression{ 81 | Expression: valueFilter, 82 | }, nil 83 | default: 84 | return nil, invalidChildTypeError(typ.ValuePath, t) 85 | } 86 | } 87 | 88 | func (p config) parseValuePath(node *ast.Node) (ValuePath, error) { 89 | if node.Type != typ.ValuePath { 90 | return ValuePath{}, invalidTypeError(typ.ValuePath, node.Type) 91 | } 92 | 93 | children := node.Children() 94 | if l := len(children); l != 2 { 95 | return ValuePath{}, invalidLengthError(typ.ValuePath, 2, l) 96 | } 97 | 98 | attrPath, err := parseAttrPath(children[0]) 99 | if err != nil { 100 | return ValuePath{}, err 101 | } 102 | 103 | valueFilter, err := p.parseValueFilter(children[1]) 104 | if err != nil { 105 | return ValuePath{}, err 106 | } 107 | 108 | return ValuePath{ 109 | AttributePath: attrPath, 110 | ValueFilter: valueFilter, 111 | }, nil 112 | } 113 | -------------------------------------------------------------------------------- /valuepath_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "fmt" 4 | 5 | func ExampleParseValuePath() { 6 | fmt.Println(ParseValuePath([]byte("emails[type eq \"work\"]"))) 7 | fmt.Println(ParseValuePath([]byte("emails[not (type eq \"work\")]"))) 8 | fmt.Println(ParseValuePath([]byte("emails[type eq \"work\" and value co \"@example.com\"]"))) 9 | // Output: 10 | // emails[type eq "work"] 11 | // emails[not(type eq "work")] 12 | // emails[type eq "work" and value co "@example.com"] 13 | } 14 | --------------------------------------------------------------------------------