feat(std/json-patch): JSON Patch (RFC 6902) with atomic apply #584

Merged
argus merged 2 commits from loop/581-json-patch into main 2026-10-29 22:15:56 +00:00
Collaborator

Closes #581

Task

Step 2 of #81: std/json-patch, JSON Patch (RFC 6902) on JsonValue, with the public json-patch-tests suite run in full.

What and why

  • New package std/json-patch: JsonPatch (a list of PatchOperation), PatchOperation (add, remove, replace, move, copy, test), PatchError and PatchProblem.
  • JsonPatch reads from and writes to the RFC array through Encode and Decode; a decoding error names the position of the operation.
  • apply is atomic and never changes the document it is given. The first failing operation gives a PatchError (position, operation, pointer, kind, detail).
  • Semantics per RFC: add inserts into arrays (index up to the length, or -; a larger index is an error) and replaces the document at ""; remove and replace need an existing target; move is remove then add and refuses to move into its own child; copy is get then add; test compares as section 4.6 says.
  • Docs page docs/standard-library/json-patch.md, index entry, ROADMAP row (RFC 6902 done, RFC 7396 open), skills/ regenerated.

Decision for review: Decoder.untyped

JsonValue had Encode but no Decode, so a patch (value is any JSON) could not be decoded through the pull vocabulary. I added a defaulted method Decoder.untyped() (answers a DecodeError unless a format overrides it), ValueDecoder.untyped() (hands over the buffered EncodedValue) and extend JsonValue with Decode. Every buffering format gets it; a format that needs the type keeps the default. This also lets a record have a JsonValue field. It has its own tests in std/json/tests/untyped.test.trb.

Suite and exclusions

  • tests/suite-cases.trb is generated from tests.json and spec_tests.json at commit 2a928f9044aad35c74e2788d498bcf2c6b91adea, 112 cases, one test per case in suite.test.trb.
  • The suite is under the Apache License 2.0 (its README), not MIT as the issue said; the notice in the file is Apache's.
  • The suite has no cases that need arbitrary precision (the generator asserts that), so no case is excluded for that.
  • The suite marks four cases disabled. Two are valid and run like any other. Of the two that expect a refusal for an op member written twice, spec_tests #13 passes anyway (the kept member is a remove of a member that is not there). tests #85 cannot be refused: a JSON object is read into a map that keeps one member per name, so the patch is read as a valid move. It is the one exclusion, with its reason; the test still applies the patch, so the exclusion fails loudly if a reader ever learns to refuse repeated members.

Test

  • Locally with a nightly torb: std/json-patch/tests/json-patch.test.trb 49 passed, suite.test.trb 115 passed, std/json/tests/untyped.test.trb 4 passed; torb format on every touched file; torb docs source on the new package; the two examples of the page run with the printed output.
  • sh tools/check-commits.sh origin/main: clean.
  • sh tools/gates.sh a: forge CI (not run locally).
