diff --git a/docs/API.md b/docs/API.md index 7deb51e..0be99a8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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 }` diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..facea37 --- /dev/null +++ b/example_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Petr BalvĂ­n (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"} +} diff --git a/examples/basic/main.go b/examples/basic/main.go index 5c457fb..89adfff 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -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 }