5.7 KiB
Architecture
How interpres is put together. Every file, package and arrow below exists in the source tree; nothing is aspirational.
Overview
interpres is one public library package, one command, and one example. The library implements the whole of TOML 1.0 and 1.1, decoding and encoding, in the standard library alone; the command wraps the parser for the toml-test compliance harness, against which it stands at 214 valid and 467 invalid cases with zero failures; the example demonstrates the API.
flowchart TD
CLI[cmd/interpres-decode<br/>toml-test adapter] --> API
EX[examples/basic<br/>usage demo] --> API
subgraph Lib [package interpres]
API[interpres.go<br/>public API and types]
API --> P[parser.go<br/>recursive-descent parser]
API --> DEC[decode.go<br/>tree onto Go values]
API --> ENC[encode.go<br/>Go values to TOML]
P --> N[number.go<br/>numeric tokens]
P --> DT[datetime.go<br/>date-time atoms and types]
end
The public API in interpres.go is a thin facade: every entry point funnels
into ParseContext for parsing and into the unexported decoder and encoder
for the reflection work. parser.go owns the grammar; it leans on
number.go and datetime.go for the two token families that need their own
strict validation.
Packages
| Path | Responsibility |
|---|---|
. (package interpres) |
The whole library. interpres.go declares the exported surface (Parse, Unmarshal, Marshal, the *Context variants, Decoder, Encoder, Marshaler, Unmarshaler, SyntaxError, the local date-time types); everything below it is unexported. |
cmd/interpres-decode |
The toml-test adapter. Reads TOML on stdin, writes tagged JSON on stdout. Owns no parsing logic. |
examples/basic |
A runnable tour of the API. Documentation in executable form, not part of the library. |
Inside the library package, one file owns one concern:
| File | Responsibility |
|---|---|
parser.go |
The recursive-descent parser. Produces the map[string]any tree and enforces the structural rules of TOML 1.0 and 1.1 (table redefinitions, dotted keys, arrays of tables, multi-line inline tables). Reports a 1-based line on failure. |
number.go |
Strict numeric tokens: integers in the four radixes with _ separators, and floats including inf and nan. Rejects leading zeros, misplaced underscores and malformed fractions. |
datetime.go |
The three local date-time wrapper types and parseDateTime, which classifies a token into the four date-time kinds under the strict TOML grammar. |
decode.go |
Maps the parsed tree onto Go values by reflection: struct fields, maps, slices, scalar conversion with overflow checks, Unmarshaler dispatch. |
encode.go |
The reverse walk: builds an intermediate tomlDoc per table (which is what preserves declaration order and enables the group-by-kind partition) and then emits it as TOML. |
The boundary that matters: parser.go produces only untyped trees
(map[string]any, []any, []map[string]any, scalars); decode.go and
encode.go are the only files that touch reflect; the command never touches
either, it consumes Parse alone.
Data flow
Decoding is parse, then one reflection walk. SyntaxError values are produced
inside parser.go and returned as-is; conversion errors are produced inside
decode.go and wrapped with the key path as they unwind.
sequenceDiagram
participant Caller
participant API as interpres.go
participant P as parser.go
participant D as decode.go
Caller->>API: Unmarshal(data, v)
API->>P: ParseContext(ctx, data)
P->>P: number and datetime atoms
P-->>API: map tree or *SyntaxError
API->>D: decode(tree, reflect value)
D-->>API: nil or wrapped field error
API-->>Caller: error
Encoding walks the other way. encode.go first builds a tomlDoc from the
value, then emits it; the two phases are why GroupByKind can reorder entries
without a second reflection pass, and why cancellation is checked during both.
sequenceDiagram
participant Caller
participant API as interpres.go
participant B as encode.go build
participant E as encode.go emit
Caller->>API: Marshal(v)
API->>B: build tomlDoc from struct or map
B-->>API: tomlDoc or error
API->>E: emitDoc(doc)
E-->>API: bytes or error
API-->>Caller: bytes, error
State and lifetime
- The exported
DecoderandEncoderhold configuration only. EveryDecode,DecodeContext,MarshalandMarshalContextcall allocates its own unexported worker, so a configured type is safe for concurrent use; the setter methods are not, and must finish before the value is shared. - The parser is allocated per
ParseContextcall; the parser itself caches nothing between documents. - The one piece of shared state is the struct-schema cache in
decode.go: async.Mapkeyed byreflect.Type, holding the flattened field layout the decoder and the encoder both consult. A schema is immutable once published, so concurrent callers only race to build an identical value, the same trade-offencoding/json's field cache makes. The cache grows with the number of distinct struct types, never with document size. - The date-time wrappers are values, not pointers, and are immutable in use.
- Nothing in the library starts goroutines; apart from the schema cache above, which never mutates a published entry, there is no shared mutable state.
Dependencies
None. go.mod declares the module and the Go version and carries no requires;
the library imports the standard library only, which is the point of the
project. The toml-test binary is a development and CI tool, never a module
dependency.