Skip to content

Commit

Permalink
Merge pull request #232 from ipld/codec-facades
Browse files Browse the repository at this point in the history
helper methods for encoding and decoding
  • Loading branch information
warpfork authored Aug 19, 2021
2 parents d9795f3 + 8710e38 commit a47ecf9
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
167 changes: 167 additions & 0 deletions codecHelpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package ipld

import (
"bytes"
"io"
"reflect"

"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/schema"
)

// Encode serializes the given Node using the given Encoder function,
// returning the serialized data or an error.
//
// The exact result data will depend the node content and on the encoder function,
// but for example, using a json codec on a node with kind map will produce
// a result starting in `{`, etc.
//
// Encode will automatically switch to encoding the representation form of the Node,
// if it discovers the Node matches the schema.TypedNode interface.
// This is probably what you want, in most cases;
// if this is not desired, you can use the underlaying functions directly
// (just look at the source of this function for an example of how!).
//
// If you would like this operation, but applied directly to a golang type instead of a Node,
// look to the Marshal function.
func Encode(n Node, encFn Encoder) ([]byte, error) {
var buf bytes.Buffer
err := EncodeStreaming(&buf, n, encFn)
return buf.Bytes(), err
}

// EncodeStreaming is like Encode, but emits output to an io.Writer.
func EncodeStreaming(wr io.Writer, n Node, encFn Encoder) error {
if tn, ok := n.(schema.TypedNode); ok {
n = tn.Representation()
}
return encFn(n, wr)
}

// Decode parses the given bytes into a Node using the given Decoder function,
// returning a new Node or an error.
//
// The new Node that is returned will be the implementation from the node/basicnode package.
// This implementation of Node will work for storing any kind of data,
// but note that because it is general, it is also not necessarily optimized.
// If you want more control over what kind of Node implementation (and thus memory layout) is used,
// or want to use features like IPLD Schemas (which can be engaged by using a schema.TypedPrototype),
// then look to the DecodeUsingPrototype family of functions,
// which accept more parameters in order to give you that kind of control.
//
// If you would like this operation, but applied directly to a golang type instead of a Node,
// look to the Unmarshal function.
func Decode(b []byte, decFn Decoder) (Node, error) {
return DecodeUsingPrototype(b, decFn, basicnode.Prototype.Any)
}

// DecodeStreaming is like Decode, but works on an io.Reader for input.
func DecodeStreaming(r io.Reader, decFn Decoder) (Node, error) {
return DecodeStreamingUsingPrototype(r, decFn, basicnode.Prototype.Any)
}

// DecodeUsingPrototype is like Decode, but with a NodePrototype parameter,
// which gives you control over the Node type you'll receive,
// and thus control over the memory layout, and ability to use advanced features like schemas.
// (Decode is simply this function, but hardcoded to use basicnode.Prototype.Any.)
//
// DecodeUsingPrototype internally creates a NodeBuilder, and thows it away when done.
// If building a high performance system, and creating data of the same shape repeatedly,
// you may wish to use NodeBuilder directly, so that you can control and avoid these allocations.
//
// For symmetry with the behavior of Encode, DecodeUsingPrototype will automatically
// switch to using the representation form of the node for decoding
// if it discovers the NodePrototype matches the schema.TypedPrototype interface.
// This is probably what you want, in most cases;
// if this is not desired, you can use the underlaying functions directly
// (just look at the source of this function for an example of how!).
func DecodeUsingPrototype(b []byte, decFn Decoder, np NodePrototype) (Node, error) {
return DecodeStreamingUsingPrototype(bytes.NewReader(b), decFn, np)
}

// DecodeStreamingUsingPrototype is like DecodeUsingPrototype, but works on an io.Reader for input.
func DecodeStreamingUsingPrototype(r io.Reader, decFn Decoder, np NodePrototype) (Node, error) {
if tnp, ok := np.(schema.TypedPrototype); ok {
np = tnp.Representation()
}
nb := np.NewBuilder()
if err := decFn(nb, r); err != nil {
return nil, err
}
return nb.Build(), nil
}

// Marshal accepts a pointer to a Go value and an IPLD schema type,
// and encodes the representation form of that data (which may be configured with the schema!)
// using the given Encoder function.
//
// Marshal uses the node/bindnode subsystem.
// See the documentation in that package for more details about its workings.
// Please note that this subsystem is relatively experimental at this time.
//
// The schema.Type parameter is optional, and can be nil.
// If given, it controls what kind of schema.Type (and what kind of representation strategy!)
// to use when processing the data.
// If absent, a default schema.Type will be inferred based on the golang type
// (so, a struct in go will be inferred to have a schema with a similar struct, and the default representation strategy (e.g. map), etc).
// Note that not all features of IPLD Schemas can be inferred from golang types alone.
// For example, to use union types, the schema parameter will be required.
// Similarly, to use most kinds of non-default representation strategy, the schema parameter is needed in order to convey that intention.
func Marshal(encFn Encoder, bind interface{}, typ schema.Type) ([]byte, error) {
n := bindnode.Wrap(bind, typ)
return Encode(n.Representation(), encFn)
}

// MarshalStreaming is like Marshal, but emits output to an io.Writer.
func MarshalStreaming(wr io.Writer, encFn Encoder, bind interface{}, typ schema.Type) error {
n := bindnode.Wrap(bind, typ)
return EncodeStreaming(wr, n.Representation(), encFn)
}

