additional tests for optional selectors
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user