Files
interpres/examples/basic/main.go
T

184 lines
5.4 KiB
Go
Raw Normal View History

2026-08-19 13:05:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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"
2026-08-19 13:05:00 +02:00
"fmt"
"io"
"os"
"time"
2026-09-19 00:14:39 +02:00
"sourcedock.dev/petrbalvin/interpres/v2"
2026-08-19 13:05:00 +02:00
)
// 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.
2026-08-19 13:05:00 +02:00
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"`
2026-08-19 13:05:00 +02:00
}
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)
2026-09-22 01:09:00 +02:00
out2, err := interpres.NewEncoder().Layout(interpres.LayoutKindDeclaration).Marshal(cfg)
2026-08-19 13:05:00 +02:00
if err != nil {
fmt.Fprintln(stderr, "marshal:", err)
return 1
}
2026-09-22 01:09:00 +02:00
fmt.Fprintf(stdout, "\n--- marshal (LayoutKindDeclaration) ---\n%s", out2)
2026-08-19 13:05:00 +02:00
// 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)
2026-08-19 13:05:00 +02:00
return 0
}