Skip to content

Commit

Permalink
Merge pull request #2 from zelloptt/patch-support-eq-filter-on-adding
Browse files Browse the repository at this point in the history
Patch support eq filter on adding child property into an empty complex property
  • Loading branch information
ihorserba authored Apr 4, 2024
2 parents 1943351 + 21dd16a commit 23ccdf0
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 14 deletions.
16 changes: 15 additions & 1 deletion pkg/v2/crud/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,23 @@ func Add(resource *prop.Resource, path string, value interface{}) error {
return err
}

return defaultTraverse(resource.RootProperty(), skipMainSchemaNamespace(resource, head), func(nav prop.Navigator) error {
isFound := false
err = defaultTraverse(resource.RootProperty(), skipMainSchemaNamespace(resource, head), func(nav prop.Navigator) error {
isFound = true
return nav.Add(value).Error()
})
if err != nil || isFound {
return err
}
// If not found - add a new property using the values from eq filter operator
cb := func(nav prop.Navigator, value interface{}) error {
if err := nav.Error(); err != nil {
return err
}
nav.Add(value)
return nil
}
return eqFilterTraverse(value, resource.RootProperty(), skipMainSchemaNamespace(resource, head), cb)
}

// Replace value in SCIM resource at the given SCIM path. If SCIM path is empty, the root of the resource
Expand Down
17 changes: 17 additions & 0 deletions pkg/v2/crud/crud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,23 @@ func (s *CrudTestSuite) TestAdd() {
assert.Equal(t, "6546579", r.Navigator().Dot("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User").Dot("employeeNumber").Current().Raw())
},
},
{
name: "add a non-existent property using eq filter path into an empty complex multiValued property",
getResource: func(t *testing.T) *prop.Resource {
return prop.NewResource(s.resourceType)
},
path: `emails[primary eq true].value`,
value: "bar",
expect: func(t *testing.T, r *prop.Resource, err error) {
assert.Nil(t, err)
assert.Equal(t, []interface{}{
map[string]interface{}{
"value": "bar",
"primary": true,
},
}, r.Navigator().Dot("emails").Current().Raw())
},
},
}

for _, test := range tests {
Expand Down
167 changes: 154 additions & 13 deletions pkg/v2/crud/traverse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 (
Expand Down

0 comments on commit 23ccdf0

Please sign in to comment.