forked from imulab/go-scim
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from zelloptt/patch-support-eq-filter-on-adding
Patch support eq filter on adding child property into an empty complex property
- Loading branch information
Showing
3 changed files
with
186 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,31 +8,79 @@ import ( | |
"github.com/imulab/go-scim/pkg/v2/spec" | ||
) | ||
|
||
func defaultTraverse(property prop.Property, query *expr.Expression, callback func(nav prop.Navigator) error) error { | ||
type traverseCb func(nav prop.Navigator) error | ||
type traverseValueModifiedCb func(nav prop.Navigator, value interface{}) error | ||
|
||
func defaultTraverse(property prop.Property, query *expr.Expression, callback traverseCb) error { | ||
cb := func(nav prop.Navigator, query *expr.Expression) error { | ||
return callback(nav) | ||
} | ||
tr := traverser{ | ||
nav: prop.Navigate(property), | ||
callback: cb, | ||
elementStrategy: selectAllStrategy, | ||
traverseStrategy: traverseAll, | ||
} | ||
return tr.traverse(query) | ||
} | ||
|
||
// A single 'Eq' filter can be used to add a new attribute. | ||
// This traverse calls the callback with the modified value using such filter. | ||
// The operation like: | ||
// | ||
// { | ||
// "op": "add", | ||
// "path": "emails[type eq \"work\"].value", | ||
// "value": "[email protected]" | ||
// } | ||
// | ||
// Adds a new property: | ||
// | ||
// "emails": [ | ||
// { | ||
// "type": "work", | ||
// "value": "[email protected]" | ||
// } | ||
// ] | ||
func eqFilterTraverse(value interface{}, property prop.Property, query *expr.Expression, callback traverseValueModifiedCb) error { | ||
cb := func(nav prop.Navigator, query *expr.Expression) error { | ||
v, err := composeValueByEqFilter(value, query, nav) | ||
if err != nil { | ||
return err | ||
} | ||
return callback(nav, v) | ||
} | ||
return traverser{ | ||
nav: prop.Navigate(property), | ||
callback: callback, | ||
elementStrategy: selectAllStrategy, | ||
nav: prop.Navigate(property), | ||
callback: cb, | ||
elementStrategy: selectAllStrategy, | ||
traverseStrategy: traverseToSingleEqFilter, | ||
}.traverse(query) | ||
} | ||
|
||
func primaryOrFirstTraverse(property prop.Property, query *expr.Expression, callback func(nav prop.Navigator) error) error { | ||
func primaryOrFirstTraverse(property prop.Property, query *expr.Expression, callback traverseCb) error { | ||
cb := func(nav prop.Navigator, query *expr.Expression) error { | ||
return callback(nav) | ||
} | ||
return traverser{ | ||
nav: prop.Navigate(property), | ||
callback: callback, | ||
elementStrategy: primaryOrFirstStrategy, | ||
nav: prop.Navigate(property), | ||
callback: cb, | ||
elementStrategy: primaryOrFirstStrategy, | ||
traverseStrategy: traverseAll, | ||
}.traverse(query) | ||
} | ||
|
||
type traverser struct { | ||
nav prop.Navigator // stateful navigator for the resource being traversed | ||
callback func(nav prop.Navigator) error // callback function to be invoked when target is reached | ||
elementStrategy elementStrategy // strategy to select element properties to traverse for multiValued properties | ||
nav prop.Navigator // stateful navigator for the resource being traversed | ||
elementStrategy elementStrategy // strategy to select element properties to traverse for multiValued properties | ||
traverseStrategy traverseStrategy // strategy to stop traversing the query | ||
callback func(nav prop.Navigator, query *expr.Expression) error // callback to be invoked when target is reached | ||
} | ||
|
||
func (t traverser) traverse(query *expr.Expression) error { | ||
if query == nil { | ||
return t.callback(t.nav) | ||
traverseDone := t.traverseStrategy() | ||
if traverseDone(t.nav, query) { | ||
return t.callback(t.nav, query) | ||
} | ||
|
||
if query.IsRootOfFilter() { | ||
|
@@ -49,6 +97,53 @@ func (t traverser) traverse(query *expr.Expression) error { | |
return t.traverseNext(query) | ||
} | ||
|
||
func composeValueByEqFilter(value interface{}, query *expr.Expression, nav prop.Navigator) (interface{}, error) { | ||
var err error | ||
var filterValue interface{} | ||
keyValue := "" | ||
filterKey := "" | ||
|
||
if query == nil { | ||
return nil, fmt.Errorf("%w: no filter found", spec.ErrInvalidFilter) | ||
} | ||
|
||
if query.Left() != nil && query.Left().IsPath() { | ||
filterKey = query.Left().Token() | ||
} | ||
if query.Next() != nil && query.Next().IsPath() { | ||
if query.Next().Next() != nil { | ||
return nil, fmt.Errorf("%w: only a single Eq filter is applicable", spec.ErrInvalidFilter) | ||
} | ||
keyValue = query.Next().Token() | ||
} | ||
if filterKey == "" || keyValue == "" { | ||
return nil, fmt.Errorf("%w: filter is not supported", spec.ErrInvalidFilter) | ||
} | ||
if query.Right() != nil && query.Right().IsLiteral() { | ||
// add a child to the copy of the target property to parse allowed type of filterValue | ||
propCopy := nav.Current().Clone() | ||
navCopy := prop.Navigate(propCopy) | ||
navCopy.Add(map[string]interface{}{}) | ||
navCopy.At(0).Dot(filterKey) | ||
if navCopy.HasError() { | ||
// the child does not have a sub property by filterKey | ||
return nil, fmt.Errorf("%w: invalid filter: %w", spec.ErrInvalidFilter, navCopy.Error()) | ||
} | ||
filterValue, err = evaluator{}.normalize( | ||
navCopy.Current().Attribute(), | ||
query.Right().Token(), | ||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("%w: invalid filter value: %w", spec.ErrInvalidFilter, err) | ||
} | ||
} | ||
return []interface{}{ | ||
map[string]interface{}{ | ||
keyValue: value, | ||
filterKey: filterValue, | ||
}}, nil | ||
} | ||
|
||
func (t traverser) traverseNext(query *expr.Expression) error { | ||
t.nav.Dot(query.Token()) | ||
if err := t.nav.Error(); err != nil { | ||
|
@@ -96,6 +191,52 @@ func (t traverser) traverseQualifiedElements(filter *expr.Expression) error { | |
}) | ||
} | ||
|
||
type traverseStrategy func() func(nav prop.Navigator, query *expr.Expression) bool | ||
|
||
var ( | ||
// strategy to traverse all query | ||
traverseAll traverseStrategy = func() func(nav prop.Navigator, query *expr.Expression) bool { | ||
return func(nav prop.Navigator, query *expr.Expression) bool { | ||
return query == nil | ||
} | ||
} | ||
|
||
// strategy to get the root of the only Eq filter | ||
traverseToSingleEqFilter traverseStrategy = func() func(nav prop.Navigator, query *expr.Expression) bool { | ||
return func(nav prop.Navigator, query *expr.Expression) bool { | ||
if query == nil { | ||
// If query has been traversed and there is no Eq filter - finish the traverse | ||
return true | ||
} | ||
if !query.IsRootOfFilter() { | ||
// Looking for the root of an Eq filter | ||
return false | ||
} | ||
if !nav.Current().Attribute().MultiValued() { | ||
// Filter is not applicable to a singular attribute | ||
return false | ||
} | ||
if query.Token() != expr.Eq { | ||
// Only an Eq filter is supported | ||
return false | ||
} | ||
if query.Left() == nil || !query.Left().IsPath() { | ||
// The left expression should reflect an attribute path | ||
return false | ||
} | ||
if query.Next() == nil || !query.Next().IsPath() || query.Next().Next() != nil { | ||
// Only a single non-complex filter is supported | ||
return false | ||
} | ||
if query.Right() == nil || !query.Right().IsLiteral() { | ||
// The right expression should be a value assignable to an attribute | ||
return false | ||
} | ||
return true | ||
} | ||
} | ||
) | ||
|
||
type elementStrategy func(multiValuedComplex prop.Property) func(index int, child prop.Property) bool | ||
|
||
var ( | ||
|