feat: reorganize packages

This commit is contained in:
Steve Moyer
2024-09-24 11:40:28 -04:00
parent 5202056cc7
commit 6075c19957
36 changed files with 61 additions and 22 deletions

93
pkg/command/command.go Normal file
View File

@@ -0,0 +1,93 @@
package command
import (
"fmt"
"strings"
)
const separator = "/"
var _ fmt.Stringer = (*Command)(nil)
// Command is a concrete messages (a "verb") that MUST be unambiguously
// interpretable by the Subject of a UCAN.
//
// A [Command] is composed of a leading slash which is optionally followed
// by one or more slash-separated Segments of lowercase characters.
//
// [Command]: https://github.com/ucan-wg/spec#command
type Command struct {
segments []string
}
// New creates a validated command from the provided list of segment strings.
// An error is returned if an invalid Command would be formed
func New(segments ...string) Command {
return Command{segments: segments}
}
// Parse verifies that the provided string contains the required
// [segment structure] and, if valid, returns the resulting
// Command.
//
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
func Parse(s string) (Command, error) {
if !strings.HasPrefix(s, "/") {
return Command{}, ErrRequiresLeadingSlash
}
if len(s) > 1 && strings.HasSuffix(s, "/") {
return Command{}, ErrDisallowsTrailingSlash
}
if s != strings.ToLower(s) {
return Command{}, ErrRequiresLowercase
}
// The leading slash will result in the first element from strings.Split
// being an empty string which is removed as strings.Join will ignore it.
return Command{strings.Split(s, "/")[1:]}, nil
}
// MustParse is the same as Parse, but panic() if the parsing fail.
func MustParse(s string) Command {
c, err := Parse(s)
if err != nil {
panic(err)
}
return c
}
// [Top] is the most powerful capability.
//
// This function returns a Command that is a wildcard and therefore represents the
// most powerful ability. As such, it should be handled with care and used sparingly.
//
// [Top]: https://github.com/ucan-wg/spec#-aka-top
func Top() Command {
return New()
}
// IsValid returns true if the provided string is a valid UCAN command.
func IsValid(s string) bool {
_, err := Parse(s)
return err == nil
}
// Join appends segments to the end of this command using the required
// segment separator.
func (c Command) Join(segments ...string) Command {
return Command{append(c.segments, segments...)}
}
// Segments returns the ordered segments that comprise the Command as a
// slice of strings.
func (c Command) Segments() []string {
return c.segments
}
// String returns the composed representation the command. This is also
// the required wire representation (before IPLD encoding occurs.)
func (c Command) String() string {
return "/" + strings.Join(c.segments, "/")
}

View File

@@ -0,0 +1,21 @@
package command
import "fmt"
// ErrRequiresLeadingSlash is returned when a parsing a string that
// doesn't start with a [leading slash character].
//
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
var ErrRequiresLeadingSlash = fmt.Errorf("a command requires a leading slash character")
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
// with a trailing slash character].
//
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
var ErrDisallowsTrailingSlash = fmt.Errorf("a command must not include a trailing slash")
// ErrRequiresLowercase is returned if a Command contains, or would contain,
// [uppercase unicode characters].
//
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
var ErrRequiresLowercase = fmt.Errorf("UCAN path segments must must not contain upper-case characters")

144
pkg/command/command_test.go Normal file
View File

@@ -0,0 +1,144 @@
package command_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/command"
)
func TestTop(t *testing.T) {
require.Equal(t, "/", command.Top().String())
}
func TestIsValidCommand(t *testing.T) {
t.Parallel()
t.Run("succeeds when", func(t *testing.T) {
t.Parallel()
for _, testcase := range validTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
require.True(t, command.IsValid(testcase.inp))
})
}
})
t.Run("fails when", func(t *testing.T) {
t.Parallel()
for _, testcase := range invalidTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
require.False(t, command.IsValid(testcase.inp))
})
}
})
}
func TestParseCommand(t *testing.T) {
t.Parallel()
t.Run("succeeds when", func(t *testing.T) {
t.Parallel()
for _, testcase := range validTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
cmd, err := command.Parse("/elem0/elem1/elem2")
require.NoError(t, err)
require.NotNil(t, cmd)
})
}
})
t.Run("fails when", func(t *testing.T) {
t.Parallel()
for _, testcase := range invalidTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
cmd, err := command.Parse(testcase.inp)
require.ErrorIs(t, err, testcase.err)
require.Equal(t, command.Command{}, cmd)
})
}
})
}
type testcase struct {
name string
inp string
}
func validTestcases(t *testing.T) []testcase {
t.Helper()
cmds := []string{
"/",
"/crud",
"/crud/create",
"/stack/pop",
"/crypto/sign",
"/foo/bar/baz/qux/quux",
"/ほげ/ふが",
}
testcases := make([]testcase, len(cmds))
for i, inp := range cmds {
testcases[i] = testcase{
name: "Command is " + inp,
inp: inp,
}
}
return testcases
}
type errorTestcase struct {
testcase
err error
}
func invalidTestcases(t *testing.T) []errorTestcase {
t.Helper()
return []errorTestcase{
{
testcase: testcase{
name: "leading slash is missing",
inp: "elem0/elem1/elem2",
},
err: command.ErrRequiresLeadingSlash,
},
{
testcase: testcase{
name: "trailing slash is present",
inp: "/elem0/elem1/elem2/",
},
err: command.ErrDisallowsTrailingSlash,
},
{
testcase: testcase{
name: "uppercase character are present",
inp: "/elem0/Elem1/elem2",
},
err: command.ErrRequiresLowercase,
},
}
}

View File

@@ -2,12 +2,16 @@ package meta
import (
"errors"
"fmt"
"reflect"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/basicnode"
)
var ErrUnsupported = errors.New("failure adding unsupported type to meta")
var ErrNotFound = errors.New("key-value not found in meta")
// Meta is a container for meta key-value pairs in a UCAN token.
@@ -113,8 +117,20 @@ func (m *Meta) Add(key string, val any) error {
case datamodel.Node:
m.Values[key] = val
default:
panic("invalid value type")
return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val))
}
m.Keys = append(m.Keys, key)
return nil
}
func fqtn(val any) string {
var name string
t := reflect.TypeOf(val)
for t.Kind() == reflect.Pointer {
name += "*"
t = t.Elem()
}
return name + t.PkgPath() + "." + t.Name()
}

23
pkg/meta/meta_test.go Normal file
View File

@@ -0,0 +1,23 @@
package meta_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/meta"
"gotest.tools/v3/assert"
)
func TestMeta_Add(t *testing.T) {
t.Parallel()
type Unsupported struct{}
t.Run("error if not primative or Node", func(t *testing.T) {
t.Parallel()
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
require.ErrorIs(t, err, meta.ErrUnsupported)
assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported")
})
}

79
pkg/policy/glob.go Normal file
View File

