Change-Id: Id6a9ecbfb04886e6d96750b1451c29dc3f68154e Reviewed-on: https://cl.tvl.fyi/c/depot/+/2307 Tested-by: BuildkiteCI Reviewed-by: lukegb <lukegb@tvl.fyi>
		
			
				
	
	
		
			186 lines
		
	
	
	
		
			5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
	
		
			5 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;
 | |
| }
 |