2026-09-22 00:55:02 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-09-22 21:15:07 +02:00
|
|
|
"strconv"
|
2026-09-22 00:55:02 +02:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
"unicode"
|
2026-09-22 21:15:07 +02:00
|
|
|
"unicode/utf8"
|
2026-09-22 00:55:02 +02:00
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/interpres/v2"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// inferStruct reads a TOML document and writes a Go struct definition shaped
|
|
|
|
|
// 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
|
2026-09-22 21:15:07 +02:00
|
|
|
// 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.
|
2026-09-22 00:55:02 +02:00
|
|
|
func inferStruct(data []byte, stdout io.Writer) error {
|
|
|
|
|
doc, err := interpres.Parse(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-22 21:15:07 +02:00
|
|
|
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
|
2026-09-22 00:55:02 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 21:15:07 +02:00
|
|
|
// 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()))
|
2026-09-22 00:55:02 +02:00
|
|
|
for _, key := range t.Keys() {
|
|
|
|
|
entry, _ := t.Get(key)
|
2026-09-22 21:15:07 +02:00
|
|
|
out = append(out, inferredField{key: key, entry: entry})
|
2026-09-22 00:55:02 +02:00
|
|
|
}
|
2026-09-22 21:15:07 +02:00
|
|
|
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)
|
2026-09-22 00:55:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// goTypeOf names the Go type the decoded value asks for.
|
|
|
|
|
func goTypeOf(val any) string {
|
|
|
|
|
switch val.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
return "string"
|
|
|
|
|
case bool:
|
|
|
|
|
return "bool"
|
|
|
|
|
case int64:
|
|
|
|
|
return "int64"
|
|
|
|
|
case float64:
|
|
|
|
|
return "float64"
|
|
|
|
|
case interpres.OffsetDateTime:
|
|
|
|
|
return "interpres.OffsetDateTime"
|
|
|
|
|
case interpres.LocalDateTime:
|
|
|
|
|
return "interpres.LocalDateTime"
|
|
|
|
|
case interpres.LocalDate:
|
|
|
|
|
return "interpres.LocalDate"
|
|
|
|
|
case interpres.LocalTime:
|
|
|
|
|
return "interpres.LocalTime"
|
|
|
|
|
case time.Time:
|
|
|
|
|
return "time.Time"
|
|
|
|
|
case []any:
|
|
|
|
|
return "[]any"
|
|
|
|
|
case map[string]any:
|
|
|
|
|
return "map[string]any"
|
|
|
|
|
}
|
|
|
|
|
return "any"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// goFieldName cleans a document key into an exported Go identifier: the
|
2026-09-22 21:15:07 +02:00
|
|
|
// 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.
|
2026-09-22 00:55:02 +02:00
|
|
|
func goFieldName(key string, invented map[string]bool) string {
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
nextUpper := true
|
|
|
|
|
for _, r := range key {
|
|
|
|
|
switch {
|
|
|
|
|
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
|
|
|
|
if nextUpper {
|
|
|
|
|
r = unicode.ToUpper(r)
|
|
|
|
|
nextUpper = false
|
|
|
|
|
}
|
|
|
|
|
b.WriteRune(r)
|
|
|
|
|
default:
|
|
|
|
|
nextUpper = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
name := b.String()
|
|
|
|
|
if name == "" {
|
|
|
|
|
name = "Field"
|
|
|
|
|
}
|
2026-09-22 21:15:07 +02:00
|
|
|
// 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
|
2026-09-22 00:55:02 +02:00
|
|
|
}
|
|
|
|
|
for invented[name] {
|
|
|
|
|
name += "2"
|
|
|
|
|
}
|
|
|
|
|
invented[name] = true
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inferScalarType names the Go element type of a scalar array when every
|
|
|
|
|
// element agrees, and any when they do not.
|
|
|
|
|
func inferScalarType(items []any) string {
|
|
|
|
|
seen := ""
|
|
|
|
|
for i, item := range items {
|
|
|
|
|
t := goTypeOf(item)
|
|
|
|
|
if i == 0 {
|
|
|
|
|
seen = t
|
|
|
|
|
} else if t != seen {
|
|
|
|
|
return "any"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if seen == "" {
|
|
|
|
|
return "any"
|
|
|
|
|
}
|
|
|
|
|
return seen
|
|
|
|
|
}
|