Skip to content

Commit

Permalink
Merge pull request #199 from ipld/selector-helpers
Browse files Browse the repository at this point in the history
selectors: docs enhancements, new construction helpers.
  • Loading branch information
warpfork authored Aug 12, 2021
2 parents 5371558 + 3eb2c31 commit e617585
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 5 deletions.
94 changes: 94 additions & 0 deletions traversal/selector/parse/selector_parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
selectorparse package contains some helpful functions for parsing the serial form of Selectors.
Some common selectors are also exported as pre-compiled variables,
both for convenience of use and to be readable as examples.
*/
package selectorparse

import (
"strings"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/node/basic"
"github.com/ipld/go-ipld-prime/traversal/selector"
)

// ParseJSONSelector accepts a string of json which will be parsed as a selector,
// and returns an ipld.Node of the parsed Data Model.
// The returned ipld.Node is suitable to hand to `selector.CompileSelector`,
// or, could be composed programmatically with other Data Model selector clauses
// and then compiled later.
//
// The selector will be checked for compileability, and an error returned if it is not.
func ParseJSONSelector(jsonStr string) (ipld.Node, error) {
nb := basicnode.Prototype.Any.NewBuilder()
if err := dagjson.Decode(nb, strings.NewReader(jsonStr)); err != nil {
return nil, err
}
// Compile it, because that's where all of our error checking is right now.
// ... but throw that result away, because the point of this method is to return nodes that you can compose further.
// Ideally, we'd have just used Schemas for this check,
// which would be cheaper than running the full compile,
// and also more correct (because it would let us parse incomplete phrases that won't compile alone),
// but that's not currently how the Selectors code is implemented. Future work!
n := nb.Build()
if _, err := selector.CompileSelector(n); err != nil {
return nil, err
}
return n, nil
}

// ParseJSONSelector accepts a string of json which will be parsed as a selector,
// and returns a compiled and ready-to-run Selector.
//
// ParseJSONSelector is functionally equivalent to combining ParseJSONSelector and CompileSelector into one step.
func ParseAndCompileJSONSelector(jsonStr string) (selector.Selector, error) {
nb := basicnode.Prototype.Any.NewBuilder()
if err := dagjson.Decode(nb, strings.NewReader(jsonStr)); err != nil {
return nil, err
}
if s, err := selector.CompileSelector(nb.Build()); err != nil {
return nil, err
} else {
return s, nil
}
}

func must(s selector.Selector, e error) selector.Selector {
if e != nil {
panic(e)
}
return s
}

// CommonSelector_MatchPoint is a selector that matches exactly one thing: the first node it touches.
// It doesn't walk anywhere at all.
//
// This is not a very useful selector, but is an example of how selectors can be written.
var CommonSelector_MatchPoint = must(ParseAndCompileJSONSelector(`{".":{}}`))

// CommonSelector_MatchChildren will examine the node it is applied to,
// walk to each of its children, and match the children.
// It does not recurse.
// Note that the root node itself is visited (necessarily!) but it is not "matched".
var CommonSelector_MatchChildren = must(ParseAndCompileJSONSelector(`{"a":{">":{".":{}}}}`))

// CommonSelector_ExploreAllRecursively is a selector that walks over a graph of data,
// recursively, without limit (!) until it reaches every part of the graph.
// (This is safe to assume will halt eventually, because in IPLD, we work with DAGs --
// although it still may be a bad idea to do this in practice,
// because you could accidentally do this on terabytes of linked data, and that would still take a while!)
//
// It does not actually _match_ anything at all.
// That means if you're intercepting block loads (e.g. you're looking at calls to LinkSystem.StorageReadOpener), you'll see them;
// and if you're using `traversal.AdvVisitFn`, you'll still hear about nodes visited during the exploration;
// however, if you're using just `traversal.VisitFn`, nothing is considered "matched", so that callback will never be called.
var CommonSelector_ExploreAllRecursively = must(ParseAndCompileJSONSelector(`{"R":{"l":{"none":{}},":>":{"a":{">":{"@":{}}}}}}`))

// CommonSelector_MatchAllRecursively is like CommonSelector_ExploreAllRecursively, but also matching everything it touches.
// The first thing inside the recursion is an ExploreUnion clause (which means the selection continues with multiple logical paths);
// the first thing inside that union clause is a Matcher clause;
// the second thing inside that union is the ExploreAll clause, which gets us deeper, and then that contains the ExploreRecursiveEdge.
var CommonSelector_MatchAllRecursively = must(ParseAndCompileJSONSelector(`{"R":{"l":{"none":{}},":>":{"|":[{".":{}},{"a":{">":{"@":{}}}}]}}}`))
3 changes: 3 additions & 0 deletions traversal/selector/parse/selector_parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package selectorparse

// nothing -- this file just makes sure the vars get initialized, which is a defacto test.
37 changes: 32 additions & 5 deletions traversal/selector/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,28 @@ import (
ipld "github.com/ipld/go-ipld-prime"
)

// Selector is the programmatic representation of an IPLD Selector Node
// and can be applied to traverse a given IPLD DAG
// Selector is a "compiled" and executable IPLD Selector.
// It can be put to work with functions like traversal.Walk,
// which will use the Selector's guidance to decide how to traverse an IPLD data graph.
//
// A Selector is created by parsing an IPLD Data Model document that declares a Selector
// (this is accomplished with functions like CompileSelector).
// Alternatively, there is a builder subpackage,
// which is useful if you would rather create the Selector declaration programmatically in golang.
//
// There is no way to go backwards from this "compiled" Selector type into the declarative IPLD data model information that produced it.
// That declaration information is discarded after compilation in order to limit the amount of memory held.
// Therefore, if you're building APIs about Selector composition, keep in mind that
// you'll probably want to approach this be composing the Data Model declaration documents,
// not be composing this type, which is only for the "compiled" result.
type Selector interface {
Interests() []ipld.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, ipld.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
}

// REVIEW: do ParsedParent and ParseContext need to be exported? They're mostly used during the compilation process.

// ParsedParent is created whenever you are parsing a selector node that may have
// child selectors nodes that need to know it
type ParsedParent interface {
Expand All @@ -25,9 +39,22 @@ type ParseContext struct {
parentStack []ParsedParent
}

// ParseSelector creates a Selector that can be traversed from an IPLD Selector node
func ParseSelector(n ipld.Node) (Selector, error) {
return ParseContext{}.ParseSelector(n)
// CompileSelector accepts an ipld.Node which should contain data that declares a Selector.
// The data layout expected for this declaration is documented in https://ipld.io/specs/selectors/ .
//
// If the Selector is compiled successfully, it is returned.
// Otherwise, if the given data Node doesn't match the expected shape for a Selector declaration,
// or there are any other problems compiling the selector
// (such as a recursion edge with no enclosing recursion declaration, etc),
// then nil and an error will be returned.
func CompileSelector(dmt ipld.Node) (Selector, error) {
return ParseContext{}.ParseSelector(dmt)
}

// ParseSelector is an alias for CompileSelector, and is deprecated.
// Prefer CompileSelector.
func ParseSelector(dmt ipld.Node) (Selector, error) {
return CompileSelector(dmt)
}

// ParseSelector creates a Selector from an IPLD Selector Node with the given context
Expand Down

0 comments on commit e617585

Please sign in to comment.