fix(cmd): long-form flags, honest counts and safer inference

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-22 21:15:07 +02:00
parent b7f39435e1
commit 4900367970
8 changed files with 791 additions and 224 deletions
+99 -41
View File
@@ -6,9 +6,11 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strconv"
"strings" "strings"
"time" "time"
"unicode" "unicode"
"unicode/utf8"
"sourcedock.dev/petrbalvin/interpres/v2" "sourcedock.dev/petrbalvin/interpres/v2"
) )
@@ -17,63 +19,115 @@ import (
// like the document: one field per key in written order, nested tables as // like the document: one field per key in written order, nested tables as
// nested struct types, an array of tables as a slice, and the field names // nested struct types, an array of tables as a slice, and the field names
// invented from the keys. It is the onboarding aid: the printed type compiles // invented from the keys. It is the onboarding aid: the printed type compiles
// and decodes the document it came from. // and decodes the document it came from. The definition is built whole and
// written with a single call, so a failing standard output surfaces as one
// error instead of being dropped mid-print.
func inferStruct(data []byte, stdout io.Writer) error { func inferStruct(data []byte, stdout io.Writer) error {
doc, err := interpres.Parse(data) doc, err := interpres.Parse(data)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(stdout, "// Generated by interpres-decode -struct; decode with") body := &strings.Builder{}
fmt.Fprintln(stdout, "// sourcedock.dev/petrbalvin/interpres/v2.") fmt.Fprintln(body, "// Generated by interpres-decode --struct; decode with")
fmt.Fprintln(stdout, "type inferred struct {") fmt.Fprintln(body, "// sourcedock.dev/petrbalvin/interpres/v2.")
if err := writeInferredFields(stdout, doc.Root(), map[string]bool{}); err != nil { fmt.Fprintln(body, "type inferred struct {")
writeInferredFields(body, tableFields(doc.Root()), map[string]bool{})
fmt.Fprintln(body, "}")
_, err = io.WriteString(stdout, body.String())
return err return err
}
fmt.Fprintln(stdout, "}")
return nil
} }
// writeInferredFields writes one field per entry of the table. invented // inferredField is one document key with the entry it is inferred from.
// tracks the field names already used at one level, so two keys that clean type inferredField struct {
// to the same name do not collide. key string
func writeInferredFields(w io.Writer, t *interpres.Table, invented map[string]bool) error { entry *interpres.Entry
}
// tableFields lists a table's entries in written order.
func tableFields(t *interpres.Table) []inferredField {
out := make([]inferredField, 0, len(t.Keys()))
for _, key := range t.Keys() { for _, key := range t.Keys() {
entry, _ := t.Get(key) entry, _ := t.Get(key)
name := goFieldName(key, invented) out = append(out, inferredField{key: key, entry: entry})
}
return out
}
// mergedTableFields merges the key sets of an array's elements in first-seen
// order. An array's type has to cover every element, and a key may appear
// only in a later one, so the first element alone does not decide the shape;
// each key is inferred from the first element that carries it.
func mergedTableFields(tables []*interpres.Table) []inferredField {
var out []inferredField
seen := map[string]bool{}
for _, t := range tables {
for _, f := range tableFields(t) {
if seen[f.key] {
continue
}
seen[f.key] = true
out = append(out, f)
}
}
return out
}
// writeInferredFields writes one field per entry, in the order given.
// invented tracks the field names already used at one level, so two keys
// that clean to the same name do not collide.
func writeInferredFields(w *strings.Builder, fields []inferredField, invented map[string]bool) {
for _, f := range fields {
writeInferredField(w, f, invented)
}
}
// writeInferredField writes one field for one entry: an array of tables as a
// slice of structs, a child table as a nested struct, and everything else as
// the scalar or slice the decoded value names.
func writeInferredField(w *strings.Builder, f inferredField, invented map[string]bool) {
name := goFieldName(f.key, invented)
// An array of tables carries a node per element; the nodes of a value // An array of tables carries a node per element; the nodes of a value
// array are nil wherever an element is not a table, so the nils give // array are nil wherever an element is not a table. Every node present
// it away. // is what tells the two apart: [1, {x=1}] stays a value array even
var tables []*interpres.Table // though one of its elements is a table.
for _, el := range entry.Elements() { elements := f.entry.Elements()
if el != nil { allTables := len(elements) > 0
tables = append(tables, el) for _, el := range elements {
if el == nil {
allTables = false
break
} }
} }
if len(tables) > 0 { if allTables {
// The type comes from the first element.
fmt.Fprintf(w, "\t%s []struct {\n", name) fmt.Fprintf(w, "\t%s []struct {\n", name)
if err := writeInferredFields(w, tables[0], map[string]bool{}); err != nil { writeInferredFields(w, mergedTableFields(elements), map[string]bool{})
return err fmt.Fprintf(w, "\t} %s\n", structTag(f.key))
return
} }
fmt.Fprintf(w, "\t} `toml:%q`\n", key) if child := f.entry.Table(); child != nil {
continue
}
val := entry.Value()
if child := entry.Table(); child != nil {
fmt.Fprintf(w, "\t%s struct {\n", name) fmt.Fprintf(w, "\t%s struct {\n", name)
if err := writeInferredFields(w, child, map[string]bool{}); err != nil { writeInferredFields(w, tableFields(child), map[string]bool{})
return err fmt.Fprintf(w, "\t} %s\n", structTag(f.key))
} return
fmt.Fprintf(w, "\t} `toml:%q`\n", key)
continue
} }
val := f.entry.Value()
if items, ok := val.([]any); ok { if items, ok := val.([]any); ok {
fmt.Fprintf(w, "\t%s []%s `toml:%q`\n", name, inferScalarType(items), key) fmt.Fprintf(w, "\t%s []%s %s\n", name, inferScalarType(items), structTag(f.key))
continue return
} }
fmt.Fprintf(w, "\t%s %s `toml:%q`\n", name, goTypeOf(val), key) fmt.Fprintf(w, "\t%s %s %s\n", name, goTypeOf(val), structTag(f.key))
}
// structTag renders the toml tag of one key as a Go string literal. The raw
// backtick literal is the conventional shape, but a key carrying a backtick
// would end that literal early and the printed definition would not compile,
// so such tags are rendered with strconv.Quote instead.
func structTag(key string) string {
tag := `toml:"` + key + `"`
if !strings.ContainsAny(tag, "`\r") {
return "`" + tag + "`"
} }
return nil return strconv.Quote(tag)
} }
// goTypeOf names the Go type the decoded value asks for. // goTypeOf names the Go type the decoded value asks for.
@@ -106,8 +160,10 @@ func goTypeOf(val any) string {
} }
// goFieldName cleans a document key into an exported Go identifier: the // goFieldName cleans a document key into an exported Go identifier: the
// words the punctuation splits become capitalised runs, a leading digit gains // words the punctuation splits become capitalised runs, a leading digit
// an underscore, and a collision with an earlier name gains a counter. // gains a Field prefix, because an underscore would leave the field
// unexported and the decoder would skip it, and a collision with an earlier
// name gains a counter.
func goFieldName(key string, invented map[string]bool) string { func goFieldName(key string, invented map[string]bool) string {
var b strings.Builder var b strings.Builder
nextUpper := true nextUpper := true
@@ -127,8 +183,10 @@ func goFieldName(key string, invented map[string]bool) string {
if name == "" { if name == "" {
name = "Field" name = "Field"
} }
if unicode.IsDigit(rune(name[0])) { // The first rune is decoded rather than taken as a byte, because a key
name = "_" + name // may open with a digit beyond ASCII.
if first, _ := utf8.DecodeRuneInString(name); unicode.IsDigit(first) {
name = "Field" + name
} }
for invented[name] { for invented[name] {
name += "2" name += "2"
+73 -31
View File
@@ -4,16 +4,16 @@
// Command interpres-decode is the toml-test harness adapter and a TOML // Command interpres-decode is the toml-test harness adapter and a TOML
// validator. Without flags it reads a TOML document from standard input and // validator. Without flags it reads a TOML document from standard input and
// writes the toml-test "tagged JSON" representation to standard output. With // writes the toml-test "tagged JSON" representation to standard output. With
// -encode it is the reverse: it reads tagged JSON and writes the TOML document // --encode it is the reverse: it reads tagged JSON and writes the TOML document
// it describes. With -validate it checks the named documents, or standard // it describes. With --validate it checks the named documents, or standard
// input when none are named, and exits non-zero on the first invalid one: // input when none are named, and exits non-zero when one is invalid:
// //
// interpres-decode -validate config.toml // interpres-decode --validate config.toml
// interpres-decode -encode < case.json // interpres-decode --encode < case.json
// //
// Run the official suite in both directions against the adapter with: // Run the official suite in both directions against the adapter with:
// //
// toml-test test -decoder=./interpres-decode -encoder='./interpres-decode -encode' // toml-test test -decoder=./interpres-decode -encoder='./interpres-decode --encode'
package main package main
import ( import (
@@ -39,11 +39,16 @@ func main() {
} }
// Run runs the command line and returns the process exit code: 0 success, // Run runs the command line and returns the process exit code: 0 success,
// 1 an invalid document, 2 a usage, reading, encoding, or // 1 an invalid document, 2 a usage, reading, writing, encoding, or
// unsupported-value error. // unsupported-value error.
func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("interpres-decode", flag.ContinueOnError) fs := flag.NewFlagSet("interpres-decode", flag.ContinueOnError)
fs.SetOutput(stderr) // The flag package's own diagnostics and default usage render flags
// with a single dash, while the command spells every flag in its
// two-dash long form, the form the manpage documents. Its output is
// therefore discarded and the usage below is the only one printed.
fs.SetOutput(io.Discard)
fs.Usage = func() {}
version := fs.Bool("version", false, "print the version and exit") version := fs.Bool("version", false, "print the version and exit")
validate := fs.Bool("validate", false, "validate the documents instead of emitting tagged JSON") validate := fs.Bool("validate", false, "validate the documents instead of emitting tagged JSON")
encode := fs.Bool("encode", false, "read tagged JSON from stdin and write TOML instead") encode := fs.Bool("encode", false, "read tagged JSON from stdin and write TOML instead")
@@ -52,12 +57,18 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
schemaType := fs.String("schema", "", "write a TOML template for the named struct type; the source file follows as the first argument") schemaType := fs.String("schema", "", "write a TOML template for the named struct type; the source file follows as the first argument")
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) { if errors.Is(err, flag.ErrHelp) {
usage(stdout)
return 0 return 0
} }
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
usage(stderr)
return 2 return 2
} }
if *version { if *version {
fmt.Fprintf(stdout, "interpres-decode %s\n", versionString()) if _, err := fmt.Fprintf(stdout, "interpres-decode %s\n", versionString()); err != nil {
fmt.Fprintln(stderr, "interpres-decode: write stdout:", err)
return 2
}
return 0 return 0
} }
modes := 0 modes := 0
@@ -70,13 +81,19 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
modes++ modes++
} }
if modes > 1 { if modes > 1 {
fmt.Fprintln(stderr, "interpres-decode: -validate, -encode, -struct and -schema cannot be combined") fmt.Fprintln(stderr, "interpres-decode: --validate, --encode, --struct and --schema cannot be combined")
return 2
}
// --json shapes the decoding output only, so it is rejected with every
// mode uniformly instead of being silently ignored by some of them.
if *plainJSON && modes > 0 {
fmt.Fprintln(stderr, "interpres-decode: --json shapes the decoder output and cannot be combined with --encode, --struct, --validate or --schema")
return 2 return 2
} }
if *schemaType != "" { if *schemaType != "" {
rest := fs.Args() rest := fs.Args()
if len(rest) != 1 { if len(rest) != 1 {
fmt.Fprintln(stderr, "interpres-decode: -schema needs the type name and exactly one Go source file") fmt.Fprintln(stderr, "interpres-decode: --schema needs the type name and exactly one Go source file")
return 2 return 2
} }
if err := runSchema(*schemaType, rest[0], stdout); err != nil { if err := runSchema(*schemaType, rest[0], stdout); err != nil {
@@ -88,12 +105,8 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if *validate { if *validate {
return validatePaths(fs.Args(), stdin, stderr) return validatePaths(fs.Args(), stdin, stderr)
} }
if *encode && *plainJSON {
fmt.Fprintln(stderr, "interpres-decode: -json shapes the decoder output and cannot be combined with -encode")
return 2
}
if fs.NArg() > 0 { if fs.NArg() > 0 {
fmt.Fprintln(stderr, "interpres-decode: the adapter mode takes no arguments; name files with -validate") fmt.Fprintln(stderr, "interpres-decode: the adapter mode takes no arguments; name files with --validate")
return 2 return 2
} }
if *encode { if *encode {
@@ -101,19 +114,25 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
} }
data, err := io.ReadAll(stdin) data, err := io.ReadAll(stdin)
if err != nil { if err != nil {
fmt.Fprintln(stderr, "read stdin:", err) fmt.Fprintln(stderr, "interpres-decode: read stdin:", err)
return 2 return 2
} }
if *infer { if *infer {
if err := inferStruct(data, stdout); err != nil { if err := inferStruct(data, stdout); err != nil {
fmt.Fprintf(stderr, "interpres-decode: %v\n", err) fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
// A document that fails to parse keeps the adapter's invalid
// exit; anything else, a failed write among them, is a tool
// failure.
if _, ok := errors.AsType[*interpres.SyntaxError](err); ok {
return 1 return 1
} }
return 2
}
return 0 return 0
} }
tree, err := interpres.ParseMap(data) tree, err := interpres.ParseMap(data)
if err != nil { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 1 return 1
} }
if *plainJSON { if *plainJSON {
@@ -121,25 +140,48 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
enc.SetEscapeHTML(false) enc.SetEscapeHTML(false)
enc.SetIndent("", " ") enc.SetIndent("", " ")
if err := enc.Encode(plainJSONValue(tree)); err != nil { if err := enc.Encode(plainJSONValue(tree)); err != nil {
fmt.Fprintln(stderr, "encode:", err) fmt.Fprintln(stderr, "interpres-decode: encode:", err)
return 2 return 2
} }
return 0 return 0
} }
tagged, err := tag(tree) tagged, err := tag(tree)
if err != nil { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 2 return 2
} }
enc := json.NewEncoder(stdout) enc := json.NewEncoder(stdout)
enc.SetEscapeHTML(false) enc.SetEscapeHTML(false)
if err := enc.Encode(tagged); err != nil { if err := enc.Encode(tagged); err != nil {
fmt.Fprintln(stderr, "encode:", err) fmt.Fprintln(stderr, "interpres-decode: encode:", err)
return 2 return 2
} }
return 0 return 0
} }
// usage prints the command line summary, with every flag in its two-dash
// long form: the flag package's default usage printer renders a single dash,
// and the manpage and docs/CLI.md spell the flags the way this text does.
func usage(w io.Writer) {
fmt.Fprint(w, `Usage: interpres-decode [flags]
Without a mode flag the command reads one TOML document from standard input
and writes the toml-test tagged JSON representation to standard output.
--encode read tagged JSON from standard input and write TOML
instead
--help print this usage
--json with the default mode, print plain indented JSON
instead of tagged JSON
--schema TYPE write a TOML template for the named struct type; the
Go source file follows as the first argument
--struct infer a Go struct definition from the document on
standard input and print it
--validate validate the documents instead of emitting tagged JSON
--version print the version and exit
`)
}
// versionString names the version the binary was built at: the module // versionString names the version the binary was built at: the module
// version the toolchain recorded, which is the tag when the release pipeline // version the toolchain recorded, which is the tag when the release pipeline
// builds it, and (devel) for an ordinary build from a working tree. // builds it, and (devel) for an ordinary build from a working tree.
@@ -226,8 +268,8 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
return 2 return 2
} }
} }
valid := true
checked := 0 checked := 0
invalid := 0
for _, p := range files { for _, p := range files {
name := p name := p
var data []byte var data []byte
@@ -244,17 +286,17 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
} }
checked++ checked++
if _, err := interpres.ParseMap(data); err != nil { if _, err := interpres.ParseMap(data); err != nil {
fmt.Fprintf(stderr, "%s: %v\n", name, err) fmt.Fprintf(stderr, "interpres-decode: %s: %v\n", name, err)
valid = false invalid++
} }
} }
// The single-document run stays quiet on success, the contract the // The single-document run stays quiet on success, the contract the
// compliance tooling relies on; a directory walk closes with the // compliance tooling relies on; a directory walk closes with the
// summary that makes the sweep readable. // summary that makes the sweep readable.
if dirs > 0 { if dirs > 0 {
fmt.Fprintf(stderr, "checked %d documents, %d invalid\n", checked, map[bool]int{true: 0, false: 1}[valid]) fmt.Fprintf(stderr, "checked %d documents, %d invalid\n", checked, invalid)
} }
if !valid { if invalid > 0 {
return 1 return 1
} }
return 0 return 0
@@ -265,17 +307,17 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
func encodeJSON(stdin io.Reader, stdout, stderr io.Writer) int { func encodeJSON(stdin io.Reader, stdout, stderr io.Writer) int {
data, err := io.ReadAll(stdin) data, err := io.ReadAll(stdin)
if err != nil { if err != nil {
fmt.Fprintln(stderr, "read stdin:", err) fmt.Fprintln(stderr, "interpres-decode: read stdin:", err)
return 2 return 2
} }
var desc any var desc any
if err := json.Unmarshal(data, &desc); err != nil { if err := json.Unmarshal(data, &desc); err != nil {
fmt.Fprintln(stderr, "decode JSON:", err) fmt.Fprintln(stderr, "interpres-decode: decode JSON:", err)
return 2 return 2
} }
tree, err := untag(desc) tree, err := untag(desc)
if err != nil { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 2 return 2
} }
doc, ok := tree.(map[string]any) doc, ok := tree.(map[string]any)
@@ -285,11 +327,11 @@ func encodeJSON(stdin io.Reader, stdout, stderr io.Writer) int {
} }
out, err := interpres.Marshal(doc) out, err := interpres.Marshal(doc)
if err != nil { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 2 return 2
} }
if _, err := stdout.Write(out); err != nil { if _, err := stdout.Write(out); err != nil {
fmt.Fprintln(stderr, "write stdout:", err) fmt.Fprintln(stderr, "interpres-decode: write stdout:", err)
return 2 return 2
} }
return 0 return 0
+412 -28
View File
@@ -7,6 +7,8 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"go/parser"
"go/token"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -217,7 +219,7 @@ func TestTaggedHelper(t *testing.T) {
func TestValidateStdinAcceptsValidDocument(t *testing.T) { func TestValidateStdinAcceptsValidDocument(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
in := bytes.NewReader([]byte("title = \"ok\"\n")) in := bytes.NewReader([]byte("title = \"ok\"\n"))
if code := Run([]string{"-validate"}, in, &stdout, &stderr); code != 0 { if code := Run([]string{"--validate"}, in, &stdout, &stderr); code != 0 {
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
} }
if stdout.Len() != 0 || stderr.Len() != 0 { if stdout.Len() != 0 || stderr.Len() != 0 {
@@ -228,7 +230,7 @@ func TestValidateStdinAcceptsValidDocument(t *testing.T) {
func TestValidateStdinRejectsInvalidDocument(t *testing.T) { func TestValidateStdinRejectsInvalidDocument(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
in := bytes.NewReader([]byte("title = \"unterminated\n")) in := bytes.NewReader([]byte("title = \"unterminated\n"))
if code := Run([]string{"-validate"}, in, &stdout, &stderr); code != 1 { if code := Run([]string{"--validate"}, in, &stdout, &stderr); code != 1 {
t.Fatalf("Run returned %d, want 1; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 1; stderr = %q", code, stderr.String())
} }
if !strings.Contains(stderr.String(), "<stdin>") || !strings.Contains(stderr.String(), "line 1") { if !strings.Contains(stderr.String(), "<stdin>") || !strings.Contains(stderr.String(), "line 1") {
@@ -250,10 +252,10 @@ func TestValidateFiles(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if code := Run([]string{"-validate", good}, nil, &stdout, &stderr); code != 0 { if code := Run([]string{"--validate", good}, nil, &stdout, &stderr); code != 0 {
t.Fatalf("one valid file: Run returned %d, stderr = %q", code, stderr.String()) t.Fatalf("one valid file: Run returned %d, stderr = %q", code, stderr.String())
} }
if code := Run([]string{"-validate", good, bad}, nil, &stdout, &stderr); code != 1 { if code := Run([]string{"--validate", good, bad}, nil, &stdout, &stderr); code != 1 {
t.Fatalf("valid plus invalid: Run returned %d, want 1; stderr = %q", code, stderr.String()) t.Fatalf("valid plus invalid: Run returned %d, want 1; stderr = %q", code, stderr.String())
} }
if !strings.Contains(stderr.String(), bad) || !strings.Contains(stderr.String(), "line 1") { if !strings.Contains(stderr.String(), bad) || !strings.Contains(stderr.String(), "line 1") {
@@ -263,7 +265,7 @@ func TestValidateFiles(t *testing.T) {
func TestValidateMissingFileReturnsTwo(t *testing.T) { func TestValidateMissingFileReturnsTwo(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if code := Run([]string{"-validate", "no-such-file.toml"}, nil, &stdout, &stderr); code != 2 { if code := Run([]string{"--validate", "no-such-file.toml"}, nil, &stdout, &stderr); code != 2 {
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
} }
} }
@@ -274,14 +276,14 @@ func TestAdapterModeRejectsPositionalArgument(t *testing.T) {
if code := Run([]string{"file.toml"}, in, &stdout, &stderr); code != 2 { if code := Run([]string{"file.toml"}, in, &stdout, &stderr); code != 2 {
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
} }
if !strings.Contains(stderr.String(), "-validate") { if !strings.Contains(stderr.String(), "--validate") {
t.Fatalf("stderr = %q, want it to point at -validate", stderr.String()) t.Fatalf("stderr = %q, want it to point at --validate", stderr.String())
} }
} }
func TestUnknownFlagReturnsTwo(t *testing.T) { func TestUnknownFlagReturnsTwo(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if code := Run([]string{"-nope"}, nil, &stdout, &stderr); code != 2 { if code := Run([]string{"--nope"}, nil, &stdout, &stderr); code != 2 {
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
} }
} }
@@ -303,7 +305,7 @@ func TestRunEncoderScalars(t *testing.T) {
} }
` `
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := Run([]string{"-encode"}, strings.NewReader(in), &stdout, &stderr) code := Run([]string{"--encode"}, strings.NewReader(in), &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
} }
@@ -334,7 +336,7 @@ func TestRunEncoderNested(t *testing.T) {
} }
` `
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := Run([]string{"-encode"}, strings.NewReader(in), &stdout, &stderr) code := Run([]string{"--encode"}, strings.NewReader(in), &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
} }
@@ -355,7 +357,7 @@ func TestRunEncoderFloatTagDecides(t *testing.T) {
// tag decides the type; the output must stay a float. // tag decides the type; the output must stay a float.
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
in := `{"whole": {"type": "float", "value": "1"}, "exp": {"type": "float", "value": "5e+22"}}` in := `{"whole": {"type": "float", "value": "1"}, "exp": {"type": "float", "value": "5e+22"}}`
code := Run([]string{"-encode"}, strings.NewReader(in), &stdout, &stderr) code := Run([]string{"--encode"}, strings.NewReader(in), &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
} }
@@ -380,7 +382,7 @@ func TestRunEncoderRejectsBadInput(t *testing.T) {
} }
for _, c := range cases { for _, c := range cases {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := Run([]string{"-encode"}, strings.NewReader(c.in), &stdout, &stderr) code := Run([]string{"--encode"}, strings.NewReader(c.in), &stdout, &stderr)
if code != 2 { if code != 2 {
t.Errorf("%s: Run returned %d, want 2; stderr = %q", c.name, code, stderr.String()) t.Errorf("%s: Run returned %d, want 2; stderr = %q", c.name, code, stderr.String())
continue continue
@@ -396,7 +398,7 @@ func TestRunEncoderRejectsBadInput(t *testing.T) {
func TestRunEncoderFlagConflicts(t *testing.T) { func TestRunEncoderFlagConflicts(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if code := Run([]string{"-encode", "-validate"}, strings.NewReader(""), &stdout, &stderr); code != 2 { if code := Run([]string{"--encode", "--validate"}, strings.NewReader(""), &stdout, &stderr); code != 2 {
t.Errorf("Run returned %d, want 2 for the two modes together", code) t.Errorf("Run returned %d, want 2 for the two modes together", code)
} }
if !strings.Contains(stderr.String(), "cannot be combined") { if !strings.Contains(stderr.String(), "cannot be combined") {
@@ -405,7 +407,7 @@ func TestRunEncoderFlagConflicts(t *testing.T) {
stdout.Reset() stdout.Reset()
stderr.Reset() stderr.Reset()
if code := Run([]string{"-encode", "file.json"}, strings.NewReader(""), &stdout, &stderr); code != 2 { if code := Run([]string{"--encode", "file.json"}, strings.NewReader(""), &stdout, &stderr); code != 2 {
t.Errorf("Run returned %d, want 2 for an argument", code) t.Errorf("Run returned %d, want 2 for an argument", code)
} }
} }
@@ -432,7 +434,7 @@ n = "a"
t.Fatalf("decode returned %d, stderr = %q", code, stderr.String()) t.Fatalf("decode returned %d, stderr = %q", code, stderr.String())
} }
var out bytes.Buffer var out bytes.Buffer
if code := Run([]string{"-encode"}, bytes.NewReader(tagged.Bytes()), &out, &stderr); code != 0 { if code := Run([]string{"--encode"}, bytes.NewReader(tagged.Bytes()), &out, &stderr); code != 0 {
t.Fatalf("encode returned %d, stderr = %q", code, stderr.String()) t.Fatalf("encode returned %d, stderr = %q", code, stderr.String())
} }
want, err := interpres.ParseMap([]byte(doc)) want, err := interpres.ParseMap([]byte(doc))
@@ -450,7 +452,7 @@ n = "a"
func TestRunVersion(t *testing.T) { func TestRunVersion(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := Run([]string{"-version"}, strings.NewReader(""), &stdout, &stderr) code := Run([]string{"--version"}, strings.NewReader(""), &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
} }
@@ -463,7 +465,7 @@ func TestRunVersion(t *testing.T) {
func TestRunPlainJSON(t *testing.T) { func TestRunPlainJSON(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
in := strings.NewReader("host = \"db\"\nwhen = 1979-05-27T07:32:00-07:00\nitems = [1, 2]\n") in := strings.NewReader("host = \"db\"\nwhen = 1979-05-27T07:32:00-07:00\nitems = [1, 2]\n")
code := Run([]string{"-json"}, in, &stdout, &stderr) code := Run([]string{"--json"}, in, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
} }
@@ -481,18 +483,31 @@ func TestRunPlainJSON(t *testing.T) {
func TestValidateDirectorySummary(t *testing.T) { func TestValidateDirectorySummary(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "good.toml"), []byte("a = 1\n"), 0o644) if err := os.WriteFile(filepath.Join(dir, "good.toml"), []byte("a = 1\n"), 0o644); err != nil {
os.WriteFile(filepath.Join(dir, "bad.toml"), []byte("a =\n"), 0o644) t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "bad.toml"), []byte("a =\n"), 0o644); err != nil {
t.Fatal(err)
}
sub := filepath.Join(dir, "nested") sub := filepath.Join(dir, "nested")
os.Mkdir(sub, 0o755) if err := os.Mkdir(sub, 0o755); err != nil {
os.WriteFile(filepath.Join(sub, "deep.toml"), []byte("b = true\n"), 0o644) t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sub, "deep.toml"), []byte("b = true\n"), 0o644); err != nil {
t.Fatal(err)
}
// A second invalid document, so the summary's invalid count is
// exercised beyond the single failure the boolean tracked.
if err := os.WriteFile(filepath.Join(sub, "worse.toml"), []byte("c =\n"), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := Run([]string{"-validate", dir}, strings.NewReader(""), &stdout, &stderr) code := Run([]string{"--validate", dir}, strings.NewReader(""), &stdout, &stderr)
if code != 1 { if code != 1 {
t.Fatalf("Run returned %d, want 1 for a directory with an invalid file", code) t.Fatalf("Run returned %d, want 1 for a directory with invalid files", code)
} }
if !strings.Contains(stderr.String(), "checked 3 documents, 1 invalid") { if !strings.Contains(stderr.String(), "checked 4 documents, 2 invalid") {
t.Errorf("stderr = %q, want the summary", stderr.String()) t.Errorf("stderr = %q, want the summary", stderr.String())
} }
} }
@@ -500,7 +515,7 @@ func TestValidateDirectorySummary(t *testing.T) {
func TestInferStruct(t *testing.T) { func TestInferStruct(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
in := strings.NewReader("host = \"db\"\nport = 5432\ntags = [\"a\"]\n\n[server]\nname = \"edge\"\n\n[[items]]\nn = 1\n") in := strings.NewReader("host = \"db\"\nport = 5432\ntags = [\"a\"]\n\n[server]\nname = \"edge\"\n\n[[items]]\nn = 1\n")
code := Run([]string{"-struct"}, in, &stdout, &stderr) code := Run([]string{"--struct"}, in, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
} }
@@ -545,7 +560,7 @@ type Item struct {
t.Fatal(err) t.Fatal(err)
} }
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := Run([]string{"-schema", "Config", src}, strings.NewReader(""), &stdout, &stderr) code := Run([]string{"--schema", "Config", src}, strings.NewReader(""), &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
} }
@@ -572,7 +587,7 @@ func TestRunPlainJSONShapes(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
in := strings.NewReader("when = 1979-05-27T07:32:00-07:00\nd = 1979-05-27\nt = 07:32:00\nwall = 1979-05-27T07:32:00\n" + in := strings.NewReader("when = 1979-05-27T07:32:00-07:00\nd = 1979-05-27\nt = 07:32:00\nwall = 1979-05-27T07:32:00\n" +
"items = [1, \"two\"]\n\n[[tables]]\nx = true\n") "items = [1, \"two\"]\n\n[[tables]]\nx = true\n")
code := Run([]string{"-json"}, in, &stdout, &stderr) code := Run([]string{"--json"}, in, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
} }
@@ -594,7 +609,7 @@ func TestRunPlainJSONShapes(t *testing.T) {
func TestInferStructScalarShapes(t *testing.T) { func TestInferStructScalarShapes(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
in := strings.NewReader("f = 1.5\nb = true\nd = 1979-05-27\nldt = 1979-05-27T07:32:00\nlt = 07:32:00\nnums = [1, 2, 3]\nmixed = [1, \"a\"]\nempty = []\n") in := strings.NewReader("f = 1.5\nb = true\nd = 1979-05-27\nldt = 1979-05-27T07:32:00\nlt = 07:32:00\nnums = [1, 2, 3]\nmixed = [1, \"a\"]\nempty = []\n")
code := Run([]string{"-struct"}, in, &stdout, &stderr) code := Run([]string{"--struct"}, in, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String()) t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
} }
@@ -614,3 +629,372 @@ func TestInferStructScalarShapes(t *testing.T) {
} }
} }
} }
func TestRunHelpPrintsLongFlags(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Run([]string{"--help"}, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
for _, flag := range []string{"--encode", "--help", "--json", "--schema", "--struct", "--validate", "--version"} {
if !strings.Contains(out, flag) {
t.Errorf("usage output missing %q:\n%s", flag, out)
}
}
// Every flag line of the list names its flag in the two-dash long form
// only, so no line opens with a single dash.
for line := range strings.SplitSeq(strings.TrimRight(out, "\n"), "\n") {
if after, ok := strings.CutPrefix(line, " -"); ok && !strings.HasPrefix(after, "-") {
t.Errorf("usage line %q lists a flag with one dash", line)
}
}
}
func TestGoFieldName(t *testing.T) {
cases := []struct{ key, want string }{
{"host", "Host"},
{"ab", "Ab"},
{"http-host", "HttpHost"},
{"3d", "Field3d"},
{"", "Field"},
}
for _, c := range cases {
if got := goFieldName(c.key, map[string]bool{}); got != c.want {
t.Errorf("goFieldName(%q) = %q, want %q", c.key, got, c.want)
}
}
}
func TestGoFieldNameCollision(t *testing.T) {
// Two keys that clean to the same name must not collide; the counter
// keeps the fields apart and both stay exported.
invented := map[string]bool{}
cases := []struct{ key, want string }{
{"a-b", "AB"},
{"a b", "AB2"},
{"a_b", "AB22"},
}
for _, c := range cases {
if got := goFieldName(c.key, invented); got != c.want {
t.Errorf("goFieldName(%q) = %q, want %q", c.key, got, c.want)
}
}
}
func TestInferStructDigitLeadingKey(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Run([]string{"--struct"}, strings.NewReader("3d = true\n"), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
if !strings.Contains(stdout.String(), "Field3d bool") {
t.Errorf("output missing the exported Field3d field:\n%s", stdout.String())
}
}
func TestInferStructBacktickKey(t *testing.T) {
// A backtick in the key would end a raw string literal early, so the
// tag has to be rendered as an interpreted literal instead.
var stdout, stderr bytes.Buffer
code := Run([]string{"--struct"}, strings.NewReader("\"a`b\" = 1\n"), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
if !strings.Contains(out, "AB int64 \"toml:\\\"a`b\\\"\"") {
t.Errorf("output missing the quoted tag:\n%s", out)
}
// The printed definition has to compile; parsing it as Go is the
// syntax half of that proof.
if _, err := parser.ParseFile(token.NewFileSet(), "inferred.go", "package p\n\n"+out, 0); err != nil {
t.Errorf("the printed definition does not parse: %v\n%s", err, out)
}
}
func TestInferStructMergesArrayElements(t *testing.T) {
// The second element carries a key the first lacks, so the slice type
// has to be inferred from both.
var stdout, stderr bytes.Buffer
in := strings.NewReader("[[items]]\nn = 1\n\n[[items]]\nextra = \"late\"\n")
code := Run([]string{"--struct"}, in, &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{
"Items []struct {",
"N int64 `toml:\"n\"`",
"Extra string `toml:\"extra\"`",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q:\n%s", want, out)
}
}
}
func TestInferStructMixedArrayStaysValueArray(t *testing.T) {
// One table element does not make the array an array of tables; a
// struct slice would not decode the scalar element.
var stdout, stderr bytes.Buffer
code := Run([]string{"--struct"}, strings.NewReader("arr = [1, {x = 1}]\n"), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
if !strings.Contains(out, "Arr []any") {
t.Errorf("output = %q, want a value array typed []any", out)
}
if strings.Contains(out, "[]struct") {
t.Errorf("output = %q, a mixed array must not become a struct slice", out)
}
}
func TestRunStructParseError(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Run([]string{"--struct"}, strings.NewReader("a =\n"), &stdout, &stderr)
if code != 1 {
t.Fatalf("Run returned %d, want 1 (parse error); stderr = %q", code, stderr.String())
}
if stdout.Len() != 0 {
t.Errorf("stdout should be empty on parse error, got %q", stdout.String())
}
if !strings.Contains(stderr.String(), "line 1") {
t.Errorf("stderr = %q, want the library's line number", stderr.String())
}
}
func TestRunStructWriteFailure(t *testing.T) {
var stderr bytes.Buffer
code := Run([]string{"--struct"}, strings.NewReader("a = 1\n"), errorWriter{}, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2 (write error); stderr = %q", code, stderr.String())
}
}
func TestRunSchemaNeedsTypeAndExactlyOneFile(t *testing.T) {
for _, args := range [][]string{{"--schema", "Config"}, {"--schema", "Config", "a.go", "b.go"}} {
var stdout, stderr bytes.Buffer
code := Run(args, strings.NewReader(""), &stdout, &stderr)
if code != 2 {
t.Errorf("Run(%v) returned %d, want 2", args, code)
}
if !strings.Contains(stderr.String(), "--schema") {
t.Errorf("Run(%v) stderr = %q, want it to name --schema", args, stderr.String())
}
}
}
func TestRunSchemaUnparsableSource(t *testing.T) {
src := filepath.Join(t.TempDir(), "broken.go")
if err := os.WriteFile(src, []byte("this is not Go\n"), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := Run([]string{"--schema", "Config", src}, strings.NewReader(""), &stdout, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
}
if !strings.Contains(stderr.String(), "broken.go") {
t.Errorf("stderr = %q, want it to name the source file", stderr.String())
}
}
func TestRunSchemaUnknownType(t *testing.T) {
src := filepath.Join(t.TempDir(), "config.go")
body := "package cfg\n\ntype Config struct {\n\tA int `toml:\"a\"`\n}\n"
if err := os.WriteFile(src, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := Run([]string{"--schema", "Missing", src}, strings.NewReader(""), &stdout, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
}
if !strings.Contains(stderr.String(), `no struct type "Missing"`) {
t.Errorf("stderr = %q, want it to name the missing type", stderr.String())
}
}
func TestRunSchemaRecursiveType(t *testing.T) {
// A self-referential struct has no finite template; the generator has
// to name the recursion instead of exhausting the stack.
src := filepath.Join(t.TempDir(), "node.go")
body := "package cfg\n\ntype Node struct {\n\tNext *Node `toml:\"next\"`\n}\n"
if err := os.WriteFile(src, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := Run([]string{"--schema", "Node", src}, strings.NewReader(""), &stdout, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
}
for _, want := range []string{"recursive", "Node"} {
if !strings.Contains(stderr.String(), want) {
t.Errorf("stderr = %q, want it to mention %q", stderr.String(), want)
}
}
}
func TestRunSchemaMultiNameField(t *testing.T) {
// A field list may name several fields of one type; each name is one
// TOML key.
src := filepath.Join(t.TempDir(), "range.go")
if err := os.WriteFile(src, []byte("package cfg\n\ntype Range struct {\n\tMin, Max int\n}\n"), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := Run([]string{"--schema", "Range", src}, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{"min = 0", "max = 0"} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q:\n%s", want, out)
}
}
}
func TestRunSchemaEmbeddedStructs(t *testing.T) {
// The library inlines only untagged embedded structs; a tagged one
// keeps its own section.
src := filepath.Join(t.TempDir(), "embed.go")
body := `package cfg
type Inner struct {
X int ` + "`toml:\"x\"`" + `
}
type Tagged struct {
Inner ` + "`toml:\"inner\"`" + `
Y int ` + "`toml:\"y\"`" + `
}
type Flat struct {
Inner
Z int ` + "`toml:\"z\"`" + `
}
`
if err := os.WriteFile(src, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := Run([]string{"--schema", "Tagged", src}, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("Tagged: Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
if !strings.Contains(out, "y = 0") || !strings.Contains(out, "[inner]") {
t.Errorf("Tagged output = %q, want a y scalar and an [inner] section", out)
}
stdout.Reset()
stderr.Reset()
code = Run([]string{"--schema", "Flat", src}, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("Flat: Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out = stdout.String()
if !strings.Contains(out, "x = 0") || !strings.Contains(out, "z = 0") {
t.Errorf("Flat output = %q, want x and z flattened as scalars", out)
}
if strings.Contains(out, "[inner]") {
t.Errorf("Flat output = %q, an untagged embedded struct must not become a section", out)
}
}
func TestRunVersionWriteFailure(t *testing.T) {
var stderr bytes.Buffer
code := Run([]string{"--version"}, strings.NewReader(""), errorWriter{}, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2 (write error); stderr = %q", code, stderr.String())
}
if !strings.Contains(stderr.String(), "write stdout") {
t.Errorf("stderr = %q, want it to mention the failed write", stderr.String())
}
}
func TestRunSchemaWriteFailure(t *testing.T) {
src := filepath.Join(t.TempDir(), "config.go")
body := "package cfg\n\ntype Config struct {\n\tA int `toml:\"a\"`\n}\n"
if err := os.WriteFile(src, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
var stderr bytes.Buffer
code := Run([]string{"--schema", "Config", src}, strings.NewReader(""), errorWriter{}, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2 (write error); stderr = %q", code, stderr.String())
}
}
func TestRunEncodeWriteFailure(t *testing.T) {
var stderr bytes.Buffer
in := `{"a": {"type": "integer", "value": "1"}}`
code := Run([]string{"--encode"}, strings.NewReader(in), errorWriter{}, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2 (write error); stderr = %q", code, stderr.String())
}
if !strings.Contains(stderr.String(), "write stdout") {
t.Errorf("stderr = %q, want it to mention the failed write", stderr.String())
}
}
func TestRunEmptyInput(t *testing.T) {
t.Run("default", func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Run(nil, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
if strings.TrimSpace(stdout.String()) != "{}" {
t.Errorf("stdout = %q, want an empty table", stdout.String())
}
})
t.Run("encode", func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Run([]string{"--encode"}, strings.NewReader(""), &stdout, &stderr)
if code != 2 {
t.Fatalf("Run returned %d, want 2, empty input is not JSON", code)
}
})
t.Run("struct", func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Run([]string{"--struct"}, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
if !strings.Contains(stdout.String(), "type inferred struct {\n}") {
t.Errorf("stdout = %q, want an empty struct", stdout.String())
}
})
t.Run("validate", func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Run([]string{"--validate"}, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
if stdout.Len() != 0 || stderr.Len() != 0 {
t.Errorf("validate should be quiet, stdout %q stderr %q", stdout.String(), stderr.String())
}
})
}
func TestRunJSONFlagConflicts(t *testing.T) {
for _, args := range [][]string{
{"--encode", "--json"},
{"--struct", "--json"},
{"--validate", "--json"},
{"--json", "--schema", "Config", "config.go"},
} {
var stdout, stderr bytes.Buffer
code := Run(args, strings.NewReader(""), &stdout, &stderr)
if code != 2 {
t.Errorf("Run(%v) returned %d, want 2", args, code)
}
if !strings.Contains(stderr.String(), "--json") {
t.Errorf("Run(%v) stderr = %q, want it to explain the --json conflict", args, stderr.String())
}
}
}
+103 -28
View File
@@ -9,8 +9,10 @@ import (
"go/parser" "go/parser"
"go/token" "go/token"
"io" "io"
"maps"
"path/filepath" "path/filepath"
"reflect" "reflect"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
@@ -34,7 +36,9 @@ func runSchema(typeName, sourcePath string, stdout io.Writer) error {
return fmt.Errorf("no struct type %q in %s", typeName, filepath.Base(sourcePath)) return fmt.Errorf("no struct type %q in %s", typeName, filepath.Base(sourcePath))
} }
body := &strings.Builder{} body := &strings.Builder{}
writeSchemaFields(body, st, types, "") if err := writeSchemaFields(body, st, types, "", nil); err != nil {
return err
}
_, err = io.WriteString(stdout, strings.TrimLeft(body.String(), "\n")) _, err = io.WriteString(stdout, strings.TrimLeft(body.String(), "\n"))
return err return err
} }
@@ -74,9 +78,18 @@ type fieldMeta struct {
// writeSchemaFields writes the fields of one struct level: the scalar lines // writeSchemaFields writes the fields of one struct level: the scalar lines
// first, then the sections, so the template re-parses with every value under // first, then the sections, so the template re-parses with every value under
// the header it belongs to. prefix is the dotted path the nested headers // the header it belongs to. prefix is the dotted path the nested headers
// carry. // carry. path holds the struct types of the levels currently being written,
func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]*ast.StructType, prefix string) { // so a type that reaches itself is reported as recursion instead of
metas := metasOf(st, types) // exhausting the stack.
func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]*ast.StructType, prefix string, path []*ast.StructType) error {
if slices.Contains(path, st) {
return recursionError(st, types)
}
path = append(path, st)
metas, err := metasOf(st, types)
if err != nil {
return err
}
for _, m := range metas { for _, m := range metas {
if _, elemSt := elementStruct(m.typ, types); elemSt != nil { if _, elemSt := elementStruct(m.typ, types); elemSt != nil {
continue continue
@@ -98,7 +111,9 @@ func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]
writeComment(w, m.comment) writeComment(w, m.comment)
fmt.Fprintf(w, "[%s%s]\n", prefix, m.key) fmt.Fprintf(w, "[%s%s]\n", prefix, m.key)
if sub := structOf(m.typ, types); sub != nil { if sub := structOf(m.typ, types); sub != nil {
writeSchemaFields(w, sub, types, prefix+m.key+".") if err := writeSchemaFields(w, sub, types, prefix+m.key+".", path); err != nil {
return err
}
} }
fmt.Fprintln(w) fmt.Fprintln(w)
} }
@@ -109,9 +124,12 @@ func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]
} }
writeComment(w, m.comment) writeComment(w, m.comment)
fmt.Fprintf(w, "[[%s%s]]\n", prefix, m.key) fmt.Fprintf(w, "[[%s%s]]\n", prefix, m.key)
writeSchemaFields(w, elemSt, types, "") if err := writeSchemaFields(w, elemSt, types, "", path); err != nil {
return err
}
fmt.Fprintln(w) fmt.Fprintln(w)
} }
return nil
} }
// writeComment writes the comment lines above a binding. // writeComment writes the comment lines above a binding.
@@ -125,46 +143,103 @@ func writeComment(w *strings.Builder, text string) {
} }
// metasOf flattens the exported fields of a struct. The key comes from the // metasOf flattens the exported fields of a struct. The key comes from the
// toml tag, or the lower-cased field name; a `-` key drops the field. // toml tag, or the lower-cased field name; a `-` key drops the field. An
func metasOf(st *ast.StructType, types map[string]*ast.StructType) []fieldMeta { // embedded struct without a tag name flattens into its parent, the way the
// library inlines it, while a tagged one keeps its own section.
func metasOf(st *ast.StructType, types map[string]*ast.StructType) ([]fieldMeta, error) {
return flattenMetas(st, types, nil)
}
// flattenMetas is metasOf with the chain of struct types currently being
// flattened, which stops a struct that embeds itself, directly or through
// another embedded type.
func flattenMetas(st *ast.StructType, types map[string]*ast.StructType, chain map[*ast.StructType]bool) ([]fieldMeta, error) {
if chain[st] {
return nil, recursionError(st, types)
}
// A copy per branch: the chain is the path being flattened now, not the
// set ever visited, so a type embedded in two siblings is not mistaken
// for recursion.
chain = maps.Clone(chain)
if chain == nil {
chain = map[*ast.StructType]bool{}
}
chain[st] = true
var out []fieldMeta var out []fieldMeta
for _, field := range st.Fields.List { for _, field := range st.Fields.List {
if len(field.Names) == 0 {
// An untagged embedded struct flattens into the parent.
if ident, ok := baseType(field.Type).(*ast.Ident); ok {
if inner, ok := types[ident.Name]; ok {
out = append(out, metasOf(inner, types)...)
}
}
continue
}
name := field.Names[0].Name
if !ast.IsExported(name) {
continue
}
tagText := "" tagText := ""
if field.Tag != nil { if field.Tag != nil {
tagText, _ = strconv.Unquote(field.Tag.Value) tagText, _ = strconv.Unquote(field.Tag.Value)
} }
toml := reflect.StructTag(tagText).Get("toml") toml := reflect.StructTag(tagText).Get("toml")
key, opts := "", "" key, opts, _ := strings.Cut(toml, ",")
if toml != "" { if len(field.Names) == 0 {
key, opts, _ = strings.Cut(toml, ",") if key == "" {
// An untagged embedded struct flattens into its parent.
if ident, ok := baseType(field.Type).(*ast.Ident); ok {
if inner, ok := types[ident.Name]; ok {
metas, err := flattenMetas(inner, types, chain)
if err != nil {
return nil, err
}
out = append(out, metas...)
}
}
continue
} }
if key == "-" { if key == "-" {
continue continue
} }
if key == "" { // A tagged embedded struct is a section of its own; the tag
key = strings.ToLower(name) // name is the only name it has.
}
out = append(out, fieldMeta{ out = append(out, fieldMeta{
key: key, key: key,
comment: tagOption(opts, "comment="), comment: tagOption(opts, "comment="),
def: tagOption(opts, "default="), def: tagOption(opts, "default="),
typ: field.Type, typ: field.Type,
}) })
continue
} }
return out if key == "-" {
continue
}
// A field list may name several fields of one type, `Min, Max int`;
// each name is one TOML key.
for _, name := range field.Names {
if !ast.IsExported(name.Name) {
continue
}
fieldKey := key
if fieldKey == "" {
fieldKey = strings.ToLower(name.Name)
}
out = append(out, fieldMeta{
key: fieldKey,
comment: tagOption(opts, "comment="),
def: tagOption(opts, "default="),
typ: field.Type,
})
}
}
return out, nil
}
// recursionError names the struct type that reached itself. Such a type has
// no finite TOML template: every level would nest another copy of the same
// shape.
func recursionError(st *ast.StructType, types map[string]*ast.StructType) error {
return fmt.Errorf("recursive type %s: the struct contains itself, so it has no finite template", typeName(st, types))
}
// typeName names the declared struct type st refers to, and "anonymous
// struct" for a literal one that no declaration names.
func typeName(st *ast.StructType, types map[string]*ast.StructType) string {
for name, t := range types {
if t == st {
return name
}
}
return "anonymous struct"
} }
// tagOption returns the text a `name=` option carries in the option part of // tagOption returns the text a `name=` option carries in the option part of
+45 -40
View File
@@ -3,6 +3,7 @@
The reference below is taken from the program itself. `interpres-decode` is The reference below is taken from the program itself. `interpres-decode` is
the toml-test harness adapter in both directions, decoding TOML into tagged the toml-test harness adapter in both directions, decoding TOML into tagged
JSON and encoding tagged JSON back into TOML, and it also validates documents. JSON and encoding tagged JSON back into TOML, and it also validates documents.
The same reference ships as the manual page `man/interpres-decode.1`.
Install it with Go itself, no release assets involved: Install it with Go itself, no release assets involved:
```sh ```sh
@@ -13,50 +14,52 @@ go install sourcedock.dev/petrbalvin/interpres/v2/cmd/interpres-decode@latest
```sh ```sh
interpres-decode [flags] interpres-decode [flags]
interpres-decode -encode interpres-decode --encode
interpres-decode -validate [file ...] interpres-decode --validate [file ...]
interpres-decode -validate [directory ...] interpres-decode --validate [directory ...]
interpres-decode -json interpres-decode --json
interpres-decode -struct interpres-decode --struct
interpres-decode -schema TYPE file.go interpres-decode --schema TYPE file.go
interpres-decode -version interpres-decode --version
``` ```
Without `-validate`, `-encode`, `-json` or `-struct` the program is the Without `--validate`, `--encode`, `--json`, `--struct` or `--schema` the
decoding half of the toml-test adapter: it takes no arguments, reads one TOML program is the decoding half of the toml-test adapter: it takes no arguments,
document from stdin, and writes the toml-test tagged-JSON form to stdout. reads one TOML document from stdin, and writes the toml-test tagged-JSON form
Build it locally with `just build`, which compiles it into to stdout. Build it locally with `just build`, which compiles it into
`bin/interpres-decode`, or run it straight from the module directory with `bin/interpres-decode`, or run it straight from the module directory with
`just run`. `just run`.
With `-encode` the direction is reversed: the program reads a tagged-JSON With `--encode` the direction is reversed: the program reads a tagged-JSON
description from stdin and writes the TOML document it describes to stdout, description from stdin and writes the TOML document it describes to stdout,
which is the shape toml-test expects of an encoder command. It takes no which is the shape toml-test expects of an encoder command. It takes no
arguments either, and the mode flags cannot be combined. arguments either, and the mode flags cannot be combined.
With `-validate` the program parses each named file instead, or stdin when no With `--validate` the program parses each named file instead, or stdin when no
file is named, and prints one line per invalid document to stderr. It is file is named, and prints one line per invalid document to stderr. It is
quiet on valid documents, which is the shape a CI step wants. The `-` name quiet on valid documents, which is the shape a CI step wants. The `-` name
means stdin. A named directory is walked for `.toml` files, every one of them means stdin. A named directory is walked for `.toml` files, every one of them
validated, and the walk closes with a summary on stderr naming how many validated, and the walk closes with a summary on stderr naming how many
documents were checked and how many were invalid. documents were checked and how many were invalid.
With `-json` the decoding half prints plain indented JSON instead of the With `--json` the decoding half prints plain indented JSON instead of the
tagged form, the shape for people and diffs: the values keep their types as tagged form, the shape for people and diffs: the values keep their types as
JSON sees them, and the date-time wrappers print in their TOML form. JSON sees them, and the date-time wrappers print in their TOML form. The flag
shapes the decoding output only, so it is rejected together with the mode
flags.
With `-struct` the program reads a TOML document from stdin and prints a Go With `--struct` the program reads a TOML document from stdin and prints a Go
struct definition shaped like it: one field per key in written order, nested struct definition shaped like it: one field per key in written order, nested
tables as nested struct types, and an array of tables as a slice. The tables as nested struct types, and an array of tables as a slice. The
printed type compiles and decodes the document it came from. printed type compiles and decodes the document it came from.
With `-schema` the program reads a Go source file and writes a TOML template With `--schema` the program reads a Go source file and writes a TOML template
for the named struct type: one key per exported field, the `comment=` tag for the named struct type: one key per exported field, the `comment=` tag
option printed as a comment above it, and the `default=` option as the value option printed as a comment above it, and the `default=` option as the value
where one is set. It is the inverse of `-struct`, for config-driven where one is set. It is the inverse of `--struct`, for config-driven
applications that generate their example configuration from the type. applications that generate their example configuration from the type.
`-version` prints the binary's version and exits. The release pipeline builds `--version` prints the binary's version and exits. The release pipeline builds
at the tag, so a released binary prints its own tag; a build from a working at the tag, so a released binary prints its own tag; a build from a working
tree prints `(devel)`. tree prints `(devel)`.
@@ -64,21 +67,21 @@ tree prints `(devel)`.
| Flag | Effect | | Flag | Effect |
|---|---| |---|---|
| `-validate` | validate the documents instead of emitting tagged JSON; directories are walked for `.toml` files | | `--validate` | validate the documents instead of emitting tagged JSON; directories are walked for `.toml` files |
| `-encode` | read tagged JSON from stdin and write TOML instead | | `--encode` | read tagged JSON from stdin and write TOML instead |
| `-json` | with the default mode, print plain indented JSON instead of tagged JSON | | `--json` | with the default mode, print plain indented JSON instead of tagged JSON |
| `-struct` | infer a Go struct definition from the document on stdin and print it | | `--struct` | infer a Go struct definition from the document on stdin and print it |
| `-schema TYPE` | write a TOML template for the struct type TYPE from the Go source file named as the first argument | | `--schema TYPE` | write a TOML template for the struct type TYPE from the Go source file named as the first argument |
| `-version` | print the version and exit | | `--version` | print the version and exit |
| `-h` | print the usage | | `--help` | print the usage |
## Exit codes ## Exit codes
| Code | Meaning | | Code | Meaning |
|---|---| |---|---|
| `0` | adapter: the document parsed and the tagged JSON was written; encode: the TOML was written; validate: every document parsed; schema, struct, version: the output was written | | `0` | adapter: the document parsed and the tagged JSON was written; encode: the TOML was written; validate: every document parsed; schema, struct, version: the output was written |
| `1` | adapter: parse error; validate: at least one document is invalid | | `1` | adapter: parse error; validate: at least one document is invalid; struct: the document on stdin failed to parse |
| `2` | a usage error, a read failure, malformed tagged JSON, or a value with no TOML representation | | `2` | a usage error, a read or write failure, malformed tagged JSON, or a value with no TOML representation |
## Wire format ## Wire format
@@ -105,7 +108,7 @@ wrapped in an object with a `type` and a `value`:
| local date | `date-local` | `1979-05-27` | | local date | `date-local` | `1979-05-27` |
| local time | `time-local` | `07:32:00.999999` | | local time | `time-local` | `07:32:00.999999` |
The `-encode` mode reads exactly this form back. Two properties of it are The `--encode` mode reads exactly this form back. Two properties of it are
worth knowing. A float whose value has no fraction and no exponent is written worth knowing. A float whose value has no fraction and no exponent is written
as a bare integer string, `{"type": "float", "value": "1"}`, so there the tag as a bare integer string, `{"type": "float", "value": "1"}`, so there the tag
decides the type and not the literal. And the form cannot tell an array of decides the type and not the literal. And the form cannot tell an array of
@@ -126,10 +129,10 @@ port = 9090
``` ```
The output is the equivalent value tree as one JSON object. Turn a description The output is the equivalent value tree as one JSON object. Turn a description
back into TOML with `-encode`: back into TOML with `--encode`:
```sh ```sh
echo '{"title": {"type": "string", "value": "hello"}}' | ./bin/interpres-decode -encode echo '{"title": {"type": "string", "value": "hello"}}' | ./bin/interpres-decode --encode
``` ```
```toml ```toml
@@ -139,14 +142,14 @@ title = "hello"
Validate the TOML files of another repository in CI: Validate the TOML files of another repository in CI:
```sh ```sh
interpres-decode -validate config.toml deploy/example.toml interpres-decode --validate config.toml deploy/example.toml
``` ```
An invalid document reports the file and the library's line number: An invalid document reports the file and the library's line number:
```sh ```sh
$ interpres-decode -validate bad.toml $ interpres-decode --validate bad.toml
bad.toml: interpres: line 1: expected a value interpres-decode: bad.toml: interpres: line 1: expected a value
$ echo $? $ echo $?
1 1
``` ```
@@ -155,22 +158,24 @@ Sweep a whole directory tree of configuration, with the summary the walk
closes on: closes on:
```sh ```sh
$ interpres-decode -validate configs/ $ interpres-decode --validate configs/
configs/old.toml: interpres: line 3: duplicate key "port" interpres-decode: configs/old.toml: interpres: line 3: duplicate key "port"
checked 14 documents, 1 invalid checked 14 documents, 1 invalid
$ echo $? $ echo $?
1 1
``` ```
See the document a `-struct` template would decode: See the document a `--struct` template would decode:
```sh ```sh
echo 'host = "db" echo 'host = "db"
port = 5432 port = 5432
' | ./bin/interpres-decode -struct ' | ./bin/interpres-decode --struct
``` ```
```go ```go
// Generated by interpres-decode --struct; decode with
// sourcedock.dev/petrbalvin/interpres/v2.
type inferred struct { type inferred struct {
Host string `toml:"host"` Host string `toml:"host"`
Port int64 `toml:"port"` Port int64 `toml:"port"`
@@ -182,7 +187,7 @@ the Go source declares fields tagged
`toml:"host,comment=The host to dial,default=example.org"`: `toml:"host,comment=The host to dial,default=example.org"`:
```sh ```sh
./bin/interpres-decode -schema Config config.go ./bin/interpres-decode --schema Config config.go
``` ```
```toml ```toml
@@ -193,7 +198,7 @@ host = "example.org"
Print the binary's version: Print the binary's version:
```sh ```sh
$ ./bin/interpres-decode -version $ ./bin/interpres-decode --version
interpres-decode v2.0.0 interpres-decode v2.0.0
``` ```
+8 -7
View File
@@ -3,8 +3,8 @@
// Command basic demonstrates decoding and encoding a TOML document with // Command basic demonstrates decoding and encoding a TOML document with
// interpres. It exercises struct mapping, arrays of tables, Marshaler // interpres. It exercises struct mapping, arrays of tables, Marshaler
// customisation, the Decoder's strict mode, and the Encoder's policy // customisation, and the Encoder's policy options, covering every feature a
// options, covering every feature a regular user would reach for. // regular user would reach for.
package main package main
import ( import (
@@ -18,8 +18,7 @@ import (
) )
// document is a small but realistic configuration: it has scalars, a // 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 // sub-table, an array of tables, and a date-time.
// the demonstration also covers overflow-safe integer conversion.
const document = ` const document = `
title = "interpres demo" title = "interpres demo"
launched = 2024-11-04T09:00:00Z launched = 2024-11-04T09:00:00Z
@@ -62,9 +61,11 @@ type User struct {
Admin bool `toml:"admin"` Admin bool `toml:"admin"`
} }
// Port is a typed alias that controls how its value appears in TOML. The // Port is a typed string alias that carries a Marshaler. The MarshalTOML
// MarshalTOML hook returns a string, so a Port field is rendered as // hook returns the string unchanged, so a Port field is rendered as the
// "host:port" instead of the raw integer. // string it holds, a string the encoding would print the same way without
// the hook; the demonstration that a Marshaler reshapes a value is
// Endpoint's below.
type Port string type Port string
func (p Port) MarshalTOML() (any, error) { func (p Port) MarshalTOML() (any, error) {
+1 -1
View File
@@ -94,7 +94,7 @@ dev:
# Runs the official toml-test compliance suite in both directions, decoder and encoder, against the built adapter; toml-test must be on PATH (go install github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.2.0); not standard because no canonical recipe covers a domain compliance suite. # Runs the official toml-test compliance suite in both directions, decoder and encoder, against the built adapter; toml-test must be on PATH (go install github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.2.0); not standard because no canonical recipe covers a domain compliance suite.
toml-test: build toml-test: build
toml-test test -decoder=bin/interpres-decode -encoder='bin/interpres-decode -encode' -toml=1.1 toml-test test -decoder=bin/interpres-decode -encoder='bin/interpres-decode --encode' -toml=1.1
# Coverage report as an HTML map from the gate's profile; not standard because the gate needs only the numeric floor, and a browser artefact is exploration, not a gate. # Coverage report as an HTML map from the gate's profile; not standard because the gate needs only the numeric floor, and a browser artefact is exploration, not a gate.
coverage-html: test coverage-html: test
+34 -32
View File
@@ -1,4 +1,4 @@
.TH INTERPRES-DECODE 1 "2026-09-21" "interpres 2.0.0" "User Commands" .TH INTERPRES-DECODE 1 "2026-09-22" "interpres 2.0.0" "User Commands"
.SH NAME .SH NAME
interpres-decode \- TOML validator and toml-test harness adapter interpres-decode \- TOML validator and toml-test harness adapter
.SH SYNOPSIS .SH SYNOPSIS
@@ -6,38 +6,38 @@ interpres-decode \- TOML validator and toml-test harness adapter
[\fIFLAGS\fR] [\fIFLAGS\fR]
.br .br
.B interpres-decode .B interpres-decode
.B \-encode .B \-\-encode
.br .br
.B interpres-decode .B interpres-decode
.B \-validate .B \-\-validate
[\fIFILE\fR...] [\fIFILE\fR...]
.br .br
.B interpres-decode .B interpres-decode
.B \-validate .B \-\-validate
[\fIDIRECTORY\fR...] [\fIDIRECTORY\fR...]
.br .br
.B interpres-decode .B interpres-decode
.B \-json .B \-\-json
.br .br
.B interpres-decode .B interpres-decode
.B \-struct .B \-\-struct
.br .br
.B interpres-decode .B interpres-decode
.B \-schema .B \-\-schema
\fITYPE\fR \fITYPE\fR
\fIFILE.go\fR \fIFILE.go\fR
.br .br
.B interpres-decode .B interpres-decode
.B \-version .B \-\-version
.SH DESCRIPTION .SH DESCRIPTION
.B interpres-decode .B interpres-decode
is the toml-test harness adapter in both directions and a TOML validator. is the toml-test harness adapter in both directions and a TOML validator.
Without a mode flag it reads one TOML document from standard input and writes Without a mode flag it reads one TOML document from standard input and writes
the toml-test tagged-JSON representation to standard output. the toml-test tagged-JSON representation to standard output.
.B \-encode .B \-\-encode
reads a tagged-JSON description from standard input and writes the TOML reads a tagged-JSON description from standard input and writes the TOML
document it describes. document it describes.
.B \-validate .B \-\-validate
parses each named file, or standard input when none are named, and prints one parses each named file, or standard input when none are named, and prints one
line per invalid document to standard error; a named directory is walked for line per invalid document to standard error; a named directory is walked for
.B .toml .B .toml
@@ -45,48 +45,49 @@ files, every one validated, and the walk closes with a summary on standard
error naming the counts. The name error naming the counts. The name
.B \- .B \-
means standard input. means standard input.
.B \-json .B \-\-json
prints plain indented JSON instead of the tagged form. prints plain indented JSON instead of the tagged form; it shapes the decoding
.B \-struct output only, so it is rejected together with the mode flags.
.B \-\-struct
prints a Go struct definition inferred from the document on standard input. prints a Go struct definition inferred from the document on standard input.
.B \-schema .B \-\-schema
writes a TOML template for the struct type writes a TOML template for the struct type
\fITYPE\fR \fITYPE\fR
declared in the Go source file declared in the Go source file
\fIFILE.go\fR, \fIFILE.go\fR,
taking the key names, comments and defaults from the fields' tags. taking the key names, comments and defaults from the fields' tags.
.B \-version .B \-\-version
prints the binary's version and exits. prints the binary's version and exits.
.PP .PP
The mode flags The mode flags
.BR \-validate , .BR \-\-validate ,
.BR \-encode , .BR \-\-encode ,
.B \-struct .B \-\-struct
and and
.B \-schema .B \-\-schema
cannot be combined. cannot be combined.
.SH OPTIONS .SH OPTIONS
.TP .TP
.B \-validate .B \-\-validate
Validate the documents instead of emitting tagged JSON. Validate the documents instead of emitting tagged JSON.
.TP .TP
.B \-encode .B \-\-encode
Read tagged JSON from standard input and write TOML instead. Read tagged JSON from standard input and write TOML instead.
.TP .TP
.B \-json .B \-\-json
With the default mode, print plain indented JSON instead of tagged JSON. With the default mode, print plain indented JSON instead of tagged JSON.
.TP .TP
.B \-struct .B \-\-struct
Infer a Go struct definition from the document on standard input and print it. Infer a Go struct definition from the document on standard input and print it.
.TP .TP
.BI \-schema " TYPE" .BI \-\-schema " TYPE"
Write a TOML template for the struct type \fITYPE\fR; the Go source file Write a TOML template for the struct type \fITYPE\fR; the Go source file
follows as the first argument. follows as the first argument.
.TP .TP
.B \-version .B \-\-version
Print the version and exit. Print the version and exit.
.TP .TP
.B \-h .B \-\-help
Print the usage. Print the usage.
.SH EXIT STATUS .SH EXIT STATUS
.TP .TP
@@ -95,11 +96,12 @@ The document parsed and the output was written; in validate mode, every
document parsed. document parsed.
.TP .TP
.B 1 .B 1
Adapter: a parse error. Validate: at least one document is invalid. Adapter: a parse error. Validate: at least one document is invalid. Struct:
the document on standard input failed to parse.
.TP .TP
.B 2 .B 2
A usage error, a read failure, malformed tagged JSON, or a value with no TOML A usage error, a read or write failure, malformed tagged JSON, or a value
representation. with no TOML representation.
.SH EXAMPLES .SH EXAMPLES
Decode a document into tagged JSON: Decode a document into tagged JSON:
.PP .PP
@@ -113,7 +115,7 @@ Validate a directory of configuration, with the summary:
.PP .PP
.nf .nf
.RS .RS
interpres-decode -validate configs/ interpres-decode \-\-validate configs/
.RE .RE
.fi .fi
.PP .PP
@@ -121,7 +123,7 @@ Infer a Go type from a document:
.PP .PP
.nf .nf
.RS .RS
interpres-decode -struct < config.toml > config.go interpres-decode \-\-struct < config.toml > config.go
.RE .RE
.fi .fi
.PP .PP
@@ -129,7 +131,7 @@ Write the template back from the type:
.PP .PP
.nf .nf
.RS .RS
interpres-decode -schema Config config.go interpres-decode \-\-schema Config config.go
.RE .RE
.fi .fi
.SH SEE ALSO .SH SEE ALSO