// Copyright (c) 2026 Petr BalvĂ­n (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 }