Closes #581 ## Task Step 2 of #81: `std/json-patch`, JSON Patch (RFC 6902) on `JsonValue`, with the public json-patch-tests suite run in full. ## What and why - New package `std/json-patch`: `JsonPatch` (a list of `PatchOperation`), `PatchOperation` (add, remove, replace, move, copy, test), `PatchError` and `PatchProblem`. - `JsonPatch` reads from and writes to the RFC array through `Encode` and `Decode`; a decoding error names the position of the operation. - `apply` is atomic and never changes the document it is given. The first failing operation gives a `PatchError` (position, operation, pointer, kind, detail). - Semantics per RFC: `add` inserts into arrays (index up to the length, or `-`; a larger index is an error) and replaces the document at `""`; `remove` and `replace` need an existing target; `move` is remove then add and refuses to move into its own child; `copy` is get then add; `test` compares as section 4.6 says. - Docs page `docs/standard-library/json-patch.md`, index entry, ROADMAP row (RFC 6902 done, RFC 7396 open), `skills/` regenerated. ### Decision for review: `Decoder.untyped` `JsonValue` had `Encode` but no `Decode`, so a patch (`value` is any JSON) could not be decoded through the pull vocabulary. I added a defaulted method `Decoder.untyped()` (answers a `DecodeError` unless a format overrides it), `ValueDecoder.untyped()` (hands over the buffered `EncodedValue`) and `extend JsonValue with Decode`. Every buffering format gets it; a format that needs the type keeps the default. This also lets a record have a `JsonValue` field. It has its own tests in `std/json/tests/untyped.test.trb`. ### Suite and exclusions - `tests/suite-cases.trb` is generated from `tests.json` and `spec_tests.json` at commit `2a928f9044aad35c74e2788d498bcf2c6b91adea`, 112 cases, one test per case in `suite.test.trb`. - The suite is under the **Apache License 2.0** (its README), not MIT as the issue said; the notice in the file is Apache's. - The suite has no cases that need arbitrary precision (the generator asserts that), so no case is excluded for that. - The suite marks four cases `disabled`. Two are valid and run like any other. Of the two that expect a refusal for an `op` member written twice, `spec_tests` #13 passes anyway (the kept member is a `remove` of a member that is not there). `tests` #85 cannot be refused: a JSON object is read into a map that keeps one member per name, so the patch is read as a valid `move`. It is the one exclusion, with its reason; the test still applies the patch, so the exclusion fails loudly if a reader ever learns to refuse repeated members. ## Test - Locally with a nightly `torb`: `std/json-patch/tests/json-patch.test.trb` 49 passed, `suite.test.trb` 115 passed, `std/json/tests/untyped.test.trb` 4 passed; `torb format` on every touched file; `torb docs source` on the new package; the two examples of the page run with the printed output. - `sh tools/check-commits.sh origin/main`: clean. - `sh tools/gates.sh a`: forge CI (not run locally).
JsonPatch is a list of PatchOperation (add, remove, replace, move, copy, test) that reads from and writes to the
JSON array of RFC 6902 through Encode and Decode, and apply carries it out on a JsonValue. It is atomic: the first
operation that fails gives a PatchError (position, operation, pointer, kind, detail) and no document.

JsonValue now reads itself through Decode, so a patch decodes from any format that can hand over a value without a
type. Decoder gets the defaulted method untyped for that; ValueDecoder, which every buffering format shares, answers
it, and a format that needs the type to read keeps the default, a DecodeError.

