Files
interpres/cmd/interpres-decode/main.go
T
petrbalvin 1c7329aeea
Test / test (push) Successful in 1m32s
build: move the module path to /v2
Assisted-by: GLM 5.3 Flash
2026-09-19 00:14:39 +02:00

185 lines
4.6 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
// -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
//
// Run the official suite against the adapter with:
//
// toml-test ./interpres-decode
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")
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
}
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
}
// 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
}
// 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)
}
}