From bccaf087c8f9b02cdf94058dcfa19586326ddf4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sat, 19 Sep 2026 11:38:19 +0200 Subject: [PATCH] feat(cmd): add the encoder mode to the toml-test adapter Assisted-by: DeepSeek V4.1 Flash --- .gitea/workflows/test.yml | 8 +- CHANGELOG.md | 4 + CONTRIBUTING.md | 4 +- README.md | 4 +- cmd/interpres-decode/main.go | 199 +++++++++++++++++++++++++++++- cmd/interpres-decode/main_test.go | 163 ++++++++++++++++++++++++ docs/ARCHITECTURE.md | 8 +- docs/CLI.md | 52 ++++++-- docs/DEVELOPMENT.md | 2 +- justfile | 4 +- 10 files changed, 418 insertions(+), 30 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 99c9e47..888447c 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -105,6 +105,8 @@ jobs: run: go build -o bin/interpres-decode ./cmd/interpres-decode - name: Compliance suite - # interpres implements TOML 1.1; the mode is pinned so an upstream - # default change cannot silently move the corpus. - run: bin/toml-test test -decoder=bin/interpres-decode -toml=1.1 + # interpres implements TOML 1.1, and the suite runs both directions: the decoder + # on the valid and invalid corpora, the encoder on the tagged JSON of the valid + # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8944182..937c034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, `1h30m0s`, because TOML has no duration type; the decoder reads that string 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9d23e1..3119429 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,8 +45,8 @@ just test formatting pass are three commits, never one. 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 - gate. Parser and decoder changes must also keep the toml-test suite at zero - failures, checked with `just toml-test`. + gate. Parser, decoder and encoder changes must also keep both directions of + the toml-test suite at zero failures, checked with `just toml-test`. 6. Update the documentation when the public API, the configuration or the behaviour changes; the documents move in the same commit as the behaviour they describe. diff --git a/README.md b/README.md index c471173..c70a5b1 100644 --- a/README.md +++ b/README.md @@ -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 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: -214 valid and 467 invalid cases, zero failures. +214 valid, 467 invalid and 214 encoder cases, zero failures. ## 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/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 diff --git a/cmd/interpres-decode/main.go b/cmd/interpres-decode/main.go index 1b1a856..c1a9e9f 100644 --- a/cmd/interpres-decode/main.go +++ b/cmd/interpres-decode/main.go @@ -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 = ` 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 diff --git a/cmd/interpres-decode/main_test.go b/cmd/interpres-decode/main_test.go index b28e6bd..abaf2e5 100644 --- a/cmd/interpres-decode/main_test.go +++ b/cmd/interpres-decode/main_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "os" + "reflect" "strings" "testing" "time" @@ -283,3 +284,165 @@ func TestUnknownFlagReturnsTwo(t *testing.T) { 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()) + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 39b1cb4..855cc22 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,9 +7,9 @@ source tree; nothing is aspirational. 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 -standard library alone; the command wraps the parser for the toml-test -compliance harness, against which it stands at 214 valid and 467 invalid cases -with zero failures; the example demonstrates the API. +standard library alone; the command wraps the parser and the encoder for the +toml-test compliance harness, against which it stands at 214 valid, 467 invalid +and 214 encoder cases with zero failures; the example demonstrates the API. ```mermaid flowchart TD @@ -36,7 +36,7 @@ strict validation. | 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. | -| `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. | Inside the library package, one file owns one concern: diff --git a/docs/CLI.md b/docs/CLI.md index 1a76fd8..ae62acb 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1,8 +1,9 @@ # Command line The reference below is taken from the program itself. `interpres-decode` is -the toml-test harness adapter, and it also validates documents. Install it -with Go itself, no release assets involved: +the toml-test harness adapter in both directions, decoding TOML into tagged +JSON and encoding tagged JSON back into TOML, and it also validates documents. +Install it with Go itself, no release assets involved: ```sh 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 interpres-decode [flags] +interpres-decode -encode interpres-decode -validate [file ...] ``` -Without `-validate` the program is the toml-test adapter: it takes no -arguments, reads one TOML document from stdin, and writes the toml-test -tagged-JSON form to stdout. Build it locally with `just build`, which -compiles it into `bin/interpres-decode`, or run it straight from the module -directory with `just run`. +Without `-validate` or `-encode` the program is the decoding half of the +toml-test adapter: it takes no arguments, reads one TOML document from stdin, +and writes the toml-test tagged-JSON form to stdout. Build it locally with +`just build`, which compiles it into `bin/interpres-decode`, or run it +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 file is named, and prints one line per invalid document to stderr. It is @@ -31,15 +38,16 @@ means stdin. | Flag | Effect | |---|---| | `-validate` | validate the documents instead of emitting tagged JSON | +| `-encode` | read tagged JSON from stdin and write TOML instead | | `-h` | print the usage | ## Exit codes | 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 | -| `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 @@ -66,6 +74,14 @@ wrapped in an object with a `type` and a `value`: | local date | `date-local` | `1979-05-27` | | 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 Echo a small document through the adapter: @@ -78,8 +94,18 @@ port = 9090 ' | ./bin/interpres-decode ``` -The output is the equivalent value tree as one JSON object. Validate the -TOML files of another repository in CI: +The output is the equivalent value tree as one JSON object. Turn a description +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 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 -`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). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index dbdd1b1..2179b68 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -41,7 +41,7 @@ prints the same list. | `just run` | `go run ./cmd/interpres-decode`, reads TOML from stdin | | `just dev` | the same run, for iterating | | `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 install` | builds, then copies the binary into `~/.local/bin` (`BINDIR` overrides) | | `just uninstall` | removes the installed binary | diff --git a/justfile b/justfile index 0751b64..677ca1a 100644 --- a/justfile +++ b/justfile @@ -92,9 +92,9 @@ run: dev: 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 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-html: test