Unverified Commit 2fd79241 authored by Eric Myhre's avatar Eric Myhre Committed by GitHub

Merge pull request #12 from ipld/selectors

Begin Selector implementation!
parents 44cd9a8e cd9283dd
......@@ -67,6 +67,16 @@ func (p Path) Join(p2 Path) Path {
return p
}
// AppendSegment is as per Join, but a shortcut when appending single segments.
func (p Path) AppendSegment(ps string) Path {
l := len(p.segments)
combinedSegments := make([]string, l+1)
copy(combinedSegments, p.segments)
combinedSegments[l] = ps
p.segments = combinedSegments
return p
}
// Parent returns a path with the last of its segments popped off (or
// the zero path if it's already empty).
func (p Path) Parent() Path {
......
......@@ -6,8 +6,9 @@ import (
ipld "github.com/ipld/go-ipld-prime"
)
// This file defines interfaces for things users provide.
//------------------------------------------------
// This file defines interfaces for things users provide,
// plus a few of the parameters they'll need to receieve.
//--------------------------------------------------------
// VisitFn is a read-only visitor.
type VisitFn func(TraversalProgress, ipld.Node) error
......@@ -19,7 +20,13 @@ type TransformFn func(TraversalProgress, ipld.Node) (ipld.Node, error)
type AdvVisitFn func(TraversalProgress, ipld.Node, TraversalReason) error
// TraversalReason provides additional information to traversals using AdvVisitFn.
type TraversalReason byte // enum = SelectionMatch | SelectionParent | SelectionCandidate // probably only pointful for block edges?
type TraversalReason byte
const (
TraversalReason_SelectionMatch TraversalReason = 'm' // Tells AdvVisitFn that this node was explicitly selected. (This is the set of nodes that VisitFn is called for.)
TraversalReason_SelectionParent TraversalReason = 'p' // Tells AdvVisitFn that this node is a parent of one that will be explicitly selected. (These calls only happen if the feature is enabled -- enabling parent detection requires a different algorithm and adds some overhead.)
TraversalReason_SelectionCandidate TraversalReason = 'x' // Tells AdvVisitFn that this node was visited while searching for selection matches. It is not necessarily implied that any explicit match will be a child of this node; only that we had to consider it. (Merkle-proofs generally need to include any node in this group.)
)
type TraversalProgress struct {
Cfg *TraversalConfig
......
package selector
import (
ipld "github.com/ipld/go-ipld-prime"
)
// SelectAll is a non-recursive kleene-star match (e.g., it's `./*`).
// If SelectAll is a leaf in a Selector tree, it will match all content;
// if it has a 'next' selector (e.g., it's like `./*/foo`), it'll yield
// that next selector for explore of any and all pathsegments.
type SelectAll struct {
next Selector // set to SelectTrue at parse time if appropriate.
}
func (s SelectAll) Interests() []PathSegment {
return nil
}
func (s SelectAll) Explore(n ipld.Node, p PathSegment) Selector {
return s.next
}
func (s SelectAll) Decide(n ipld.Node) bool {
return false // this is an intermediate selector: it doesn't itself call for a thing, only indirectly does so by sometimes returning SelectTrue.
}
package selector
import (
"fmt"
ipld "github.com/ipld/go-ipld-prime"
)
// SelectFields selects some fields by name (or index),
// and may contain more nested selectors per field.
//
// If you're familiar with GraphQL queries, you can thing of SelectFields
// as similar to the basic unit of composition in GraphQL queries.
//
// SelectFields also works for selecting specific elements out of a list;
// if the "field" is a base-10 int, it will be coerced and do the right thing.
// SelectIndexes is more appropriate, however, and should be preferred.
type SelectFields struct {
selections map[string]Selector
interests []PathSegment // keys of above; already boxed as that's the only way we consume them
}
func (s SelectFields) Interests() []PathSegment {
return s.interests
}
func (s SelectFields) Explore(n ipld.Node, p PathSegment) Selector {
return s.selections[p.String()]
}
func (s SelectFields) Decide(n ipld.Node) bool {
return false // this is an intermediate selector: it doesn't itself call for a thing, only indirectly does so by sometimes returning SelectTrue.
}
func ParseSelectFields(n ipld.Node) (Selector, error) {
if n.ReprKind() != ipld.ReprKind_Map {
return nil, fmt.Errorf("selector spec parse rejected: selector body must be a map")
}
x := SelectFields{
make(map[string]Selector, n.Length()),
make([]PathSegment, 0, n.Length()),
}
for itr := n.MapIterator(); !itr.Done(); {
kn, v, err := itr.Next()
if err != nil {
return nil, fmt.Errorf("error during selector spec parse: %s", err)
}
kstr, _ := kn.AsString()
x.interests = append(x.interests, PathSegmentString{kstr})
switch v.ReprKind() {
case ipld.ReprKind_Map: // deeper!
x.selections[kstr], err = ParseSelector(v)
if err != nil {
return nil, err
}
case ipld.ReprKind_Bool:
b, _ := v.AsBool()
if !b {
// FUTURE: boolean-as-unit is not currently expressible in the schema spec; might be something we want, just for human ergonomics.
return nil, fmt.Errorf("selector spec parse rejected: entries in selectFields must be either a nested selector or the value 'true'")
}
x.selections[kstr] = SelectTrue{}
}
}
return x, nil
}
package selector
import (
ipld "github.com/ipld/go-ipld-prime"
)
// SelectAll is a dummy selector that other selectors can return to say
// "the content at this path? definitely this".
type SelectTrue struct{}
func (s SelectTrue) Interests() []PathSegment {
return []PathSegment{}
}
func (s SelectTrue) Explore(n ipld.Node, p PathSegment) Selector {
return nil
}
func (s SelectTrue) Decide(n ipld.Node) bool {
return true
}
package selector
import (
ipld "github.com/ipld/go-ipld-prime"
)
// implementation note: union selectors can be generated at selector evaluation time!
// for example, globstar selectors do this: `**/foo` implicitly generates a union
// after the first depth in order to check deeper star as well as the 'foo' match.
//
// imagine this example:
//
// a selector like `**/?oo/bar` is applied...
//
// ```
// ./zot/ -- prefix matches **
// ./zot/zoo/ -- prefix matches **, prefix matches **/?oo
// ./zot/zoo/foo -- prefix matches **, prefix matches **/?oo
// ./zot/zoo/foo/bar -- prefix matches **, FULL MATCH **/?oo/bar
// ./zot/zoo/foo/bar/baz -- prefix matches **
// ```
//
// as you can see, a union selector reasonably expresses the intermediate state
// needed during handling several of these paths.
// SelectUnion combines two or more other selectors and aggregates their behavior;
// if something is matched by any of the composed selectors, it's matched by the union.
type SelectUnion struct {
Members []Selector
}
func (s SelectUnion) Interests() []PathSegment {
// Check for any high-cardinality selectors first; if so, shortcircuit.
// (n.b. we're assuming the 'Interests' method is cheap here.)
for _, m := range s.Members {
if m.Interests() == nil {
return nil
}
}
// Accumulate the whitelist of interesting path segments.
v := []PathSegment{}
for _, m := range s.Members {
v = append(v, m.Interests()...)
}
return v
}
func (s SelectUnion) Explore(n ipld.Node, p PathSegment) Selector {
// this needs to call Explore for each member,
// and if more than one member returns a selector,
// we compose them into a new union automatically and return that.
panic("TODO")
}
func (s SelectUnion) Decide(n ipld.Node) bool {
for _, m := range s.Members {
if m.Decide(n) {
return true
}
}
return false
}
package selector
import ipld "github.com/ipld/go-ipld-prime"
import (
"fmt"
"strconv"
ipld "github.com/ipld/go-ipld-prime"
)
type Selector interface {
Explore(ipld.Node) (ipld.MapIterator, ipld.ListIterator, Selector)
Interests() []PathSegment // returns the segments we're likely interested in **or nil** if we're a high-cardinality or expression based matcher and need all segments proposed to us.
Explore(ipld.Node, PathSegment) Selector // explore one step -- iteration comes from outside (either whole node, or by following suggestions of Interests). returns nil if no interest. you have to traverse to the next node yourself (the selector doesn't do it for you because you might be considering multiple selection reasons at the same time).
Decide(ipld.Node) bool
}
func ReifySelector(cidRootedSelector ipld.Node) (Selector, error) {
return nil, nil
func ParseSelector(n ipld.Node) (Selector, error) {
if n.ReprKind() != ipld.ReprKind_Map {
return nil, fmt.Errorf("selector spec parse rejected: selector is a keyed union and thus must be a map")
}
if n.Length() != 1 {
return nil, fmt.Errorf("selector spec parse rejected: selector 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 selector body comes next.
// (This switch is where the keyed union discriminators concretely happen.)
switch kstr {
case "f":
return ParseSelectFields(v)
// FUTURE:
// case "a":
// return ParseSelectAll(v)
// case "i":
// return ParseSelectIndexes(v)
// case "r":
// return ParseSelectRange(v)
// case "+":
// return ParseSelectTrue(v)
default:
return nil, fmt.Errorf("selector spec parse rejected: %q is not a known member of the selector union", kstr)
}
}
type PathSegment interface {
String() string
Index() (int, error)
}
type PathSegmentString struct {
S string
}
type PathSegmentInt struct {
I int
}
func (ps PathSegmentString) String() string {
return ps.S
}
func (ps PathSegmentString) Index() (int, error) {
return strconv.Atoi(ps.S)
}
func (ps PathSegmentInt) String() string {
return strconv.Itoa(ps.I)
}
func (ps PathSegmentInt) Index() (int, error) {
return ps.I, nil
}
......@@ -19,8 +19,8 @@ func TraverseTransform(n ipld.Node, s selector.Selector, fn TransformFn) (ipld.N
func (tp TraversalProgress) Traverse(n ipld.Node, s selector.Selector, fn VisitFn) error {
tp.init()
return tp.TraverseInformatively(n, s, func(tp TraversalProgress, n ipld.Node, tr TraversalReason) error {
if tr != 1 {
return tp.traverseInformatively(n, s, func(tp TraversalProgress, n ipld.Node, tr TraversalReason) error {
if tr != TraversalReason_SelectionMatch {
return nil
}
return fn(tp, n)
......@@ -28,7 +28,47 @@ func (tp TraversalProgress) Traverse(n ipld.Node, s selector.Selector, fn VisitF
}
func (tp TraversalProgress) TraverseInformatively(n ipld.Node, s selector.Selector, fn AdvVisitFn) error {
panic("TODO")
tp.init()
return tp.traverseInformatively(n, s, fn)
}
func (tp TraversalProgress) traverseInformatively(n ipld.Node, s selector.Selector, fn AdvVisitFn) error {
if s.Decide(n) {
if err := fn(tp, n, TraversalReason_SelectionMatch); err != nil {
return err
}
} else {
if err := fn(tp, n, TraversalReason_SelectionCandidate); err != nil {
return err
}
}
nk := n.ReprKind()
switch nk {
case ipld.ReprKind_Map, ipld.ReprKind_List: // continue
default:
return nil
}
// TODO: should only do this full loop if high-cardinality indicated.
// attn := s.Interests()
// if attn == nil {
// FIXME need another kind switch here, and list support!
for itr := n.MapIterator(); !itr.Done(); {
k, v, err := itr.Next()
if err != nil {
return err
}
kstr, _ := k.AsString()
sNext := s.Explore(n, selector.PathSegmentString{kstr})
if sNext != nil {
// TODO when link load is implemented, it should go roughly here.
tpNext := tp
tpNext.Path = tp.Path.AppendSegment(kstr)
if err := tpNext.traverseInformatively(v, sNext, fn); err != nil {
return err
}
}
}
return nil
}
func (tp TraversalProgress) TraverseTransform(n ipld.Node, s selector.Selector, fn TransformFn) (ipld.Node, error) {
......
package traversal_test
import (
"testing"
. "github.com/warpfork/go-wish"
ipld "github.com/ipld/go-ipld-prime"
_ "github.com/ipld/go-ipld-prime/encoding/dagjson"
"github.com/ipld/go-ipld-prime/fluent"
"github.com/ipld/go-ipld-prime/traversal"
"github.com/ipld/go-ipld-prime/traversal/selector"
)
/* Remember, we've got the following fixtures in scope:
var (
leafAlpha, leafAlphaLnk = encode(fnb.CreateString("alpha"))
leafBeta, leafBetaLnk = encode(fnb.CreateString("beta"))
middleMapNode, middleMapNodeLnk = encode(fnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("foo"), vnb.CreateBool(true))
mb.Insert(knb.CreateString("bar"), vnb.CreateBool(false))
mb.Insert(knb.CreateString("nested"), vnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("alink"), vnb.CreateLink(leafAlphaLnk))
mb.Insert(knb.CreateString("nonlink"), vnb.CreateString("zoo"))
}))
}))
middleListNode, middleListNodeLnk = encode(fnb.CreateList(func(lb fluent.ListBuilder, vnb fluent.NodeBuilder) {
lb.Append(vnb.CreateLink(leafAlphaLnk))
lb.Append(vnb.CreateLink(leafAlphaLnk))
lb.Append(vnb.CreateLink(leafBetaLnk))
lb.Append(vnb.CreateLink(leafAlphaLnk))
}))
rootNode, rootNodeLnk = encode(fnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("plain"), vnb.CreateString("olde string"))
mb.Insert(knb.CreateString("linkedString"), vnb.CreateLink(leafAlphaLnk))
mb.Insert(knb.CreateString("linkedMap"), vnb.CreateLink(middleMapNodeLnk))
mb.Insert(knb.CreateString("linkedList"), vnb.CreateLink(middleListNodeLnk))
}))
)
*/
// covers traverse using a variety of selectors.
// all cases here use one already-loaded Node; no link-loading exercised.
func TestTraverse(t *testing.T) {
t.Run("traverse selecting true should visit the root", func(t *testing.T) {
err := traversal.Traverse(fnb.CreateString("x"), selector.SelectTrue{}, func(tp traversal.TraversalProgress, n ipld.Node) error {
Wish(t, n, ShouldEqual, fnb.CreateString("x"))
Wish(t, tp.Path.String(), ShouldEqual, ipld.Path{}.String())
return nil
})
Wish(t, err, ShouldEqual, nil)
})
t.Run("traverse selecting true should visit only the root and no deeper", func(t *testing.T) {
err := traversal.Traverse(middleMapNode, selector.SelectTrue{}, func(tp traversal.TraversalProgress, n ipld.Node) error {
Wish(t, n, ShouldEqual, middleMapNode)
Wish(t, tp.Path.String(), ShouldEqual, ipld.Path{}.String())
return nil
})
Wish(t, err, ShouldEqual, nil)
})
t.Run("traverse selecting fields should work", func(t *testing.T) {
sn := fnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("f"), vnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("foo"), vnb.CreateBool(true))
mb.Insert(knb.CreateString("bar"), vnb.CreateBool(true))
}))
})
s, err := selector.ParseSelector(sn)
Require(t, err, ShouldEqual, nil)
var order int
err = traversal.Traverse(middleMapNode, s, func(tp traversal.TraversalProgress, n ipld.Node) error {
switch order {
case 0:
Wish(t, n, ShouldEqual, fnb.CreateBool(true))
Wish(t, tp.Path.String(), ShouldEqual, "foo")
case 1:
Wish(t, n, ShouldEqual, fnb.CreateBool(false))
Wish(t, tp.Path.String(), ShouldEqual, "bar")
}
order++
return nil
})
Wish(t, err, ShouldEqual, nil)
Wish(t, order, ShouldEqual, 2)
})
t.Run("traverse selecting fields recursively should work", func(t *testing.T) {
sn := fnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("f"), vnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("foo"), vnb.CreateBool(true))
mb.Insert(knb.CreateString("nested"), vnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("f"), vnb.CreateMap(func(mb fluent.MapBuilder, knb fluent.NodeBuilder, vnb fluent.NodeBuilder) {
mb.Insert(knb.CreateString("nonlink"), vnb.CreateBool(true))
}))
}))
}))
})
s, err := selector.ParseSelector(sn)
Require(t, err, ShouldEqual, nil)
var order int
err = traversal.Traverse(middleMapNode, s, func(tp traversal.TraversalProgress, n ipld.Node) error {
switch order {
case 0:
Wish(t, n, ShouldEqual, fnb.CreateBool(true))
Wish(t, tp.Path.String(), ShouldEqual, "foo")
case 1:
Wish(t, n, ShouldEqual, fnb.CreateString("zoo"))
Wish(t, tp.Path.String(), ShouldEqual, "nested/nonlink")
}
order++
return nil
})
Wish(t, err, ShouldEqual, nil)
Wish(t, order, ShouldEqual, 2)
})
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment