about summary refs log tree commit diff
path: root/nix/mergePatch/default.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;
}