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

signaling ADLs in selectors #301

Merged
merged 13 commits into from
Jan 10, 2022
Merged
1 change: 1 addition & 0 deletions linking/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type LinkSystem struct {
StorageReadOpener BlockReadOpener
TrustedStorage bool
NodeReifier NodeReifier
KnownReifiers map[string]NodeReifier
}

// The following three types are the key functionality we need from a "blockstore".
Expand Down
12 changes: 12 additions & 0 deletions traversal/selector/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type SelectorSpecBuilder interface {
ExploreIndex(index int64, next SelectorSpec) SelectorSpec
ExploreRange(start, end int64, next SelectorSpec) SelectorSpec
ExploreFields(ExploreFieldsSpecBuildingClosure) SelectorSpec
ExploreInterpretAs(as string, next SelectorSpec) SelectorSpec
Matcher() SelectorSpec
}

Expand Down Expand Up @@ -150,6 +151,17 @@ func (ssb *selectorSpecBuilder) ExploreFields(specBuilder ExploreFieldsSpecBuild
}
}

func (ssb *selectorSpecBuilder) ExploreInterpretAs(as string, next SelectorSpec) SelectorSpec {
return selectorSpec{
fluent.MustBuildMap(ssb.np, 1, func(na fluent.MapAssembler) {
na.AssembleEntry(selector.SelectorKey_ExploreInterpretAs).CreateMap(1, func(na fluent.MapAssembler) {
na.AssembleEntry(selector.SelectorKey_As).AssignString(as)
na.AssembleEntry(selector.SelectorKey_Next).AssignNode(next.Node())
})
}),
}
}

