policy: implement partial matching, to evaluate in multiple steps with fail early

This commit is contained in:
Michael Muré
2024-10-24 13:50:56 +02:00
parent 6d0fbd4d5a
commit 7662fe34db
3 changed files with 136 additions and 65 deletions

View File

@@ -8,8 +8,6 @@ import (
"github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/node/basicnode"
) )
// TODO: remove entirely?
var Bool = basicnode.NewBool var Bool = basicnode.NewBool
var Int = basicnode.NewInt var Int = basicnode.NewInt
var Float = basicnode.NewFloat var Float = basicnode.NewFloat

View File

@@ -12,141 +12,215 @@ import (
// Match determines if the IPLD node satisfies the policy. // Match determines if the IPLD node satisfies the policy.
func (p Policy) Match(node datamodel.Node) bool { func (p Policy) Match(node datamodel.Node) bool {
for _, stmt := range p { for _, stmt := range p {
ok := matchStatement(stmt, node) res, _ := matchStatement(stmt, node)
if !ok { switch res {
case matchResultNoData, matchResultFalse:
return false return false
case matchResultTrue:
// continue
} }
} }
return true return true
} }
func matchStatement(statement Statement, node ipld.Node) bool { // PartialMatch returns false IIF one of the Statement has the corresponding data and doesn't match.
switch statement.Kind() { // If the data is missing or the Statement is matching, true is returned.
//
// This allows performing the policy checking in multiple steps, and find immediately if a Statement already failed.
// A final call to Match is necessary to make sure that the policy is fully matched, with no missing data
// (apart from optional values).
//
// The first Statement failing to match is returned as well.
func (p Policy) PartialMatch(node datamodel.Node) (bool, Statement) {
for _, stmt := range p {
res, leaf := matchStatement(stmt, node)
switch res {
case matchResultFalse:
return false, leaf
case matchResultNoData, matchResultTrue:
// continue
}
}
return true, nil
}
type matchResult int8
const (
matchResultTrue matchResult = iota
matchResultFalse
matchResultNoData
)
// matchStatement evaluate the policy against the given ipld.Node and returns:
// - matchResultTrue: if the selector matched and the statement evaluated to true.
// - matchResultFalse: if the selector matched and the statement evaluated to false.
// - matchResultNoData: if the selector didn't match the expected data.
// For matchResultTrue and matchResultNoData, the leaf-most (innermost) statement failing to be true is returned,
// as well as the corresponding root-most encompassing statement.
func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Statement) {
var boolToRes = func(v bool) (matchResult, Statement) {
if v {
return matchResultTrue, nil
} else {
return matchResultFalse, cur
}
}
switch cur.Kind() {
case KindEqual: case KindEqual:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
return datamodel.DeepEqual(s.value, res) return boolToRes(datamodel.DeepEqual(s.value, res))
} }
case KindGreaterThan: case KindGreaterThan:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, res, gt) return boolToRes(isOrdered(s.value, res, gt))
} }
case KindGreaterThanOrEqual: case KindGreaterThanOrEqual:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, res, gte) return boolToRes(isOrdered(s.value, res, gte))
} }
case KindLessThan: case KindLessThan:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, res, lt) return boolToRes(isOrdered(s.value, res, lt))
} }
case KindLessThanOrEqual: case KindLessThanOrEqual:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, res, lte) return boolToRes(isOrdered(s.value, res, lte))
} }
case KindNot: case KindNot:
if s, ok := statement.(negation); ok { if s, ok := cur.(negation); ok {
return !matchStatement(s.statement, node) res, leaf := matchStatement(s.statement, node)
switch res {
case matchResultNoData:
return matchResultNoData, leaf
case matchResultTrue:
return matchResultFalse, leaf
case matchResultFalse:
return matchResultTrue, leaf
}
} }
case KindAnd: case KindAnd:
if s, ok := statement.(connective); ok { if s, ok := cur.(connective); ok {
for _, cs := range s.statements { for _, cs := range s.statements {
r := matchStatement(cs, node) res, leaf := matchStatement(cs, node)
if !r { switch res {
return false case matchResultNoData:
return matchResultNoData, leaf
case matchResultTrue:
// continue
case matchResultFalse:
return matchResultFalse, leaf
} }
} }
return true return matchResultTrue, nil
} }
case KindOr: case KindOr:
if s, ok := statement.(connective); ok { if s, ok := cur.(connective); ok {
if len(s.statements) == 0 { if len(s.statements) == 0 {
return true return matchResultTrue, nil
} }
for _, cs := range s.statements { for _, cs := range s.statements {
r := matchStatement(cs, node) res, leaf := matchStatement(cs, node)
if r { switch res {
return true case matchResultNoData:
return matchResultNoData, leaf
case matchResultTrue:
return matchResultTrue, leaf
case matchResultFalse:
// continue
} }
} }
return false return matchResultFalse, cur
} }
case KindLike: case KindLike:
if s, ok := statement.(wildcard); ok { if s, ok := cur.(wildcard); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
v, err := res.AsString() v, err := res.AsString()
if err != nil { if err != nil {
return false // not a string return matchResultFalse, cur // not a string
} }
return s.pattern.Match(v) return boolToRes(s.pattern.Match(v))
} }
case KindAll: case KindAll:
if s, ok := statement.(quantifier); ok { if s, ok := cur.(quantifier); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
it := res.ListIterator() it := res.ListIterator()
if it == nil { if it == nil {
return false // not a list return matchResultFalse, cur // not a list
} }
for !it.Done() { for !it.Done() {
_, v, err := it.Next() _, v, err := it.Next()
if err != nil { if err != nil {
return false panic("should never happen")
} }
ok := matchStatement(s.statement, v) matchRes, leaf := matchStatement(s.statement, v)
if !ok { switch matchRes {
return false case matchResultNoData:
return matchResultNoData, leaf
case matchResultTrue:
// continue
case matchResultFalse:
return matchResultFalse, leaf
} }
} }
return true return matchResultTrue, nil
} }
case KindAny: case KindAny:
if s, ok := statement.(quantifier); ok { if s, ok := cur.(quantifier); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil || res == nil {
return false return matchResultNoData, cur
} }
it := res.ListIterator() it := res.ListIterator()
if it == nil { if it == nil {
return false // not a list return matchResultFalse, cur // not a list
} }
for !it.Done() { for !it.Done() {
_, v, err := it.Next() _, v, err := it.Next()
if err != nil { if err != nil {
return false panic("should never happen")
} }
ok := matchStatement(s.statement, v) matchRes, leaf := matchStatement(s.statement, v)
if ok { switch matchRes {
return true case matchResultNoData:
return matchResultNoData, leaf
case matchResultTrue:
return matchResultTrue, nil
case matchResultFalse:
// continue
} }
} }
return false return matchResultFalse, cur
} }
} }
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind())) panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
} }
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool { func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {

View File

@@ -9,7 +9,6 @@ import (
) )
var ( var (
identity = Selector{segment{str: ".", identity: true}}
indexRegex = regexp.MustCompile(`^-?\d+$`) indexRegex = regexp.MustCompile(`^-?\d+$`)
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`) sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`) fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
@@ -23,7 +22,7 @@ func Parse(str string) (Selector, error) {
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0])) return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
} }
if str == "." { if str == "." {
return identity, nil return Selector{segment{str: ".", identity: true}}, nil
} }
if str == ".?" { if str == ".?" {
return Selector{segment{str: ".?", identity: true, optional: true}}, nil return Selector{segment{str: ".?", identity: true, optional: true}}, nil