feat: usage example
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/interpres"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type Config struct {
|
||||||
|
Title string `toml:"title"`
|
||||||
|
Launched time.Time `toml:"launched"`
|
||||||
|
Debug bool `toml:"debug"`
|
||||||
|
Server Server `toml:"server"`
|
||||||
|
Users []User `toml:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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().GroupByKind(false).Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(stderr, "marshal:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stdout, "\n--- marshal (GroupByKind=false) ---\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))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunPrintsConfigAndMarshal(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
if code := Run(&stdout, &stderr); code != 0 {
|
||||||
|
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
got := stdout.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
"title: interpres demo",
|
||||||
|
"launched: 2024-11-04T09:00:00Z",
|
||||||
|
"debug: false",
|
||||||
|
"server: 127.0.0.1:9090",
|
||||||
|
"user: petr",
|
||||||
|
"admin=true",
|
||||||
|
"admin=false",
|
||||||
|
"--- marshal (group by kind, default) ---",
|
||||||
|
`title = "interpres demo"`,
|
||||||
|
"--- marshal (GroupByKind=false) ---",
|
||||||
|
"[server]",
|
||||||
|
"port = 9090",
|
||||||
|
"[[users]]",
|
||||||
|
"--- round-trip ---",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("output missing %q\n----\n%s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunMarshalerFieldRendersAsScalar(t *testing.T) {
|
||||||
|
// The Port Marshaler hook returns a string, so the field becomes a
|
||||||
|
// scalar "9000:9001" inside [srv] rather than an integer.
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
if code := Run(&stdout, &stderr); code != 0 {
|
||||||
|
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
`port = "9000:9001"`,
|
||||||
|
`addr = "10.0.0.1:8080"`,
|
||||||
|
`tag = "primary"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(stdout.String(), want) {
|
||||||
|
t.Errorf("output missing %q\n----\n%s", want, stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user