Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sml-jsonpatch

CI

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 a Json.json document, 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. apply is all-or-nothing: if any operation fails -- including a test whose value doesn't match -- the whole patch is discarded and NONE comes back, never a partial result.

Vendored dependency

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.

API

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
end

Note 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).

Example

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)

Build & test

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 clean

Installing with smlpkg

smlpkg add github.com/sjqtentacles/sml-jsonpatch
smlpkg sync

Reference lib/github.com/sjqtentacles/sml-jsonpatch/jsonpatch.mlb from your own .mlb (MLton / MLKit), or feed sources.mlb to tools/polybuild (Poly/ML).

Layout

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

Tests

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.

License

MIT. See LICENSE.

About

JSON Patch (RFC 6902) and JSON Pointer (RFC 6901) in pure Standard ML

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages