# toml Zero-dependency TOML encoder/decoder for Go. Implements a practical subset of TOML v1.0 with documented deviations. Standard library only. ## Install go get github.com/lixenwraith/toml ## Usage ### Unmarshal ```go input := []byte(` initial = "Idle" [states.Idle] parent = "Root" transitions = [ { trigger = "Start", target = "Active" } ] `) type Transition struct { Trigger string `toml:"trigger"` Target string `toml:"target"` Guard string `toml:"guard,omitempty"` } type State struct { Parent string `toml:"parent"` Transitions []Transition `toml:"transitions"` } type Config struct { Initial string `toml:"initial"` States map[string]*State `toml:"states"` } var cfg Config err := toml.Unmarshal(input, &cfg) ``` ### Marshal ```go out, err := toml.Marshal(cfg) // deterministic: keys sorted alphabetically ``` ### Decode `Decode(data any, v any)` maps an already-parsed `map[string]any` onto a target. `Unmarshal` = `Parse` + `Decode`. ## Architecture Unmarshal: []byte → Lexer → tokens → Parser → map[string]any → Decode (reflection) → target Marshal: value → encoder (reflection, two-pass) → []byte ### Lexer (`lexer.go`) - Single-pass, UTF-8 aware byte scanner with rune lookahead (`peekAt`). - Emits: ident, string, integer, float, bool, punctuation, newline, comment. - Number/key disambiguation happens at the token level: `1.5` → Float, `a.b` → Ident Dot Ident, `1.2.3` → Error (multi-dot). Hex/octal/binary prefixes and exponent forms are validated during scanning. - Basic strings only. Escapes: `\"` `\\` `\n` `\t` `\r`. Unknown escape sequences are preserved verbatim (spec requires an error — see Deviations). ### Parser (`parser.go`) - Recursive descent with a two-token window (`curToken`/`peekToken`). Comments are skipped during token advance. - Output model: tables → `map[string]any`, arrays → `[]any`, arrays of tables → `[]map[string]any`. - Table headers (`[a.b]`) always resolve from the root; a `current` cursor tracks the active table scope for subsequent key/value pairs. - Dotted keys create intermediate maps. Traversal through an existing `[[array]]` descends into its last element (TOML semantics). - Conflict detection at assignment: duplicate keys, scalar-vs-table redefinition, table-vs-array-of-tables shadowing. Numeric keys are rejected in all positions (bare, quoted, dotted segments). - Inline tables are immutable (later dotted keys or `[headers]` targeting them error); value nesting (arrays/inline tables) capped at 1000 levels. ### Decoder (`decode.go`) - Kind-switch reflection. Pointers are auto-allocated at any depth (`******int` works). Nil maps and slices are materialized. - Struct fields match by `toml` tag first, then exact (case-sensitive) field name. `toml:"-"` skips. Unexported fields are skipped. - Numeric coercion: any parser-produced int/uint/float64 converts to the target numeric kind. Unknown keys in input are ignored. - Numeric decode errors on target overflow (`300` → `int8` errors); non-empty interface targets error unless assignable; unsupported target kinds (chan, func, complex, fixed arrays) error; `uint`/`uint64` sources above `MaxInt64` are rejected. ### Encoder (`encode.go`) - Two-pass emission per table: scalars and inline arrays first, then `[tables]` and `[[arrays of tables]]`. Guarantees keys are defined before sub-tables, i.e. output is always valid TOML. - Keys sorted alphabetically → deterministic output for diffing/VCS. - Bare-key validation mirrors the lexer's rules: keys that would lex as numbers or booleans (`true`, `123a`, `-1x`) are quoted, so output always re-parses (round-trip safe). - `omitempty` honored; nil pointers and `toml:"-"` fields skipped; root must be a struct or map. - Floats use shortest `'g'` form at the source bit width (large magnitudes emit exponent notation); `NaN`/`±Inf` return an error; arrays mixing tables and scalars return an error (parser accepts them — encode-side limitation); struct keys sort by resolved tag name. ## Type Mapping | TOML | Parser output (`map[string]any`) | Decode targets | |-----------------|----------------------------------|-----------------------------------| | string | `string` | `string`, `any` | | integer | `int` | any int/uint kind, float kinds | | float | `float64` | `float32`, `float64` | | boolean | `bool` | `bool` | | array | `[]any` | `[]T`, `any` | | table | `map[string]any` | struct, `map[string]T`, `any` | | array of tables | `[]map[string]any` | `[]T`, `[]*T` | ## Deviations from TOML v1.0 Intentional restrictions: - Numeric keys are forbidden everywhere: `123 = 1`, `[456]`, `"789"`, `a.1.b` all error. Stricter than spec. Extensions (accepted, though spec forbids): - Newlines inside inline tables (multi-line inline tables parse). - Trailing commas in inline tables. Relaxations: - Unknown string escapes are preserved instead of erroring. - Explicit table redefinition (`[a]` … `[a]`) is not rejected; the second header reopens the table. Value/table conflicts are still caught. ## Limitations Not supported: - Date/time types (all four TOML forms). Workaround: store RFC 3339 strings and convert with `time.Parse` at the call site; strings round-trip cleanly. - Literal strings `'...'` and multi-line strings `"""..."""`, `'''...'''`. - Underscore digit separators (`1_000`). - Arrays mixing tables and scalar values are not encodable. - `inf` / `nan` float literals. - Custom marshaling interfaces (`Marshaler`/`Unmarshaler`, `encoding.TextMarshaler`). - Embedded struct field promotion in decode. Platform note: TOML integers parse to Go `int`; 64-bit platforms assumed (amd64/arm64) and enforced at compile time (32-bit build fails). Overflow of int64 during parse errors; 32-bit truncation of `int(int64)` is not guarded. ## License BSD-3-Clause (see LICENSE).