Files
interpres/docs/ARCHITECTURE.md
T

128 lines
6.4 KiB
Markdown
Raw Normal View History

2026-08-19 18:44:00 +02:00
# 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.1, decoding and encoding, in the
standard library alone; the command wraps the parser and the encoder for the
toml-test compliance harness, against which it stands at 214 valid, 467 invalid
and 214 encoder cases with zero failures; the example demonstrates the API.
2026-08-19 18:44:00 +02:00
```mermaid
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, the option constructors, `Marshaler`, `Unmarshaler`, `SyntaxError`, the local date-time types); everything below it is unexported. |
| `cmd/interpres-decode` | The toml-test adapter, both directions. Reads TOML on stdin, writes tagged JSON on stdout; with `-encode` it reads tagged JSON and writes TOML. Owns no parsing logic and no emission logic. |
2026-08-19 18:44:00 +02:00
| `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, records the nodes a [Document](API.md#documents) is built from, and enforces the structural rules of TOML 1.1 (table redefinitions, dotted keys, arrays of tables, multi-line inline tables). Reports a 1-based line on failure. |
| `document.go` | The parsed-document types: `Document`, `Table` and `Entry`, which carry the key order, whether a table was written inline, and the comments. The values they expose are the parser's own tree, not a copy. |
2026-08-19 18:44:00 +02:00
| `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.
```mermaid
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
2026-09-22 01:09:00 +02:00
value, then emits it; the two phases are why `Layout` can reorder entries
2026-08-19 18:44:00 +02:00
without a second reflection pass, and why cancellation is checked during both.
```mermaid
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 option values are stateless: every `Unmarshal`, `Marshal` and their
variants apply their own options into a per-call unexported worker, so the
entries are safe for concurrent use.
- The parser is allocated per `ParseContext` call; the parser itself caches
nothing between documents.
- The shared state is a set of caches and pools whose entries are immutable
once published, each growing with the number of distinct types rather than
with document size: the struct-schema cache in `decode.go` (a `sync.Map`
keyed on `reflect.Type`, holding the flattened field layout the decoder and
the encoder both consult), the per-type interface flag caches in `decode.go`
and `encode.go` (recording where `Marshaler`, `Unmarshaler` and the text
interfaces can be found, so a walk builds an interface value only where the
assertion can succeed), each fronted by a monomorphic hint holding the type
resolved last, and the encoder's output-buffer pool in `encode.go`
(`sync.Pool`, buffers returned to it only within a 1 MiB retention cap). A
published schema or flag set never mutates, so concurrent callers only race
to build an identical value, the same trade-off `encoding/json`'s field
cache makes.
2026-08-19 18:44:00 +02:00
- The date-time wrappers are values, not pointers, and are immutable in use.
- Nothing in the library starts goroutines; apart from the caches and the pool
above, which never mutate a published entry, there is no shared mutable
state.
2026-08-19 18:44:00 +02:00
## 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.