From 6d85b2ba3c53be03d99c0b3c628297bbf7bba937 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Fri, 1 Nov 2024 13:07:46 +0100 Subject: [PATCH] additional tests for optional selectors --- pkg/policy/literal/literal.go | 57 +++++++++++++++++++++++ pkg/policy/match.go | 15 +++++++ pkg/policy/match_test.go | 80 ++++++++++++++++++++++++++++++++- pkg/policy/selector/selector.go | 8 ++++ 4 files changed, 159 insertions(+), 1 deletion(-) diff --git a/pkg/policy/literal/literal.go b/pkg/policy/literal/literal.go index 16bc04b..3e6dd8e 100644 --- a/pkg/policy/literal/literal.go +++ b/pkg/policy/literal/literal.go @@ -2,6 +2,8 @@ package literal import ( + "fmt" + "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" @@ -24,3 +26,58 @@ func Null() ipld.Node { nb.AssignNull() return nb.Build() } + +// Map creates an IPLD node from a map[string]interface{} +func Map(v interface{}) (ipld.Node, error) { + m, ok := v.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("expected map[string]interface{}, got %T", v) + } + + nb := basicnode.Prototype.Map.NewBuilder() + ma, err := nb.BeginMap(int64(len(m))) + if err != nil { + return nil, err + } + + for k, v := range m { + if err := ma.AssembleKey().AssignString(k); err != nil { + return nil, err + } + + switch x := v.(type) { + case string: + if err := ma.AssembleValue().AssignString(x); err != nil { + return nil, err + } + case []interface{}: + lb := basicnode.Prototype.List.NewBuilder() + la, err := lb.BeginList(int64(len(x))) + if err != nil { + return nil, err + } + if err := la.Finish(); err != nil { + return nil, err + } + if err := ma.AssembleValue().AssignNode(lb.Build()); err != nil { + return nil, err + } + case map[string]interface{}: + nestedNode, err := Map(x) // recursive call for nested maps + if err != nil { + return nil, err + } + if err := ma.AssembleValue().AssignNode(nestedNode); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported value type: %T", v) + } + } + + if err := ma.Finish(); err != nil { + return nil, err + } + + return nb.Build(), nil +} diff --git a/pkg/policy/match.go b/pkg/policy/match.go index 4b0d414..bdf66e4 100644 --- a/pkg/policy/match.go +++ b/pkg/policy/match.go @@ -72,6 +72,9 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat if s, ok := cur.(equality); ok { res, err := s.selector.Select(node) if err != nil || res == nil { + if s.selector.IsOptional() { + return matchResultTrue, nil + } return matchResultNoData, cur } return boolToRes(datamodel.DeepEqual(s.value, res)) @@ -80,6 +83,9 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat if s, ok := cur.(equality); ok { res, err := s.selector.Select(node) if err != nil || res == nil { + if s.selector.IsOptional() { + return matchResultTrue, nil + } return matchResultNoData, cur } return boolToRes(isOrdered(s.value, res, gt)) @@ -88,6 +94,9 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat if s, ok := cur.(equality); ok { res, err := s.selector.Select(node) if err != nil || res == nil { + if s.selector.IsOptional() { + return matchResultTrue, nil + } return matchResultNoData, cur } return boolToRes(isOrdered(s.value, res, gte)) @@ -96,6 +105,9 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat if s, ok := cur.(equality); ok { res, err := s.selector.Select(node) if err != nil || res == nil { + if s.selector.IsOptional() { + return matchResultTrue, nil + } return matchResultNoData, cur } return boolToRes(isOrdered(s.value, res, lt)) @@ -104,6 +116,9 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat if s, ok := cur.(equality); ok { res, err := s.selector.Select(node) if err != nil || res == nil { + if s.selector.IsOptional() { + return matchResultTrue, nil + } return matchResultNoData, cur } return boolToRes(isOrdered(s.value, res, lte)) diff --git a/pkg/policy/match_test.go b/pkg/policy/match_test.go index 9e3de4a..46a4a4f 100644 --- a/pkg/policy/match_test.go +++ b/pkg/policy/match_test.go @@ -9,6 +9,7 @@ import ( "github.com/ipld/go-ipld-prime/codec/dagjson" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ucan-wg/go-ucan/pkg/policy/literal" @@ -457,7 +458,7 @@ func TestPolicyExamples(t *testing.T) { require.False(t, evaluate(`["all", ".a", [">", ".b", 0]]`, data)) }) - t.Run("Any", func(t *testing.T) { + t.Run("Map", func(t *testing.T) { data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`) require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data)) @@ -512,3 +513,80 @@ func FuzzMatch(f *testing.F) { policy.Match(dataNode) }) } + +func TestOptionalSelectors(t *testing.T) { + tests := []struct { + name string + policy Policy + data interface{} + expected bool + }{ + { + name: "missing optional field returns true", + policy: MustConstruct(Equal(".field?", literal.String("value"))), + data: map[string]interface{}{}, + expected: true, + }, + { + name: "present optional field with matching value returns true", + policy: MustConstruct(Equal(".field?", literal.String("value"))), + data: map[string]interface{}{"field": "value"}, + expected: true, + }, + { + name: "present optional field with non-matching value returns false", + policy: MustConstruct(Equal(".field?", literal.String("value"))), + data: map[string]interface{}{"field": "other"}, + expected: false, + }, + { + name: "missing non-optional field returns false", + policy: MustConstruct(Equal(".field", literal.String("value"))), + data: map[string]interface{}{}, + expected: false, + }, + { + name: "nested missing non-optional field returns false", + policy: MustConstruct(Equal(".outer?.inner", literal.String("value"))), + data: map[string]interface{}{"outer": map[string]interface{}{}}, + expected: false, + }, + { + name: "completely missing nested optional path returns true", + policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))), + data: map[string]interface{}{}, + expected: true, + }, + { + name: "partially present nested optional path with missing end returns true", + policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))), + data: map[string]interface{}{"outer": map[string]interface{}{}}, + expected: true, + }, + { + name: "optional array index returns true when array is empty", + policy: MustConstruct(Equal(".array[0]?", literal.String("value"))), + data: map[string]interface{}{"array": []interface{}{}}, + expected: true, + }, + { + name: "non-optional array index returns false when array is empty", + policy: MustConstruct(Equal(".array[0]", literal.String("value"))), + data: map[string]interface{}{"array": []interface{}{}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nb := basicnode.Prototype.Map.NewBuilder() + n, err := literal.Map(tt.data) + assert.NoError(t, err) + err = nb.AssignNode(n) + assert.NoError(t, err) + + result := tt.policy.Match(nb.Build()) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/policy/selector/selector.go b/pkg/policy/selector/selector.go index 149078d..0d13a47 100644 --- a/pkg/policy/selector/selector.go +++ b/pkg/policy/selector/selector.go @@ -33,6 +33,14 @@ func (s Selector) String() string { return res.String() } +func (s Selector) IsOptional() bool { + if len(s) == 0 { + return false + } + + return s[len(s)-1].optional +} + type segment struct { str string identity bool