130 lines
3.2 KiB
Go
130 lines
3.2 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
// Command interpres-decode reads a TOML document from standard input and writes
|
||
|
|
// the toml-test "tagged JSON" representation to standard output.
|
||
|
|
//
|
||
|
|
// It exits non-zero on a parse error, which is how the toml-test harness checks
|
||
|
|
// that invalid documents are rejected. Run the official suite against it with:
|
||
|
|
//
|
||
|
|
// toml-test ./interpres-decode
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"math"
|
||
|
|
"os"
|
||
|
|
"strconv"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/interpres"
|
||
|
|
)
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
os.Exit(Run(os.Stdin, os.Stdout, os.Stderr))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run reads a TOML document from stdin, emits the toml-test tagged-JSON form
|
||
|
|
// on stdout, and returns the process exit code (0 success, 1 parse error,
|
||
|
|
// 2 I/O, encoding, or unsupported-value error).
|
||
|
|
func Run(stdin io.Reader, stdout, stderr io.Writer) int {
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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)
|
||
|
|
}
|
||
|
|
}
|