@@ -0,0 +1,79 @@
package policy
import "fmt"
type glob string
// parseGlob ensures that the pattern conforms to the spec: only '*' and escaped '\*' are allowed.
func parseGlob(pattern string) (glob, error) {
for i := 0; i < len(pattern); i++ {
if pattern[i] == '*' {
continue
}
if pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == '*' {
i++ // skip the escaped '*'
continue
}
if pattern[i] == '\\' && i+1 < len(pattern) {
i++ // skip the escaped character
continue
}
if pattern[i] == '\\' {
return "", fmt.Errorf("invalid escape sequence")
}
}
return glob(pattern), nil
}
func mustParseGlob(pattern string) glob {
g, err := parseGlob(pattern)
if err != nil {
panic(err)
}
return g
}
// Match matches a string against the glob pattern with * wildcards, handling escaped '\*' literals.
func (pattern glob) Match(str string) bool {
// i is the index for the pattern
// j is the index for the string
var i, j int
// starIdx keeps track of the position of the last * in the pattern.
// matchIdx keeps track of the position in the string where the last * matched.
var starIdx, matchIdx int = -1, -1
for j < len(str) {
if i < len(pattern) && (pattern[i] == str[j] || pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == str[j]) {
// characters match or if there's an escaped character that matches
if pattern[i] == '\\' {
// skip the escape character
i++
}
i++
j++
} else if i < len(pattern) && pattern[i] == '*' {
// there's a * wildcard in the pattern
starIdx = i
matchIdx = j
i++
} else if starIdx != -1 {
// there's a previous * wildcard, backtrack
i = starIdx + 1
matchIdx++
j = matchIdx
} else {
// no match found
return false
}
}
// check for remaining characters in the pattern
for i < len(pattern) && pattern[i] == '*' {
i++
}
// the entire pattern is processed, it's a match
return i == len(pattern)
}

73
pkg/policy/glob_test.go Normal file
View File

@@ -0,0 +1,73 @@
package policy
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSimpleGlobMatch(t *testing.T) {
tests := []struct {
pattern string
str string
matches bool
}{
// Basic matching
{"*", "anything", true},
{"a*", "abc", true},
{"*c", "abc", true},
{"a*c", "abc", true},
{"a*c", "abxc", true},
{"a*c", "ac", true},
{"a*c", "a", false},
{"a*c", "ab", false},
// Escaped characters
{"a\\*c", "a*c", true},
{"a\\*c", "abc", false},
// Mixed wildcards and literals
{"a*b*c", "abc", true},
{"a*b*c", "aXbYc", true},
{"a*b*c", "aXbY", false},
{"a*b*c", "abYc", true},
{"a*b*c", "aXbc", true},
{"a*b*c", "aXbYcZ", false},
// Edge cases
{"", "", true},
{"", "a", false},
{"*", "", true},
{"*", "a", true},
{"\\*", "*", true},
{"\\*", "a", false},
// Specified test cases
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Dan, Erin, Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob , Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol", false},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol!", false},
{"Alice\\*, Bob*, Carol.", "Alice, Bob, Carol.", false},
{"Alice\\*, Bob*, Carol.", "Alice Cooper, Bob, Carol.", false},
{"Alice\\*, Bob*, Carol.", " Alice*, Bob, Carol. ", false},
}
for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) {
g, err := parseGlob(tt.pattern)
require.NoError(t, err)
require.Equal(t, tt.matches, g.Match(tt.str))
})
}
}
func BenchmarkGlob(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
g := mustParseGlob("Alice\\*, Bob*, Carol.")
g.Match("Alice*, Bob*, Carol!")
}
}

269
pkg/policy/ipld.go Normal file
View File

@@ -0,0 +1,269 @@
package policy
import (
"fmt"
"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/must"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
)
func FromIPLD(node datamodel.Node) (Policy, error) {
return statementsFromIPLD("/", node)
}
func FromDagJson(json string) (Policy, error) {
nodes, err := ipld.Decode([]byte(json), dagjson.Decode)
if err != nil {
return nil, err
}
return FromIPLD(nodes)
}
func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
// sanity checks
if node.Kind() != datamodel.Kind_List {
return nil, ErrNotATuple(path)
}
if node.Length() != 2 && node.Length() != 3 {
return nil, ErrUnrecognizedShape(path)
}
// extract operator
opNode, _ := node.LookupByIndex(0)
if opNode.Kind() != datamodel.Kind_String {
return nil, ErrNotAString(path)
}
op := must.String(opNode)
arg2AsSelector := func(op string) (selector.Selector, error) {
nd, _ := node.LookupByIndex(1)
if nd.Kind() != datamodel.Kind_String {
return nil, ErrNotAString(combinePath(path, op, 1))
}
sel, err := selector.Parse(must.String(nd))
if err != nil {
return nil, ErrInvalidSelector(combinePath(path, op, 1), err)
}
return sel, nil
}
switch node.Length() {
case 2:
switch op {
case KindNot:
arg2, _ := node.LookupByIndex(1)
statement, err := statementFromIPLD(combinePath(path, op, 1), arg2)
if err != nil {
return nil, err
}
return Not(statement), nil
case KindAnd, KindOr:
arg2, _ := node.LookupByIndex(1)
statement, err := statementsFromIPLD(combinePath(path, op, 1), arg2)
if err != nil {
return nil, err
}
return connective{kind: op, statements: statement}, nil
default:
return nil, ErrUnrecognizedOperator(path, op)
}
case 3:
switch op {
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
sel, err := arg2AsSelector(op)
if err != nil {
return nil, err
}
arg3, _ := node.LookupByIndex(2)
return equality{kind: op, selector: sel, value: arg3}, nil
case KindLike:
sel, err := arg2AsSelector(op)
if err != nil {
return nil, err
}
pattern, _ := node.LookupByIndex(2)
if pattern.Kind() != datamodel.Kind_String {
return nil, ErrNotAString(combinePath(path, op, 2))
}
res, err := Like(sel, must.String(pattern))
if err != nil {
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
}
return res, nil
case KindAll, KindAny:
sel, err := arg2AsSelector(op)
if err != nil {
return nil, err
}
statementsNode, _ := node.LookupByIndex(2)
statement, err := statementFromIPLD(combinePath(path, op, 1), statementsNode)
if err != nil {
return nil, err
}
return quantifier{kind: op, selector: sel, statement: statement}, nil
default:
return nil, ErrUnrecognizedOperator(path, op)
}
default:
return nil, ErrUnrecognizedShape(path)
}
}
func statementsFromIPLD(path string, node datamodel.Node) ([]Statement, error) {
// sanity checks
if node.Kind() != datamodel.Kind_List {
return nil, ErrNotATuple(path)
}
if node.Length() == 0 {
return nil, nil
}
res := make([]Statement, node.Length())
for i := int64(0); i < node.Length(); i++ {
nd, _ := node.LookupByIndex(i)
statement, err := statementFromIPLD(fmt.Sprintf("%s%d/", path, i), nd)
if err != nil {
return nil, err
}
res[i] = statement
}
return res, nil
}
func (p Policy) ToIPLD() (datamodel.Node, error) {
return statementsToIPLD(p)
}
func statementsToIPLD(statements []Statement) (datamodel.Node, error) {
list := basicnode.Prototype.List.NewBuilder()
// can't error, we have the right builder.
listBuilder, _ := list.BeginList(int64(len(statements)))
for _, argStatement := range statements {
node, err := statementToIPLD(argStatement)
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignNode(node)
if err != nil {
return nil, err
}
}
err := listBuilder.Finish()
if err != nil {
return nil, err
}
return list.Build(), nil
}
func statementToIPLD(statement Statement) (datamodel.Node, error) {
list := basicnode.Prototype.List.NewBuilder()
length := int64(3)
switch statement.(type) {
case negation, connective:
length = 2
}
// can't error, we have the right builder.
listBuilder, _ := list.BeginList(length)
switch statement := statement.(type) {
case equality:
err := listBuilder.AssembleValue().AssignString(statement.kind)
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignString(statement.selector.String())
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignNode(statement.value)
if err != nil {
return nil, err
}
case negation:
err := listBuilder.AssembleValue().AssignString(statement.Kind())
if err != nil {
return nil, err
}
node, err := statementToIPLD(statement.statement)
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignNode(node)
if err != nil {
return nil, err
}
case connective:
err := listBuilder.AssembleValue().AssignString(statement.kind)
if err != nil {
return nil, err
}
args, err := statementsToIPLD(statement.statements)
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignNode(args)
if err != nil {
return nil, err
}
case wildcard:
err := listBuilder.AssembleValue().AssignString(statement.Kind())
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignString(statement.selector.String())
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignString(string(statement.pattern))
if err != nil {
return nil, err
}
case quantifier:
err := listBuilder.AssembleValue().AssignString(statement.kind)
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignString(statement.selector.String())
if err != nil {
return nil, err
}
args, err := statementToIPLD(statement.statement)
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignNode(args)
if err != nil {
return nil, err
}
}
err := listBuilder.Finish()
if err != nil {
return nil, err
}
return list.Build(), nil
}
func combinePath(prev string, operator string, index int) string {
return fmt.Sprintf("%s%d-%s/", prev, index, operator)
}

