feat(cmd): add version, plain json, struct inference and schema modes
Test / test (push) Successful in 1m35s
Test / test (push) Successful in 1m35s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"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
|
||||
// and decodes the document it came from.
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// words the punctuation splits become capitalised runs, a leading digit gains
|
||||
// an underscore, 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
|
||||
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"
|
||||
}
|
||||
if unicode.IsDigit(rune(name[0])) {
|
||||
name = "_" + name
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user