Skip to content

Commit b9ce851

Browse files
committed
Fresh take on codec APIs, and some tokenization utilities.
The tokenization system may look familiar to refmt's tokens -- and indeed it surely is inspired by and in the same pattern -- but it hews a fair bit closer to the IPLD Data Model definitions of kinds, and it also includes links as a token kind. Presense of link as a token kind means if we build codecs around these, the handling of links will be better and most consistently abstracted (the current dagjson and dagcbor implementations are instructive for what an odd mess it is when you have most of the tokenization happen before you get to the level that figures out links; I think we can improve on that code greatly by moving the barriers around a bit). I made both all-at-once and pumpable versions of both the token producers and the token consumers. Each are useful in different scenarios. The pumpable versions are probably generally a bit slower, but they're also more composable. (The all-at-once versions can't be glued to each other; only to pumpable versions.) Some new and much reduced contracts for codecs are added, but not yet implemented by anything in this comment. The comments on them are lengthy and detail the ways I'm thinking that codecs should be (re)implemented in the future to maximize usability and performance and also allow some configurability. (The current interfaces "work", but irritate me a great deal every time I use them; to be honest, I just plain guessed badly at what the API here should be the first time I did it. Configurability should be both easy to *not* engage in, but also easier if you do (and in pariticular, not require reaching to *another* library's packages to do it!).) More work will be required to bring this to fruition. It may be particularly interesting to notice that the tokenization systems also allow complex keys -- maps and lists can show up as the keys to maps! This is something not allowed by the data model (and for dare I say obvious reasons)... but it's something that's possible at the schema layer (e.g. structs with representation strategies that make them representable as strings can be used as map keys), so, these functions support it.
1 parent e255109 commit b9ce851

File tree

7 files changed

+850
-0
lines changed

7 files changed

+850
-0
lines changed

codec/api.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package codec
2+
3+
import (
4+
"io"
5+
6+
"github.com/ipld/go-ipld-prime"
7+
)
8+
9+
// TODO: i still can't decide between marshaller vs encoder terminology.
10+
// i liked the defn i had in refmt: encoder handles tokens-to-bytes; marshaller handles trees to tokens.
11+
// but that distinction doesn't exist here.
12+
// if it did, we'd need a token type.
13+
// and in that case the encoder parts would be an internal codec code reuse choice, not necessary to expose.
14+
// if this was the case, it would suggest these functions should be called marshaller.
15+
// an alternate definition is: marshallers are things that twist a structure to a tokenizable form;
16+
// but going from tree (already trivially tokenizable) to serial is still considered an encoder.
17+
// i could also see this definition holding water, and it appears to be what i'm rolling with at the moment.
18+
//
19+
// maybe we really should make a TokenWalker thing. Put it in codectools.
20+
// i still really don't know how we'd describe links in that, though. it's really hard to claim links are a token.
21+
// maybe we can cram links into some sort of "extra" field in the token union.
22+
23+
// Encoder is the essential definition of a function that takes IPLD Data Model data in memory and serializes it.
24+
// IPLD Codecs are written by implementing this function interface (as well as (typically) a matched Decoder).
25+
//
26+
// Encoder functions can be composed into an ipld.LinkSystem to provide
27+
// a "one stop shop" API for handling content addressable storage.
28+
// Encoder functions can also be used directly if you want to handle serial data streams.
29+
//
30+
// Most codec packages will have a ReusableEncoder type
31+
// (which contains any working memory needed by the encoder implementation,
32+
// as well as any configuration options),
33+
// and that type will have an Encode function matching this interface.
34+
//
35+
// By convention, codec packages that have a multicodec contract will also have
36+
// a package-scope exported function called Encode which also matches this interface,
37+
// and is the equivalent of creating a zero-value ReusableEncoder (aka, default config)
38+
// and using its Encode method.
39+
// This package-scope function will typically also internally use a sync.Pool
40+
// to keep some ReusableEncoder values on hand to avoid unnecesary allocations.
41+
//
42+
// Note that a ReusableEncoder type that supports configuration options
43+
// does not functionally expose those options when invoked by the multicodec system --
44+
// multicodec indicators do not provide room for extended configuration info.
45+
// Codecs that expose configuration options are doing so for library users to enjoy;
46+
// it does not mean those non-default configurations will necessarly be available
47+
// in all scenarios that use codecs indirectly.
48+
// There is also no standard interface for such configurations: by nature,
49+
// if they exist at all, they vary per codec.
50+
type Encoder func(data ipld.Node, output io.Writer) error
51+
52+
// Decoder is the essential definiton of a function that consumes serial data and unfurls it into IPLD Data Model-compatible in-memory representations.
53+
// IPLD Codecs are written by implementing this function interface (as well as (typically) a matched Encoder).
54+
//
55+
// Decoder is the dual of Encoder.
56+
// Most of the documentation for the Encoder function interface
57+
// also applies wholesale to the Decoder interface.
58+
type Decoder func(into ipld.NodeAssembler, input io.Reader) error
59+
60+
type ErrBudgetExhausted struct{}
61+
62+
func (e ErrBudgetExhausted) Error() string {
63+
return "decoder resource budget exhausted (message too long or too complex)"
64+
}

codec/codectools/token.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package codectools
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ipld/go-ipld-prime"
7+
)
8+
9+
type Token struct {
10+
Kind TokenKind
11+
12+
Length int // Present for MapOpen or ListOpen. May be -1 for "unknown" (e.g. a json tokenizer will yield this).
13+
Bool bool // Value. Union: only has meaning if Kind is TokenKind_Bool.
14+
Int int64 // Value. Union: only has meaning if Kind is TokenKind_Int.
15+
Float float64 // Value. Union: only has meaning if Kind is TokenKind_Float.
16+
Str string // Value. Union: only has meaning if Kind is TokenKind_String. ('Str' rather than 'String' to avoid collision with method.)
17+
Bytes []byte // Value. Union: only has meaning if Kind is TokenKind_Bytes.
18+
Link ipld.Link // Value. Union: only has meaning if Kind is TokenKind_Link.
19+
20+
Node ipld.Node // Direct pointer to the original data, if this token is used to communicate data during a walk of existing in-memory data. Absent when token is being used during deserialization.
21+
22+
// TODO: position info? We seem to want this basically everywhere the token goes, so it might as well just live here.
23+
// Putting this position info into the token would require writing those fields many times, though;
24+
// hopefully we can also use them as the primary accounting position then, or else this might be problematic for speed.
25+
}
26+
27+
func (tk Token) String() string {
28+
switch tk.Kind {
29+
case TokenKind_MapOpen:
30+
return fmt.Sprintf("<%c:%d>", tk.Kind, tk.Length)
31+
case TokenKind_MapClose:
32+
return fmt.Sprintf("<%c>", tk.Kind)
33+
case TokenKind_ListOpen:
34+
return fmt.Sprintf("<%c:%d>", tk.Kind, tk.Length)
35+
case TokenKind_ListClose:
36+
return fmt.Sprintf("<%c>", tk.Kind)
37+
case TokenKind_Null:
38+
return fmt.Sprintf("<%c>", tk.Kind)
39+
case TokenKind_Bool:
40+
return fmt.Sprintf("<%c:%v>", tk.Kind, tk.Bool)
41+
case TokenKind_Int:
42+
return fmt.Sprintf("<%c:%v>", tk.Kind, tk.Int)
43+
case TokenKind_Float:
44+
return fmt.Sprintf("<%c:%v>", tk.Kind, tk.Float)
45+
case TokenKind_String:
46+
return fmt.Sprintf("<%c:%q>", tk.Kind, tk.Str)
47+
case TokenKind_Bytes:
48+
return fmt.Sprintf("<%c:%x>", tk.Kind, tk.Bytes)
49+
case TokenKind_Link:
50+
return fmt.Sprintf("<%c:%v>", tk.Kind, tk.Link)
51+
default:
52+
return "<INVALID>"
53+
}
54+
}
55+
56+
type TokenKind uint8
57+
58+
const (
59+
TokenKind_MapOpen = '{'
60+
TokenKind_MapClose = '}'
61+
TokenKind_ListOpen = '['
62+
TokenKind_ListClose = ']'
63+
TokenKind_Null = '0'
64+
TokenKind_Bool = 'b'
65+
TokenKind_Int = 'i'
66+
TokenKind_Float = 'f'
67+
TokenKind_String = 's'
68+
TokenKind_Bytes = 'x'
69+
TokenKind_Link = '/'
70+
)
71+
72+
type ErrMalformedTokenSequence struct {
73+
Detail string
74+
}
75+
76+
func (e ErrMalformedTokenSequence) Error() string {
77+
return "malformed token sequence: " + e.Detail
78+
}

0 commit comments

Comments
 (0)