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;
}