43
pkg/policy/ipld_errors.go Normal file
View File

@@ -0,0 +1,43 @@
package policy
import "fmt"
type errWithPath struct {
path string
msg string
}
func (e errWithPath) Error() string {
return fmt.Sprintf("IPLD path '%s': %s", e.path, e.msg)
}
func ErrInvalidSelector(path string, err error) error {
return errWithPath{path: path, msg: fmt.Sprintf("invalid selector: %s", err)}
}
func ErrInvalidPattern(path string, err error) error {
return errWithPath{path: path, msg: fmt.Sprintf("invalid pattern: %s", err)}
}
func ErrNotAString(path string) error {
return errWithPath{path: path, msg: ""}
}
func ErrUnrecognizedOperator(path string, op string) error {
return errWithPath{path: path, msg: fmt.Sprintf("unrecognized operator '%s'", safeStr(op))}
}
func ErrUnrecognizedShape(path string) error {
return errWithPath{path: path, msg: "unrecognized shape"}
}
func ErrNotATuple(path string) error {
return errWithPath{path: path, msg: "not a tuple"}
}
func safeStr(str string) string {
if len(str) > 10 {
return str[:10]
}
return str
}

43
pkg/policy/ipld_test.go Normal file
View File

@@ -0,0 +1,43 @@
package policy
import (
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/stretchr/testify/require"
)
func TestIpldRoundTrip(t *testing.T) {
const illustrativeExample = `
[
["==", ".status", "draft"],
["all", ".reviewer", ["like", ".email", "*@example.com"]],
["any", ".tags",
["or", [
["==", ".", "news"],
["==", ".", "press"]
]]
]
]`
for _, tc := range []struct {
name, dagJsonStr string
}{
{"illustrativeExample", illustrativeExample},
} {
nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode)
require.NoError(t, err)
pol, err := FromIPLD(nodes)
require.NoError(t, err)
wroteIpld, err := pol.ToIPLD()
require.NoError(t, err)
wroteAsDagJson, err := ipld.Encode(wroteIpld, dagjson.Encode)
require.NoError(t, err)
require.JSONEq(t, tc.dagJsonStr, string(wroteAsDagJson))
}
}

View File

@@ -0,0 +1,52 @@
package literal
import (
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/node/basicnode"
)
func Node(n ipld.Node) ipld.Node {
return n
}
func Link(cid ipld.Link) ipld.Node {
nb := basicnode.Prototype.Link.NewBuilder()
nb.AssignLink(cid)
return nb.Build()
}
func Bool(val bool) ipld.Node {
nb := basicnode.Prototype.Bool.NewBuilder()
nb.AssignBool(val)
return nb.Build()
}
func Int(val int64) ipld.Node {
nb := basicnode.Prototype.Int.NewBuilder()
nb.AssignInt(val)
return nb.Build()
}
func Float(val float64) ipld.Node {
nb := basicnode.Prototype.Float.NewBuilder()
nb.AssignFloat(val)
return nb.Build()
}
func String(val string) ipld.Node {
nb := basicnode.Prototype.String.NewBuilder()
nb.AssignString(val)
return nb.Build()
}
func Bytes(val []byte) ipld.Node {
nb := basicnode.Prototype.Bytes.NewBuilder()
nb.AssignBytes(val)
return nb.Build()
}
func Null() ipld.Node {
nb := basicnode.Prototype.Any.NewBuilder()
nb.AssignNull()
return nb.Build()
}

183
pkg/policy/match.go Normal file
View File

@@ -0,0 +1,183 @@
package policy
import (
"cmp"
"fmt"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/must"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
)
// Match determines if the IPLD node matches the policy document.
func Match(policy Policy, node ipld.Node) bool {
for _, stmt := range policy {
ok := matchStatement(stmt, node)
if !ok {
return false
}
}
return true
}
func matchStatement(statement Statement, node ipld.Node) bool {
switch statement.Kind() {
case KindEqual:
if s, ok := statement.(equality); ok {
one, many, err := selector.Select(s.selector, node)
if err != nil {
return false
}
if one != nil {
return datamodel.DeepEqual(s.value, one)
}
if many != nil {
for _, n := range many {
if eq := datamodel.DeepEqual(s.value, n); eq {
return true
}
}
}
return false
}
case KindGreaterThan:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
if err != nil || one == nil {
return false
}
return isOrdered(s.value, one, gt)
}
case KindGreaterThanOrEqual:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
if err != nil || one == nil {
return false
}
return isOrdered(s.value, one, gte)
}
case KindLessThan:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
if err != nil || one == nil {
return false
}
return isOrdered(s.value, one, lt)
}
case KindLessThanOrEqual:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
if err != nil || one == nil {
return false
}
return isOrdered(s.value, one, lte)
}
case KindNot:
if s, ok := statement.(negation); ok {
return !matchStatement(s.statement, node)
}
case KindAnd:
if s, ok := statement.(connective); ok {
for _, cs := range s.statements {
r := matchStatement(cs, node)
if !r {
return false
}
}
return true
}
case KindOr:
if s, ok := statement.(connective); ok {
if len(s.statements) == 0 {
return true
}
for _, cs := range s.statements {
r := matchStatement(cs, node)
if r {
return true
}
}
return false
}
case KindLike:
if s, ok := statement.(wildcard); ok {
one, _, err := selector.Select(s.selector, node)
if err != nil || one == nil {
return false
}
v, err := one.AsString()
if err != nil {
return false
}
return s.pattern.Match(v)
}
case KindAll:
if s, ok := statement.(quantifier); ok {
_, many, err := selector.Select(s.selector, node)
if err != nil || many == nil {
return false
}
for _, n := range many {
ok := matchStatement(s.statement, n)
if !ok {
return false
}
}
return true
}
case KindAny:
if s, ok := statement.(quantifier); ok {
one, many, err := selector.Select(s.selector, node)
if err != nil {
return false
}
if one != nil {
ok := matchStatement(s.statement, one)
if ok {
return true
}
}
if many != nil {
for _, n := range many {
ok := matchStatement(s.statement, n)
if ok {
return true
}
}
}
return false
}
}
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind()))
}
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
a := must.Int(actual)
b := must.Int(expected)
return satisfies(cmp.Compare(a, b))
}
if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float {
a, err := actual.AsFloat()
if err != nil {
panic(fmt.Errorf("extracting node float: %w", err))
}
b, err := expected.AsFloat()
if err != nil {
panic(fmt.Errorf("extracting selector float: %w", err))
}
return satisfies(cmp.Compare(a, b))
}
return false
}
func gt(order int) bool { return order == 1 }
func gte(order int) bool { return order == 0 || order == 1 }
func lt(order int) bool { return order == -1 }
func lte(order int) bool { return order == 0 || order == -1 }

