{ lib, depot, ... }: /* JSON Merge-Patch for nix Spec: https://tools.ietf.org/html/rfc7396 An algorithm for changing and removing fields in nested objects. For example, given the following original document: { a = "b"; c = { d = "e"; f = "g"; } } Changing the value of `a` and removing `f` can be achieved by merging the patch { a = "z"; c.f = null; } which results in { a = "z"; c = { d = "e"; }; } Pseudo-code: define MergePatch(Target, Patch): if Patch is an Object: if Target is not an Object: Target = {} # Ignore the contents and set it to an empty Object for each Name/Value pair in Patch: if Value is null: if Name exists in Target: remove the Name/Value pair from Target else: Target[Name] = MergePatch(Target[Name], Value) return Target else: return Patch */ let foldlAttrs = op: init: attrs: lib.foldl' op init (lib.mapAttrsToList lib.nameValuePair attrs); mergePatch = target: patch: if lib.isAttrs patch then let target' = if lib.isAttrs target then target else {}; in foldlAttrs (acc: patchEl: if patchEl.value == null then removeAttrs acc [ patchEl.name ] else acc // { ${patchEl.name} = mergePatch (acc.${patchEl.name} or "unnused") patchEl.value; }) target' patch else patch; inherit (depot.nix.runTestsuite) runTestsuite it assertEq ; tests = let # example target from the RFC testTarget = { a = "b"; c = { d = "e"; f = "g"; }; }; # example patch from the RFC testPatch = { a = "z"; c.f = null; }; emptyPatch = it "the empty patch returns the original target" [ (assertEq "id" (mergePatch testTarget {}) testTarget) ]; nonAttrs = it "one side is a non-attrset value" [ (assertEq "target is a value means the value is replaced by the patch" (mergePatch 42 testPatch) (mergePatch {} testPatch)) (assertEq "patch is a value means it replaces target alltogether" (mergePatch testTarget 42) 42) ]; rfcExamples = it "the examples from the RFC" [ (assertEq "a subset is deleted and overwritten" (mergePatch testTarget testPatch) { a = "z"; c = { d = "e"; }; }) (assertEq "a more complicated example from the example section" (mergePatch { title = "Goodbye!"; author = { givenName = "John"; familyName = "Doe"; }; tags = [ "example" "sample" ]; content = "This will be unchanged"; } { title = "Hello!"; phoneNumber = "+01-123-456-7890"; author.familyName = null; tags = [ "example" ]; }) { title = "Hello!"; phoneNumber = "+01-123-456-7890"; author = { givenName = "John"; }; tags = [ "example" ]; content = "This will be unchanged"; }) ]; rfcTests = let r = index: target: patch: res: (assertEq "test number ${toString index}" (mergePatch target patch) res); in it "the test suite from the RFC" [ (r 1 {"a" = "b";} {"a" = "c";} {"a" = "c";}) (r 2 {"a" = "b";} {"b" = "c";} {"a" = "b"; "b" = "c";}) (r 3 {"a" = "b";} {"a" = null;} {}) (r 4 {"a" = "b"; "b" = "c";} {"a" = null;} {"b" = "c";}) (r 5 {"a" = ["b"];} {"a" = "c";} {"a" = "c";}) (r 6 {"a" = "c";} {"a" = ["b"];} {"a" = ["b"];}) (r 7 {"a" = {"b" = "c";}; } {"a" = {"b" = "d"; "c" = null;};} {"a" = {"b" = "d";};}) (r 8 {"a" = [{"b" = "c";}];} {"a" = [1];} {"a" = [1];}) (r 9 ["a" "b"] ["c" "d"] ["c" "d"]) (r 10 {"a" = "b";} ["c"] ["c"]) (r 11 {"a" = "foo";} null null) (r 12 {"a" = "foo";} "bar" "bar") (r 13 {"e" = null;} {"a" = 1;} {"e" = null; "a" = 1;}) (r 14 [1 2] {"a" = "b"; "c" = null;} {"a" = "b";}) (r 15 {} {"a" = {"bb" = {"ccc" = null;};};} {"a" = {"bb" = {};};}) ]; in runTestsuite "mergePatch" [ emptyPatch nonAttrs rfcExamples rfcTests ]; in { __functor = _: mergePatch; inherit tests; }