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
+109 -51
View File
@@ -6,9 +6,11 @@ package main
import (
"fmt"
"io"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"sourcedock.dev/petrbalvin/interpres/v2"
)
@@ -17,63 +19,115 @@ import (
// 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
// 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 {
doc, err := interpres.Parse(data)
if err != nil {
return err
}
fmt.Fprintln(stdout, "// Generated by interpres-decode -struct; decode with")
fmt.Fprintln(stdout, "// sourcedock.dev/petrbalvin/interpres/v2.")
fmt.Fprintln(stdout, "type inferred struct {")
if err := writeInferredFields(stdout, doc.Root(), map[string]bool{}); err != nil {
return err
}
fmt.Fprintln(stdout, "}")
return nil
body := &strings.Builder{}
fmt.Fprintln(body, "// Generated by interpres-decode --struct; decode with")
fmt.Fprintln(body, "// sourcedock.dev/petrbalvin/interpres/v2.")
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
}
// writeInferredFields writes one field per entry of the table. 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 io.Writer, t *interpres.Table, invented map[string]bool) error {
// inferredField is one document key with the entry it is inferred from.
type inferredField struct {
key string
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() {
entry, _ := t.Get(key)
name := goFieldName(key, invented)
// 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
// it away.
var tables []*interpres.Table
for _, el := range entry.Elements() {
if el != nil {
tables = append(tables, el)
}
}
if len(tables) > 0 {
// The type comes from the first element.
fmt.Fprintf(w, "\t%s []struct {\n", name)
if err := writeInferredFields(w, tables[0], map[string]bool{}); err != nil {
return err
}
fmt.Fprintf(w, "\t} `toml:%q`\n", key)
continue
}
val := entry.Value()
if child := entry.Table(); child != nil {
fmt.Fprintf(w, "\t%s struct {\n", name)
if err := writeInferredFields(w, child, map[string]bool{}); err != nil {
return err
}
fmt.Fprintf(w, "\t} `toml:%q`\n", key)
continue
}
if items, ok := val.([]any); ok {
fmt.Fprintf(w, "\t%s []%s `toml:%q`\n", name, inferScalarType(items), key)
continue
}
fmt.Fprintf(w, "\t%s %s `toml:%q`\n", name, goTypeOf(val), key)
out = append(out, inferredField{key: key, entry: entry})
}
return nil
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
// array are nil wherever an element is not a table. Every node present
// is what tells the two apart: [1, {x=1}] stays a value array even
// though one of its elements is a table.
elements := f.entry.Elements()
allTables := len(elements) > 0
for _, el := range elements {
if el == nil {
allTables = false
break
}
}
if allTables {
fmt.Fprintf(w, "\t%s []struct {\n", name)
writeInferredFields(w, mergedTableFields(elements), map[string]bool{})
fmt.Fprintf(w, "\t} %s\n", structTag(f.key))
return
}
if child := f.entry.Table(); child != nil {
fmt.Fprintf(w, "\t%s struct {\n", name)
writeInferredFields(w, tableFields(child), map[string]bool{})
fmt.Fprintf(w, "\t} %s\n", structTag(f.key))
return
}
val := f.entry.Value()
if items, ok := val.([]any); ok {
fmt.Fprintf(w, "\t%s []%s %s\n", name, inferScalarType(items), structTag(f.key))
return
}
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 strconv.Quote(tag)
}
// 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
// words the punctuation splits become capitalised runs, a leading digit gains
// an underscore, and a collision with an earlier name gains a counter.
// words the punctuation splits become capitalised runs, a leading digit
// 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 {
var b strings.Builder
nextUpper := true
@@ -127,8 +183,10 @@ func goFieldName(key string, invented map[string]bool) string {
if name == "" {
name = "Field"
}
if unicode.IsDigit(rune(name[0])) {
name = "_" + name
// The first rune is decoded rather than taken as a byte, because a key
// may open with a digit beyond ASCII.
if first, _ := utf8.DecodeRuneInString(name); unicode.IsDigit(first) {
name = "Field" + name
}
for invented[name] {
name += "2"