policy: IPLD encode/decode
Still WIP
This commit is contained in:
252
capability/policy/ipld.go
Normal file
252
capability/policy/ipld.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/v1/capability/policy/selector"
|
||||
)
|
||||
|
||||
func PolicyFromIPLD(node datamodel.Node) (Policy, error) {
|
||||
return statementsFromIPLD("/", node)
|
||||
}
|
||||
|
||||
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() (selector.Selector, error) {
|
||||
nd, _ := node.LookupByIndex(1)
|
||||
if nd.Kind() != datamodel.Kind_String {
|
||||
return nil, ErrNotAString(path + "1/")
|
||||
}
|
||||
sel, err := selector.Parse(must.String(nd))
|
||||
if err != nil {
|
||||
return nil, ErrInvalidSelector(path+"1/", err)
|
||||
}
|
||||
return sel, nil
|
||||
}
|
||||
|
||||
switch node.Length() {
|
||||
case 2:
|
||||
switch op {
|
||||
case KindNot:
|
||||
arg2, _ := node.LookupByIndex(1)
|
||||
statement, err := statementFromIPLD(path+"1/", arg2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Not(statement), nil
|
||||
|
||||
case KindAnd, KindOr:
|
||||
arg2, _ := node.LookupByIndex(1)
|
||||
statement, err := statementsFromIPLD(path+"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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arg3, _ := node.LookupByIndex(2)
|
||||
return equality{kind: op, selector: sel, value: arg3}, nil
|
||||
|
||||
case KindLike:
|
||||
sel, err := arg2AsSelector()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pattern, _ := node.LookupByIndex(2)
|
||||
if pattern.Kind() != datamodel.Kind_String {
|
||||
return nil, ErrNotAString(path + "2/")
|
||||
}
|
||||
res, err := Like(sel, must.String(pattern))
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPattern(path+"2/", err)
|
||||
}
|
||||
return res, nil
|
||||
|
||||
case KindAll, KindAny:
|
||||
sel, err := arg2AsSelector()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statementsNodes, _ := node.LookupByIndex(2)
|
||||
statements, err := statementsFromIPLD(path+"1/", statementsNodes)
|
||||
return quantifier{kind: op, selector: sel, statements: statements}, 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, ErrEmptyList(path)
|
||||
}
|
||||
|
||||
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(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 := statementsToIPLD(statement.statements)
|
||||
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
|
||||
}
|
||||
47
capability/policy/ipld_errors.go
Normal file
47
capability/policy/ipld_errors.go
Normal file
@@ -0,0 +1,47 @@
|
||||
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 ErrEmptyList(path string) error {
|
||||
return errWithPath{path: path, msg: "empty list"}
|
||||
}
|
||||
|
||||
func safeStr(str string) string {
|
||||
if len(str) > 10 {
|
||||
return str[:10]
|
||||
}
|
||||
return str
|
||||
}
|
||||
48
capability/policy/ipld_test.go
Normal file
48
capability/policy/ipld_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"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, dagjson string
|
||||
}{
|
||||
{"illustrativeExample", illustrativeExample},
|
||||
} {
|
||||
// strip all spaces and carriage return
|
||||
asDagJson := strings.Join(strings.Fields(tc.dagjson), "")
|
||||
|
||||
nodes, err := ipld.Decode([]byte(asDagJson), dagjson.Decode)
|
||||
require.NoError(t, err)
|
||||
|
||||
pol, err := PolicyFromIPLD(nodes)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Println(pol)
|
||||
|
||||
wroteIpld, err := pol.ToIPLD()
|
||||
require.NoError(t, err)
|
||||
|
||||
wroteAsDagJson, err := ipld.Encode(wroteIpld, dagjson.Encode)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, asDagJson, string(wroteAsDagJson))
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
@@ -294,8 +293,7 @@ func TestMatch(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("wildcard", func(t *testing.T) {
|
||||
glb, err := glob.Compile(`Alice\*, Bob*, Carol.`)
|
||||
require.NoError(t, err)
|
||||
pattern := `Alice\*, Bob*, Carol.`
|
||||
|
||||
for _, s := range []string{
|
||||
"Alice*, Bob, Carol.",
|
||||
@@ -310,7 +308,10 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := Policy{Like(selector.MustParse("."), glb)}
|
||||
statement, err := Like(selector.MustParse("."), pattern)
|
||||
require.NoError(t, err)
|
||||
|
||||
pol := Policy{statement}
|
||||
ok := Match(pol, nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -331,7 +332,10 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := Policy{Like(selector.MustParse("."), glb)}
|
||||
statement, err := Like(selector.MustParse("."), pattern)
|
||||
require.NoError(t, err)
|
||||
|
||||
pol := Policy{statement}
|
||||
ok := Match(pol, nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
KindAny = "any"
|
||||
)
|
||||
|
||||
type Policy = []Statement
|
||||
type Policy []Statement
|
||||
|
||||
type Statement interface {
|
||||
Kind() string
|
||||
@@ -126,41 +126,31 @@ func Not(stmt Statement) NegationStatement {
|
||||
return negation{stmt}
|
||||
}
|
||||
|
||||
type conjunction struct {
|
||||
type connective struct {
|
||||
kind string
|
||||
statements []Statement
|
||||
}
|
||||
|
||||
func (n conjunction) Kind() string {
|
||||
return KindAnd
|
||||
func (c connective) Kind() string {
|
||||
return c.kind
|
||||
}
|
||||
|
||||
func (n conjunction) Value() []Statement {
|
||||
return n.statements
|
||||
func (c connective) Value() []Statement {
|
||||
return c.statements
|
||||
}
|
||||
|
||||
func And(stmts ...Statement) ConjunctionStatement {
|
||||
return conjunction{stmts}
|
||||
}
|
||||
|
||||
type disjunction struct {
|
||||
statements []Statement
|
||||
}
|
||||
|
||||
func (n disjunction) Kind() string {
|
||||
return KindOr
|
||||
}
|
||||
|
||||
func (n disjunction) Value() []Statement {
|
||||
return n.statements
|
||||
return connective{KindAnd, stmts}
|
||||
}
|
||||
|
||||
func Or(stmts ...Statement) DisjunctionStatement {
|
||||
return disjunction{stmts}
|
||||
return connective{KindOr, stmts}
|
||||
}
|
||||
|
||||
type wildcard struct {
|
||||
selector selector.Selector
|
||||
glob glob.Glob
|
||||
pattern string
|
||||
glob glob.Glob // not serialized
|
||||
}
|
||||
|
||||
func (n wildcard) Kind() string {
|
||||
@@ -175,14 +165,18 @@ func (n wildcard) Value() glob.Glob {
|
||||
return n.glob
|
||||
}
|
||||
|
||||
func Like(selector selector.Selector, glob glob.Glob) WildcardStatement {
|
||||
return wildcard{selector, glob}
|
||||
func Like(selector selector.Selector, pattern string) (WildcardStatement, error) {
|
||||
g, err := glob.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wildcard{selector, pattern, g}, nil
|
||||
}
|
||||
|
||||
type quantifier struct {
|
||||
kind string
|
||||
selector selector.Selector
|
||||
policy Policy
|
||||
kind string
|
||||
selector selector.Selector
|
||||
statements []Statement
|
||||
}
|
||||
|
||||
func (n quantifier) Kind() string {
|
||||
@@ -194,7 +188,7 @@ func (n quantifier) Selector() selector.Selector {
|
||||
}
|
||||
|
||||
func (n quantifier) Value() Policy {
|
||||
return n.policy
|
||||
return n.statements
|
||||
}
|
||||
|
||||
func All(selector selector.Selector, policy ...Statement) QuantifierStatement {
|
||||
|
||||
Reference in New Issue
Block a user