From 8434b2efa7685966e011238ef2b2190759c8e350 Mon Sep 17 00:00:00 2001 From: Phil Hunt Date: Sun, 20 Nov 2022 12:25:33 -0800 Subject: [PATCH] Corrected scim filter to support parenthesis/precedence filters --- ast.go | 35 +++++++++++++++++-------- filter.go | 13 ++++++++++ filter_test.go | 17 ++++++++++--- internal/grammar/filter.go | 45 +++++++++++++++++++++++++-------- internal/grammar/filter_test.go | 4 +-- internal/types/types.go | 2 ++ 6 files changed, 90 insertions(+), 26 deletions(-) diff --git a/ast.go b/ast.go index bcaba7f..84e6c11 100644 --- a/ast.go +++ b/ast.go @@ -30,6 +30,8 @@ const ( AND LogicalOperator = "and" // OR is the logical operation or (||). OR LogicalOperator = "or" + + OPEN LogicalOperator = "(" ) // AttributeExpression represents an attribute expression/filter. @@ -57,9 +59,10 @@ 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 +// +// ^ ^ ^ +// URIPrefix | SubAttribute +// AttributeName type AttributePath struct { URIPrefix *string AttributeName string @@ -100,10 +103,11 @@ type CompareOperator string // Expression is a type to assign to implemented expressions. // Valid expressions are: -// - ValuePath -// - AttributeExpression -// - LogicalExpression -// - NotExpression +// - ValuePath +// - AttributeExpression +// - LogicalExpression +// - NotExpression +// - PrecedenceExpression type Expression interface { exprNode() } @@ -123,6 +127,16 @@ func (*LogicalExpression) exprNode() {} // LogicalOperator represents a logical operation such as 'and' / 'or'. type LogicalOperator string +type PrecedenceExpression struct { + Expression Expression +} + +func (e PrecedenceExpression) String() string { + return fmt.Sprintf("(%v)", e.Expression) +} + +func (*PrecedenceExpression) exprNode() {} + // NotExpression represents an 'not' node. type NotExpression struct { Expression Expression @@ -137,9 +151,10 @@ func (*NotExpression) exprNode() {} // Path describes the target of a PATCH operation. Path can have an optional // ValueExpression and SubAttribute. // e.g. members[value eq "2819c223-7f76-453a-919d-413861904646"].displayName -// ^ ^ ^ -// | ValueExpression SubAttribute -// AttributePath +// +// ^ ^ ^ +// | ValueExpression SubAttribute +// AttributePath type Path struct { AttributePath AttributePath ValueExpression Expression diff --git a/filter.go b/filter.go index 180a6f1..80e5d92 100644 --- a/filter.go +++ b/filter.go @@ -141,6 +141,19 @@ func (p config) parseFilterValue(node *ast.Node) (Expression, error) { return &NotExpression{ Expression: exp, }, nil + case typ.FilterPrecedence: + children := node.Children() + if l := len(children); l != 1 { + return nil, invalidLengthError(typ.FilterPrecedence, 1, l) + } + + exp, err := p.parseFilterOr(children[0]) + if err != nil { + return nil, err + } + return &PrecedenceExpression{ + Expression: exp, + }, nil case typ.FilterOr: return p.parseFilterOr(node) default: diff --git a/filter_test.go b/filter_test.go index 342ea59..ec34b58 100644 --- a/filter_test.go +++ b/filter_test.go @@ -2,13 +2,16 @@ package filter import ( "fmt" + "strings" "testing" ) func ExampleParseFilter_and() { - fmt.Println(ParseFilter([]byte("title pr and userType eq \"Employee\""))) + ast, _ := ParseFilter([]byte("title pr and userType eq \"Employee\"")) + + fmt.Println(ast) // Output: - // title pr and userType eq "Employee" + // title pr and userType eq "Employee" } func ExampleParseFilter_attrExp() { @@ -38,7 +41,7 @@ func ExampleParseFilter_or() { func ExampleParseFilter_parentheses() { fmt.Println(ParseFilter([]byte("(emails.type eq \"work\")"))) // Output: - // emails.type eq "work" + // (emails.type eq "work") } func ExampleParseFilter_valuePath() { @@ -99,7 +102,13 @@ func TestParseFilter(t *testing.T) { "name pr or userName pr or title pr", } { t.Run(example, func(t *testing.T) { - if _, err := ParseFilter([]byte(example)); err != nil { + + if strings.HasPrefix(example, "userType") { + fmt.Println("Test:" + example) + } + ast, err := ParseFilter([]byte(example)) + fmt.Println(ast) + if err != nil { t.Error(err) } }) diff --git a/internal/grammar/filter.go b/internal/grammar/filter.go index 0089ed0..194126b 100644 --- a/internal/grammar/filter.go +++ b/internal/grammar/filter.go @@ -31,10 +31,21 @@ func FilterNot(p *ast.Parser) (*ast.Node, error) { return p.Expect(ast.Capture{ Type: typ.FilterNot, TypeStrings: typ.Stringer, - Value: op.And{ - parser.CheckStringCI("not"), - op.MinZero(SP), - FilterParentheses, + Value: op.Or{ + op.And{ + parser.CheckStringCI("not"), + op.MinZero(SP), + '(', + FilterOr, + op.MinZero(SP), + ')', + }, + op.And{ + parser.CheckStringCI("not"), + op.MinZero(SP), + // FilterValue, + FilterOr, + }, }, }) } @@ -56,13 +67,27 @@ func FilterOr(p *ast.Parser) (*ast.Node, error) { } func FilterParentheses(p *ast.Parser) (*ast.Node, error) { - return p.Expect(op.And{ - '(', - op.MinZero(SP), - FilterOr, - op.MinZero(SP), - ')', + return p.Expect(ast.Capture{ + Type: typ.FilterPrecedence, + TypeStrings: typ.Stringer, + Value: op.And{ + '(', + op.MinZero(SP), + FilterOr, + op.MinZero(SP), + ')', + }, }) + /* + return p.Expect(op.And{ + '(', + op.MinZero(SP), + FilterOr, + op.MinZero(SP), + ')', + }) + + */ } func FilterValue(p *ast.Parser) (*ast.Node, error) { diff --git a/internal/grammar/filter_test.go b/internal/grammar/filter_test.go index 4cc6ba1..fc2d2c0 100644 --- a/internal/grammar/filter_test.go +++ b/internal/grammar/filter_test.go @@ -47,6 +47,6 @@ func ExampleFilterParentheses() { p("userType eq \"Employee\" and (emails.type eq \"work\")") p("userType eq \"Employee\" and (emails co \"example.com\" or emails.value co \"example.org\")") // Output: - // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Employee\""]]],["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"],["AttrName","type"]]],["CompareOp","eq"],["String","\"work\""]]]]]]]]]]] - // ["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\""]]]]]]]]]]] + // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Employee\""]]],["FilterPrecedence",[["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"],["AttrName","type"]]],["CompareOp","eq"],["String","\"work\""]]]]]]]]]]]]] + // ["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","userType"]]],["CompareOp","eq"],["String","\"Employee\""]]],["FilterPrecedence",[["FilterOr",[["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"]]],["CompareOp","co"],["String","\"example.com\""]]]]],["FilterAnd",[["AttrExp",[["AttrPath",[["AttrName","emails"],["AttrName","value"]]],["CompareOp","co"],["String","\"example.org\""]]]]]]]]]]]]] } diff --git a/internal/types/types.go b/internal/types/types.go index be46eb9..1136b2f 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -6,6 +6,7 @@ const ( FilterOr FilterAnd FilterNot + FilterPrecedence Path @@ -42,6 +43,7 @@ var Stringer = []string{ "FilterOr", "FilterAnd", "FilterNot", + "FilterPrecedence", "Path",