Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add link conditions for 'stop-at' expression in ExploreRecursive selector #214

Merged
merged 2 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions traversal/selector/condition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package selector

import (
"fmt"

ipld "github.com/ipld/go-ipld-prime"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
)

// Condition provides a mechanism for matching and limiting matching and
// exploration of selectors.
// Not all types of conditions which are imagined in the selector specification
// are encoded at present, instead we currently only implement a subset that
// is sufficient for initial pressing use cases.
type Condition struct {
mode ConditionMode
match ipld.Node
}

// A ConditionMode is the keyed representation for the union that is the condition
type ConditionMode string

const (
ConditionMode_Link ConditionMode = "/"
)

// Match decides if a given ipld.Node matches the condition.
func (c *Condition) Match(n ipld.Node) bool {
switch c.mode {
case ConditionMode_Link:
if n.Kind() != ipld.Kind_Link {
return false
}
lnk, err := n.AsLink()
if err != nil {
return false
}
match, err := c.match.AsLink()
if err != nil {
return false
}
cidlnk, ok := lnk.(cidlink.Link)
cidmatch, ok2 := match.(cidlink.Link)
if ok && ok2 {
return cidmatch.Equals(cidlnk.Cid)
}
return match.String() == lnk.String()
default:
return false
}
}

// ParseCondition assembles a Condition from a condition selector node
func (pc ParseContext) ParseCondition(n ipld.Node) (Condition, error) {
willscott marked this conversation as resolved.
Show resolved Hide resolved
if n.Kind() != ipld.Kind_Map {
return Condition{}, fmt.Errorf("selector spec parse rejected: condition body must be a map")
}
if n.Length() != 1 {
return Condition{}, fmt.Errorf("selector spec parse rejected: condition is a keyed union and thus must be single-entry map")
}
kn, v, _ := n.MapIterator().Next()
kstr, _ := kn.AsString()
// Switch over the single key to determine which condition body comes next.
// (This switch is where the keyed union discriminators concretely happen.)
switch ConditionMode(kstr) {
case ConditionMode_Link:
if _, err := v.AsLink(); err != nil {
return Condition{}, fmt.Errorf("selector spec parse rejected: condition_link must be a link")
}
return Condition{mode: ConditionMode_Link, match: v}, nil
default:
return Condition{}, fmt.Errorf("selector spec parse rejected: %q is not a known member of the condition union", kstr)
}
}
44 changes: 44 additions & 0 deletions traversal/selector/condition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package selector

import (
"fmt"
"testing"

"github.com/ipfs/go-cid"
. "github.com/warpfork/go-wish"

"github.com/ipld/go-ipld-prime/fluent"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
)

func TestParseCondition(t *testing.T) {
t.Run("parsing non map node should error", func(t *testing.T) {
sn := basicnode.NewInt(0)
_, err := ParseContext{}.ParseCondition(sn)
Wish(t, err, ShouldEqual, fmt.Errorf("selector spec parse rejected: condition body must be a map"))
})
t.Run("parsing map node without field should error", func(t *testing.T) {
sn := fluent.MustBuildMap(basicnode.Prototype__Map{}, 0, func(na fluent.MapAssembler) {})
_, err := ParseContext{}.ParseCondition(sn)
Wish(t, err, ShouldEqual, fmt.Errorf("selector spec parse rejected: condition is a keyed union and thus must be single-entry map"))
})

t.Run("parsing map node keyed to invalid type should error", func(t *testing.T) {
sn := fluent.MustBuildMap(basicnode.Prototype__Map{}, 1, func(na fluent.MapAssembler) {
na.AssembleEntry(string(ConditionMode_Link)).AssignInt(0)
})
_, err := ParseContext{}.ParseCondition(sn)
Wish(t, err, ShouldEqual, fmt.Errorf("selector spec parse rejected: condition_link must be a link"))
})
t.Run("parsing map node with condition field with valid selector node should parse", func(t *testing.T) {
lnk := cidlink.Link{Cid: cid.Undef}
sn := fluent.MustBuildMap(basicnode.Prototype__Map{}, 1, func(na fluent.MapAssembler) {
na.AssembleEntry(string(ConditionMode_Link)).AssignLink(lnk)
})
s, err := ParseContext{}.ParseCondition(sn)
Wish(t, err, ShouldEqual, nil)
lnkNode := basicnode.NewLink(lnk)
Wish(t, s, ShouldEqual, Condition{mode: ConditionMode_Link, match: lnkNode})
})
}
24 changes: 19 additions & 5 deletions traversal/selector/exploreRecursive.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type ExploreRecursive struct {
sequence Selector // selector for element we're interested in
current Selector // selector to apply to the current node
limit RecursionLimit // the limit for this recursive selector
stopAt *Condition // a condition for not exploring the node or children
}

// RecursionLimit_Mode is an enum that represents the type of a recursion limit
Expand Down Expand Up @@ -86,23 +87,27 @@ func (s ExploreRecursive) Interests() []ipld.PathSegment {

// Explore returns the node's selector for all fields
func (s ExploreRecursive) Explore(n ipld.Node, p ipld.PathSegment) Selector {
if s.stopAt != nil && s.stopAt.Match(n) {
return nil
}

nextSelector := s.current.Explore(n, p)
limit := s.limit

if nextSelector == nil {
return nil
}
if !s.hasRecursiveEdge(nextSelector) {
return ExploreRecursive{s.sequence, nextSelector, limit}
return ExploreRecursive{s.sequence, nextSelector, limit, s.stopAt}
}
switch limit.mode {
case RecursionLimit_Depth:
if limit.depth < 2 {
return s.replaceRecursiveEdge(nextSelector, nil)
}
return ExploreRecursive{s.sequence, s.replaceRecursiveEdge(nextSelector, s.sequence), RecursionLimit{RecursionLimit_Depth, limit.depth - 1}}
return ExploreRecursive{s.sequence, s.replaceRecursiveEdge(nextSelector, s.sequence), RecursionLimit{RecursionLimit_Depth, limit.depth - 1}, s.stopAt}
case RecursionLimit_None:
return ExploreRecursive{s.sequence, s.replaceRecursiveEdge(nextSelector, s.sequence), limit}
return ExploreRecursive{s.sequence, s.replaceRecursiveEdge(nextSelector, s.sequence), limit, s.stopAt}
default:
panic("Unsupported recursion limit type")
}
Expand Down Expand Up @@ -149,7 +154,7 @@ func (s ExploreRecursive) replaceRecursiveEdge(nextSelector Selector, replacemen
return nextSelector
}

// Decide always returns false because this is not a matcher
// Decide if a node directly matches
func (s ExploreRecursive) Decide(n ipld.Node) bool {
return s.current.Decide(n)
}
Expand Down Expand Up @@ -192,7 +197,16 @@ func (pc ParseContext) ParseExploreRecursive(n ipld.Node) (Selector, error) {
if erc.edgesFound == 0 {
return nil, fmt.Errorf("selector spec parse rejected: ExploreRecursive must have at least one ExploreRecursiveEdge")
}
return ExploreRecursive{selector, selector, limit}, nil
var stopCondition *Condition
stop, err := n.LookupByString(SelectorKey_StopAt)
if err == nil {
condition, err := pc.ParseCondition(stop)
if err != nil {
return nil, err
}
stopCondition = &condition
}
return ExploreRecursive{selector, selector, limit, stopCondition}, nil
}

func parseLimit(n ipld.Node) (RecursionLimit, error) {
Expand Down
Loading