Skip to content

Commit

Permalink
Merge pull request #264 from ipld/assignnode-clarifications-and-copy-…
Browse files Browse the repository at this point in the history
…func

Improve docs for AssignNode; and datamodel.Copy function.
  • Loading branch information
warpfork authored Oct 14, 2021
2 parents 58f2aaf + 40696f1 commit 7d17700
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 5 deletions.
110 changes: 110 additions & 0 deletions datamodel/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package datamodel

import (
"fmt"
)

// Copy does an explicit shallow copy of a Node's data into a NodeAssembler.
//
// This can be used to flip data from one memory layout to another
// (for example, from basicnode to using using bindnode,
// or to codegenerated node implementations,
// or to or from ADL nodes, etc).
//
// The copy is implemented by ranging over the contents if it's a recursive kind,
// and for each of them, using `AssignNode` on the child values;
// for scalars, it's just calling the appropriate `Assign*` method.
//
// Many NodeAssembler implementations use this as a fallback behavior in their
// `AssignNode` method (that is, they call to this function after all other special
// faster shortcuts they might prefer to employ, such as direct struct copying
// if they share internal memory layouts, etc, have been tried already).
//
func Copy(n Node, na NodeAssembler) error {
switch n.Kind() {
case Kind_Null:
if n.IsAbsent() {
return fmt.Errorf("copying an absent node makes no sense")
}
return na.AssignNull()
case Kind_Bool:
v, err := n.AsBool()
if err != nil {
return fmt.Errorf("node violated contract: promised to be %v kind, but AsBool method returned %w", n.Kind(), err)
}
return na.AssignBool(v)
case Kind_Int:
v, err := n.AsInt()
if err != nil {
return fmt.Errorf("node violated contract: promised to be %v kind, but AsInt method returned %w", n.Kind(), err)
}
return na.AssignInt(v)
case Kind_Float:
v, err := n.AsFloat()
if err != nil {
return fmt.Errorf("node violated contract: promised to be %v kind, but AsFloat method returned %w", n.Kind(), err)
}
return na.AssignFloat(v)
case Kind_String:
v, err := n.AsString()
if err != nil {
return fmt.Errorf("node violated contract: promised to be %v kind, but AsString method returned %w", n.Kind(), err)
}
return na.AssignString(v)
case Kind_Bytes:
v, err := n.AsBytes()
if err != nil {
return fmt.Errorf("node violated contract: promised to be %v kind, but AsBytes method returned %w", n.Kind(), err)
}
return na.AssignBytes(v)
case Kind_Link:
v, err := n.AsLink()
if err != nil {
return fmt.Errorf("node violated contract: promised to be %v kind, but AsLink method returned %w", n.Kind(), err)
}
return na.AssignLink(v)
case Kind_Map:
ma, err := na.BeginMap(n.Length())
if err != nil {
return err
}
itr := n.MapIterator()
for !itr.Done() {
k, v, err := itr.Next()
if err != nil {
return err
}
if v.IsAbsent() {
continue
}
if err := ma.AssembleKey().AssignNode(k); err != nil {
return err
}
if err := ma.AssembleValue().AssignNode(v); err != nil {
return err
}
}
return ma.Finish()
case Kind_List:
la, err := na.BeginList(n.Length())
if err != nil {
return err
}
itr := n.ListIterator()
for !itr.Done() {
_, v, err := itr.Next()
if err != nil {
return err
}
if v.IsAbsent() {
continue
}
if err := la.AssembleValue().AssignNode(v); err != nil {
return err
}
}
return la.Finish()
default:
return fmt.Errorf("node has invalid kind %v", n.Kind())
}
}
31 changes: 26 additions & 5 deletions datamodel/nodeBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,36 @@ package datamodel
// NodeAssembler is the interface that describes all the ways we can set values
// in a node that's under construction.
//
// To create a Node, you should start with a NodeBuilder (which contains a
// A NodeAssembler is about filling in data.
// To create a new Node, you should start with a NodeBuilder (which contains a
// superset of the NodeAssembler methods, and can return the finished Node
// from its `Build` method).
// While continuing to build a recursive structure from there,
// you'll see NodeAssembler for all the child values.
//
// For filling scalar data, there's a `Assign{Kind}` method for each kind;
// after calling one of these methods, the data is filled in, and the assembler is done.
// For recursives, there are `BeginMap` and `BeginList` methods,
// which return an object that needs further manipulation to fill in the contents.
//
// There is also one special method: `AssignNode`.
// `AssignNode` takes another `Node` as a parameter,
// and should should internally call one of the other `Assign*` or `Begin*` (and subsequent) functions
// as appropriate for the kind of the `Node` it is given.
// This is roughly equivalent to using the `Copy` function (and is often implemented using it!), but
// `AssignNode` may also try to take faster shortcuts in some implementations, when it detects they're possible.
// (For example, for typed nodes, if they're the same type, lots of checking can be skipped.
// For nodes implemented with pointers, lots of copying can be skipped.
// For nodes that can detect the argument has the same memory layout, faster copy mechanisms can be used; etc.)
//
// Why do both this and the NodeBuilder interface exist?
// When creating trees of nodes, recursion works over the NodeAssembler interface.
// This is important to efficient library internals, because avoiding the
// In short: NodeBuilder is when you want to cause an allocation;
// NodeAssembler can be used to just "fill in" memory.
// (In the internal gritty details: separate interfaces, one of which lacks a
// `Build` method, helps us write efficient library internals: avoiding the
// requirement to be able to return a Node at any random point in the process
// relieves internals from needing to implement 'freeze' features.
// (This is useful in turn because implementing those 'freeze' features in a
// This is useful in turn because implementing those 'freeze' features in a
// language without first-class/compile-time support for them (as golang is)
// would tend to push complexity and costs to execution time; we'd rather not.)
type NodeAssembler interface {
Expand Down Expand Up @@ -69,7 +89,8 @@ type MapAssembler interface {
// just feed data and check errors), but it's here.
//
// For all Data Model maps, this will answer with a basic concept of "string".
// For Schema typed maps, this may answer with a more complex type (potentially even a struct type).
// For Schema typed maps, this may answer with a more complex type
// (potentially even a struct type or union type -- anything that can have a string representation).
KeyPrototype() NodePrototype

// ValuePrototype returns a NodePrototype that knows how to build values this map can contain.
Expand Down

0 comments on commit 7d17700

Please sign in to comment.