541
pkg/policy/match_test.go Normal file
View File

@@ -0,0 +1,541 @@
package policy
import (
"fmt"
"testing"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"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/require"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
)
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()
pol := Policy{Equal(selector.MustParse("."), literal.String("test"))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))}
ok = Match(pol, nd)
require.False(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.Int(138))}
ok = Match(pol, nd)
require.False(t, ok)
})
t.Run("int", func(t *testing.T) {
np := basicnode.Prototype.Int
nb := np.NewBuilder()
nb.AssignInt(138)
nd := nb.Build()
pol := Policy{Equal(selector.MustParse("."), literal.Int(138))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))}
ok = Match(pol, nd)
require.False(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
ok = Match(pol, nd)
require.False(t, ok)
})
t.Run("float", func(t *testing.T) {
np := basicnode.Prototype.Float
nb := np.NewBuilder()
nb.AssignFloat(1.138)
nd := nb.Build()
pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))}
ok = Match(pol, nd)
require.False(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
ok = Match(pol, nd)
require.False(t, ok)
})
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()
pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))}
ok = Match(pol, nd)
require.False(t, ok)
pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))}
ok = Match(pol, nd)
require.False(t, ok)
})
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()
pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))}
ok = Match(pol, nd)
require.True(t, ok)
pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))}
ok = Match(pol, nd)
require.False(t, ok)
pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))}
ok = Match(pol, nd)
require.False(t, ok)
})
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()
pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))}
ok = Match(pol, nd)
require.False(t, ok)
})
})
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()
pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))}
ok := Match(pol, nd)
require.True(t, ok)
})
t.Run("gte int", func(t *testing.T) {
np := basicnode.Prototype.Int
nb := np.NewBuilder()
nb.AssignInt(138)
nd := nb.Build()
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))}
ok = Match(pol, nd)
require.True(t, ok)
})
t.Run("gt float", func(t *testing.T) {
np := basicnode.Prototype.Float
nb := np.NewBuilder()
nb.AssignFloat(1.38)
nd := nb.Build()
pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))}
ok := Match(pol, nd)
require.True(t, ok)
})
t.Run("gte float", func(t *testing.T) {
np := basicnode.Prototype.Float
nb := np.NewBuilder()
nb.AssignFloat(1.38)
nd := nb.Build()
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))}
ok = Match(pol, nd)
require.True(t, ok)
})
t.Run("lt int", func(t *testing.T) {
np := basicnode.Prototype.Int
nb := np.NewBuilder()
nb.AssignInt(138)
nd := nb.Build()
pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))}
ok := Match(pol, nd)
require.True(t, ok)
})
t.Run("lte int", func(t *testing.T) {
np := basicnode.Prototype.Int
nb := np.NewBuilder()
nb.AssignInt(138)
nd := nb.Build()
pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))}
ok = Match(pol, nd)
require.True(t, ok)
})
})
t.Run("negation", func(t *testing.T) {
np := basicnode.Prototype.Bool
nb := np.NewBuilder()
nb.AssignBool(false)
nd := nb.Build()
pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))}
ok = Match(pol, nd)
require.False(t, ok)
})
t.Run("conjunction", func(t *testing.T) {
np := basicnode.Prototype.Int
nb := np.NewBuilder()
nb.AssignInt(138)
nd := nb.Build()
pol := Policy{
And(
GreaterThan(selector.MustParse("."), literal.Int(1)),
LessThan(selector.MustParse("."), literal.Int(1138)),
),
}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{
And(
GreaterThan(selector.MustParse("."), literal.Int(1)),
Equal(selector.MustParse("."), literal.Int(1138)),
),
}
ok = Match(pol, nd)
require.False(t, ok)
pol = Policy{And()}
ok = Match(pol, nd)
require.True(t, ok)
})
t.Run("disjunction", func(t *testing.T) {
np := basicnode.Prototype.Int
nb := np.NewBuilder()
nb.AssignInt(138)
nd := nb.Build()
pol := Policy{
Or(
GreaterThan(selector.MustParse("."), literal.Int(138)),
LessThan(selector.MustParse("."), literal.Int(1138)),
),
}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{
Or(
GreaterThan(selector.MustParse("."), literal.Int(138)),
Equal(selector.MustParse("."), literal.Int(1138)),
),
}
ok = Match(pol, nd)
require.False(t, ok)
pol = Policy{Or()}
ok = Match(pol, nd)
require.True(t, ok)
})
t.Run("wildcard", func(t *testing.T) {
pattern := `Alice\*, Bob*, Carol.`
for _, s := range []string{
"Alice*, Bob, Carol.",
"Alice*, Bob, Dan, Erin, Carol.",
"Alice*, Bob , Carol.",
"Alice*, Bob*, Carol.",
} {
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()
statement, err := Like(selector.MustParse("."), pattern)
require.NoError(t, err)
pol := Policy{statement}
ok := Match(pol, nd)
require.True(t, ok)
})
}(s)
}
for _, s := range []string{
"Alice*, Bob, Carol",
"Alice*, Bob*, Carol!",
"Alice Cooper, Bob, Carol.",
"Alice, Bob, Carol.",
" Alice*, Bob, Carol. ",
} {
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()
statement, err := Like(selector.MustParse("."), pattern)
require.NoError(t, err)
pol := Policy{statement}
ok := Match(pol, nd)
require.False(t, ok)
})
}(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()
pol := Policy{
All(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(2)),
),
}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{
All(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(20)),
),
}
ok = Match(pol, nd)
require.False(t, ok)
})
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()
pol := Policy{
Any(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(60)),
),
}
ok := Match(pol, nd)
require.True(t, ok)
pol = Policy{
Any(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(100)),
),
}
ok = Match(pol, nd)
require.False(t, ok)
})
})
}
func TestPolicyExamples(t *testing.T) {
makeNode := func(data string) ipld.Node {
nd, err := ipld.Decode([]byte(data), dagjson.Decode)
require.NoError(t, err)
return nd
}
evaluate := func(statement string, data ipld.Node) bool {
// we need to wrap statement with [] to make them a policy
policy := fmt.Sprintf("[%s]", statement)
pol, err := FromDagJson(policy)
require.NoError(t, err)
return Match(pol, data)
}
t.Run("And", func(t *testing.T) {
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
require.True(t, evaluate(`["and", []]`, data))
require.True(t, evaluate(`
["and", [
["==", ".name", "Katie"],
[">=", ".age", 21]
]]`, data))
require.False(t, evaluate(`
["and", [
["==", ".name", "Katie"],
[">=", ".age", 21],
["==", ".nationalities", ["American"]]
]]`, data))
})
t.Run("Or", func(t *testing.T) {
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
require.True(t, evaluate(`["or", []]`, data))
require.True(t, evaluate(`
["or", [
["==", ".name", "Katie"],
[">", ".age", 45]
]]
`, data))
})
t.Run("Not", func(t *testing.T) {
data := makeNode(`{ "name": "Katie", "nationalities": ["Canadian", "South African"] }`)
require.True(t, evaluate(`
["not",
["and", [
["==", ".name", "Katie"],
["==", ".nationalities", ["American"]]
]]
]
`, data))
})
t.Run("All", func(t *testing.T) {
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
require.False(t, evaluate(`["all", ".a", [">", ".b", 0]]`, data))
})
t.Run("Any", func(t *testing.T) {
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data))
})
}
func FuzzMatch(f *testing.F) {
// Policy + Data examples
f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`))
f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`))
f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`))
f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))
f.Add([]byte(`[["and", [["==", ".role", "admin"], [">=", ".experience", 5]]]]`), []byte(`{"role": "admin", "experience": 6}`))
f.Add([]byte(`[["or", [["==", ".department", "HR"], ["==", ".department", "Finance"]]]]`), []byte(`{"department": "HR"}`))
f.Add([]byte(`[["like", ".email", "*@company.com"]]`), []byte(`{"email": "user@company.com"}`))
f.Add([]byte(`[["all", ".projects", [">", ".budget", 10000]]]`), []byte(`{"projects": [{"budget": 15000}, {"budget": 8000}]}`))
f.Add([]byte(`[["any", ".skills", ["==", ".", "Go"]]]`), []byte(`{"skills": ["Go", "Python", "JavaScript"]}`))
f.Add(
[]byte(`[["and", [
["==", ".name", "Bob"],
["or", [[">", ".age", 25],["==", ".status", "active"]]],
["all", ".tasks", ["==", ".completed", true]]
]]]`),
[]byte(`{
"name": "Bob",
"age": 26,
"status": "active",
"tasks": [{"completed": true}, {"completed": true}, {"completed": false}]
}`),
)
f.Fuzz(func(t *testing.T, policyBytes []byte, dataBytes []byte) {
policyNode, err := ipld.Decode(policyBytes, dagjson.Decode)
if err != nil {
t.Skip()
}
dataNode, err := ipld.Decode(dataBytes, dagjson.Decode)
if err != nil {
t.Skip()
}
// policy node -> policy object
policy, err := FromIPLD(policyNode)
if err != nil {
t.Skip()
}
Match(policy, dataNode)
})
}

124
pkg/policy/policy.go Normal file
View File

@@ -0,0 +1,124 @@
package policy
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
import (
"github.com/ipld/go-ipld-prime"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
)
const (
KindEqual = "==" // implemented by equality
KindGreaterThan = ">" // implemented by equality
KindGreaterThanOrEqual = ">=" // implemented by equality
KindLessThan = "<" // implemented by equality
KindLessThanOrEqual = "<=" // implemented by equality
KindNot = "not" // implemented by negation
KindAnd = "and" // implemented by connective
KindOr = "or" // implemented by connective
KindLike = "like" // implemented by wildcard
KindAll = "all" // implemented by quantifier
KindAny = "any" // implemented by quantifier
)
type Policy []Statement
type Statement interface {
Kind() string
}
type equality struct {
kind string
selector selector.Selector
value ipld.Node
}
func (e equality) Kind() string {
return e.kind
}
func Equal(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindEqual, selector: selector, value: value}
}
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindGreaterThan, selector: selector, value: value}
}
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindGreaterThanOrEqual, selector: selector, value: value}
}
func LessThan(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindLessThan, selector: selector, value: value}
}
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindLessThanOrEqual, selector: selector, value: value}
}
type negation struct {
statement Statement
}
func (n negation) Kind() string {
return KindNot
}
func Not(stmt Statement) Statement {
return negation{statement: stmt}
}
type connective struct {
kind string
statements []Statement
}
func (c connective) Kind() string {
return c.kind
}
func And(stmts ...Statement) Statement {
return connective{kind: KindAnd, statements: stmts}
}
func Or(stmts ...Statement) Statement {
return connective{kind: KindOr, statements: stmts}
}
type wildcard struct {
selector selector.Selector
pattern glob
}
func (n wildcard) Kind() string {
return KindLike
}
func Like(selector selector.Selector, pattern string) (Statement, error) {
g, err := parseGlob(pattern)
if err != nil {
return nil, err
}
return wildcard{selector: selector, pattern: g}, nil
}
type quantifier struct {
kind string
selector selector.Selector
statement Statement
}
func (n quantifier) Kind() string {
return n.kind
}
func All(selector selector.Selector, statement Statement) Statement {
return quantifier{kind: KindAll, selector: selector, statement: statement}
}
func Any(selector selector.Selector, statement Statement) Statement {
return quantifier{kind: KindAny, selector: selector, statement: statement}
}

View File

@@ -0,0 +1,160 @@
package selector
import (
"fmt"
"strconv"
"strings"
)
func Parse(str string) (Selector, error) {
if len(str) == 0 {
return nil, newParseError("empty selector", str, 0, "")
}
if string(str[0]) != "." {
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
}
col := 0
var sel Selector
for _, tok := range tokenize(str) {
seg := tok
opt := strings.HasSuffix(tok, "?")
if opt {
seg = tok[0 : len(tok)-1]
}
switch seg {
case ".":
if len(sel) > 0 && sel[len(sel)-1].Identity() {
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
}
sel = append(sel, Identity)
case "[]":
sel = append(sel, segment{tok, false, opt, true, nil, "", 0})
default:
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
lookup := seg[1 : len(seg)-1]
if indexRegex.MatchString(lookup) { // index
idx, err := strconv.Atoi(lookup)
if err != nil {
return nil, newParseError("invalid index", str, col, tok)
}
sel = append(sel, segment{str: tok, optional: opt, index: idx})
} else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field
sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]})
} else if sliceRegex.MatchString(lookup) { // slice [3:5] or [:5] or [3:]
var rng []int
splt := strings.Split(lookup, ":")
if splt[0] == "" {
rng = append(rng, 0)
} else {
i, err := strconv.Atoi(splt[0])
if err != nil {
return nil, newParseError("invalid slice index", str, col, tok)
}
rng = append(rng, i)
}
if splt[1] != "" {
i, err := strconv.Atoi(splt[1])
if err != nil {
return nil, newParseError("invalid slice index", str, col, tok)
}
rng = append(rng, i)
}
sel = append(sel, segment{str: tok, optional: opt, slice: rng})
} else {
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
}
} else if fieldRegex.MatchString(seg) {
sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]})
} else {
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
}
}
col += len(tok)
}
return sel, nil
}
func MustParse(sel string) Selector {
s, err := Parse(sel)
if err != nil {
panic(err)
}
return s
}
func tokenize(str string) []string {
var toks []string
col := 0
ofs := 0
ctx := ""
for col < len(str) {
char := string(str[col])
if char == "\"" && string(str[col-1]) != "\\" {
col++
if ctx == "\"" {
ctx = ""
} else {
ctx = "\""
}
continue
}
if ctx == "\"" {
col++
continue
}
if char == "." || char == "[" {
if ofs < col {
toks = append(toks, str[ofs:col])
}
ofs = col
}
col++
}
if ofs < col && ctx != "\"" {
toks = append(toks, str[ofs:col])
}
return toks
}
type parseerr struct {
msg string
src string
col int
tok string
}
func (p parseerr) Name() string {
return "ParseError"
}
func (p parseerr) Message() string {
return p.msg
}
func (p parseerr) Column() int {
return p.col
}
func (p parseerr) Error() string {
return p.msg
}
func (p parseerr) Source() string {
return p.src
}
func (p parseerr) Token() string {
return p.tok
}
func newParseError(message string, source string, column int, token string) error {
return parseerr{message, source, column, token}
}

View File

@@ -0,0 +1,462 @@
package selector
import (
"fmt"
"regexp"
"strings"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/schema"
)
// Selector describes a UCAN policy selector, as specified here:
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
type Selector []segment
func (s Selector) String() string {
var res strings.Builder
for _, seg := range s {
res.WriteString(seg.String())
}
return res.String()
}
var Identity = segment{".", true, false, false, nil, "", 0}
var (
indexRegex = regexp.MustCompile(`^-?\d+$`)
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
)
type segment struct {
str string
identity bool
optional bool
iterator bool
slice []int
field string
index int
}
// String returns the segment's string representation.
func (s segment) String() string {
return s.str
}
// Identity flags that this selector is the identity selector.
func (s segment) Identity() bool {
return s.identity
}
// Optional flags that this selector is optional.
func (s segment) Optional() bool {
return s.optional
}
// Iterator flags that this selector is an iterator segment.
func (s segment) Iterator() bool {
return s.iterator
}
// Slice flags that this segment targets a range of a slice.
func (s segment) Slice() []int {
return s.slice
}
// Field is the name of a field in a struct/map.
func (s segment) Field() string {
return s.field
}
// Index is an index of a slice.
func (s segment) Index() int {
return s.index
}
// Select uses a selector to extract an IPLD node or set of nodes from the
// passed subject node.
func Select(sel Selector, subject ipld.Node) (ipld.Node, []ipld.Node, error) {
return resolve(sel, subject, nil)
}
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) {
cur := subject
for i, seg := range sel {
if seg.Identity() {
continue
}
// 1st level: handle the different segment types (iterator, field, slice, index)
// 2nd level: handle different node kinds (list, map, string, bytes)
switch {
case seg.Iterator():
if cur == nil || cur.Kind() == datamodel.Kind_Null {
if seg.Optional() {
// build empty list
nb := basicnode.Prototype.List.NewBuilder()
assembler, err := nb.BeginList(0)
if err != nil {
return nil, nil, err
}
if err = assembler.Finish(); err != nil {
return nil, nil, err
}
return nb.Build(), nil, nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
}
} else {
var many []ipld.Node
switch cur.Kind() {
case datamodel.Kind_List:
it := cur.ListIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return nil, nil, err
}
// check if there are more iterator segments
if len(sel) > i+1 && sel[i+1].Iterator() {
if v.Kind() == datamodel.Kind_List {
// recursively resolve the remaining selector segments
var o ipld.Node
var m []ipld.Node
o, m, err = resolve(sel[i+1:], v, at)
if err != nil {
// if the segment is optional and an error occurs, skip the current iteration.
if seg.Optional() {
continue
} else {
return nil, nil, err
}
}
if m != nil {
many = append(many, m...)
} else if o != nil {
many = append(many, o)
}
} else {
// if the current value is not a list and the next segment is optional, skip the current iteration
if sel[i+1].Optional() {
continue
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at)
}
}
} else {
// if there are no more iterator segments, append the current value to the result
many = append(many, v)
}
}
case datamodel.Kind_Map:
it := cur.MapIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return nil, nil, err
}
if len(sel) > i+1 && sel[i+1].Iterator() {
if v.Kind() == datamodel.Kind_List {
var o ipld.Node
var m []ipld.Node
o, m, err = resolve(sel[i+1:], v, at)
if err != nil {
if seg.Optional() {
continue
} else {
return nil, nil, err
}
}
if m != nil {
many = append(many, m...)
} else if o != nil {
many = append(many, o)
}
} else {
if sel[i+1].Optional() {
continue
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at)
}
}
} else {
many = append(many, v)
}
}
default:
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
}
return nil, many, nil
}
case seg.Field() != "":
at = append(at, seg.Field())
if cur == nil {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
}
} else {
switch cur.Kind() {
case datamodel.Kind_Map:
n, err := cur.LookupByString(seg.Field())
if err != nil {
if isMissing(err) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
}
} else {
return nil, nil, err
}
} else {
cur = n
}
case datamodel.Kind_List:
var many []ipld.Node
it := cur.ListIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return nil, nil, err
}
if v.Kind() == datamodel.Kind_Map {
n, err := v.LookupByString(seg.Field())
if err == nil {
many = append(many, n)
}
}
}
if len(many) > 0 {
cur = nil
return nil, many, nil
} else if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("no elements in list have field named: %s", seg.Field()), at)
}
default:
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
}
}
}
case seg.Slice() != nil:
if cur == nil {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
}
} else {
slice := seg.Slice()
var start, end, length int64
switch cur.Kind() {
case datamodel.Kind_List:
length = cur.Length()
start, end = resolveSliceIndices(slice, length)
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
length = int64(len(b))
start, end = resolveSliceIndices(slice, length)
case datamodel.Kind_String:
str, _ := cur.AsString()
length = int64(len(str))
start, end = resolveSliceIndices(slice, length)
default:
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
}
if start < 0 || end < start || end > length {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("slice out of bounds: [%d:%d]", start, end), at)
}
} else {
switch cur.Kind() {
case datamodel.Kind_List:
if end > cur.Length() {
end = cur.Length()
}
nb := basicnode.Prototype.List.NewBuilder()
assembler, _ := nb.BeginList(int64(end - start))
for i := start; i < end; i++ {
item, _ := cur.LookupByIndex(int64(i))
assembler.AssembleValue().AssignNode(item)
}
assembler.Finish()
cur = nb.Build()
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
l := int64(len(b))
if end > l {
end = l
}
cur = basicnode.NewBytes(b[start:end])
case datamodel.Kind_String:
str, _ := cur.AsString()
l := int64(len(str))
if end > l {
end = l
}
cur = basicnode.NewString(str[start:end])
}
}
}
default:
at = append(at, fmt.Sprintf("%d", seg.Index()))
if cur == nil {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
}
} else {
idx := seg.Index()
switch cur.Kind() {
case datamodel.Kind_List:
if idx < 0 {
idx = int(cur.Length()) + idx
}
if idx < 0 || idx >= int(cur.Length()) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
}
} else {
cur, _ = cur.LookupByIndex(int64(idx))
}
case datamodel.Kind_String:
str, _ := cur.AsString()
if idx < 0 {
idx = len(str) + idx
}
if idx < 0 || idx >= len(str) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
}
} else {
cur = basicnode.NewString(string(str[idx]))
}
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
if idx < 0 {
idx = len(b) + idx
}
if idx < 0 || idx >= len(b) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
}
} else {
cur = basicnode.NewInt(int64(b[idx]))
}
default:
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
}
}
}
}
return cur, nil, nil
}
// resolveSliceIndices resolves the start and end indices for slicing a list or byte array.
//
// It takes the slice indices from the selector segment and the length of the list or byte array,
// and returns the resolved start and end indices. Negative indices are supported.
//
// Parameters:
// - slice: The slice indices from the selector segment.
// - length: The length of the list or byte array being sliced.
//
// Returns:
// - start: The resolved start index for slicing.
// - end: The resolved end index for slicing.
func resolveSliceIndices(slice []int, length int64) (int64, int64) {
start, end := int64(0), length
if len(slice) > 0 {
start = int64(slice[0])
if start < 0 {
start = length + start
if start < 0 {
start = 0
}
}
}
if len(slice) > 1 {
end = int64(slice[1])
if end <= 0 {
end = length + end
if end < start {
end = start
}
}
}
return start, end
}
func kindString(n datamodel.Node) string {
if n == nil {
return "null"
}
return n.Kind().String()
}
func isMissing(err error) bool {
if _, ok := err.(datamodel.ErrNotExists); ok {
return true
}
if _, ok := err.(schema.ErrNoSuchField); ok {
return true
}
if _, ok := err.(schema.ErrInvalidKey); ok {
return true
}
return false
}
type resolutionerr struct {
msg string
at []string
}
func (r resolutionerr) Name() string {
return "ResolutionError"
}
func (r resolutionerr) Message() string {
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
}
func (r resolutionerr) At() []string {
return r.at
}
func (r resolutionerr) Error() string {
return r.Message()
}
func newResolutionError(message string, at []string) error {
return resolutionerr{message, at}
}

View File

@@ -0,0 +1,499 @@
package selector
import (
"fmt"
"strings"
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"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"
)
func TestParse(t *testing.T) {
t.Run("identity", func(t *testing.T) {
sel, err := Parse(".")
require.NoError(t, err)
require.Equal(t, 1, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
})
t.Run("field", func(t *testing.T) {
sel, err := Parse(".foo")
require.NoError(t, err)
require.Equal(t, 1, len(sel))
require.False(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Equal(t, sel[0].Field(), "foo")
require.Empty(t, sel[0].Index())
})
t.Run("explicit field", func(t *testing.T) {
sel, err := Parse(`.["foo"]`)
require.NoError(t, err)
require.Equal(t, 2, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
require.False(t, sel[1].Identity())
require.False(t, sel[1].Optional())
require.False(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Equal(t, sel[1].Field(), "foo")
require.Empty(t, sel[1].Index())
})
t.Run("index", func(t *testing.T) {
sel, err := Parse(".[138]")
require.NoError(t, err)
require.Equal(t, 2, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
require.False(t, sel[1].Identity())
require.False(t, sel[1].Optional())
require.False(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Empty(t, sel[1].Field())
require.Equal(t, sel[1].Index(), 138)
})
t.Run("negative index", func(t *testing.T) {
sel, err := Parse(".[-138]")
require.NoError(t, err)
require.Equal(t, 2, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
require.False(t, sel[1].Identity())
require.False(t, sel[1].Optional())
require.False(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Empty(t, sel[1].Field())
require.Equal(t, sel[1].Index(), -138)
})
t.Run("iterator", func(t *testing.T) {
sel, err := Parse(".[]")
require.NoError(t, err)
require.Equal(t, 2, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
require.False(t, sel[1].Identity())
require.False(t, sel[1].Optional())
require.True(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Empty(t, sel[1].Field())
require.Empty(t, sel[1].Index())
})
t.Run("optional field", func(t *testing.T) {
sel, err := Parse(".foo?")
require.NoError(t, err)
require.Equal(t, 1, len(sel))
require.False(t, sel[0].Identity())
require.True(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Equal(t, sel[0].Field(), "foo")
require.Empty(t, sel[0].Index())
})
t.Run("optional explicit field", func(t *testing.T) {
sel, err := Parse(`.["foo"]?`)
require.NoError(t, err)
require.Equal(t, 2, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
require.False(t, sel[1].Identity())
require.True(t, sel[1].Optional())
require.False(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Equal(t, sel[1].Field(), "foo")
require.Empty(t, sel[1].Index())
})
t.Run("optional index", func(t *testing.T) {
sel, err := Parse(".[138]?")
require.NoError(t, err)
require.Equal(t, 2, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
require.False(t, sel[1].Identity())
require.True(t, sel[1].Optional())
require.False(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Empty(t, sel[1].Field())
require.Equal(t, sel[1].Index(), 138)
})
t.Run("optional iterator", func(t *testing.T) {
sel, err := Parse(".[]?")
require.NoError(t, err)
require.Equal(t, 2, len(sel))
require.True(t, sel[0].Identity())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Empty(t, sel[0].Field())
require.Empty(t, sel[0].Index())
require.False(t, sel[1].Identity())
require.True(t, sel[1].Optional())
require.True(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Empty(t, sel[1].Field())
require.Empty(t, sel[1].Index())
})
t.Run("nesting", func(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())
require.False(t, sel[0].Optional())
require.False(t, sel[0].Iterator())
require.Empty(t, sel[0].Slice())
require.Equal(t, sel[0].Field(), "foo")
require.Empty(t, sel[0].Index())
require.True(t, sel[1].Identity())
require.False(t, sel[1].Optional())
require.False(t, sel[1].Iterator())
require.Empty(t, sel[1].Slice())
require.Empty(t, sel[1].Field())
require.Empty(t, sel[1].Index())
require.False(t, sel[2].Identity())
require.False(t, sel[2].Optional())
require.False(t, sel[2].Iterator())
require.Empty(t, sel[2].Slice())
require.Equal(t, sel[2].Field(), "bar")
require.Empty(t, sel[2].Index())
require.True(t, sel[3].Identity())
require.False(t, sel[3].Optional())
require.False(t, sel[3].Iterator())
require.Empty(t, sel[3].Slice())
require.Empty(t, sel[3].Field())
require.Empty(t, sel[3].Index())
require.False(t, sel[4].Identity())
require.True(t, sel[4].Optional())
require.False(t, sel[4].Iterator())
require.Empty(t, sel[4].Slice())
require.Empty(t, sel[4].Field())
require.Equal(t, sel[4].Index(), 138)
require.False(t, sel[5].Identity())
require.False(t, sel[5].Optional())
require.False(t, sel[5].Iterator())
require.Empty(t, sel[5].Slice())
require.Equal(t, sel[5].Field(), "baz")
require.Empty(t, sel[5].Index())
require.False(t, sel[6].Identity())
require.False(t, sel[6].Optional())
require.False(t, sel[6].Iterator())
require.Equal(t, sel[6].Slice(), []int{1})
require.Empty(t, sel[6].Field())
require.Empty(t, sel[6].Index())
})
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)
})
}
func printSegments(s Selector) {
for i, seg := range s {
fmt.Printf("%d: %s\n", i, seg.String())
}
}
func TestSelect(t *testing.T) {
type name struct {
First string
Middle *string
Last string
}
type interest struct {
Name string
Outdoor bool
Experience int
}
type user struct {
Name name
Age int
Nationalities []string
Interests []interest
}
ts, err := ipld.LoadSchemaBytes([]byte(`
type User struct {
name Name
age Int
nationalities [String]
interests [Interest]
}
type Name struct {
first String
middle optional String
last String
}
type Interest struct {
name String
outdoor Bool
experience Int
}
`))
require.NoError(t, err)
typ := ts.TypeByName("User")
am := "Joan"
alice := user{
Name: name{First: "Alice", Middle: &am, Last: "Wonderland"},
Age: 24,
Nationalities: []string{"British"},
Interests: []interest{
{Name: "Cycling", Outdoor: true, Experience: 4},
{Name: "Chess", Outdoor: false, Experience: 2},
},
}
bob := user{
Name: name{First: "Bob", Last: "Builder"},
Age: 35,
Nationalities: []string{"Canadian", "South African"},
Interests: []interest{
{Name: "Snowboarding", Outdoor: true, Experience: 8},
{Name: "Reading", Outdoor: false, Experience: 25},
},
}
anode := bindnode.Wrap(&alice, typ)
bnode := bindnode.Wrap(&bob, typ)
t.Run("identity", func(t *testing.T) {
sel, err := Parse(".")
require.NoError(t, err)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
fmt.Println(printer.Sprint(one))
age := must.Int(must.Node(one.LookupByString("age")))
require.Equal(t, int64(alice.Age), age)
})
t.Run("nested property", func(t *testing.T) {
sel, err := Parse(".name.first")
require.NoError(t, err)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
fmt.Println(printer.Sprint(one))
name := must.String(one)
require.Equal(t, alice.Name.First, name)
one, many, err = Select(sel, bnode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
fmt.Println(printer.Sprint(one))
name = must.String(one)
require.Equal(t, bob.Name.First, name)
})
t.Run("optional nested property", func(t *testing.T) {
sel, err := Parse(".name.middle?")
require.NoError(t, err)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
fmt.Println(printer.Sprint(one))
name := must.String(one)
require.Equal(t, *alice.Name.Middle, name)
one, many, err = Select(sel, bnode)
require.NoError(t, err)
require.Empty(t, one)
require.Empty(t, many)
})
t.Run("not exists", func(t *testing.T) {
sel, err := Parse(".name.foo")
require.NoError(t, err)
one, many, err := Select(sel, anode)
require.Error(t, err)
require.Empty(t, one)
require.Empty(t, many)
fmt.Println(err)
require.ErrorAs(t, err, &resolutionerr{}, "error was not a resolution error")
})
t.Run("optional not exists", func(t *testing.T) {
sel, err := Parse(".name.foo?")
require.NoError(t, err)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.Empty(t, one)
require.Empty(t, many)
})
t.Run("iterator", func(t *testing.T) {
sel, err := Parse(".interests[]")
require.NoError(t, err)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.Empty(t, one)
require.NotEmpty(t, many)
for _, n := range many {
fmt.Println(printer.Sprint(n))
}
iname := must.String(must.Node(many[0].LookupByString("name")))
require.Equal(t, alice.Interests[0].Name, iname)
iname = must.String(must.Node(many[1].LookupByString("name")))
require.Equal(t, alice.Interests[1].Name, iname)
})
t.Run("map iterator", func(t *testing.T) {
sel, err := Parse(".interests[0][]")
require.NoError(t, err)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.Empty(t, one)
require.NotEmpty(t, many)
for _, n := range many {
fmt.Println(printer.Sprint(n))
}
require.Equal(t, alice.Interests[0].Name, must.String(many[0]))
require.Equal(t, alice.Interests[0].Experience, int(must.Int(many[2])))
})
}
func FuzzParse(f *testing.F) {
selectorCorpus := []string{
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
`.length`, `.[4]`,
}
for _, selector := range selectorCorpus {
f.Add(selector)
}
f.Fuzz(func(t *testing.T, selector string) {
// only look for panic()
_, _ = Parse(selector)
})
}
func FuzzParseAndSelect(f *testing.F) {
selectorCorpus := []string{
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
`.length`, `.[4]`,
}
subjectCorpus := []string{
`{"x":1}`, `[1, 2]`, `null`, `[[1], 2, [3]]`, `{"x": 1 }`, `{"x": 1}`,
`[1, 2]`, `[1, 2]`, `"Hi"`, `{"/":{"bytes":"AAE"}`, `[0, 1, 2]`,
`[0, 1, 2]`, `[0, 1, 2]`, `"hello"`, `{"/":{"bytes":"AAEC"}}`, `{}`,
`null`, `[]`, `{}`, `[1, 2]`, `[0, 1]`, `null`, `[[1], 2, [3]]`, `{}`,
`null`, `[]`, `[1, 2]`, `[0, 1]`,
}
for i := 0; ; i++ {
switch {
case i < len(selectorCorpus) && i < len(subjectCorpus):
f.Add(selectorCorpus[i], subjectCorpus[i])
continue
case i > len(selectorCorpus):
f.Add("", subjectCorpus[i])
continue
case i > len(subjectCorpus):
f.Add(selectorCorpus[i], "")
continue
}
break
}
f.Fuzz(func(t *testing.T, selector, subject string) {
sel, err := Parse(selector)
if err != nil {
t.Skip()
}
np := basicnode.Prototype.Any
nb := np.NewBuilder()
err = dagjson.Decode(nb, strings.NewReader(subject))
if err != nil {
t.Skip()
}
node := nb.Build()
if node == nil {
t.Skip()
}
// look for panic()
_, _, _ = Select(sel, node)
})
}

View File

@@ -0,0 +1,163 @@
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"
)
// TestSupported Forms runs tests against the Selector according to the
// proposed "Supported Forms" presented in this GitHub issue:
// https://github.com/ucan-wg/delegation/issues/5#issue-2154766496
func TestSupportedForms(t *testing.T) {
type Testcase struct {
Name string
Selector string
Input string
Output string
}
// Pass
for _, testcase := range []Testcase{
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `[]`},
{Name: "Optional Iterator", Selector: `.[][]?`, Input: `[[1], 2, [3]]`, Output: `[1, 3]`},
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
{Name: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}}`, Output: `0`},
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
{Name: "String Slice", Selector: `.[0:2]`, Input: `"hello"`, Output: `"he"`},
{Name: "Bytes Index", Selector: `.[1:]`, Input: `{"/":{"bytes":"AAEC"}}`, Output: `{"/":{"bytes":"AQI"}}`},
} {
tc := testcase
t.Run("Pass: "+tc.Name, func(t *testing.T) {
t.Parallel()
sel, err := selector.Parse(tc.Selector)
require.NoError(t, err)
// attempt to select
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
require.NoError(t, err)
require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set)
// make an IPLD List node from a []datamodel.Node
if node == nil {
nb := basicnode.Prototype.List.NewBuilder()
la, err := nb.BeginList(int64(len(nodes)))
require.NoError(t, err)
for _, n := range nodes {
// TODO: This code is probably not needed if the Select operation properly prunes nil values - e.g.: Optional Iterator
if n == nil {
n = datamodel.Null
}
require.NoError(t, la.AssembleValue().AssignNode(n))
}
require.NoError(t, la.Finish())
node = nb.Build()
}
exp := makeNode(t, tc.Output)
equalIPLD(t, exp, node)
})
}
// null
for _, testcase := range []Testcase{
{Name: "Optional Missing Key", Selector: `.x?`, Input: `{}`},
{Name: "Optional Null Key", Selector: `.x?`, Input: `null`},
{Name: "Optional Array Key", Selector: `.x?`, Input: `[]`},
{Name: "Optional Quoted Key", Selector: `.["x"]?`, Input: `{}`},
{Name: ".length?", Selector: `.length?`, Input: `[1, 2]`},
{Name: "Optional Index", Selector: `.[4]?`, Input: `[0, 1]`},
} {
tc := testcase
t.Run("Null: "+tc.Name, func(t *testing.T) {
t.Parallel()
sel, err := selector.Parse(tc.Selector)
require.NoError(t, err)
// attempt to select
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
require.NoError(t, err)
// TODO: should Select return a single node which is sometimes a list or null?
// require.Equal(t, datamodel.Null, node)
assert.Nil(t, node)
assert.Empty(t, nodes)
})
}
// error
for _, testcase := range []Testcase{
{Name: "Null Iterator", Selector: `.[]`, Input: `null`},
{Name: "Nested Iterator", Selector: `.[][]`, Input: `[[1], 2, [3]]`},
{Name: "Missing Key", Selector: `.x`, Input: `{}`},
{Name: "Null Key", Selector: `.x`, Input: `null`},
{Name: "Array Key", Selector: `.x`, Input: `[]`},
{Name: ".length", Selector: `.length`, Input: `[1, 2]`},
{Name: "Out of bound Index", Selector: `.[4]`, Input: `[0, 1]`},
} {
tc := testcase
t.Run("Null: "+tc.Name, func(t *testing.T) {
t.Parallel()
sel, err := selector.Parse(tc.Selector)
require.NoError(t, err)
// attempt to select
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
require.Error(t, err)
assert.Nil(t, node)
assert.Empty(t, nodes)
})
}
}
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()
np := basicnode.Prototype.Any
nb := np.NewBuilder()
require.NoError(t, dagjson.Decode(nb, strings.NewReader(dagJsonInput)))
node := nb.Build()
require.NotNil(t, node)
return node
}