diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..ca2d430 --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,151 @@ +// 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 ( + "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 +} diff --git a/examples/basic/main_test.go b/examples/basic/main_test.go new file mode 100644 index 0000000..3519cca --- /dev/null +++ b/examples/basic/main_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2026 Petr Balvín (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()) + } + } +}