6.1 KiB
6.1 KiB
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
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
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; acurrentcursor 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
(
******intworks). Nil maps and slices are materialized. - Struct fields match by
tomltag 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→int8errors); non-empty interface targets error unless assignable; unsupported target kinds (chan, func, complex, fixed arrays) error;uint/uint64sources aboveMaxInt64are 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). omitemptyhonored; nil pointers andtoml:"-"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/±Infreturn 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.ball 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.Parseat 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/nanfloat 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).