193 lines
4.9 KiB
Nix
193 lines
4.9 KiB
Nix
|
{ 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;
|
||
|
}
|