Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
@@ -4,14 +4,16 @@
|
||||
// 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:
|
||||
// -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 against the adapter with:
|
||||
// Run the official suite in both directions against the adapter with:
|
||||
//
|
||||
// toml-test ./interpres-decode
|
||||
// toml-test test -decoder=./interpres-decode -encoder='./interpres-decode -encode'
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -39,12 +41,17 @@ 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)
|
||||
}
|
||||
@@ -52,6 +59,9 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
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)
|
||||
@@ -109,6 +119,187 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
|
||||
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 = <val>` 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.Parse([]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":
|
||||
_, ok := v.(time.Time)
|
||||
return ok
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user