170 lines
4.3 KiB
Markdown
170 lines
4.3 KiB
Markdown
# interpres
|
|||
|
|
|
||
|
|
A TOML 1.0 parser and encoder for Go, written with the standard library alone.
|
||
|
|
`interpres` (Latin for *interpreter*) gives zero-dependency programs an
|
||
|
|
`encoding/json`-style API for reading and writing TOML, and passes the entire
|
||
|
|
official [toml-test](https://github.com/toml-lang/toml-test) suite: 185 valid
|
||
|
|
and 371 invalid cases, zero failures.
|
||
|
|
|
||
|
|
## Features
|
||
|
|
|
||
|
|
- **Full TOML 1.0**: bare, quoted and dotted keys; tables and arrays of tables;
|
||
|
|
basic and literal strings including multiline; integers in the four radixes
|
||
|
|
with `_` separators; floats with exponents, `inf` and `nan`; booleans; the
|
||
|
|
four date-time kinds; arrays and inline tables.
|
||
|
|
- **Decoding and encoding**: `Parse` for an untyped tree, `Unmarshal` and
|
||
|
|
`Marshal` for structs and maps, mirroring `encoding/json`.
|
||
|
|
- **Strict decoding**: `NewDecoder().DisallowUnknownFields()` rejects keys that
|
||
|
|
match no destination field, at every struct depth.
|
||
|
|
- **Custom types**: `Marshaler` and `Unmarshaler` let a type control its own
|
||
|
|
TOML representation in both directions.
|
||
|
|
- **Cancellation**: every entry point has a `*Context` sibling that honours a
|
||
|
|
`context.Context`.
|
||
|
|
- **Configurable emission**: `Encoder` options for declaration-order output,
|
||
|
|
omitting empty arrays, and literal multiline strings.
|
||
|
|
|
||
|
|
## Install
|
||
|
|
|
||
|
|
As a library:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
go get sourcedock.dev/petrbalvin/interpres
|
||
|
|
```
|
||
|
|
|
||
|
|
Requires Go 1.27.0 or newer. The module imports only the standard library.
|
||
|
|
|
||
|
|
## Quick start
|
||
|
|
|
||
|
|
```sh
|
||
|
|
git clone https://sourcedock.dev/petrbalvin/interpres.git
|
||
|
|
cd interpres
|
||
|
|
just example
|
||
|
|
```
|
||
|
|
|
||
|
|
`just example` runs the tour in `examples/basic`: it decodes an embedded
|
||
|
|
document into a struct, prints it, and re-encodes it under both `Encoder`
|
||
|
|
layouts.
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
### Decode into a struct
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Config struct {
|
||
|
|
Title string `toml:"title"`
|
||
|
|
Server struct {
|
||
|
|
Host string `toml:"host"`
|
||
|
|
Port int `toml:"port"`
|
||
|
|
} `toml:"server"`
|
||
|
|
}
|
||
|
|
|
||
|
|
var cfg Config
|
||
|
|
err := interpres.Unmarshal(data, &cfg)
|
||
|
|
```
|
||
|
|
|
||
|
|
Fields match by the `toml:"name"` tag, or by the lower-cased field name when no
|
||
|
|
tag is present; `toml:"-"` skips a field. `Parse` returns the untyped
|
||
|
|
`map[string]any` tree instead, and `UnmarshalContext` accepts a context.
|
||
|
|
|
||
|
|
### Encode from a struct
|
||
|
|
|
||
|
|
```go
|
||
|
|
out, err := interpres.Marshal(cfg)
|
||
|
|
```
|
||
|
|
|
||
|
|
writes:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
title = "example"
|
||
|
|
|
||
|
|
[server]
|
||
|
|
host = "127.0.0.1"
|
||
|
|
port = 9090
|
||
|
|
```
|
||
|
|
|
||
|
|
Tables are laid out scalars first, then sub-tables, then arrays of tables,
|
||
|
|
which is the layout that re-parses to the same tree.
|
||
|
|
|
||
|
|
### Strict decoding
|
||
|
|
|
||
|
|
```go
|
||
|
|
err := interpres.NewDecoder().
|
||
|
|
DisallowUnknownFields().
|
||
|
|
Decode(data, &cfg)
|
||
|
|
```
|
||
|
|
|
||
|
|
A key with no matching field becomes an error instead of a silent drop.
|
||
|
|
|
||
|
|
### Custom types
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Port int
|
||
|
|
|
||
|
|
func (p Port) MarshalTOML() (any, error) {
|
||
|
|
return int64(p), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type IP struct{ net.IP }
|
||
|
|
|
||
|
|
func (ip *IP) UnmarshalTOML(data any) error {
|
||
|
|
s, ok := data.(string)
|
||
|
|
if !ok {
|
||
|
|
return fmt.Errorf("ip: not a string")
|
||
|
|
}
|
||
|
|
ip.IP = net.ParseIP(s)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The value `MarshalTOML` returns is encoded in place of the receiver;
|
||
|
|
`UnmarshalTOML` receives the parsed value verbatim.
|
||
|
|
|
||
|
|
### Encoder options
|
||
|
|
|
||
|
|
```go
|
||
|
|
out, err := interpres.NewEncoder().
|
||
|
|
GroupByKind(false). // preserve declaration order
|
||
|
|
OmitEmptyArrays(). // skip empty scalar arrays
|
||
|
|
UseLiteralMultiline(80). // long multi-line strings as literal blocks
|
||
|
|
Marshal(cfg)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Cancellation
|
||
|
|
|
||
|
|
```go
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
out, err := interpres.MarshalContext(ctx, cfg)
|
||
|
|
```
|
||
|
|
|
||
|
|
`ParseContext`, `UnmarshalContext`, `(*Decoder).DecodeContext` and
|
||
|
|
`(*Encoder).MarshalContext` follow the same pattern.
|
||
|
|
|
||
|
|
The full rules for field matching, numeric conversion and emission live in
|
||
|
|
[docs/API.md](docs/API.md).
|
||
|
|
|
||
|
|
## Development
|
||
|
|
|
||
|
|
```sh
|
||
|
|
just build # compile bin/interpres-decode
|
||
|
|
just test # the suite, with the 80 percent coverage floor
|
||
|
|
just toml-test # the official compliance suite, needs toml-test on PATH
|
||
|
|
just fmt # gofmt in place
|
||
|
|
```
|
||
|
|
|
||
|
|
See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for the full workflow, and
|
||
|
|
[CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute.
|
||
|
|
|
||
|
|
## Documentation
|
||
|
|
|
||
|
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||
|
|
- [docs/API.md](docs/API.md): the API reference, decoding and encoding rules
|
||
|
|
- [docs/CLI.md](docs/CLI.md): the interpres-decode toml-test adapter
|
||
|
|
|
||
|
|
## Licence
|
||
|
|
|
||
|
|
MIT. See [LICENSE](LICENSE).
|
||
|
|
|
||
|
|
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|