// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // Command basic demonstrates decoding and encoding a TOML document with // interpres. It exercises struct mapping, arrays of tables, Marshaler // customisation, the Decoder's strict mode, and the Encoder's policy // options, covering every feature a regular user would reach for. package main import ( "errors" "fmt" "io" "os" "time" "sourcedock.dev/petrbalvin/interpres/v2" ) // document is a small but realistic configuration: it has scalars, a // sub-table, an array of tables, and a date-time. We pick a 32-bit port so // the demonstration also covers overflow-safe integer conversion. const document = ` title = "interpres demo" launched = 2024-11-04T09:00:00Z debug = false [server] host = "127.0.0.1" port = 9090 [[users]] name = "petr" admin = true [[users]] name = "guest" 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. 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"` Retries time.Duration `toml:"retries,omitzero"` } type Server struct { Host string `toml:"host"` Port int `toml:"port"` } type User struct { Name string `toml:"name"` Admin bool `toml:"admin"` } // Port is a typed alias that controls how its value appears in TOML. The // MarshalTOML hook returns a string, so a Port field is rendered as // "host:port" instead of the raw integer. type Port string func (p Port) MarshalTOML() (any, error) { return string(p), nil } // Endpoint uses a Marshaler to render its address field with a custom shape. type Endpoint struct { Host string `toml:"host"` Port int `toml:"port"` Tag string `toml:"tag"` } func (e Endpoint) MarshalTOML() (any, error) { return map[string]any{ "addr": fmt.Sprintf("%s:%d", e.Host, e.Port), "tag": e.Tag, }, nil } func main() { os.Exit(Run(os.Stdout, os.Stderr)) } // Run decodes the embedded document, prints the decoded values to stdout, // re-encodes with the default Encoder, then re-encodes again with the // group-by-kind layout disabled to show the two layouts side by side. // Returns 0 on success and 1 on any decode or encode error. func Run(stdout, stderr io.Writer) int { var cfg Config if err := interpres.Unmarshal([]byte(document), &cfg); err != nil { fmt.Fprintln(stderr, "decode:", err) return 1 } fmt.Fprintf(stdout, "title: %s\n", cfg.Title) fmt.Fprintf(stdout, "launched: %s\n", cfg.Launched.Format(time.RFC3339)) fmt.Fprintf(stdout, "debug: %t\n", cfg.Debug) fmt.Fprintf(stdout, "server: %s:%d\n", cfg.Server.Host, cfg.Server.Port) for _, u := range cfg.Users { fmt.Fprintf(stdout, "user: %-6s admin=%t\n", u.Name, u.Admin) } // Marshal a config that mixes a Marshaler field (Port) and a regular // scalar so the reader sees the hook fire mid-document. type WithMarshaler struct { Name string `toml:"name"` Srv struct { Host string `toml:"host"` Port Port `toml:"port"` } `toml:"srv"` Ep Endpoint `toml:"ep"` } hooked := WithMarshaler{ Name: "hooked", Srv: struct { Host string `toml:"host"` Port Port `toml:"port"` }{Host: "127.0.0.1", Port: "9000:9001"}, Ep: Endpoint{Host: "10.0.0.1", Port: 8080, Tag: "primary"}, } out, err := interpres.Marshal(hooked) if err != nil { fmt.Fprintln(stderr, "marshal:", err) return 1 } fmt.Fprintf(stdout, "\n--- marshal (group by kind, default) ---\n%s", out) out2, err := interpres.NewEncoder().Layout(interpres.LayoutKindDeclaration).Marshal(cfg) if err != nil { fmt.Fprintln(stderr, "marshal:", err) return 1 } fmt.Fprintf(stdout, "\n--- marshal (LayoutKindDeclaration) ---\n%s", out2) // Demonstrate Unmarshaler-style mutation: re-decode the second output to // prove it round-trips back into the same Go value. var roundTripped Config if err := interpres.Unmarshal(out2, &roundTripped); err != nil { fmt.Fprintln(stderr, "round-trip:", err) return 1 } 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 }