func (ssb *selectorSpecBuilder) Matcher() SelectorSpec {
return selectorSpec{
fluent.MustBuildMap(ssb.np, 1, func(na fluent.MapAssembler) {
Expand Down
65 changes: 65 additions & 0 deletions traversal/selector/exploreInterpretAs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package selector

import (
"fmt"

"github.com/ipld/go-ipld-prime/datamodel"
)

type ExploreInterpretAs struct {
next Selector // selector for element we're interested in
adl string // reifier for the ADL we're interested in
}

// Interests for ExploreIndex is just the index specified by the selector node
func (s ExploreInterpretAs) Interests() []datamodel.PathSegment {
return s.next.Interests()
}

// Explore returns the node's selector if
// the path matches the index for this selector or nil if not
func (s ExploreInterpretAs) Explore(n datamodel.Node, p datamodel.PathSegment) (Selector, error) {
return s.next, nil
}

// Decide always returns false because this is not a matcher
func (s ExploreInterpretAs) Decide(n datamodel.Node) bool {
return false
}

// NamedReifier indicates how this selector expects to Reify the current datamodel.Node.
func (s ExploreInterpretAs) NamedReifier() string {
return s.adl
}

// Reifiable provides a feature detection interface on selectors to understand when
// and if Reification of the datamodel.node should be attempted when performing traversals.
type Reifiable interface {
NamedReifier() string
}
Comment on lines +35 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Reviewing out loud, not a change request) This interface is implemented only once and used in only one place, so I wonder if it's necessary. Since the set of types for the compiled selectors[^1] is considered closed, it seems like we could just as well use a concrete type switch?

I'm okay leaving it this way too, because it certainly works, and I don't think it gets in the way, and perhaps someone has an idea that there will be more than one selector clause in the future which asks for a reification to be performed.

[^1] - (I wish we had done #236 by now so I could refer to what I mean by selectorengine.*...)


// ParseExploreInterpretAs assembles a Selector
// from a ExploreInterpretAs selector node
func (pc ParseContext) ParseExploreInterpretAs(n datamodel.Node) (Selector, error) {
if n.Kind() != datamodel.Kind_Map {
return nil, fmt.Errorf("selector spec parse rejected: selector body must be a map")
}
adlNode, err := n.LookupByString(SelectorKey_As)
if err != nil {
return nil, fmt.Errorf("selector spec parse rejected: fields in ExploreFields selector must be present")
willscott marked this conversation as resolved.
Show resolved Hide resolved
}
next, err := n.LookupByString(SelectorKey_Next)
if err != nil {
return nil, fmt.Errorf("selector spec parse rejected: next field must be present in ExploreAll selector")
willscott marked this conversation as resolved.
Show resolved Hide resolved
}
selector, err := pc.ParseSelector(next)
if err != nil {
return nil, err
}
adl, err := adlNode.AsString()
if err != nil {
return nil, err
}

return ExploreInterpretAs{selector, adl}, nil
}
2 changes: 2 additions & 0 deletions traversal/selector/fieldKeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
SelectorKey_ExploreUnion = "|"
SelectorKey_ExploreConditional = "&"
SelectorKey_ExploreRecursiveEdge = "@"
SelectorKey_ExploreInterpretAs = "~"
SelectorKey_Next = ">"
SelectorKey_Fields = "f>"
SelectorKey_Index = "i"
Expand All @@ -21,5 +22,6 @@ const (
SelectorKey_LimitNone = "none"
SelectorKey_StopAt = "!"
SelectorKey_Condition = "&"
SelectorKey_As = "c"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this would also be a specs change, since we merged that in the ipld/ipld repo already, but since this will be the first implementation actually using it --

I still think we should change this key. There's no way I would either look at the spec and expect it to use the constant "c" here, nor look at the constant "c" in a message and guess remotely correctly what it stands for. (To wit: I just had to look it up again.)

Can we use "as"? I don't think a second byte is going to bother anyone, and my surprise factor would go down drastically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm very scared to go back to step one given that we're now 2 months in to a very simple change that has now missed 2 shipping deadlines because of the spec then implementation then wait long time for review approach.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// not filling conditional keys since it's not complete
)
2 changes: 2 additions & 0 deletions traversal/selector/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ func (pc ParseContext) ParseSelector(n datamodel.Node) (Selector, error) {
return pc.ParseExploreRecursive(v)
case SelectorKey_ExploreRecursiveEdge:
return pc.ParseExploreRecursiveEdge(v)
case SelectorKey_ExploreInterpretAs:
return pc.ParseExploreInterpretAs(v)
case SelectorKey_Matcher:
return pc.ParseMatcher(v)
default:
Expand Down
26 changes: 26 additions & 0 deletions traversal/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,32 @@ func (prog Progress) walkAdv(n datamodel.Node, s selector.Selector, fn AdvVisitF
}
prog.Budget.NodeBudget--
}

// refiy the node if advised.
if rs, ok := s.(selector.Reifiable); ok {
adl := rs.NamedReifier()
if prog.Cfg.LinkSystem.KnownReifiers == nil {
warpfork marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("adl requested but not supported by link system: %q", adl)
}
reifier, ok := prog.Cfg.LinkSystem.KnownReifiers[adl]
if !ok {
return fmt.Errorf("unregistered adl requested: %q", adl)
}

rn, err := reifier(linking.LinkContext{
Ctx: prog.Cfg.Ctx,
LinkPath: prog.Path,
}, n, &prog.Cfg.LinkSystem)
if err != nil {
return fmt.Errorf("failed to reify node as %q: %v", adl, err)
willscott marked this conversation as resolved.
Show resolved Hide resolved
}
s, err = s.Explore(n, datamodel.PathSegment{})
if err != nil {
return err
}
n = rn
}

// Decide if this node is matched -- do callbacks as appropriate.
if s.Decide(n) {
if err := fn(prog, n, VisitReason_SelectionMatch); err != nil {
Expand Down
38 changes: 38 additions & 0 deletions traversal/walk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,41 @@ func TestWalkBlockLoadOrder(t *testing.T) {
})
})
}

func TestWalk_ADLs(t *testing.T) {
// we'll make a reifier that when it sees a list returns a custom element instead.
customReifier := func(_ linking.LinkContext, n datamodel.Node, _ *linking.LinkSystem) (datamodel.Node, error) {
if n.Kind() == datamodel.Kind_List {
return leafAlpha, nil
}
return n, nil
}

ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
efsb.Insert("linkedList", ssb.ExploreInterpretAs("linkjumper", ssb.Matcher()))
})
s, err := ss.Selector()
qt.Check(t, err, qt.IsNil)
lsys := cidlink.DefaultLinkSystem()
lsys.KnownReifiers = make(map[string]linking.NodeReifier)
lsys.KnownReifiers["linkjumper"] = customReifier
willscott marked this conversation as resolved.
Show resolved Hide resolved
lsys.SetReadStorage(&store)
var order int
err = traversal.Progress{
Cfg: &traversal.Config{
LinkSystem: lsys,
LinkTargetNodePrototypeChooser: basicnode.Chooser,
},
}.WalkMatching(rootNode, s, func(prog traversal.Progress, n datamodel.Node) error {
switch order {
case 0:
qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
qt.Check(t, prog.Path.String(), qt.Equals, "linkedList")
}
order++
return nil
})
qt.Check(t, err, qt.IsNil)
qt.Check(t, order, qt.Equals, 1)
}