Merge branch 'v1' into v1-meta-encryption
Signed-off-by: Fabio Bozzo <fabio.bozzo@gmail.com>
This commit is contained in:
206
pkg/args/args.go
Normal file
206
pkg/args/args.go
Normal file
@@ -0,0 +1,206 @@
|
||||
// Package args provides the type that represents the Arguments passed to
|
||||
// a command within an invocation.Token as well as a convenient Add method
|
||||
// to incrementally build the underlying map.
|
||||
package args
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
const (
|
||||
argsSchema = "type Args { String : Any }"
|
||||
argsName = "Args"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
)
|
||||
|
||||
func argsType() schema.Type {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes([]byte(argsSchema))
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return ts.TypeByName(argsName)
|
||||
}
|
||||
|
||||
var ErrUnsupported = fmt.Errorf("failure adding unsupported type to meta")
|
||||
|
||||
// Args are the Command's argumennts when an invocation Token is processed
|
||||
// by the executor.
|
||||
//
|
||||
// This type must be compatible with the IPLD type represented by the IPLD
|
||||
// schema { String : Any }.
|
||||
type Args struct {
|
||||
Keys []string
|
||||
Values map[string]ipld.Node
|
||||
}
|
||||
|
||||
// New returns a pointer to an initialized Args value.
|
||||
func New() *Args {
|
||||
return &Args{
|
||||
Values: map[string]ipld.Node{},
|
||||
}
|
||||
}
|
||||
|
||||
// FromIPLD unwraps an Args instance from an ipld.Node.
|
||||
func FromIPLD(node ipld.Node) (*Args, error) {
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
err = handlePanic(recover())
|
||||
}()
|
||||
|
||||
obj := bindnode.Unwrap(node)
|
||||
|
||||
args, ok := obj.(*Args)
|
||||
if !ok {
|
||||
err = fmt.Errorf("failed to convert to Args")
|
||||
}
|
||||
|
||||
return args, err
|
||||
}
|
||||
|
||||
// Add inserts a key/value pair in the Args set.
|
||||
//
|
||||
// Accepted types for val are: bool, string, int, int8, int16,
|
||||
// int32, int64, uint, uint8, uint16, uint32, float32, float64, []byte,
|
||||
// []any, map[string]any, ipld.Node and nil.
|
||||
func (m *Args) Add(key string, val any) error {
|
||||
if _, ok := m.Values[key]; ok {
|
||||
return fmt.Errorf("duplicate key %q", key)
|
||||
}
|
||||
|
||||
node, err := anyNode(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Values[key] = node
|
||||
m.Keys = append(m.Keys, key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Include merges the provided arguments into the existing arguments.
|
||||
//
|
||||
// If duplicate keys are encountered, the new value is silently dropped
|
||||
// without causing an error.
|
||||
func (m *Args) Include(other *Args) {
|
||||
for _, key := range other.Keys {
|
||||
if _, ok := m.Values[key]; ok {
|
||||
// don't overwrite
|
||||
continue
|
||||
}
|
||||
m.Values[key] = other.Values[key]
|
||||
m.Keys = append(m.Keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// ToIPLD wraps an instance of an Args with an ipld.Node.
|
||||
func (m *Args) ToIPLD() (ipld.Node, error) {
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
err = handlePanic(recover())
|
||||
}()
|
||||
|
||||
return bindnode.Wrap(m, argsType()), err
|
||||
}
|
||||
|
||||
func anyNode(val any) (ipld.Node, error) {
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
err = handlePanic(recover())
|
||||
}()
|
||||
|
||||
if val == nil {
|
||||
return literal.Null(), nil
|
||||
}
|
||||
|
||||
if cast, ok := val.(ipld.Node); ok {
|
||||
return cast, nil
|
||||
}
|
||||
|
||||
if cast, ok := val.(cid.Cid); ok {
|
||||
return literal.LinkCid(cast), err
|
||||
}
|
||||
|
||||
var rv reflect.Value
|
||||
|
||||
rv.Kind()
|
||||
|
||||
if cast, ok := val.(reflect.Value); ok {
|
||||
rv = cast
|
||||
} else {
|
||||
rv = reflect.ValueOf(val)
|
||||
}
|
||||
|
||||
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice:
|
||||
if rv.Type().Elem().Kind() == reflect.Uint8 {
|
||||
return literal.Bytes(val.([]byte)), nil
|
||||
}
|
||||
|
||||
l := make([]reflect.Value, rv.Len())
|
||||
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
l[i] = rv.Index(i)
|
||||
}
|
||||
|
||||
return literal.List(l)
|
||||
case reflect.Map:
|
||||
if rv.Type().Key().Kind() != reflect.String {
|
||||
return nil, fmt.Errorf("unsupported map key type: %s", rv.Type().Key().Name())
|
||||
}
|
||||
|
||||
m := make(map[string]reflect.Value, rv.Len())
|
||||
it := rv.MapRange()
|
||||
|
||||
for it.Next() {
|
||||
m[it.Key().String()] = it.Value()
|
||||
}
|
||||
|
||||
return literal.Map(m)
|
||||
case reflect.String:
|
||||
return literal.String(rv.String()), nil
|
||||
case reflect.Bool:
|
||||
return literal.Bool(rv.Bool()), nil
|
||||
// reflect.Int64 may exceed the safe 53-bit limit of JavaScript
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return literal.Int(rv.Int()), nil
|
||||
// reflect.Uint64 can't be safely converted to int64
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
return literal.Int(int64(rv.Uint())), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return literal.Float(rv.Float()), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Args type: %s", rv.Type().Name())
|
||||
}
|
||||
}
|
||||
|
||||
func handlePanic(rec any) error {
|
||||
if err, ok := rec.(error); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("%v", rec)
|
||||
}
|
||||
192
pkg/args/args_test.go
Normal file
192
pkg/args/args_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package args_test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
const (
|
||||
argsSchema = "type Args { String : Any }"
|
||||
argsName = "Args"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
)
|
||||
|
||||
func argsType() schema.Type {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes([]byte(argsSchema))
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return ts.TypeByName(argsName)
|
||||
}
|
||||
|
||||
func argsPrototype() schema.TypedPrototype {
|
||||
return bindnode.Prototype((*args.Args)(nil), argsType())
|
||||
}
|
||||
|
||||
func TestArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
intKey = "intKey"
|
||||
mapKey = "mapKey"
|
||||
nilKey = "nilKey"
|
||||
boolKey = "boolKey"
|
||||
linkKey = "linkKey"
|
||||
listKey = "listKey"
|
||||
nodeKey = "nodeKey"
|
||||
uintKey = "uintKey"
|
||||
bytesKey = "bytesKey"
|
||||
floatKey = "floatKey"
|
||||
stringKey = "stringKey"
|
||||
)
|
||||
|
||||
const (
|
||||
expIntVal = int64(-42)
|
||||
expBoolVal = true
|
||||
expUintVal = uint(42)
|
||||
expStringVal = "stringVal"
|
||||
)
|
||||
|
||||
var (
|
||||
expMapVal = map[string]string{"keyOne": "valOne", "keyTwo": "valTwo"}
|
||||
// expNilVal = (map[string]string)(nil)
|
||||
expLinkVal = cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")
|
||||
expListVal = []string{"elem1", "elem2", "elem3"}
|
||||
expNodeVal = literal.String("nodeVal")
|
||||
expBytesVal = []byte{0xde, 0xad, 0xbe, 0xef}
|
||||
expFloatVal = 42.0
|
||||
)
|
||||
|
||||
argsIn := args.New()
|
||||
|
||||
// WARNING: Do not change the order of these statements as this is the
|
||||
// order which will be present when decoded from DAG-CBOR (
|
||||
// per RFC7049 default canonical ordering?).
|
||||
for _, a := range []struct {
|
||||
key string
|
||||
val any
|
||||
}{
|
||||
{key: intKey, val: expIntVal},
|
||||
{key: mapKey, val: expMapVal},
|
||||
// {key: nilKey, val: expNilVal},
|
||||
{key: boolKey, val: expBoolVal},
|
||||
{key: linkKey, val: expLinkVal},
|
||||
{key: listKey, val: expListVal},
|
||||
{key: nodeKey, val: expNodeVal},
|
||||
{key: uintKey, val: expUintVal},
|
||||
{key: bytesKey, val: expBytesVal},
|
||||
{key: floatKey, val: expFloatVal},
|
||||
{key: stringKey, val: expStringVal},
|
||||
} {
|
||||
require.NoError(t, argsIn.Add(a.key, a.val))
|
||||
}
|
||||
|
||||
// Round-trip to DAG-CBOR here as ToIPLD/FromIPLD is only a wrapper
|
||||
argsOut := roundTripThroughDAGCBOR(t, argsIn)
|
||||
assert.Equal(t, argsIn, argsOut)
|
||||
|
||||
actMapVal := map[string]string{}
|
||||
mit := argsOut.Values[mapKey].MapIterator()
|
||||
|
||||
es := errorSwallower(t)
|
||||
|
||||
for !mit.Done() {
|
||||
k, v, err := mit.Next()
|
||||
require.NoError(t, err)
|
||||
ks := es(k.AsString()).(string)
|
||||
vs := es(v.AsString()).(string)
|
||||
|
||||
actMapVal[ks] = vs
|
||||
}
|
||||
|
||||
actListVal := []string{}
|
||||
lit := argsOut.Values[listKey].ListIterator()
|
||||
|
||||
for !lit.Done() {
|
||||
_, v, err := lit.Next()
|
||||
require.NoError(t, err)
|
||||
vs := es(v.AsString()).(string)
|
||||
|
||||
actListVal = append(actListVal, vs)
|
||||
}
|
||||
|
||||
assert.Equal(t, expIntVal, es(argsOut.Values[intKey].AsInt()))
|
||||
assert.Equal(t, expMapVal, actMapVal) // TODO: special accessor
|
||||
// TODO: the nil map comes back empty (but the right type)
|
||||
// assert.Equal(t, expNilVal, actNilVal)
|
||||
assert.Equal(t, expBoolVal, es(argsOut.Values[boolKey].AsBool()))
|
||||
assert.Equal(t, expLinkVal.String(), es(argsOut.Values[linkKey].AsLink()).(datamodel.Link).String()) // TODO: special accessor
|
||||
assert.Equal(t, expListVal, actListVal) // TODO: special accessor
|
||||
assert.Equal(t, expNodeVal, argsOut.Values[nodeKey])
|
||||
assert.Equal(t, expUintVal, uint(es(argsOut.Values[uintKey].AsInt()).(int64)))
|
||||
assert.Equal(t, expBytesVal, es(argsOut.Values[bytesKey].AsBytes()))
|
||||
assert.Equal(t, expFloatVal, es(argsOut.Values[floatKey].AsFloat()))
|
||||
assert.Equal(t, expStringVal, es(argsOut.Values[stringKey].AsString()))
|
||||
}
|
||||
|
||||
func TestArgs_Include(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argsIn := args.New()
|
||||
require.NoError(t, argsIn.Add("key1", "val1"))
|
||||
require.NoError(t, argsIn.Add("key2", "val2"))
|
||||
|
||||
argsOther := args.New()
|
||||
require.NoError(t, argsOther.Add("key2", "valOther")) // This should not overwrite key2 above
|
||||
require.NoError(t, argsOther.Add("key3", "val3"))
|
||||
require.NoError(t, argsOther.Add("key4", "val4"))
|
||||
|
||||
argsIn.Include(argsOther)
|
||||
|
||||
es := errorSwallower(t)
|
||||
|
||||
assert.Len(t, argsIn.Values, 4)
|
||||
assert.Equal(t, "val1", es(argsIn.Values["key1"].AsString()))
|
||||
assert.Equal(t, "val2", es(argsIn.Values["key2"].AsString()))
|
||||
assert.Equal(t, "val3", es(argsIn.Values["key3"].AsString()))
|
||||
assert.Equal(t, "val4", es(argsIn.Values["key4"].AsString()))
|
||||
}
|
||||
|
||||
func errorSwallower(t *testing.T) func(any, error) any {
|
||||
return func(val any, err error) any {
|
||||
require.NoError(t, err)
|
||||
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
func roundTripThroughDAGCBOR(t *testing.T, argsIn *args.Args) *args.Args {
|
||||
t.Helper()
|
||||
|
||||
node, err := argsIn.ToIPLD()
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := ipld.Encode(node, dagcbor.Encode)
|
||||
require.NoError(t, err)
|
||||
node, err = ipld.DecodeUsingPrototype(data, dagcbor.Decode, argsPrototype())
|
||||
require.NoError(t, err)
|
||||
|
||||
argsOut, err := args.FromIPLD(node)
|
||||
require.NoError(t, err)
|
||||
|
||||
return argsOut
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
@@ -42,6 +43,19 @@ func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
||||
return nil, fmt.Errorf("not a delegation token")
|
||||
}
|
||||
|
||||
// GetAllDelegations returns all the delegation.Token in the container.
|
||||
func (ctn Reader) GetAllDelegations() iter.Seq2[cid.Cid, *delegation.Token] {
|
||||
return func(yield func(cid.Cid, *delegation.Token) bool) {
|
||||
for c, t := range ctn {
|
||||
if t, ok := t.(*delegation.Token); ok {
|
||||
if !yield(c, t) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetInvocation returns the first found invocation.Token.
|
||||
// If none are found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -15,7 +14,9 @@ import (
|
||||
)
|
||||
|
||||
var ErrUnsupported = errors.New("failure adding unsupported type to meta")
|
||||
|
||||
var ErrNotFound = errors.New("key-value not found in meta")
|
||||
|
||||
var ErrNotEncryptable = errors.New("value of this type cannot be encrypted")
|
||||
|
||||
// Meta is a container for meta key-value pairs in a UCAN token.
|
||||
@@ -131,6 +132,9 @@ func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
||||
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
||||
// and ipld.Node.
|
||||
func (m *Meta) Add(key string, val any) error {
|
||||
if _, ok := m.Values[key]; ok {
|
||||
return fmt.Errorf("duplicate key %q", key)
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case bool:
|
||||
m.Values[key] = basicnode.NewBool(val)
|
||||
@@ -217,6 +221,11 @@ func (m *Meta) String() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ReadOnly returns a read-only version of Meta.
|
||||
func (m *Meta) ReadOnly() ReadOnly {
|
||||
return ReadOnly{m: m}
|
||||
}
|
||||
|
||||
func fqtn(val any) string {
|
||||
var name string
|
||||
|
||||
|
||||
42
pkg/meta/readonly.go
Normal file
42
pkg/meta/readonly.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
)
|
||||
|
||||
// ReadOnly wraps a Meta into a read-only facade.
|
||||
type ReadOnly struct {
|
||||
m *Meta
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetBool(key string) (bool, error) {
|
||||
return r.m.GetBool(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetString(key string) (string, error) {
|
||||
return r.m.GetString(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
||||
return r.m.GetInt64(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetFloat64(key string) (float64, error) {
|
||||
return r.m.GetFloat64(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
||||
return r.m.GetBytes(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||
return r.m.GetNode(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||
return r.m.Equals(other.m)
|
||||
}
|
||||
|
||||
func (r ReadOnly) String() string {
|
||||
return r.m.String()
|
||||
}
|
||||
@@ -2,14 +2,18 @@
|
||||
package literal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
)
|
||||
|
||||
// TODO: remove entirely?
|
||||
|
||||
var Bool = basicnode.NewBool
|
||||
var Int = basicnode.NewInt
|
||||
var Float = basicnode.NewFloat
|
||||
@@ -26,3 +30,95 @@ func Null() ipld.Node {
|
||||
nb.AssignNull()
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
// Map creates an IPLD node from a map[string]any
|
||||
func Map[T any](m map[string]T) (ipld.Node, error) {
|
||||
return qp.BuildMap(basicnode.Prototype.Any, int64(len(m)), func(ma datamodel.MapAssembler) {
|
||||
// deterministic iteration
|
||||
keys := make([]string, 0, len(m))
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
qp.MapEntry(ma, key, anyAssemble(m[key]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// List creates an IPLD node from a []any
|
||||
func List[T any](l []T) (ipld.Node, error) {
|
||||
return qp.BuildList(basicnode.Prototype.Any, int64(len(l)), func(la datamodel.ListAssembler) {
|
||||
for _, val := range l {
|
||||
qp.ListEntry(la, anyAssemble(val))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func anyAssemble(val any) qp.Assemble {
|
||||
var rt reflect.Type
|
||||
var rv reflect.Value
|
||||
|
||||
// support for recursive calls, staying in reflection land
|
||||
if cast, ok := val.(reflect.Value); ok {
|
||||
rt = cast.Type()
|
||||
rv = cast
|
||||
} else {
|
||||
rt = reflect.TypeOf(val)
|
||||
rv = reflect.ValueOf(val)
|
||||
}
|
||||
|
||||
// we need to dereference in some cases, to get the real value type
|
||||
if rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Interface {
|
||||
rv = rv.Elem()
|
||||
rt = rv.Type()
|
||||
}
|
||||
|
||||
switch rt.Kind() {
|
||||
case reflect.Array:
|
||||
if rt.Elem().Kind() == reflect.Uint8 {
|
||||
panic("bytes array are not supported yet")
|
||||
}
|
||||
return qp.List(int64(rv.Len()), func(la datamodel.ListAssembler) {
|
||||
for i := range rv.Len() {
|
||||
qp.ListEntry(la, anyAssemble(rv.Index(i)))
|
||||
}
|
||||
})
|
||||
case reflect.Slice:
|
||||
if rt.Elem().Kind() == reflect.Uint8 {
|
||||
return qp.Bytes(val.([]byte))
|
||||
}
|
||||
return qp.List(int64(rv.Len()), func(la datamodel.ListAssembler) {
|
||||
for i := range rv.Len() {
|
||||
qp.ListEntry(la, anyAssemble(rv.Index(i)))
|
||||
}
|
||||
})
|
||||
case reflect.Map:
|
||||
if rt.Key().Kind() != reflect.String {
|
||||
break
|
||||
}
|
||||
// deterministic iteration
|
||||
keys := rv.MapKeys()
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
return keys[i].String() < keys[j].String()
|
||||
})
|
||||
return qp.Map(int64(rv.Len()), func(ma datamodel.MapAssembler) {
|
||||
for _, key := range keys {
|
||||
qp.MapEntry(ma, key.String(), anyAssemble(rv.MapIndex(key)))
|
||||
}
|
||||
})
|
||||
case reflect.Bool:
|
||||
return qp.Bool(rv.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return qp.Int(rv.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return qp.Int(int64(rv.Uint()))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return qp.Float(rv.Float())
|
||||
case reflect.String:
|
||||
return qp.String(rv.String())
|
||||
default:
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported type %T", val))
|
||||
}
|
||||
|
||||
125
pkg/policy/literal/literal_test.go
Normal file
125
pkg/policy/literal/literal_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package literal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
n, err := List([]int{1, 2, 3})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_List, n.Kind())
|
||||
require.Equal(t, int64(3), n.Length())
|
||||
require.Equal(t, `list{
|
||||
0: int{1}
|
||||
1: int{2}
|
||||
2: int{3}
|
||||
}`, printer.Sprint(n))
|
||||
|
||||
n, err = List([][]int{{1, 2, 3}, {4, 5, 6}})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_List, n.Kind())
|
||||
require.Equal(t, int64(2), n.Length())
|
||||
require.Equal(t, `list{
|
||||
0: list{
|
||||
0: int{1}
|
||||
1: int{2}
|
||||
2: int{3}
|
||||
}
|
||||
1: list{
|
||||
0: int{4}
|
||||
1: int{5}
|
||||
2: int{6}
|
||||
}
|
||||
}`, printer.Sprint(n))
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
n, err := Map(map[string]any{
|
||||
"bool": true,
|
||||
"string": "foobar",
|
||||
"bytes": []byte{1, 2, 3, 4},
|
||||
"int": 1234,
|
||||
"uint": uint(12345),
|
||||
"float": 1.45,
|
||||
"slice": []int{1, 2, 3},
|
||||
"array": [2]int{1, 2},
|
||||
"map": map[string]any{
|
||||
"foo": "bar",
|
||||
"foofoo": map[string]string{
|
||||
"barbar": "foo",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
v, err := n.LookupByString("bool")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Bool, v.Kind())
|
||||
require.Equal(t, true, must(v.AsBool()))
|
||||
|
||||
v, err = n.LookupByString("string")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_String, v.Kind())
|
||||
require.Equal(t, "foobar", must(v.AsString()))
|
||||
|
||||
v, err = n.LookupByString("bytes")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Bytes, v.Kind())
|
||||
require.Equal(t, []byte{1, 2, 3, 4}, must(v.AsBytes()))
|
||||
|
||||
v, err = n.LookupByString("int")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||
require.Equal(t, int64(1234), must(v.AsInt()))
|
||||
|
||||
v, err = n.LookupByString("uint")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||
require.Equal(t, int64(12345), must(v.AsInt()))
|
||||
|
||||
v, err = n.LookupByString("float")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Float, v.Kind())
|
||||
require.Equal(t, 1.45, must(v.AsFloat()))
|
||||
|
||||
v, err = n.LookupByString("slice")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||
require.Equal(t, int64(3), v.Length())
|
||||
require.Equal(t, `list{
|
||||
0: int{1}
|
||||
1: int{2}
|
||||
2: int{3}
|
||||
}`, printer.Sprint(v))
|
||||
|
||||
v, err = n.LookupByString("array")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||
require.Equal(t, int64(2), v.Length())
|
||||
require.Equal(t, `list{
|
||||
0: int{1}
|
||||
1: int{2}
|
||||
}`, printer.Sprint(v))
|
||||
|
||||
v, err = n.LookupByString("map")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Map, v.Kind())
|
||||
require.Equal(t, int64(2), v.Length())
|
||||
require.Equal(t, `map{
|
||||
string{"foo"}: string{"bar"}
|
||||
string{"foofoo"}: map{
|
||||
string{"barbar"}: string{"foo"}
|
||||
}
|
||||
}`, printer.Sprint(v))
|
||||
}
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -10,143 +10,243 @@ import (
|
||||
)
|
||||
|
||||
// Match determines if the IPLD node satisfies the policy.
|
||||
func (p Policy) Match(node datamodel.Node) bool {
|
||||
// The first Statement failing to match is returned as well.
|
||||
func (p Policy) Match(node datamodel.Node) (bool, Statement) {
|
||||
for _, stmt := range p {
|
||||
ok := matchStatement(stmt, node)
|
||||
if !ok {
|
||||
return false
|
||||
res, leaf := matchStatement(stmt, node)
|
||||
switch res {
|
||||
case matchResultNoData, matchResultFalse:
|
||||
return false, leaf
|
||||
case matchResultOptionalNoData, matchResultTrue:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
switch statement.Kind() {
|
||||
// PartialMatch returns false IIF one non-optional Statement has the corresponding data and doesn't match.
|
||||
// If the data is missing or the non-optional Statement is matching, true is returned.
|
||||
//
|
||||
// This allows performing the policy checking in multiple steps, and find immediately if a Statement already failed.
|
||||
// A final call to Match is necessary to make sure that the policy is fully matched, with no missing data
|
||||
// (apart from optional values).
|
||||
//
|
||||
// The first Statement failing to match is returned as well.
|
||||
func (p Policy) PartialMatch(node datamodel.Node) (bool, Statement) {
|
||||
for _, stmt := range p {
|
||||
res, leaf := matchStatement(stmt, node)
|
||||
switch res {
|
||||
case matchResultFalse:
|
||||
return false, leaf
|
||||
case matchResultNoData, matchResultOptionalNoData, matchResultTrue:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type matchResult int8
|
||||
|
||||
const (
|
||||
matchResultTrue matchResult = iota // statement has data and resolve to true
|
||||
matchResultFalse // statement has data and resolve to false
|
||||
matchResultNoData // statement has no data
|
||||
matchResultOptionalNoData // statement has no data and is optional
|
||||
)
|
||||
|
||||
// matchStatement evaluate the policy against the given ipld.Node and returns:
|
||||
// - matchResultTrue: if the selector matched and the statement evaluated to true.
|
||||
// - matchResultFalse: if the selector matched and the statement evaluated to false.
|
||||
// - matchResultNoData: if the selector didn't match the expected data.
|
||||
// For matchResultTrue and matchResultNoData, the leaf-most (innermost) statement failing to be true is returned,
|
||||
// as well as the corresponding root-most encompassing statement.
|
||||
func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Statement) {
|
||||
var boolToRes = func(v bool) (matchResult, Statement) {
|
||||
if v {
|
||||
return matchResultTrue, nil
|
||||
} else {
|
||||
return matchResultFalse, cur
|
||||
}
|
||||
}
|
||||
|
||||
switch cur.Kind() {
|
||||
case KindEqual:
|
||||
if s, ok := statement.(equality); ok {
|
||||
if s, ok := cur.(equality); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
return datamodel.DeepEqual(s.value, res)
|
||||
if res == nil { // optional selector didn't match
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
return boolToRes(datamodel.DeepEqual(s.value, res))
|
||||
}
|
||||
case KindGreaterThan:
|
||||
if s, ok := statement.(equality); ok {
|
||||
if s, ok := cur.(equality); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
return isOrdered(s.value, res, gt)
|
||||
if res == nil { // optional selector didn't match
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
return boolToRes(isOrdered(s.value, res, gt))
|
||||
}
|
||||
case KindGreaterThanOrEqual:
|
||||
if s, ok := statement.(equality); ok {
|
||||
if s, ok := cur.(equality); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
return isOrdered(s.value, res, gte)
|
||||
if res == nil { // optional selector didn't match
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
return boolToRes(isOrdered(s.value, res, gte))
|
||||
}
|
||||
case KindLessThan:
|
||||
if s, ok := statement.(equality); ok {
|
||||
if s, ok := cur.(equality); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
return isOrdered(s.value, res, lt)
|
||||
if res == nil { // optional selector didn't match
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
return boolToRes(isOrdered(s.value, res, lt))
|
||||
}
|
||||
case KindLessThanOrEqual:
|
||||
if s, ok := statement.(equality); ok {
|
||||
if s, ok := cur.(equality); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
return isOrdered(s.value, res, lte)
|
||||
if res == nil { // optional selector didn't match
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
return boolToRes(isOrdered(s.value, res, lte))
|
||||
}
|
||||
case KindNot:
|
||||
if s, ok := statement.(negation); ok {
|
||||
return !matchStatement(s.statement, node)
|
||||
if s, ok := cur.(negation); ok {
|
||||
res, leaf := matchStatement(s.statement, node)
|
||||
switch res {
|
||||
case matchResultNoData, matchResultOptionalNoData:
|
||||
return res, leaf
|
||||
case matchResultTrue:
|
||||
return matchResultFalse, cur
|
||||
case matchResultFalse:
|
||||
return matchResultTrue, nil
|
||||
}
|
||||
}
|
||||
case KindAnd:
|
||||
if s, ok := statement.(connective); ok {
|
||||
if s, ok := cur.(connective); ok {
|
||||
for _, cs := range s.statements {
|
||||
r := matchStatement(cs, node)
|
||||
if !r {
|
||||
return false
|
||||
res, leaf := matchStatement(cs, node)
|
||||
switch res {
|
||||
case matchResultNoData, matchResultOptionalNoData:
|
||||
return res, leaf
|
||||
case matchResultTrue:
|
||||
// continue
|
||||
case matchResultFalse:
|
||||
return matchResultFalse, leaf
|
||||
}
|
||||
}
|
||||
return true
|
||||
return matchResultTrue, nil
|
||||
}
|
||||
case KindOr:
|
||||
if s, ok := statement.(connective); ok {
|
||||
if s, ok := cur.(connective); ok {
|
||||
if len(s.statements) == 0 {
|
||||
return true
|
||||
return matchResultTrue, nil
|
||||
}
|
||||
for _, cs := range s.statements {
|
||||
r := matchStatement(cs, node)
|
||||
if r {
|
||||
return true
|
||||
res, leaf := matchStatement(cs, node)
|
||||
switch res {
|
||||
case matchResultNoData, matchResultOptionalNoData:
|
||||
return res, leaf
|
||||
case matchResultTrue:
|
||||
return matchResultTrue, leaf
|
||||
case matchResultFalse:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return false
|
||||
return matchResultFalse, cur
|
||||
}
|
||||
case KindLike:
|
||||
if s, ok := statement.(wildcard); ok {
|
||||
if s, ok := cur.(wildcard); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
if res == nil { // optional selector didn't match
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
v, err := res.AsString()
|
||||
if err != nil {
|
||||
return false // not a string
|
||||
return matchResultFalse, cur // not a string
|
||||
}
|
||||
return s.pattern.Match(v)
|
||||
return boolToRes(s.pattern.Match(v))
|
||||
}
|
||||
case KindAll:
|
||||
if s, ok := statement.(quantifier); ok {
|
||||
if s, ok := cur.(quantifier); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
if res == nil {
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
it := res.ListIterator()
|
||||
if it == nil {
|
||||
return false // not a list
|
||||
return matchResultFalse, cur // not a list
|
||||
}
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return false
|
||||
panic("should never happen")
|
||||
}
|
||||
ok := matchStatement(s.statement, v)
|
||||
if !ok {
|
||||
return false
|
||||
matchRes, leaf := matchStatement(s.statement, v)
|
||||
switch matchRes {
|
||||
case matchResultNoData, matchResultOptionalNoData:
|
||||
return matchRes, leaf
|
||||
case matchResultTrue:
|
||||
// continue
|
||||
case matchResultFalse:
|
||||
return matchResultFalse, leaf
|
||||
}
|
||||
}
|
||||
return true
|
||||
return matchResultTrue, nil
|
||||
}
|
||||
case KindAny:
|
||||
if s, ok := statement.(quantifier); ok {
|
||||
if s, ok := cur.(quantifier); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return false
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
if res == nil {
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
it := res.ListIterator()
|
||||
if it == nil {
|
||||
return false // not a list
|
||||
return matchResultFalse, cur // not a list
|
||||
}
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return false
|
||||
panic("should never happen")
|
||||
}
|
||||
ok := matchStatement(s.statement, v)
|
||||
if ok {
|
||||
return true
|
||||
matchRes, leaf := matchStatement(s.statement, v)
|
||||
switch matchRes {
|
||||
case matchResultNoData, matchResultOptionalNoData:
|
||||
return matchRes, leaf
|
||||
case matchResultTrue:
|
||||
return matchResultTrue, nil
|
||||
case matchResultFalse:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return false
|
||||
return matchResultFalse, cur
|
||||
}
|
||||
}
|
||||
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind()))
|
||||
panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
|
||||
}
|
||||
|
||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||
|
||||
@@ -7,8 +7,11 @@ import (
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
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"
|
||||
@@ -17,228 +20,252 @@ import (
|
||||
func TestMatch(t *testing.T) {
|
||||
t.Run("equality", func(t *testing.T) {
|
||||
t.Run("string", func(t *testing.T) {
|
||||
np := basicnode.Prototype.String
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignString("test")
|
||||
nd := nb.Build()
|
||||
nd := literal.String("test")
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.String("test")))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("test2")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Int(138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Int(138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("float", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Float
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignFloat(1.138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Float(1.138)
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Float(1.138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("IPLD Link", func(t *testing.T) {
|
||||
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
||||
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
||||
|
||||
np := basicnode.Prototype.Link
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignLink(l0)
|
||||
nd := nb.Build()
|
||||
nd := literal.Link(l0)
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("string in map", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Map
|
||||
nb := np.NewBuilder()
|
||||
ma, _ := nb.BeginMap(1)
|
||||
ma.AssembleKey().AssignString("foo")
|
||||
ma.AssembleValue().AssignString("bar")
|
||||
ma.Finish()
|
||||
nd := nb.Build()
|
||||
nd, _ := literal.Map(map[string]any{
|
||||
"foo": "bar",
|
||||
})
|
||||
|
||||
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("string in list", func(t *testing.T) {
|
||||
np := basicnode.Prototype.List
|
||||
nb := np.NewBuilder()
|
||||
la, _ := nb.BeginList(1)
|
||||
la.AssembleValue().AssignString("foo")
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
nd, _ := literal.List([]any{"foo"})
|
||||
|
||||
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("inequality", func(t *testing.T) {
|
||||
t.Run("gt int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThan(".", literal.Int(138)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThan(".", literal.Int(140)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("gte int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(140)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("gt float", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Float
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignFloat(1.38)
|
||||
nd := nb.Build()
|
||||
nd := literal.Float(1.38)
|
||||
|
||||
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThan(".", literal.Float(2)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("gte float", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Float
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignFloat(1.38)
|
||||
nd := nb.Build()
|
||||
nd := literal.Float(1.38)
|
||||
|
||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(2)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("lt int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(LessThan(".", literal.Int(138)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(LessThan(".", literal.Int(100)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("lte int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(LessThanOrEqual(".", literal.Int(100)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("negation", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Bool
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignBool(false)
|
||||
nd := nb.Build()
|
||||
nd := literal.Bool(false)
|
||||
|
||||
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("conjunction", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(
|
||||
And(
|
||||
@@ -246,8 +273,9 @@ func TestMatch(t *testing.T) {
|
||||
LessThan(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(
|
||||
And(
|
||||
@@ -255,19 +283,18 @@ func TestMatch(t *testing.T) {
|
||||
Equal(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, MustConstruct(Equal(".", literal.Int(1138)))[0], leaf)
|
||||
|
||||
pol = MustConstruct(And())
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("disjunction", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(
|
||||
Or(
|
||||
@@ -275,8 +302,9 @@ func TestMatch(t *testing.T) {
|
||||
LessThan(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(
|
||||
Or(
|
||||
@@ -284,12 +312,14 @@ func TestMatch(t *testing.T) {
|
||||
Equal(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Or())
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("wildcard", func(t *testing.T) {
|
||||
@@ -303,14 +333,12 @@ func TestMatch(t *testing.T) {
|
||||
} {
|
||||
func(s string) {
|
||||
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
||||
np := basicnode.Prototype.String
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
nd := literal.String(s)
|
||||
|
||||
pol := MustConstruct(Like(".", pattern))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
}(s)
|
||||
}
|
||||
@@ -324,70 +352,56 @@ func TestMatch(t *testing.T) {
|
||||
} {
|
||||
func(s string) {
|
||||
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
||||
np := basicnode.Prototype.String
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
nd := literal.String(s)
|
||||
|
||||
pol := MustConstruct(Like(".", pattern))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
}(s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("quantification", func(t *testing.T) {
|
||||
buildValueNode := func(v int64) ipld.Node {
|
||||
np := basicnode.Prototype.Map
|
||||
nb := np.NewBuilder()
|
||||
ma, _ := nb.BeginMap(1)
|
||||
ma.AssembleKey().AssignString("value")
|
||||
ma.AssembleValue().AssignInt(v)
|
||||
ma.Finish()
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
t.Run("all", func(t *testing.T) {
|
||||
np := basicnode.Prototype.List
|
||||
nb := np.NewBuilder()
|
||||
la, _ := nb.BeginList(5)
|
||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
nd, _ := literal.List([]any{
|
||||
map[string]int{"value": 5},
|
||||
map[string]int{"value": 10},
|
||||
map[string]int{"value": 20},
|
||||
map[string]int{"value": 50},
|
||||
map[string]int{"value": 100},
|
||||
})
|
||||
|
||||
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, MustConstruct(GreaterThan(".value", literal.Int(20)))[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("any", func(t *testing.T) {
|
||||
np := basicnode.Prototype.List
|
||||
nb := np.NewBuilder()
|
||||
la, _ := nb.BeginList(5)
|
||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
nd, _ := literal.List([]any{
|
||||
map[string]int{"value": 5},
|
||||
map[string]int{"value": 10},
|
||||
map[string]int{"value": 20},
|
||||
map[string]int{"value": 50},
|
||||
map[string]int{"value": 100},
|
||||
})
|
||||
|
||||
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -405,7 +419,8 @@ func TestPolicyExamples(t *testing.T) {
|
||||
|
||||
pol, err := FromDagJson(policy)
|
||||
require.NoError(t, err)
|
||||
return pol.Match(data)
|
||||
res, _ := pol.Match(data)
|
||||
return res
|
||||
}
|
||||
|
||||
t.Run("And", func(t *testing.T) {
|
||||
@@ -509,6 +524,435 @@ func FuzzMatch(f *testing.F) {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
policy.Match(dataNode)
|
||||
_, _ = policy.Match(dataNode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOptionalSelectors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policy Policy
|
||||
data map[string]any
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "missing optional field returns true",
|
||||
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
||||
data: map[string]any{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "present optional field with matching value returns true",
|
||||
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
||||
data: map[string]any{"field": "value"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "present optional field with non-matching value returns false",
|
||||
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
||||
data: map[string]any{"field": "other"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "missing non-optional field returns false",
|
||||
policy: MustConstruct(Equal(".field", literal.String("value"))),
|
||||
data: map[string]any{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nested missing non-optional field returns false",
|
||||
policy: MustConstruct(Equal(".outer?.inner", literal.String("value"))),
|
||||
data: map[string]any{"outer": map[string]any{}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "completely missing nested optional path returns true",
|
||||
policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))),
|
||||
data: map[string]any{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "partially present nested optional path with missing end returns true",
|
||||
policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))),
|
||||
data: map[string]any{"outer": map[string]any{}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "optional array index returns true when array is empty",
|
||||
policy: MustConstruct(Equal(".array[0]?", literal.String("value"))),
|
||||
data: map[string]any{"array": []any{}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-optional array index returns false when array is empty",
|
||||
policy: MustConstruct(Equal(".array[0]", literal.String("value"))),
|
||||
data: map[string]any{"array": []any{}},
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
err = nb.AssignNode(n)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, _ := tt.policy.Match(nb.Build())
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The unique behaviour of PartialMatch is that it should return true for missing non-optional data (unlike Match).
|
||||
func TestPartialMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policy Policy
|
||||
data map[string]any
|
||||
expectedMatch bool
|
||||
expectedStmt Statement
|
||||
}{
|
||||
{
|
||||
name: "returns true for missing non-optional field",
|
||||
policy: MustConstruct(
|
||||
Equal(".field", literal.String("value")),
|
||||
),
|
||||
data: map[string]any{},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true when present data matches",
|
||||
policy: MustConstruct(
|
||||
Equal(".foo", literal.String("correct")),
|
||||
Equal(".missing", literal.String("whatever")),
|
||||
),
|
||||
data: map[string]any{
|
||||
"foo": "correct",
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns false with failing statement for present but non-matching value",
|
||||
policy: MustConstruct(
|
||||
Equal(".foo", literal.String("value1")),
|
||||
Equal(".bar", literal.String("value2")),
|
||||
),
|
||||
data: map[string]any{
|
||||
"foo": "wrong",
|
||||
"bar": "value2",
|
||||
},
|
||||
expectedMatch: false,
|
||||
expectedStmt: MustConstruct(
|
||||
Equal(".foo", literal.String("value1")),
|
||||
)[0],
|
||||
},
|
||||
{
|
||||
name: "continues past missing data until finding actual mismatch",
|
||||
policy: MustConstruct(
|
||||
Equal(".missing", literal.String("value")),
|
||||
Equal(".present", literal.String("wrong")),
|
||||
),
|
||||
data: map[string]any{
|
||||
"present": "actual",
|
||||
},
|
||||
expectedMatch: false,
|
||||
expectedStmt: MustConstruct(
|
||||
Equal(".present", literal.String("wrong")),
|
||||
)[0],
|
||||
},
|
||||
|
||||
// Optional fields
|
||||
{
|
||||
name: "returns false when optional field present but wrong",
|
||||
policy: MustConstruct(
|
||||
Equal(".field?", literal.String("value")),
|
||||
),
|
||||
data: map[string]any{
|
||||
"field": "wrong",
|
||||
},
|
||||
expectedMatch: false,
|
||||
expectedStmt: MustConstruct(
|
||||
Equal(".field?", literal.String("value")),
|
||||
)[0],
|
||||
},
|
||||
|
||||
// Like pattern matching
|
||||
{
|
||||
name: "returns true for matching like pattern",
|
||||
policy: MustConstruct(
|
||||
Like(".pattern", "test*"),
|
||||
),
|
||||
data: map[string]any{
|
||||
"pattern": "testing123",
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns false for non-matching like pattern",
|
||||
policy: MustConstruct(
|
||||
Like(".pattern", "test*"),
|
||||
),
|
||||
data: map[string]any{
|
||||
"pattern": "wrong123",
|
||||
},
|
||||
expectedMatch: false,
|
||||
expectedStmt: MustConstruct(
|
||||
Like(".pattern", "test*"),
|
||||
)[0],
|
||||
},
|
||||
|
||||
// Array quantifiers
|
||||
{
|
||||
name: "all matches when every element satisfies condition",
|
||||
policy: MustConstruct(
|
||||
All(".numbers", Equal(".", literal.Int(1))),
|
||||
),
|
||||
data: map[string]interface{}{
|
||||
"numbers": []interface{}{1, 1, 1},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "all fails when any element doesn't satisfy",
|
||||
policy: MustConstruct(
|
||||
All(".numbers", Equal(".", literal.Int(1))),
|
||||
),
|
||||
data: map[string]interface{}{
|
||||
"numbers": []interface{}{1, 2, 1},
|
||||
},
|
||||
expectedMatch: false,
|
||||
expectedStmt: MustConstruct(
|
||||
Equal(".", literal.Int(1)),
|
||||
)[0],
|
||||
},
|
||||
{
|
||||
name: "any succeeds when one element matches",
|
||||
policy: MustConstruct(
|
||||
Any(".numbers", Equal(".", literal.Int(2))),
|
||||
),
|
||||
data: map[string]interface{}{
|
||||
"numbers": []interface{}{1, 2, 3},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "any fails when no elements match",
|
||||
policy: MustConstruct(
|
||||
Any(".numbers", Equal(".", literal.Int(4))),
|
||||
),
|
||||
data: map[string]interface{}{
|
||||
"numbers": []interface{}{1, 2, 3},
|
||||
},
|
||||
expectedMatch: false,
|
||||
expectedStmt: MustConstruct(
|
||||
Any(".numbers", Equal(".", literal.Int(4))),
|
||||
)[0],
|
||||
},
|
||||
|
||||
// Complex nested case
|
||||
{
|
||||
name: "complex nested policy",
|
||||
policy: MustConstruct(
|
||||
And(
|
||||
Equal(".required", literal.String("present")),
|
||||
Equal(".optional?", literal.String("value")),
|
||||
Any(".items",
|
||||
And(
|
||||
Equal(".name", literal.String("test")),
|
||||
Like(".id", "ID*"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
data: map[string]any{
|
||||
"required": "present",
|
||||
"items": []any{
|
||||
map[string]any{
|
||||
"name": "wrong",
|
||||
"id": "ID123",
|
||||
},
|
||||
map[string]any{
|
||||
"name": "test",
|
||||
"id": "ID456",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
|
||||
// missing optional values for all the operators
|
||||
{
|
||||
name: "returns true for missing optional equal",
|
||||
policy: MustConstruct(
|
||||
Equal(".field?", literal.String("value")),
|
||||
),
|
||||
data: map[string]any{},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true for missing optional like pattern",
|
||||
policy: MustConstruct(
|
||||
Like(".pattern?", "test*"),
|
||||
),
|
||||
data: map[string]any{},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true for missing optional greater than",
|
||||
policy: MustConstruct(
|
||||
GreaterThan(".number?", literal.Int(5)),
|
||||
),
|
||||
data: map[string]any{},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true for missing optional less than",
|
||||
policy: MustConstruct(
|
||||
LessThan(".number?", literal.Int(5)),
|
||||
),
|
||||
data: map[string]any{},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true for missing optional array with all",
|
||||
policy: MustConstruct(
|
||||
All(".numbers?", Equal(".", literal.Int(1))),
|
||||
),
|
||||
data: map[string]any{},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true for missing optional array with any",
|
||||
policy: MustConstruct(
|
||||
Any(".numbers?", Equal(".", literal.Int(1))),
|
||||
),
|
||||
data: map[string]any{},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true for complex nested optional paths",
|
||||
policy: MustConstruct(
|
||||
And(
|
||||
Equal(".required", literal.String("present")),
|
||||
Any(".optional_array?",
|
||||
And(
|
||||
Equal(".name?", literal.String("test")),
|
||||
Like(".id?", "ID*"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
data: map[string]any{
|
||||
"required": "present",
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
{
|
||||
name: "returns true for partially present nested optional paths",
|
||||
policy: MustConstruct(
|
||||
And(
|
||||
Equal(".required", literal.String("present")),
|
||||
Any(".items",
|
||||
And(
|
||||
Equal(".name", literal.String("test")),
|
||||
Like(".optional_id?", "ID*"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
data: map[string]any{
|
||||
"required": "present",
|
||||
"items": []any{
|
||||
map[string]any{
|
||||
"name": "test",
|
||||
// optional_id is missing
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedStmt: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
node, err := literal.Map(tt.data)
|
||||
require.NoError(t, err)
|
||||
|
||||
match, stmt := tt.policy.PartialMatch(node)
|
||||
require.Equal(t, tt.expectedMatch, match)
|
||||
if tt.expectedStmt == nil {
|
||||
require.Nil(t, stmt)
|
||||
} else {
|
||||
require.Equal(t, tt.expectedStmt, stmt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvocationValidation applies the example policy to the second
|
||||
// example arguments as defined in the [Validation] section of the
|
||||
// invocation specification.
|
||||
//
|
||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||
func TestInvocationValidationSpecExamples(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pol := MustConstruct(
|
||||
Equal(".from", literal.String("alice@example.com")),
|
||||
Any(".to", Like(".", "*@example.com")),
|
||||
)
|
||||
|
||||
t.Run("with passing args", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
||||
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.String("bob@example.com"))
|
||||
qp.ListEntry(la, qp.String("carol@not.example.com"))
|
||||
}))
|
||||
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
||||
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exec, stmt := pol.Match(argsNode)
|
||||
assert.True(t, exec)
|
||||
assert.Nil(t, stmt)
|
||||
})
|
||||
|
||||
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
||||
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.String("bob@null.com"))
|
||||
qp.ListEntry(la, qp.String("carol@elsewhere.example.com"))
|
||||
}))
|
||||
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
||||
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exec, stmt := pol.Match(argsNode)
|
||||
assert.False(t, exec)
|
||||
assert.NotNil(t, stmt)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
identity = Selector{segment{str: ".", identity: true}}
|
||||
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
||||
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
||||
@@ -23,7 +22,7 @@ func Parse(str string) (Selector, error) {
|
||||
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
||||
}
|
||||
if str == "." {
|
||||
return identity, nil
|
||||
return Selector{segment{str: ".", identity: true}}, nil
|
||||
}
|
||||
if str == ".?" {
|
||||
return Selector{segment{str: ".?", identity: true, optional: true}}, nil
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package selector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
@@ -354,7 +353,6 @@ func TestParse(t *testing.T) {
|
||||
str := `.foo.["bar"].[138]?.baz[1:]`
|
||||
sel, err := Parse(str)
|
||||
require.NoError(t, err)
|
||||
printSegments(sel)
|
||||
require.Equal(t, str, sel.String())
|
||||
require.Equal(t, 7, len(sel))
|
||||
require.False(t, sel[0].Identity())
|
||||
@@ -404,13 +402,11 @@ func TestParse(t *testing.T) {
|
||||
t.Run("non dotted", func(t *testing.T) {
|
||||
_, err := Parse("foo")
|
||||
require.NotNil(t, err)
|
||||
fmt.Println(err)
|
||||
})
|
||||
|
||||
t.Run("non quoted", func(t *testing.T) {
|
||||
_, err := Parse(".[foo]")
|
||||
require.NotNil(t, err)
|
||||
fmt.Println(err)
|
||||
})
|
||||
|
||||
t.Run("slice with negative start and positive end", func(t *testing.T) {
|
||||
@@ -554,9 +550,3 @@ func TestParse(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func printSegments(s Selector) {
|
||||
for i, seg := range s {
|
||||
fmt.Printf("%d: %s\n", i, seg.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package selector
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -13,7 +12,6 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -87,8 +85,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
age := must.Int(must.Node(res.LookupByString("age")))
|
||||
require.Equal(t, int64(alice.Age), age)
|
||||
})
|
||||
@@ -101,8 +97,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
name := must.String(res)
|
||||
require.Equal(t, alice.Name.First, name)
|
||||
|
||||
@@ -110,8 +104,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
name = must.String(res)
|
||||
require.Equal(t, bob.Name.First, name)
|
||||
})
|
||||
@@ -124,8 +116,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
name := must.String(res)
|
||||
require.Equal(t, *alice.Name.Middle, name)
|
||||
|
||||
@@ -142,8 +132,6 @@ func TestSelect(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
require.Empty(t, res)
|
||||
|
||||
fmt.Println(err)
|
||||
|
||||
require.ErrorAs(t, err, &resolutionerr{}, "error should be a resolution error")
|
||||
})
|
||||
|
||||
@@ -164,8 +152,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name")))
|
||||
require.Equal(t, alice.Interests[0].Name, iname)
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package selector_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
@@ -55,7 +52,7 @@ func TestSupportedForms(t *testing.T) {
|
||||
require.NotNil(t, res)
|
||||
|
||||
exp := makeNode(t, tc.Output)
|
||||
equalIPLD(t, exp, res)
|
||||
require.True(t, ipld.DeepEqual(exp, res))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,23 +103,6 @@ func TestSupportedForms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func equalIPLD(t *testing.T, expected datamodel.Node, actual datamodel.Node) bool {
|
||||
t.Helper()
|
||||
|
||||
exp, act := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
if err := dagjson.Encode(expected, exp); err != nil {
|
||||
return assert.Fail(t, "Failed to encode json for expected IPLD node")
|
||||
}
|
||||
|
||||
if err := dagjson.Encode(actual, act); err != nil {
|
||||
return assert.Fail(t, "Failed to encode JSON for actual IPLD node")
|
||||
}
|
||||
|
||||
require.JSONEq(t, exp.String(), act.String())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func makeNode(t *testing.T, dagJsonInput string) ipld.Node {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user