policy: implement the missing '!='

https://github.com/ucan-wg/delegation/pull/15
This commit is contained in:
Michael Muré
2025-01-22 14:12:41 +01:00
parent 9c141029c3
commit 7f9cb6426c
7 changed files with 252 additions and 22 deletions

View File

@@ -81,7 +81,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
}
case 3:
switch op {
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
case KindEqual, KindNotEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
sel, err := arg2AsSelector(op)
if err != nil {
return nil, err

View File

@@ -1,11 +1,14 @@
package policy
import (
"fmt"
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
)
func TestIpldRoundTrip(t *testing.T) {
@@ -21,10 +24,31 @@ func TestIpldRoundTrip(t *testing.T) {
]
]`
// must contain all the operators
const allOps = `
[
["and", [
["==", ".foo1", ".bar1"],
["!=", ".foo2", ".bar2"]
]],
["or", [
[">", ".foo5", 5.2],
[">=", ".foo6", 6.2]
]],
["not", ["like", ".foo7", "*@example.com"]],
["all", ".foo8",
["<", ".foo3", 3]
],
["any", ".foo9",
["<=", ".foo4", 4]
]
]`
for _, tc := range []struct {
name, dagJsonStr string
}{
{"illustrativeExample", illustrativeExample},
{"allOps", allOps},
} {
nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode)
require.NoError(t, err)
@@ -41,3 +65,19 @@ func TestIpldRoundTrip(t *testing.T) {
require.JSONEq(t, tc.dagJsonStr, string(wroteAsDagJson))
}
}
func TestFoo(t *testing.T) {
fmt.Println(MustConstruct(
And(
Equal(".foo1", literal.String(".bar1")),
NotEqual(".foo2", literal.String(".bar2")),
),
Or(
GreaterThan(".foo5", literal.Float(5.2)),
GreaterThanOrEqual(".foo6", literal.Float(6.2)),
),
Not(Like(".foo7", "*@example.com")),
All(".foo8", LessThan(".foo3", literal.Int(3))),
Any(".foo9", LessThanOrEqual(".foo4", literal.Int(4))),
))
}

View File

@@ -82,6 +82,17 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
}
return boolToRes(datamodel.DeepEqual(s.value, res))
}
case KindNotEqual:
if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node)
if err != nil {
return matchResultNoData, cur
}
if res == nil { // optional selector didn't match
return matchResultOptionalNoData, nil
}
return boolToRes(!datamodel.DeepEqual(s.value, res))
}
case KindGreaterThan:
if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node)

View File

@@ -16,7 +16,7 @@ import (
func TestMatch(t *testing.T) {
t.Run("equality", func(t *testing.T) {
t.Run("string", func(t *testing.T) {
t.Run("eq string", func(t *testing.T) {
nd := literal.String("test")
pol := MustConstruct(Equal(".", literal.String("test")))
@@ -35,7 +35,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("int", func(t *testing.T) {
t.Run("eq int", func(t *testing.T) {
nd := literal.Int(138)
pol := MustConstruct(Equal(".", literal.Int(138)))
@@ -54,7 +54,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("float", func(t *testing.T) {
t.Run("eq float", func(t *testing.T) {
nd := literal.Float(1.138)
pol := MustConstruct(Equal(".", literal.Float(1.138)))
@@ -73,7 +73,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("IPLD Link", func(t *testing.T) {
t.Run("eq IPLD Link", func(t *testing.T) {
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
@@ -95,7 +95,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("string in map", func(t *testing.T) {
t.Run("eq string in map", func(t *testing.T) {
nd, _ := literal.Map(map[string]any{
"foo": "bar",
})
@@ -121,7 +121,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("string in list", func(t *testing.T) {
t.Run("eq string in list", func(t *testing.T) {
nd, _ := literal.List([]any{"foo"})
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
@@ -134,6 +134,129 @@ func TestMatch(t *testing.T) {
require.False(t, ok)
require.Equal(t, pol[0], leaf)
})
// ------------
t.Run("neq string", func(t *testing.T) {
nd := literal.String("test")
pol := MustConstruct(NotEqual(".", literal.String("test")))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.String("test2")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.Int(138)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq int", func(t *testing.T) {
nd := literal.Int(138)
pol := MustConstruct(NotEqual(".", literal.Int(138)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.Int(1138)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.String("138")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq float", func(t *testing.T) {
nd := literal.Float(1.138)
pol := MustConstruct(NotEqual(".", literal.Float(1.138)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.Float(11.38)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.String("138")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq IPLD Link", func(t *testing.T) {
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
nd := literal.Link(l0)
pol := MustConstruct(NotEqual(".", literal.Link(l0)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.Link(l1)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq string in map", func(t *testing.T) {
nd, _ := literal.Map(map[string]any{
"foo": "bar",
})
pol := MustConstruct(NotEqual(".foo", literal.String("bar")))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".[\"foo\"]", literal.String("bar")))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".foo", literal.String("baz")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
// TODO: clarify how it behave when the data is missing
pol = MustConstruct(NotEqual(".foobar", literal.String("bar")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq string in list", func(t *testing.T) {
nd, _ := literal.List([]any{"foo"})
pol := MustConstruct(NotEqual(".[0]", literal.String("foo")))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
// TODO: clarify how it behave when the data is missing
pol = MustConstruct(NotEqual(".[1]", literal.String("foo")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
})
t.Run("inequality", func(t *testing.T) {
@@ -245,20 +368,61 @@ func TestMatch(t *testing.T) {
require.False(t, ok)
require.Equal(t, pol[0], leaf)
})
t.Run("lt float", func(t *testing.T) {
nd := literal.Float(1.38)
pol := MustConstruct(LessThan(".", literal.Float(1)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(LessThan(".", literal.Float(2)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("lte float", func(t *testing.T) {
nd := literal.Float(1.38)
pol := MustConstruct(LessThanOrEqual(".", literal.Float(1)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(LessThanOrEqual(".", literal.Float(2)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
})
t.Run("negation", func(t *testing.T) {
nd := literal.Bool(false)
nd, _ := literal.Map(map[string]any{
"foo": false,
})
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
pol := MustConstruct(Not(Equal(".foo", literal.Bool(true))))
ok, leaf := pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
pol = MustConstruct(Not(Equal(".foo", literal.Bool(false))))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
// TODO: clarify how it works on missing data
pol = MustConstruct(Not(Equal(".foobar", literal.Bool(true))))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("conjunction", func(t *testing.T) {
@@ -482,6 +646,7 @@ func FuzzMatch(f *testing.F) {
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(`[["!=", ".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"}`))

View File

@@ -14,6 +14,7 @@ import (
const (
KindEqual = "==" // implemented by equality
KindNotEqual = "!=" // implemented by equality
KindGreaterThan = ">" // implemented by equality
KindGreaterThanOrEqual = ">=" // implemented by equality
KindLessThan = "<" // implemented by equality
@@ -87,6 +88,13 @@ func Equal(selector string, value ipld.Node) Constructor {
}
}
func NotEqual(selector string, value ipld.Node) Constructor {
return func() (Statement, error) {
sel, err := selpkg.Parse(selector)
return equality{kind: KindNotEqual, selector: sel, value: value}, err
}
}
func GreaterThan(selector string, value ipld.Node) Constructor {
return func() (Statement, error) {
sel, err := selpkg.Parse(selector)
@@ -125,7 +133,7 @@ func (n negation) Kind() string {
func (n negation) String() string {
child := n.statement.String()
return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
return fmt.Sprintf(`["%s", %s]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
}
func Not(cstor Constructor) Constructor {
@@ -149,7 +157,7 @@ func (c connective) String() string {
for i, statement := range c.statements {
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
}
return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n "))
return fmt.Sprintf("[\"%s\", [\n %s\n]]", c.kind, strings.Join(childs, ",\n "))
}
func And(cstors ...Constructor) Constructor {
@@ -208,7 +216,7 @@ func (n quantifier) Kind() string {
func (n quantifier) String() string {
child := n.statement.String()
return fmt.Sprintf("[\"%s\", \"%s\",\n %s]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
return fmt.Sprintf("[\"%s\", \"%s\",\n %s\n]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
}
func All(selector string, cstor Constructor) Constructor {

View File

@@ -28,12 +28,14 @@ func ExamplePolicy() {
// [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ["==", ".", "press"]
// ]]
// ]
// ]
}
@@ -59,12 +61,14 @@ func ExamplePolicy_accumulate() {
// [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ["==", ".", "press"]
// ]]
// ]
// ]
}

View File

@@ -272,12 +272,14 @@ func ExampleFromSealed() {
// Policy (pol): [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ["==", ".", "press"]
// ]]
// ]
// ]
// Nonce (nonce): 000102030405060708090a0b
// Meta (meta): {}