fix(cmd): long-form flags, honest counts and safer inference
Assisted-by: GLM 5.3
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user