Hand-written tests cover each operation and each error path, and an apply whose third operation fails and leaves the
input unchanged. The docs page lists the types, operations, atomicity, error values and non-goals.
test(std/json-patch): run the public json-patch-tests suite
Some checks failed
ci / commit messages (pull_request) Successful in 4s
ci / what the change touches (pull_request) Successful in 4s
ci / the VS Code extension (editors/vscode) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 6m12s
ci / tier B (linux-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 3m30s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 3m5s
ci / every host emits the same C (pull_request) Successful in 8m29s
ci / tier A (linux-x64) (pull_request) Failing after 33m17s
ci / gates (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
703b9f915e
The cases of tests.json and spec_tests.json of the json-patch-tests suite, at a pinned commit and with the suite's
Apache-2.0 notice, are generated into tests/suite-cases.trb; suite.test.trb runs one test per case. The one case the
suite disables that expects a refusal this reader cannot give (an op member written twice, which a map keeps once) is an
exclusion with its reason and a check that it cannot go stale. The ROADMAP marks JSON Patch started: RFC 6902 done,
RFC 7396 open.
orion requested review from argus 2026-10-29 14:01:48 +00:00
orion force-pushed loop/581-json-patch from 703b9f915e
Some checks failed
ci / commit messages (pull_request) Successful in 4s
ci / what the change touches (pull_request) Successful in 4s
ci / the VS Code extension (editors/vscode) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 6m12s
ci / tier B (linux-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 3m30s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 3m5s
ci / every host emits the same C (pull_request) Successful in 8m29s
ci / tier A (linux-x64) (pull_request) Failing after 33m17s
ci / gates (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
to 194907350a
All checks were successful
ci / commit messages (pull_request) Successful in 6s
ci / what the change touches (pull_request) Successful in 6s
ci / the VS Code extension (editors/vscode) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 11s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 5m50s
ci / tier B (linux-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m56s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m59s
ci / every host emits the same C (pull_request) Successful in 8m15s
ci / tier A (linux-x64) (pull_request) Successful in 30m36s
ci / pack the seed (pull_request) Successful in 9s
ci / archives (pull_request) Has been skipped
ci / gates (pull_request) Successful in 0s
2026-10-29 14:54:33 +00:00
Compare
argus approved these changes 2026-10-29 22:15:54 +00:00
argus left a comment

Checklist on 1949073:

  • Tests: ok. 49 unit tests, the full json-patch-tests suite (112 cases, one test each, the single exclusion tests #85 is justified, applied anyway and guarded by a test), 4 tests for Decoder.untyped/JsonValue Decode. CI success on this head (tier A, gates, bootstrap, pdisc, commit messages).
  • Correctness: ok. Add/remove/replace/move/copy/test follow RFC 6902 (array insert up to length or -, move into own child refused, move onto itself a no-op after the existence check, test per 4.6, whole-document remove refused, index token validation incl. leading zeros and overlong digits). apply works on immutable values, so it is atomic.
  • Scope: ok. Matches #581 / step 2 of #81; the Decoder.untyped addition is the minimum needed to decode value, stated and tested in the PR.
  • Security/secrets: ok. No secrets, no new dependencies; Apache-2.0 notice for the vendored suite is in place.
  • Data: ok. Additive; the new Decoder method has a default, so existing formats (e.g. Yaml) keep working and are tested to answer a DecodeError.
  • Docs: ok. New page, index, encoding.md, json.md, ROADMAP, regenerated skills; pdisc green.
  • Architecture: ok. Defaulted trait method on a public trait, documented in docs/standard-library/encoding.md.

Non-blocking:

  • docs/design/ENCODING.md (Decoder listing around line 331) does not mention untyped(); add it in a follow-up so the design record matches the library.
Checklist on 1949073: - Tests: ok. 49 unit tests, the full json-patch-tests suite (112 cases, one test each, the single exclusion tests #85 is justified, applied anyway and guarded by a test), 4 tests for `Decoder.untyped`/`JsonValue` Decode. CI success on this head (tier A, gates, bootstrap, pdisc, commit messages). - Correctness: ok. Add/remove/replace/move/copy/test follow RFC 6902 (array insert up to length or `-`, move into own child refused, move onto itself a no-op after the existence check, `test` per 4.6, whole-document remove refused, index token validation incl. leading zeros and overlong digits). `apply` works on immutable values, so it is atomic. - Scope: ok. Matches #581 / step 2 of #81; the `Decoder.untyped` addition is the minimum needed to decode `value`, stated and tested in the PR. - Security/secrets: ok. No secrets, no new dependencies; Apache-2.0 notice for the vendored suite is in place. - Data: ok. Additive; the new Decoder method has a default, so existing formats (e.g. Yaml) keep working and are tested to answer a DecodeError. - Docs: ok. New page, index, encoding.md, json.md, ROADMAP, regenerated skills; pdisc green. - Architecture: ok. Defaulted trait method on a public trait, documented in docs/standard-library/encoding.md. Non-blocking: - docs/design/ENCODING.md (Decoder listing around line 331) does not mention `untyped()`; add it in a follow-up so the design record matches the library.
argus merged commit 16fe80aef5 into main 2026-10-29 22:15:56 +00:00
argus deleted branch loop/581-json-patch 2026-10-29 22:15:56 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language!584
No description provided.