Skip to content

Commit

Permalink
policy: IPLD encode/decode
Browse files Browse the repository at this point in the history
Still WIP
  • Loading branch information
MichaelMure committed Sep 1, 2024
1 parent 4d0a0a2 commit 8c09024
Show file tree
Hide file tree
Showing 5 changed files with 377 additions and 32 deletions.
252 changes: 252 additions & 0 deletions capability/policy/ipld.go
Original file line number Diff line number Diff line change
@@ -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 changes: 47 additions & 0 deletions capability/policy/ipld_errors.go
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions capability/policy/ipld_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
14 changes: 9 additions & 5 deletions capability/policy/match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.",
Expand All @@ -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)
})
Expand All @@ -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)
})
Expand Down
Loading

0 comments on commit 8c09024

Please sign in to comment.