RFC 6901 (JSON Pointer) and RFC 6902 (JSON Patch) in pure Standard ML, built
on this fleet's sml-json
Json.json AST.
No FFI, no threads, no clock, no randomness: the same inputs always produce the same outputs under MLton and Poly/ML. All patch application is purely functional -- applying an operation never mutates the input document, it builds a new tree sharing whatever subtrees weren't on the path to the edited location.
- JSON Pointer (
pointerGet): resolves a"/foo/0/bar"-style path against aJson.jsondocument, with~1->/and~0->~unescaping. - JSON Patch (
parseOps+apply): parses a JSON array of operation objects (add,remove,replace,move,copy,test) and applies them to a document in sequence.applyis all-or-nothing: if any operation fails -- including atestwhose value doesn't match -- the whole patch is discarded andNONEcomes back, never a partial result.
This is a runtime dependency, not test-only: sml-json's Json.json type
is this library's document type. lib/github.com/sjqtentacles/sml-json/
and its own dependency lib/github.com/sjqtentacles/sml-parsec/ are
vendored copies of those repos' sources (the same layout sml-forms uses),
and sml.pkg declares github.com/sjqtentacles/sml-json in require.
signature JSONPATCH =
sig
val pointerGet : Json.json -> string -> Json.json option
datatype patchOp =
Add of {path : string, value : Json.json}
| Remove of {path : string}
| Replace of {path : string, value : Json.json}
| Move of {from : string, path : string}
| Copy of {from : string, path : string}
| Test of {path : string, value : Json.json}
val parseOps : Json.json -> patchOp list option
val apply : Json.json -> patchOp list -> Json.json option
endNote on naming: the task/RFC-shaped API sketch this library follows names
the operation datatype op, but op is an SML reserved word (op +
prefix notation) and cannot be a type identifier -- it's named patchOp
here instead.
parseOps rejects a patch document if the top level isn't a JSON array, an
element isn't an object, "op" is missing/unrecognized, or a field
required by that op (path, value, from) is missing or the wrong JSON
type (path/from must be JSON strings). apply rejects an operation
whose parent path doesn't exist or isn't the right container kind, an
out-of-bounds or malformed (non-numeric, leading-zero) array index,
move/copy from a nonexistent location, a move where from is a
proper prefix of path (moving a value into one of its own children), or
a test whose value doesn't deep-equal the current value (object member
order is not significant for that comparison, per RFC 8259; array order
is).
val doc = valOf (case Json.parseJson "{\"a\":1}" of CharParsec.Ok v => SOME v | _ => NONE)
val patch = valOf (case Json.parseJson "[{\"op\":\"add\",\"path\":\"/b\",\"value\":2}]"
of CharParsec.Ok v => SOME v | _ => NONE)
val SOME ops = Jsonpatch.parseOps patch
val SOME result = Jsonpatch.apply doc ops (* {"a":1,"b":2} *)Running examples/demo.sml with make example prints:
Before:
{"name":"Widget","priceCents":999,"tags":["new"],"stock":{"warehouse":12}}
Patch:
[{"op":"replace","path":"/priceCents","value":1250},{"op":"add","path":"/tags/-","value":"sale"},{"op":"remove","path":"/stock/warehouse"},{"op":"add","path":"/stock/online","value":40},{"op":"copy","from":"/name","path":"/label"},{"op":"test","path":"/tags/0","value":"new"}]
After:
{
"name": "Widget",
"priceCents": 1250,
"tags": [
"new",
"sale"
],
"stock": {
"online": 40
},
"label": "Widget"
}
Pointer lookups against the result:
/priceCents = 1250
/tags = ["new","sale"]
/stock = {"online":40}
/label = "Widget"
A failing `test` aborts the whole patch:
apply -> NONE (whole patch discarded, doc unchanged)
Requires MLton and/or Poly/ML.
make test # build + run the suite under MLton
make test-poly # run the suite under Poly/ML
make all-tests # both + byte-identical gate
make example # build + run the demo
make cleansmlpkg add github.com/sjqtentacles/sml-jsonpatch
smlpkg syncReference lib/github.com/sjqtentacles/sml-jsonpatch/jsonpatch.mlb from
your own .mlb (MLton / MLKit), or feed sources.mlb to tools/polybuild
(Poly/ML).
sml.pkg smlpkg manifest (requires sml-json)
Makefile MLton + Poly/ML targets
.github/workflows/ci.yml CI: MLton + Poly/ML
lib/github.com/sjqtentacles/sml-jsonpatch/
jsonpatch.sig JSONPATCH signature
jsonpatch.sml pointer resolution, patch parsing, functional apply
sources.mlb ordered source list (pulls in vendored sml-json)
jsonpatch.mlb public basis
lib/github.com/sjqtentacles/sml-json/ vendored (runtime dependency)
lib/github.com/sjqtentacles/sml-parsec/ vendored (sml-json's dependency)
examples/
demo.sml apply a fixed patch sequence to a fixed document
test/
harness.sml shared assertion harness
test.sml RFC 6901 + RFC 6902 vectors (52 checks)
entry.sml / main.sml
tools/polybuild Poly/ML build wrapper
52 deterministic checks. JSON Pointer: the canonical example document and
lookups from RFC 6901 section 5 ("Examples"), plus malformed-pointer and
out-of-range negative cases. JSON Patch: a representative subset (~35
cases) transcribed from the official
json-patch/json-patch-tests
tests.json, spanning add/remove/replace/move/copy/test,
array index shifting and --append (including at nested depth), leading-zero
and non-numeric index rejection, move's cyclic-move guard, and -- the
easy-to-miss edge case -- a failing test aborting the entire patch
(including operations that had already succeeded earlier in the sequence),
verified both via the expected-document check and directly asserting
apply returns NONE rather than a partial result. Run make all-tests to
verify identical output under both compilers.
MIT. See LICENSE.