-
Notifications
You must be signed in to change notification settings - Fork 50
/
codecHelpers.go
170 lines (158 loc) · 8.37 KB
/
codecHelpers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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 throws 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, opts ...bindnode.Option) ([]byte, error) {
n := bindnode.Wrap(bind, typ, opts...)
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, opts ...bindnode.Option) error {
n := bindnode.Wrap(bind, typ, opts...)
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, opts ...bindnode.Option) (Node, error) {
return UnmarshalStreaming(bytes.NewReader(b), decFn, bind, typ, opts...)
}
// UnmarshalStreaming is like Unmarshal, but works on an io.Reader for input.
func UnmarshalStreaming(r io.Reader, decFn Decoder, bind interface{}, typ schema.Type, opts ...bindnode.Option) (Node, error) {
// Decode is fairly straightforward.
np := bindnode.Prototype(bind, typ, opts...)
n, err := DecodeStreamingUsingPrototype(r, decFn, np.Representation())
if err != nil {
return nil, err
}
// ... 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, opts...)
return n, err
}