// Unmarshal accepts a pointer to a Go value and an IPLD schema type,
// and fills the value with data by decoding into it with the given Decoder function.
//
// Unmarshal uses the node/bindnode subsystem.
// See the documentation in that package for more details about its workings.
// Please note that this subsystem is relatively experimental at this time.
//
// The schema.Type parameter is optional, and can be nil.
// If given, it controls what kind of schema.Type (and what kind of representation strategy!)
// to use when processing the data.
// If absent, a default schema.Type will be inferred based on the golang type
// (so, a struct in go will be inferred to have a schema with a similar struct, and the default representation strategy (e.g. map), etc).
// Note that not all features of IPLD Schemas can be inferred from golang types alone.
// For example, to use union types, the schema parameter will be required.
// Similarly, to use most kinds of non-default representation strategy, the schema parameter is needed in order to convey that intention.
//
// In contrast to some other unmarshal conventions common in golang,
// notice that we also return a Node value.
// This Node points to the same data as the value you handed in as the bind parameter,
// while making it available to read and iterate and handle as a ipld datamodel.Node.
// If you don't need that interface, or intend to re-bind it later, you can discard that value.
//
// The 'bind' parameter may be nil.
// In that case, the type of the nil is still used to infer what kind of value to return,
// and a Node will still be returned based on that type.
// bindnode.Unwrap can be used on that Node and will still return something
// of the same golang type as the typed nil that was given as the 'bind' parameter.
func Unmarshal(b []byte, decFn Decoder, bind interface{}, typ schema.Type) (Node, error) {
return UnmarshalStreaming(bytes.NewReader(b), decFn, bind, typ)
}

// UnmarshalStreaming is like Unmarshal, but works on an io.Reader for input.
func UnmarshalStreaming(r io.Reader, decFn Decoder, bind interface{}, typ schema.Type) (Node, error) {
// Decode is fairly straightforward.
np := bindnode.Prototype(bind, typ)
n, err := DecodeStreamingUsingPrototype(r, decFn, np.Representation())
// ... but our approach above allocated new memory, and we have to copy it back out.
// In the future, the bindnode API could be improved to make this easier.
if !reflect.ValueOf(bind).IsNil() {
reflect.ValueOf(bind).Elem().Set(reflect.ValueOf(bindnode.Unwrap(n)).Elem())
}
// ... and we also have to re-bind a new node to the 'bind' value,
// because probably the user will be surprised if mutating 'bind' doesn't affect the Node later.
n = bindnode.Wrap(bind, typ)
return n, err
}
60 changes: 60 additions & 0 deletions codecHelpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ipld_test

import (
"fmt"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/json"
"github.com/ipld/go-ipld-prime/must"
"github.com/ipld/go-ipld-prime/schema"
)

func Example_Marshal() {
type Foobar struct {
Foo string
Bar string
}
encoded, err := ipld.Marshal(json.Encode, &Foobar{"wow", "whee"}, nil)
fmt.Printf("error: %v\n", err)
fmt.Printf("data: %s\n", string(encoded))

// Output:
// error: <nil>
// data: {
// "Foo": "wow",
// "Bar": "whee"
// }
}

// TODO: Example_Unmarshal, which uses nil and infers a typesystem. However, to match Example_Unmarshal_withSchema, that appears to need more features in bindnode.

func Example_Unmarshal_withSchema() {
typesys := schema.MustTypeSystem(
schema.SpawnStruct("Foobar",
[]schema.StructField{
schema.SpawnStructField("foo", "String", false, false),
schema.SpawnStructField("bar", "String", false, false),
},
schema.SpawnStructRepresentationMap(nil),
),
schema.SpawnString("String"),
)

type Foobar struct {
Foo string
Bar string
}
serial := []byte(`{"foo":"wow","bar":"whee"}`)
foobar := Foobar{}
n, err := ipld.Unmarshal(serial, json.Decode, &foobar, typesys.TypeByName("Foobar"))
fmt.Printf("error: %v\n", err)
fmt.Printf("go struct: %v\n", foobar)
fmt.Printf("node kind and length: %s, %d\n", n.Kind(), n.Length())
fmt.Printf("node lookup 'foo': %q\n", must.String(must.Node(n.LookupByString("foo"))))

// Output:
// error: <nil>
// go struct: {wow whee}
// node kind and length: map, 2
// node lookup 'foo': "wow"
}
19 changes: 19 additions & 0 deletions schema/tmpBuilders.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ import (
// (It would mean the golang types don't tell you whether the values have been checked for global properties or not, but, eh.)
// (It's not really compatible with "Prototype and Type are the same thing for codegen'd stuff", either (or, we need more interfaces, and to *really* lean into them), but maybe that's okay.)

func SpawnTypeSystem(types ...Type) (*TypeSystem, []error) {
ts := TypeSystem{}
ts.Init()
for _, typ := range types {
ts.Accumulate(typ)
}
if errs := ts.ValidateGraph(); errs != nil {
return nil, errs
}
return &ts, nil
}
func MustTypeSystem(types ...Type) *TypeSystem {
if ts, err := SpawnTypeSystem(types...); err != nil {
panic(err)
} else {
return ts
}
}

func SpawnString(name TypeName) *TypeString {
return &TypeString{typeBase{name, nil}}
}
Expand Down

0 comments on commit a47ecf9

Please sign in to comment.