2026-08-19 11:23:00 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
2026-09-17 21:23:29 +02:00
|
|
|
// 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
|
|
|
|
|
// -validate it checks the named documents, or standard input when none are
|
|
|
|
|
// named, and exits non-zero on the first invalid one:
|
2026-08-19 11:23:00 +02:00
|
|
|
//
|
2026-09-17 21:23:29 +02:00
|
|
|
// interpres-decode -validate config.toml
|
|
|
|
|
//
|
|
|
|
|
// Run the official suite against the adapter with:
|
2026-08-19 11:23:00 +02:00
|
|
|
//
|
|
|
|
|
// toml-test ./interpres-decode
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
2026-09-17 21:23:29 +02:00
|
|
|
"errors"
|
|
|
|
|
"flag"
|
2026-08-19 11:23:00 +02:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"math"
|
|
|
|
|
"os"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/interpres"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
2026-09-17 21:23:29 +02:00
|
|
|
os.Exit(Run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
|
2026-08-19 11:23:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-17 21:23:29 +02:00
|
|
|
// 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")
|
|
|
|
|
if err := fs.Parse(args); err != nil {
|
|
|
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-08-19 11:23:00 +02:00
|
|
|
data, err := io.ReadAll(stdin)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintln(stderr, "read stdin:", err)
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
tree, err := interpres.Parse(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
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 21:23:29 +02:00
|
|
|
// 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 = "<stdin>"
|
|
|
|
|
} else {
|
|
|
|
|
data, err = os.ReadFile(p)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(stderr, "interpres-decode: %s: %v\n", name, err)
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
if _, err := interpres.Parse(data); err != nil {
|
|
|
|
|
fmt.Fprintf(stderr, "%s: %v\n", name, err)
|
|
|
|
|
valid = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !valid {
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 11:23:00 +02:00
|
|
|
// 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.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)
|
|
|
|
|
}
|
|
|
|
|
}
|