docs: add godoc examples, extend the basic example and map encoding/json
Test / test (push) Successful in 1m47s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 01:29:42 +02:00
parent f6a96379e6
commit 3406955654
3 changed files with 164 additions and 6 deletions
+23
View File
@@ -797,6 +797,29 @@ sequenceDiagram
Marshal-->>Caller: bytes, error
```
## Coming from encoding/json
The API follows the shapes encoding/json made familiar, with the differences
TOML asks for:
| encoding/json | interpres | Notes |
|---|---|---|
| `json.Unmarshal(data, v)` | `Unmarshal(data, v)` | the same shape; the value mapping is TOML's |
| `json.Marshal(v)` | `Marshal(v)` | the same shape; the output is TOML 1.1 |
| `json.MarshalAppend(buf, v)` | `MarshalAppend(buf, v)` | the same shape |
| `(*json.Decoder).DisallowUnknownFields` | `(*Decoder).DisallowUnknownFields` | the same effect; the one-shot form is `UnmarshalWithOptions` |
| `json.Number`, `(*json.Decoder).UseNumber` | `Number`, `(*Decoder).UseNumber` | the TOML literal carries its radix and separators, so `0x1f` stays `0x1f` |
| `json.MarshalIndent` | none | TOML is the presentation format; the `-json` mode of interpres-decode prints plain JSON |
| tag `json:"name,omitempty"` | tag `toml:"name,omitempty"` | the empty-value rules match encoding/json as of 2.0 |
| tag `json:"name,omitzero"` | tag `toml:"name,omitzero"` | the same, `IsZero()` honoured |
| tag `json:"name,inline"` (v2) | tag `toml:"name,inline"` | forces the inline table form on encode |
| `json.Marshaler` (`MarshalJSON`) | `Marshaler` (`MarshalTOML`) | the TOML method returns a value the encoder renders, not bytes |
| `json.Unmarshaler` (`UnmarshalJSON`) | `Unmarshaler` (`UnmarshalTOML`) | the data arrives decoded, not as bytes |
| `encoding.TextMarshaler`, `TextUnmarshaler` | honoured, the same | a type that renders itself as text becomes a TOML string, both ways |
| `*json.UnmarshalTypeError` | `*DecodeError` | the path is segments with a `String()` renderer, not a dotted string |
| `*json.SyntaxError` | `*SyntaxError` | the TOML error adds the byte `Offset` and the `Column` to the line |
| context support | `*Context` variants of every entry point | encoding/json has none |
## Types
### `type SyntaxError struct{ Line, Offset, Column int; Msg string }`
+103
View File
@@ -0,0 +1,103 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres_test
import (
"fmt"
"log"
"sourcedock.dev/petrbalvin/interpres/v2"
)
func ExampleParse() {
const doc = `
title = "interpres"
[server]
host = "127.0.0.1"
port = 9090
`
d, err := interpres.Parse([]byte(doc))
if err != nil {
log.Fatal(err)
}
for _, key := range d.Root().Keys() { // written order, not sorted
entry, _ := d.Root().Get(key)
fmt.Println(key, "=", entry.Value())
}
// Output:
// title = interpres
// server = map[host:127.0.0.1 port:9090]
}
func ExampleUnmarshal() {
type Config struct {
Host string `toml:"host"`
Port int `toml:"port"`
}
var cfg Config
err := interpres.Unmarshal([]byte("host = \"db\"\nport = 5432\n"), &cfg)
if err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Host, cfg.Port)
// Output: db 5432
}
func ExampleMarshal() {
type Server struct {
Host string `toml:"host"`
Port int `toml:"port"`
}
type Config struct {
Title string `toml:"title"`
Server Server `toml:"server"`
}
out, err := interpres.Marshal(Config{
Title: "demo",
Server: Server{Host: "127.0.0.1", Port: 9090},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", out)
// Output:
// title = "demo"
//
// [server]
// host = "127.0.0.1"
// port = 9090
}
func ExampleDecoder() {
var tree map[string]any
err := interpres.NewDecoder().
DisallowUnknownFields().
UseNumber().
Decode([]byte("rate = 1_000\n"), &tree)
if err != nil {
log.Fatal(err)
}
fmt.Println(tree["rate"], string(tree["rate"].(interpres.Number)))
// Output: 1_000 1_000
}
func ExampleEncoder() {
type Config struct {
Title string `toml:"title"`
Extras map[string]string `toml:"extras,inline"`
}
out, err := interpres.NewEncoder().
Layout(interpres.LayoutKindDeclaration).
LiteralMultiline(80).
InlineTables(40).
Marshal(Config{Title: "demo", Extras: map[string]string{"b": "two", "a": "one"}})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", out)
// Output:
// title = "demo"
// extras = {a = "one", b = "two"}
}
+38 -6
View File
@@ -8,6 +8,7 @@
package main
import (
"errors"
"fmt"
"io"
"os"
@@ -39,13 +40,16 @@ admin = false
// Config mirrors the document above. The Server field is a named struct so
// the reader sees explicit subtable boundaries; Users is a slice of named
// structs so the array-of-tables path is exercised.
// structs so the array-of-tables path is exercised. Retries carries the
// `omitzero` tag option: a zero value of the field's type drops from the
// output, and a `time.Duration` zero is zero nanoseconds.
type Config struct {
Title string `toml:"title"`
Launched time.Time `toml:"launched"`
Debug bool `toml:"debug"`
Server Server `toml:"server"`
Users []User `toml:"users"`
Title string `toml:"title"`
Launched time.Time `toml:"launched"`
Debug bool `toml:"debug"`
Server Server `toml:"server"`
Users []User `toml:"users"`
Retries time.Duration `toml:"retries,omitzero"`
}
type Server struct {
@@ -147,5 +151,33 @@ func Run(stdout, stderr io.Writer) int {
fmt.Fprintf(stdout, "\n--- round-trip --- ok (title=%q, users=%d)\n",
roundTripped.Title, len(roundTripped.Users))
// Typed errors: a decode failure names the key path it failed at, and
// errors.AsType reaches the DecodeError to read the path and the cause
// separately, without parsing the message text.
bad := []byte("[[users]]\nname = \"x\"\nadmin = \"not-a-bool\"\n")
var badCfg Config
err = interpres.Unmarshal(bad, &badCfg)
if err == nil {
fmt.Fprintln(stderr, "expected a decode error")
return 1
}
if de, ok := errors.AsType[*interpres.DecodeError](err); ok {
fmt.Fprintf(stdout, "\n--- typed error --- path %s: %v\n", de.Path.String(), de.Err)
} else {
fmt.Fprintln(stderr, "expected a DecodeError")
return 1
}
// omitzero: the retries field carries the tag option and a zero duration,
// so the re-encoded config above simply has no retries line. Give it a
// value and the line appears.
cfg.Retries = 30 * time.Second
out3, err := interpres.Marshal(cfg)
if err != nil {
fmt.Fprintln(stderr, "marshal:", err)
return 1
}
fmt.Fprintf(stdout, "\n--- omitzero ---\n%s", out3)
return 0
}