// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // Command interpres-decode is the toml-test harness adapter and a TOML // validator. Without flags it reads a TOML document from standard input and // 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 // 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: // // interpres-decode -validate config.toml // interpres-decode -encode < case.json // // Run the official suite in both directions against the adapter with: // // toml-test test -decoder=./interpres-decode -encoder='./interpres-decode -encode' package main import ( "encoding/json" "errors" "flag" "fmt" "io" "math" "os" "strconv" "time" "sourcedock.dev/petrbalvin/interpres/v2" ) func main() { os.Exit(Run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) } // Run runs the command line and returns the process exit code: 0 success, // 1 an invalid document, 2 a usage, reading, encoding, or // unsupported-value error. func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("interpres-decode", flag.ContinueOnError) fs.SetOutput(stderr) 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") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return 0 } return 2 } if *validate && *encode { fmt.Fprintln(stderr, "interpres-decode: -validate and -encode cannot be combined") return 2 } if *validate { return validatePaths(fs.Args(), stdin, stderr) } if fs.NArg() > 0 { fmt.Fprintln(stderr, "interpres-decode: the adapter mode takes no arguments; name files with -validate") return 2 } if *encode { return encodeJSON(stdin, stdout, stderr) } data, err := io.ReadAll(stdin) if err != nil { fmt.Fprintln(stderr, "read stdin:", err) return 2 } tree, err := interpres.ParseMap(data) if err != nil { fmt.Fprintln(stderr, err) return 1 } tagged, err := tag(tree) if err != nil { fmt.Fprintln(stderr, err) return 2 } enc := json.NewEncoder(stdout) enc.SetEscapeHTML(false) if err := enc.Encode(tagged); err != nil { fmt.Fprintln(stderr, "encode:", err) return 2 } return 0 } // validatePaths parses every named file, or standard input when none are // named, and reports each invalid document on stderr. It returns 0 when all // documents parse, 1 when one does not, and 2 on a usage or read failure. func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int { if len(paths) == 0 { paths = []string{"-"} } valid := true for _, p := range paths { name := p var data []byte var err error if p == "-" { data, err = io.ReadAll(stdin) name = "" } else { data, err = os.ReadFile(p) } if err != nil { fmt.Fprintf(stderr, "interpres-decode: %s: %v\n", name, err) return 2 } if _, err := interpres.ParseMap(data); err != nil { fmt.Fprintf(stderr, "%s: %v\n", name, err) valid = false } } if !valid { return 1 } return 0 } // encodeJSON reads a toml-test tagged JSON description from standard input and // writes the TOML document it describes to standard output. func encodeJSON(stdin io.Reader, stdout, stderr io.Writer) int { data, err := io.ReadAll(stdin) if err != nil { fmt.Fprintln(stderr, "read stdin:", err) return 2 } var desc any if err := json.Unmarshal(data, &desc); err != nil { fmt.Fprintln(stderr, "decode JSON:", err) return 2 } tree, err := untag(desc) if err != nil { fmt.Fprintln(stderr, err) return 2 } doc, ok := tree.(map[string]any) if !ok { fmt.Fprintln(stderr, "interpres-decode: the description must be a JSON object at the top level") return 2 } out, err := interpres.Marshal(doc) if err != nil { fmt.Fprintln(stderr, err) return 2 } if _, err := stdout.Write(out); err != nil { fmt.Fprintln(stderr, "write stdout:", err) return 2 } return 0 } // untag converts a toml-test JSON description into the value tree Marshal // expects: a JSON object becomes a map[string]any, a JSON array becomes a // []any, and an object carrying exactly the keys "type" and "value" becomes // the Go value for that TOML type. func untag(v any) (any, error) { switch x := v.(type) { case map[string]any: if typ, val, ok := taggedValue(x); ok { return decodeTagged(typ, val) } out := make(map[string]any, len(x)) for k, e := range x { u, err := untag(e) if err != nil { return nil, fmt.Errorf("%s: %w", k, err) } out[k] = u } return out, nil case []any: out := make([]any, len(x)) for i, e := range x { u, err := untag(e) if err != nil { return nil, fmt.Errorf("[%d]: %w", i, err) } out[i] = u } return asTables(out), nil default: return nil, fmt.Errorf("unsupported JSON value %T", v) } } // asTables returns the elements as a []map[string]any when there is at least // one and every element is a table, the shape the encoder renders as an array // of tables. The tagged JSON cannot tell an array of tables from a value array // of inline tables, and both parse back to the same value, so the header form // is chosen because it is the one the encoder otherwise never exercises. An // empty array stays a []any, because TOML has no empty array of tables. func asTables(items []any) any { if len(items) == 0 { return items } tbls := make([]map[string]any, len(items)) for i, e := range items { tbl, ok := e.(map[string]any) if !ok { return items } tbls[i] = tbl } return tbls } // taggedValue reports whether m is a toml-test value object: a JSON object of // exactly the two string keys "type" and "value", carrying a type this adapter // knows. Any other object is a table. func taggedValue(m map[string]any) (typ, val string, ok bool) { if len(m) != 2 { return "", "", false } ts, ok := m["type"].(string) if !ok || !knownType(ts) { return "", "", false } vs, ok := m["value"].(string) if !ok { return "", "", false } return ts, vs, true } func knownType(typ string) bool { switch typ { case "string", "integer", "float", "bool", "datetime", "datetime-local", "date-local", "time-local": return true } return false } // decodeTagged returns the Go value for one tagged JSON value. Every type but // string is parsed by the library itself, so the adapter and the library agree // on what an integer, a float or a date-time is. func decodeTagged(typ, val string) (any, error) { if typ == "string" { return val, nil } v, err := parseAtom(val) if err != nil { return nil, fmt.Errorf("%s %q: %w", typ, val, err) } // A float with no fractional part and no exponent is described by a bare // integer literal, so here the tag decides and not the literal. if n, ok := v.(int64); ok && typ == "float" { return float64(n), nil } if !typeMatches(typ, v) { return nil, fmt.Errorf("%s %q parsed as %T", typ, val, v) } return v, nil } // parseAtom parses one bare TOML value, by handing `v = ` to the library's // parser and requiring the result to hold exactly that one statement, so a // value carrying a newline or a comment cannot smuggle a second one in. func parseAtom(val string) (any, error) { tree, err := interpres.ParseMap([]byte("v = " + val + "\n")) if err != nil { return nil, err } if len(tree) != 1 { return nil, errors.New("not a single bare value") } return tree["v"], nil } // typeMatches reports whether v is the Go value the tagged type names. func typeMatches(typ string, v any) bool { switch typ { case "integer": _, ok := v.(int64) return ok case "float": _, ok := v.(float64) return ok case "bool": _, ok := v.(bool) return ok case "datetime": switch v.(type) { case time.Time, interpres.OffsetDateTime: return true } return false case "datetime-local": _, ok := v.(interpres.LocalDateTime) return ok case "date-local": _, ok := v.(interpres.LocalDate) return ok case "time-local": _, ok := v.(interpres.LocalTime) return ok } return false } // tag converts an interpres value into its toml-test tagged-JSON form. Tables // become JSON objects and arrays become JSON arrays; scalars are wrapped in a // {"type", "value"} object. An error is returned for value types the encoder // has no tagged representation for. func tag(v any) (any, error) { switch x := v.(type) { case map[string]any: out := make(map[string]any, len(x)) for k, val := range x { tagged, err := tag(val) if err != nil { return nil, err } out[k] = tagged } return out, nil case []any: out := make([]any, len(x)) for i, e := range x { tagged, err := tag(e) if err != nil { return nil, err } out[i] = tagged } return out, nil case []map[string]any: out := make([]any, len(x)) for i, e := range x { tagged, err := tag(e) if err != nil { return nil, err } out[i] = tagged } return out, nil case string: return tagged("string", x), nil case bool: return tagged("bool", strconv.FormatBool(x)), nil case int64: return tagged("integer", strconv.FormatInt(x, 10)), nil case float64: return tagged("float", formatFloat(x)), nil case time.Time: return tagged("datetime", x.Format(time.RFC3339Nano)), nil case interpres.OffsetDateTime: return tagged("datetime", x.Format(time.RFC3339Nano)), nil case interpres.LocalDateTime: return tagged("datetime-local", x.Format("2006-01-02T15:04:05.999999999")), nil case interpres.LocalDate: return tagged("date-local", x.Format("2006-01-02")), nil case interpres.LocalTime: return tagged("time-local", x.Format("15:04:05.999999999")), nil default: return nil, fmt.Errorf("unsupported value type %T", v) } } func tagged(typ, value string) map[string]string { return map[string]string{"type": typ, "value": value} } func formatFloat(f float64) string { switch { case math.IsInf(f, 1): return "inf" case math.IsInf(f, -1): return "-inf" case math.IsNaN(f): return "nan" default: return strconv.FormatFloat(f, 'g', -1, 64) } }