Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
@@ -105,6 +105,8 @@ jobs:
|
|||||||
run: go build -o bin/interpres-decode ./cmd/interpres-decode
|
run: go build -o bin/interpres-decode ./cmd/interpres-decode
|
||||||
|
|
||||||
- name: Compliance suite
|
- name: Compliance suite
|
||||||
# interpres implements TOML 1.1; the mode is pinned so an upstream
|
# interpres implements TOML 1.1, and the suite runs both directions: the decoder
|
||||||
# default change cannot silently move the corpus.
|
# on the valid and invalid corpora, the encoder on the tagged JSON of the valid
|
||||||
run: bin/toml-test test -decoder=bin/interpres-decode -toml=1.1
|
# one. The mode is pinned so an upstream default change cannot silently move the
|
||||||
|
# corpus.
|
||||||
|
run: bin/toml-test test -decoder=bin/interpres-decode -encoder='bin/interpres-decode -encode' -toml=1.1
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- `time.Duration` is encoded in its canonical Go form as a TOML string,
|
- `time.Duration` is encoded in its canonical Go form as a TOML string,
|
||||||
`1h30m0s`, because TOML has no duration type; the decoder reads that string
|
`1h30m0s`, because TOML has no duration type; the decoder reads that string
|
||||||
back and still accepts a bare integer as the nanosecond count.
|
back and still accepts a bare integer as the nanosecond count.
|
||||||
|
- `interpres-decode -encode`, the adapter's other direction: it reads the
|
||||||
|
toml-test tagged JSON from stdin and writes the TOML document it describes.
|
||||||
|
The compliance suite now runs the encoder as well as the decoder, 214
|
||||||
|
encoder cases against the tagged JSON of the valid corpus.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -45,8 +45,8 @@ just test
|
|||||||
formatting pass are three commits, never one.
|
formatting pass are three commits, never one.
|
||||||
4. Record every user-visible change in `CHANGELOG.md` under `## [development]`.
|
4. Record every user-visible change in `CHANGELOG.md` under `## [development]`.
|
||||||
5. Add or update tests. Coverage stays at 80 percent or more; it is a hard
|
5. Add or update tests. Coverage stays at 80 percent or more; it is a hard
|
||||||
gate. Parser and decoder changes must also keep the toml-test suite at zero
|
gate. Parser, decoder and encoder changes must also keep both directions of
|
||||||
failures, checked with `just toml-test`.
|
the toml-test suite at zero failures, checked with `just toml-test`.
|
||||||
6. Update the documentation when the public API, the configuration or the
|
6. Update the documentation when the public API, the configuration or the
|
||||||
behaviour changes; the documents move in the same commit as the behaviour
|
behaviour changes; the documents move in the same commit as the behaviour
|
||||||
they describe.
|
they describe.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ A TOML 1.1 parser and encoder for Go, written with the standard library
|
|||||||
alone. `interpres` (Latin for *interpreter*) gives zero-dependency
|
alone. `interpres` (Latin for *interpreter*) gives zero-dependency
|
||||||
programs an `encoding/json`-style API for reading and writing TOML, and passes
|
programs an `encoding/json`-style API for reading and writing TOML, and passes
|
||||||
the entire official [toml-test](https://github.com/toml-lang/toml-test) suite:
|
the entire official [toml-test](https://github.com/toml-lang/toml-test) suite:
|
||||||
214 valid and 467 invalid cases, zero failures.
|
214 valid, 467 invalid and 214 encoder cases, zero failures.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for the full workflow, and
|
|||||||
|
|
||||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||||
- [docs/API.md](docs/API.md): the API reference, decoding and encoding rules
|
- [docs/API.md](docs/API.md): the API reference, decoding and encoding rules
|
||||||
- [docs/CLI.md](docs/CLI.md): the interpres-decode toml-test adapter and validator
|
- [docs/CLI.md](docs/CLI.md): the interpres-decode adapter and validator
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
// Command interpres-decode is the toml-test harness adapter and a TOML
|
// Command interpres-decode is the toml-test harness adapter and a TOML
|
||||||
// validator. Without flags it reads a TOML document from standard input and
|
// validator. Without flags it reads a TOML document from standard input and
|
||||||
// writes the toml-test "tagged JSON" representation to standard output. With
|
// writes the toml-test "tagged JSON" representation to standard output. With
|
||||||
// -validate it checks the named documents, or standard input when none are
|
// -encode it is the reverse: it reads tagged JSON and writes the TOML document
|
||||||
// named, and exits non-zero on the first invalid one:
|
// 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 -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
|
package main
|
||||||
|
|
||||||
import (
|
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 := flag.NewFlagSet("interpres-decode", flag.ContinueOnError)
|
||||||
fs.SetOutput(stderr)
|
fs.SetOutput(stderr)
|
||||||
validate := fs.Bool("validate", false, "validate the documents instead of emitting tagged JSON")
|
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 err := fs.Parse(args); err != nil {
|
||||||
if errors.Is(err, flag.ErrHelp) {
|
if errors.Is(err, flag.ErrHelp) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
if *validate && *encode {
|
||||||
|
fmt.Fprintln(stderr, "interpres-decode: -validate and -encode cannot be combined")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
if *validate {
|
if *validate {
|
||||||
return validatePaths(fs.Args(), stdin, stderr)
|
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")
|
fmt.Fprintln(stderr, "interpres-decode: the adapter mode takes no arguments; name files with -validate")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
if *encode {
|
||||||
|
return encodeJSON(stdin, stdout, stderr)
|
||||||
|
}
|
||||||
data, err := io.ReadAll(stdin)
|
data, err := io.ReadAll(stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, "read stdin:", err)
|
fmt.Fprintln(stderr, "read stdin:", err)
|
||||||
@@ -109,6 +119,187 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
|
|||||||
return 0
|
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
|
// 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
|
// 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
|
// {"type", "value"} object. An error is returned for value types the encoder
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -283,3 +284,165 @@ func TestUnknownFlagReturnsTwo(t *testing.T) {
|
|||||||
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
|
t.Fatalf("Run returned %d, want 2; stderr = %q", code, stderr.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- encoder mode ----------------------------------------------------------
|
||||||
|
|
||||||
|
func TestRunEncoderScalars(t *testing.T) {
|
||||||
|
in := `{
|
||||||
|
"s": {"type": "string", "value": "quote \" and backslash \\"},
|
||||||
|
"nl": {"type": "string", "value": "line1\nline2"},
|
||||||
|
"i": {"type": "integer", "value": "-9223372036854775808"},
|
||||||
|
"g": {"type": "float", "value": "1.5"},
|
||||||
|
"f": {"type": "float", "value": "inf"},
|
||||||
|
"b": {"type": "bool", "value": "false"},
|
||||||
|
"dt": {"type": "datetime", "value": "1979-05-27T07:32:00-07:00"},
|
||||||
|
"ldt": {"type": "datetime-local", "value": "1979-05-27T07:32:00"},
|
||||||
|
"ld": {"type": "date-local", "value": "1979-05-27"},
|
||||||
|
"lt": {"type": "time-local", "value": "07:32:00.999"}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := Run([]string{"-encode"}, strings.NewReader(in), &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
want := "b = false\n" +
|
||||||
|
"dt = 1979-05-27T07:32:00-07:00\n" +
|
||||||
|
"f = inf\n" +
|
||||||
|
"g = 1.5\n" +
|
||||||
|
"i = -9223372036854775808\n" +
|
||||||
|
"ld = 1979-05-27\n" +
|
||||||
|
"ldt = 1979-05-27T07:32:00\n" +
|
||||||
|
"lt = 07:32:00.999000000\n" +
|
||||||
|
"nl = \"line1\\nline2\"\n" +
|
||||||
|
"s = \"quote \\\" and backslash \\\\\"\n"
|
||||||
|
if stdout.String() != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", stdout.String(), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunEncoderNested(t *testing.T) {
|
||||||
|
in := `{
|
||||||
|
"tbl": {"x": {"type": "bool", "value": "true"},
|
||||||
|
"sub": {"y": {"type": "integer", "value": "1"}}},
|
||||||
|
"items": [{"n": {"type": "string", "value": "a"}},
|
||||||
|
{"n": {"type": "string", "value": "b"}}],
|
||||||
|
"list": [{"type": "integer", "value": "1"}, {"type": "string", "value": "two"}],
|
||||||
|
"emptyTbl": {},
|
||||||
|
"emptyArr": []
|
||||||
|
}
|
||||||
|
`
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := Run([]string{"-encode"}, strings.NewReader(in), &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
want := "emptyArr = []\n" +
|
||||||
|
"list = [1, \"two\"]\n" +
|
||||||
|
"\n[emptyTbl]\n" +
|
||||||
|
"\n[tbl]\nx = true\n" +
|
||||||
|
"\n[tbl.sub]\ny = 1\n" +
|
||||||
|
"\n[[items]]\nn = \"a\"\n" +
|
||||||
|
"\n[[items]]\nn = \"b\"\n"
|
||||||
|
if stdout.String() != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", stdout.String(), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunEncoderFloatTagDecides(t *testing.T) {
|
||||||
|
// A float with no fraction is described by a bare integer literal, so the
|
||||||
|
// tag decides the type; the output must stay a float.
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
in := `{"whole": {"type": "float", "value": "1"}, "exp": {"type": "float", "value": "5e+22"}}`
|
||||||
|
code := Run([]string{"-encode"}, strings.NewReader(in), &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
if want := "exp = 5e+22\nwhole = 1.0\n"; stdout.String() != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", stdout.String(), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunEncoderRejectsBadInput(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"not-json", "not json", "decode JSON"},
|
||||||
|
{"top-level-array", `[{"type": "integer", "value": "1"}]`, "must be a JSON object"},
|
||||||
|
{"untagged-scalar", `{"x": 1}`, "unsupported JSON value"},
|
||||||
|
{"literal-mismatch", `{"x": {"type": "integer", "value": "1.5"}}`, "parsed as float64"},
|
||||||
|
{"offset-for-local", `{"x": {"type": "datetime-local", "value": "1979-05-27T07:32:00Z"}}`, "parsed as time.Time"},
|
||||||
|
{"bad-literal", `{"x": {"type": "date-local", "value": "nope"}}`, "date-local"},
|
||||||
|
{"smuggled-statement", `{"x": {"type": "integer", "value": "1\nx = 2"}}`, "not a single bare value"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := Run([]string{"-encode"}, strings.NewReader(c.in), &stdout, &stderr)
|
||||||
|
if code != 2 {
|
||||||
|
t.Errorf("%s: Run returned %d, want 2; stderr = %q", c.name, code, stderr.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), c.want) {
|
||||||
|
t.Errorf("%s: stderr = %q, want it to mention %q", c.name, stderr.String(), c.want)
|
||||||
|
}
|
||||||
|
if stdout.Len() != 0 {
|
||||||
|
t.Errorf("%s: stdout should be empty, got %q", c.name, stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunEncoderFlagConflicts(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
if code := Run([]string{"-encode", "-validate"}, strings.NewReader(""), &stdout, &stderr); code != 2 {
|
||||||
|
t.Errorf("Run returned %d, want 2 for the two modes together", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "cannot be combined") {
|
||||||
|
t.Errorf("stderr = %q, want it to explain the conflict", stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.Reset()
|
||||||
|
stderr.Reset()
|
||||||
|
if code := Run([]string{"-encode", "file.json"}, strings.NewReader(""), &stdout, &stderr); code != 2 {
|
||||||
|
t.Errorf("Run returned %d, want 2 for an argument", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeAfterDecodeRoundTrip(t *testing.T) {
|
||||||
|
doc := `title = "x"
|
||||||
|
flt = 1.5
|
||||||
|
whole = 7.0
|
||||||
|
big = 9223372036854775807
|
||||||
|
when = 1979-05-27T07:32:00-07:00
|
||||||
|
day = 1979-05-27
|
||||||
|
clock = 07:32:00.999
|
||||||
|
list = [1, "two"]
|
||||||
|
multi = "a\nb"
|
||||||
|
|
||||||
|
[tbl]
|
||||||
|
x = true
|
||||||
|
|
||||||
|
[[items]]
|
||||||
|
n = "a"
|
||||||
|
`
|
||||||
|
var tagged, stderr bytes.Buffer
|
||||||
|
if code := Run(nil, strings.NewReader(doc), &tagged, &stderr); code != 0 {
|
||||||
|
t.Fatalf("decode returned %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
var out bytes.Buffer
|
||||||
|
if code := Run([]string{"-encode"}, bytes.NewReader(tagged.Bytes()), &out, &stderr); code != 0 {
|
||||||
|
t.Fatalf("encode returned %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
want, err := interpres.Parse([]byte(doc))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse of the original: %v", err)
|
||||||
|
}
|
||||||
|
got, err := interpres.Parse(out.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse of the encoder output (%q): %v", out.String(), err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, got) {
|
||||||
|
t.Errorf("round trip changed the document:\noriginal: %#v\nencoded: %#v\noutput: %q", want, got, out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ source tree; nothing is aspirational.
|
|||||||
|
|
||||||
interpres is one public library package, one command, and one example. The
|
interpres is one public library package, one command, and one example. The
|
||||||
library implements the whole of TOML 1.1, decoding and encoding, in the
|
library implements the whole of TOML 1.1, decoding and encoding, in the
|
||||||
standard library alone; the command wraps the parser for the toml-test
|
standard library alone; the command wraps the parser and the encoder for the
|
||||||
compliance harness, against which it stands at 214 valid and 467 invalid cases
|
toml-test compliance harness, against which it stands at 214 valid, 467 invalid
|
||||||
with zero failures; the example demonstrates the API.
|
and 214 encoder cases with zero failures; the example demonstrates the API.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -36,7 +36,7 @@ strict validation.
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `.` (package `interpres`) | The whole library. `interpres.go` declares the exported surface (`Parse`, `Unmarshal`, `Marshal`, the `*Context` variants, `Decoder`, `Encoder`, `Marshaler`, `Unmarshaler`, `SyntaxError`, the local date-time types); everything below it is unexported. |
|
| `.` (package `interpres`) | The whole library. `interpres.go` declares the exported surface (`Parse`, `Unmarshal`, `Marshal`, the `*Context` variants, `Decoder`, `Encoder`, `Marshaler`, `Unmarshaler`, `SyntaxError`, the local date-time types); everything below it is unexported. |
|
||||||
| `cmd/interpres-decode` | The toml-test adapter. Reads TOML on stdin, writes tagged JSON on stdout. Owns no parsing logic. |
|
| `cmd/interpres-decode` | The toml-test adapter, both directions. Reads TOML on stdin, writes tagged JSON on stdout; with `-encode` it reads tagged JSON and writes TOML. Owns no parsing logic and no emission logic. |
|
||||||
| `examples/basic` | A runnable tour of the API. Documentation in executable form, not part of the library. |
|
| `examples/basic` | A runnable tour of the API. Documentation in executable form, not part of the library. |
|
||||||
|
|
||||||
Inside the library package, one file owns one concern:
|
Inside the library package, one file owns one concern:
|
||||||
|
|||||||
+40
-12
@@ -1,8 +1,9 @@
|
|||||||
# Command line
|
# Command line
|
||||||
|
|
||||||
The reference below is taken from the program itself. `interpres-decode` is
|
The reference below is taken from the program itself. `interpres-decode` is
|
||||||
the toml-test harness adapter, and it also validates documents. Install it
|
the toml-test harness adapter in both directions, decoding TOML into tagged
|
||||||
with Go itself, no release assets involved:
|
JSON and encoding tagged JSON back into TOML, and it also validates documents.
|
||||||
|
Install it with Go itself, no release assets involved:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install sourcedock.dev/petrbalvin/interpres/v2/cmd/interpres-decode@latest
|
go install sourcedock.dev/petrbalvin/interpres/v2/cmd/interpres-decode@latest
|
||||||
@@ -12,14 +13,20 @@ go install sourcedock.dev/petrbalvin/interpres/v2/cmd/interpres-decode@latest
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
interpres-decode [flags]
|
interpres-decode [flags]
|
||||||
|
interpres-decode -encode
|
||||||
interpres-decode -validate [file ...]
|
interpres-decode -validate [file ...]
|
||||||
```
|
```
|
||||||
|
|
||||||
Without `-validate` the program is the toml-test adapter: it takes no
|
Without `-validate` or `-encode` the program is the decoding half of the
|
||||||
arguments, reads one TOML document from stdin, and writes the toml-test
|
toml-test adapter: it takes no arguments, reads one TOML document from stdin,
|
||||||
tagged-JSON form to stdout. Build it locally with `just build`, which
|
and writes the toml-test tagged-JSON form to stdout. Build it locally with
|
||||||
compiles it into `bin/interpres-decode`, or run it straight from the module
|
`just build`, which compiles it into `bin/interpres-decode`, or run it
|
||||||
directory with `just run`.
|
straight from the module directory with `just run`.
|
||||||
|
|
||||||
|
With `-encode` the direction is reversed: the program reads a tagged-JSON
|
||||||
|
description from stdin and writes the TOML document it describes to stdout,
|
||||||
|
which is the shape toml-test expects of an encoder command. It takes no
|
||||||
|
arguments either, and `-validate` and `-encode` cannot be combined.
|
||||||
|
|
||||||
With `-validate` the program parses each named file instead, or stdin when no
|
With `-validate` the program parses each named file instead, or stdin when no
|
||||||
file is named, and prints one line per invalid document to stderr. It is
|
file is named, and prints one line per invalid document to stderr. It is
|
||||||
@@ -31,15 +38,16 @@ means stdin.
|
|||||||
| Flag | Effect |
|
| Flag | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `-validate` | validate the documents instead of emitting tagged JSON |
|
| `-validate` | validate the documents instead of emitting tagged JSON |
|
||||||
|
| `-encode` | read tagged JSON from stdin and write TOML instead |
|
||||||
| `-h` | print the usage |
|
| `-h` | print the usage |
|
||||||
|
|
||||||
## Exit codes
|
## Exit codes
|
||||||
|
|
||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `0` | adapter: the document parsed and the tagged JSON was written; validate: every document parsed |
|
| `0` | adapter: the document parsed and the tagged JSON was written; encode: the TOML was written; validate: every document parsed |
|
||||||
| `1` | adapter: parse error; validate: at least one document is invalid |
|
| `1` | adapter: parse error; validate: at least one document is invalid |
|
||||||
| `2` | a usage error, a read failure, or a value with no tagged representation |
|
| `2` | a usage error, a read failure, malformed tagged JSON, or a value with no TOML representation |
|
||||||
|
|
||||||
## Wire format
|
## Wire format
|
||||||
|
|
||||||
@@ -66,6 +74,14 @@ wrapped in an object with a `type` and a `value`:
|
|||||||
| local date | `date-local` | `1979-05-27` |
|
| local date | `date-local` | `1979-05-27` |
|
||||||
| local time | `time-local` | `07:32:00.999999` |
|
| local time | `time-local` | `07:32:00.999999` |
|
||||||
|
|
||||||
|
The `-encode` mode reads exactly this form back. Two properties of it are
|
||||||
|
worth knowing. A float whose value has no fraction and no exponent is written
|
||||||
|
as a bare integer string, `{"type": "float", "value": "1"}`, so there the tag
|
||||||
|
decides the type and not the literal. And the form cannot tell an array of
|
||||||
|
tables from a value array of inline tables, so the adapter writes the header
|
||||||
|
form, `[[a]]`, for an array whose every element is a JSON object; a mixed
|
||||||
|
array keeps the value form.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Echo a small document through the adapter:
|
Echo a small document through the adapter:
|
||||||
@@ -78,8 +94,18 @@ port = 9090
|
|||||||
' | ./bin/interpres-decode
|
' | ./bin/interpres-decode
|
||||||
```
|
```
|
||||||
|
|
||||||
The output is the equivalent value tree as one JSON object. Validate the
|
The output is the equivalent value tree as one JSON object. Turn a description
|
||||||
TOML files of another repository in CI:
|
back into TOML with `-encode`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo '{"title": {"type": "string", "value": "hello"}}' | ./bin/interpres-decode -encode
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
title = "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate the TOML files of another repository in CI:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
interpres-decode -validate config.toml deploy/example.toml
|
interpres-decode -validate config.toml deploy/example.toml
|
||||||
@@ -101,5 +127,7 @@ just toml-test
|
|||||||
```
|
```
|
||||||
|
|
||||||
That recipe needs the `toml-test` binary on `PATH`, installed with
|
That recipe needs the `toml-test` binary on `PATH`, installed with
|
||||||
`go install github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.2.0`. The full
|
`go install github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.2.0`. It runs
|
||||||
|
the suite in both directions: the decoder against the valid and invalid
|
||||||
|
corpora, and the encoder against the tagged JSON of the valid one. The full
|
||||||
reference for the library itself is [API.md](API.md).
|
reference for the library itself is [API.md](API.md).
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ prints the same list.
|
|||||||
| `just run` | `go run ./cmd/interpres-decode`, reads TOML from stdin |
|
| `just run` | `go run ./cmd/interpres-decode`, reads TOML from stdin |
|
||||||
| `just dev` | the same run, for iterating |
|
| `just dev` | the same run, for iterating |
|
||||||
| `just example` | `go run ./examples/basic`, the usage tour |
|
| `just example` | `go run ./examples/basic`, the usage tour |
|
||||||
| `just toml-test` | builds the adapter and runs the official toml-test compliance suite against it |
|
| `just toml-test` | builds the adapter and runs the official toml-test compliance suite against it, decoder and encoder |
|
||||||
| `just coverage-html` | `just test`, then `go tool cover -html` into `coverage.html` |
|
| `just coverage-html` | `just test`, then `go tool cover -html` into `coverage.html` |
|
||||||
| `just install` | builds, then copies the binary into `~/.local/bin` (`BINDIR` overrides) |
|
| `just install` | builds, then copies the binary into `~/.local/bin` (`BINDIR` overrides) |
|
||||||
| `just uninstall` | removes the installed binary |
|
| `just uninstall` | removes the installed binary |
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ run:
|
|||||||
dev:
|
dev:
|
||||||
go run -buildvcs=true {{package}}
|
go run -buildvcs=true {{package}}
|
||||||
|
|
||||||
# Runs the official toml-test compliance suite against the built adapter; toml-test must be on PATH (go install github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.2.0); not standard because no canonical recipe covers a domain compliance suite.
|
# Runs the official toml-test compliance suite in both directions, decoder and encoder, against the built adapter; toml-test must be on PATH (go install github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.2.0); not standard because no canonical recipe covers a domain compliance suite.
|
||||||
toml-test: build
|
toml-test: build
|
||||||
toml-test test -decoder=bin/interpres-decode -toml=1.1
|
toml-test test -decoder=bin/interpres-decode -encoder='bin/interpres-decode -encode' -toml=1.1
|
||||||
|
|
||||||
# Coverage report as an HTML map from the gate's profile; not standard because the gate needs only the numeric floor, and a browser artefact is exploration, not a gate.
|
# Coverage report as an HTML map from the gate's profile; not standard because the gate needs only the numeric floor, and a browser artefact is exploration, not a gate.
|
||||||
coverage-html: test
|
coverage-html: test
|
||||||
|
|||||||
Reference in New Issue
Block a user