lib: split merge
out of the toplevel
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
{ lib, ... }@moduleArgs:
|
{ lib, ... }@moduleArgs:
|
||||||
|
|
||||||
rec {
|
let
|
||||||
|
sane-lib = rec {
|
||||||
feeds = import ./feeds.nix moduleArgs;
|
feeds = import ./feeds.nix moduleArgs;
|
||||||
fs = import ./fs.nix moduleArgs;
|
fs = import ./fs.nix moduleArgs;
|
||||||
|
merge = import ./merge.nix ({ inherit sane-lib; } // moduleArgs);
|
||||||
path = import ./path.nix moduleArgs;
|
path = import ./path.nix moduleArgs;
|
||||||
types = import ./types.nix moduleArgs;
|
types = import ./types.nix moduleArgs;
|
||||||
|
|
||||||
|
# re-exports
|
||||||
|
inherit (merge) mergeTopLevel mkTypedMerge;
|
||||||
|
|
||||||
# like `builtins.listToAttrs` but any duplicated `name` throws error on access.
|
# like `builtins.listToAttrs` but any duplicated `name` throws error on access.
|
||||||
# Type: listToDisjointAttrs :: [{ name :: String, value :: Any }] -> AttrSet
|
# Type: listToDisjointAttrs :: [{ name :: String, value :: Any }] -> AttrSet
|
||||||
listToDisjointAttrs = l: flattenAttrsets (builtins.map nameValueToAttrs l);
|
listToDisjointAttrs = l: flattenAttrsets (builtins.map nameValueToAttrs l);
|
||||||
@@ -46,6 +51,10 @@ rec {
|
|||||||
mapToAttrs = f: list: listToDisjointAttrs (builtins.map f list);
|
mapToAttrs = f: list: listToDisjointAttrs (builtins.map f list);
|
||||||
|
|
||||||
# flatten a nested AttrSet into a list of { path = [String]; value } items.
|
# flatten a nested AttrSet into a list of { path = [String]; value } items.
|
||||||
|
# the output contains only non-attr leafs.
|
||||||
|
# so e.g. { a.b = 1; } -> [ { path = ["a" "b"]; value = 1; } ]
|
||||||
|
# but e.g. { a.b = {}; } -> []
|
||||||
|
#
|
||||||
# Type: flattenAttrs :: AttrSet[AttrSet|Any] -> [{ path :: String, value :: Any }]
|
# Type: flattenAttrs :: AttrSet[AttrSet|Any] -> [{ path :: String, value :: Any }]
|
||||||
flattenAttrs = flattenAttrs' [];
|
flattenAttrs = flattenAttrs' [];
|
||||||
flattenAttrs' = path: value: if builtins.isAttrs value then (
|
flattenAttrs' = path: value: if builtins.isAttrs value then (
|
||||||
@@ -59,100 +68,5 @@ rec {
|
|||||||
inherit path value;
|
inherit path value;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
};
|
||||||
# like `mkMerge`, but tries to do normal attribute merging by default and only creates `mkMerge`
|
in sane-lib
|
||||||
# entries at the highest point where paths overlap between items.
|
|
||||||
mergeTopLevel = l:
|
|
||||||
if builtins.length l == 0 then
|
|
||||||
lib.mkMerge []
|
|
||||||
else if builtins.length l == 1 then
|
|
||||||
lib.head l
|
|
||||||
else if builtins.all isAttrsNotMerge l then
|
|
||||||
# merge each toplevel attribute
|
|
||||||
lib.zipAttrsWith (_name: mergeTopLevel) l
|
|
||||||
else
|
|
||||||
lib.mkMerge l;
|
|
||||||
|
|
||||||
# tests that `i` is a normal attrs, and not something make with `lib.mkMerge`.
|
|
||||||
isAttrsNotMerge = i: builtins.isAttrs i && i._type or "" != "merge";
|
|
||||||
|
|
||||||
|
|
||||||
# type-checked `lib.mkMerge`, intended to be usable at the top of a file.
|
|
||||||
# `take` is a function which defines a spec enforced against every item to be merged.
|
|
||||||
# for example:
|
|
||||||
# take = f: { x = f.x; y.z = f.y.z; };
|
|
||||||
# - the output is guaranteed to have an `x` attribute and a `y.z` attribute and nothing else.
|
|
||||||
# - each output is a `lib.mkMerge` of the corresponding paths across the input lists.
|
|
||||||
# - if an item in the input list defines an attr not captured by `f`, this function will throw.
|
|
||||||
#
|
|
||||||
# Type: mkTypedMerge :: (Attrs -> Attrs) -> [Attrs] -> Attrs
|
|
||||||
mkTypedMerge = take: l:
|
|
||||||
let
|
|
||||||
pathsToMerge = findTerminalPaths take [];
|
|
||||||
merged = builtins.map (p: lib.setAttrByPath p (mergeAtPath p l)) pathsToMerge;
|
|
||||||
in
|
|
||||||
assert builtins.all (i: assertTakesEveryAttr take i []) l;
|
|
||||||
flattenAttrsets merged;
|
|
||||||
|
|
||||||
# `take` is as in mkTypedMerge. this function queries which items `take` is interested in.
|
|
||||||
# for example:
|
|
||||||
# take = f: { x = f.x; y.z = f.y.z; };
|
|
||||||
# - for `path == []` we return the toplevel attr names: [ "x" "y"]
|
|
||||||
# - for `path == [ "y" ]` we return [ "z" ]
|
|
||||||
# - for `path == [ "x" ]` or `path == [ "y" "z" ]` we return []
|
|
||||||
#
|
|
||||||
# Type: findSubNames :: (Attrs -> Attrs) -> [String] -> [String]
|
|
||||||
findSubNames = take: path:
|
|
||||||
let
|
|
||||||
# define the current path, but nothing more.
|
|
||||||
curLevel = lib.setAttrByPath path {};
|
|
||||||
# `take` will either set:
|
|
||||||
# - { $path = path } => { $path = {} };
|
|
||||||
# - { $path.next = path.next } => { $path = { next = ?; } }
|
|
||||||
# so, index $path into the output of `take`,
|
|
||||||
# and if it has any attrs that means we're interested in those too.
|
|
||||||
nextLevel = lib.getAttrFromPath path (take curLevel);
|
|
||||||
in
|
|
||||||
builtins.attrNames nextLevel;
|
|
||||||
|
|
||||||
# computes a list of all terminal paths that `take` is interested in,
|
|
||||||
# where each path is a list of attr names to descend to reach that terminal.
|
|
||||||
# Type: findTerminalPaths :: (Attrs -> Attrs) -> [String] -> [[String]]
|
|
||||||
findTerminalPaths = take: path:
|
|
||||||
let
|
|
||||||
subNames = findSubNames take path;
|
|
||||||
in if subNames == [] then
|
|
||||||
[ path ]
|
|
||||||
else
|
|
||||||
let
|
|
||||||
terminalsPerChild = builtins.map (name: findTerminalPaths take (path ++ [name])) subNames;
|
|
||||||
in
|
|
||||||
lib.concatLists terminalsPerChild;
|
|
||||||
|
|
||||||
# merges all present values for the provided path
|
|
||||||
# Type: mergeAtPath :: [String] -> [Attrs] -> (lib.mkMerge)
|
|
||||||
mergeAtPath = path: l:
|
|
||||||
let
|
|
||||||
itemsToMerge = builtins.filter (lib.hasAttrByPath path) l;
|
|
||||||
in lib.mkMerge (builtins.map (lib.getAttrFromPath path) itemsToMerge);
|
|
||||||
|
|
||||||
# throw if `item` includes any data not wanted by `take`.
|
|
||||||
# this is recursive: `path` tracks the current location being checked.
|
|
||||||
assertTakesEveryAttr = take: item: path:
|
|
||||||
let
|
|
||||||
takesSubNames = findSubNames take path;
|
|
||||||
itemSubNames = findSubNames (_: item) path;
|
|
||||||
unexpectedNames = lib.subtractLists takesSubNames itemSubNames;
|
|
||||||
takesEverySubAttr = builtins.all (name: assertTakesEveryAttr take item (path ++ [name])) itemSubNames;
|
|
||||||
in
|
|
||||||
if takesSubNames == [] then
|
|
||||||
# this happens when the user takes this whole subtree: i.e. *all* subnames are accepted.
|
|
||||||
true
|
|
||||||
else if unexpectedNames != [] then
|
|
||||||
let
|
|
||||||
p = lib.concatStringsSep "." (path ++ lib.sublist 0 1 unexpectedNames);
|
|
||||||
in
|
|
||||||
throw ''unexpected entry: ${p}''
|
|
||||||
else
|
|
||||||
takesEverySubAttr;
|
|
||||||
}
|
|
||||||
|
99
modules/lib/merge.nix
Normal file
99
modules/lib/merge.nix
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{ lib, sane-lib, ... }:
|
||||||
|
|
||||||
|
rec {
|
||||||
|
# like `mkMerge`, but tries to do normal attribute merging by default and only creates `mkMerge`
|
||||||
|
# entries at the highest point where paths overlap between items.
|
||||||
|
mergeTopLevel = l:
|
||||||
|
if builtins.length l == 0 then
|
||||||
|
lib.mkMerge []
|
||||||
|
else if builtins.length l == 1 then
|
||||||
|
lib.head l
|
||||||
|
else if builtins.all isAttrsNotMerge l then
|
||||||
|
# merge each toplevel attribute
|
||||||
|
lib.zipAttrsWith (_name: mergeTopLevel) l
|
||||||
|
else
|
||||||
|
lib.mkMerge l;
|
||||||
|
|
||||||
|
# tests that `i` is a normal attrs, and not something make with `lib.mkMerge`.
|
||||||
|
isAttrsNotMerge = i: builtins.isAttrs i && i._type or "" != "merge";
|
||||||
|
|
||||||
|
|
||||||
|
# type-checked `lib.mkMerge`, intended to be usable at the top of a file.
|
||||||
|
# `take` is a function which defines a spec enforced against every item to be merged.
|
||||||
|
# for example:
|
||||||
|
# take = f: { x = f.x; y.z = f.y.z; };
|
||||||
|
# - the output is guaranteed to have an `x` attribute and a `y.z` attribute and nothing else.
|
||||||
|
# - each output is a `lib.mkMerge` of the corresponding paths across the input lists.
|
||||||
|
# - if an item in the input list defines an attr not captured by `f`, this function will throw.
|
||||||
|
#
|
||||||
|
# Type: mkTypedMerge :: (Attrs -> Attrs) -> [Attrs] -> Attrs
|
||||||
|
mkTypedMerge = take: l:
|
||||||
|
let
|
||||||
|
pathsToMerge = findTerminalPaths take [];
|
||||||
|
merged = builtins.map (p: lib.setAttrByPath p (mergeAtPath p l)) pathsToMerge;
|
||||||
|
in
|
||||||
|
assert builtins.all (i: assertTakesEveryAttr take i []) l;
|
||||||
|
sane-lib.flattenAttrsets merged;
|
||||||
|
|
||||||
|
# `take` is as in mkTypedMerge. this function queries which items `take` is interested in.
|
||||||
|
# for example:
|
||||||
|
# take = f: { x = f.x; y.z = f.y.z; };
|
||||||
|
# - for `path == []` we return the toplevel attr names: [ "x" "y"]
|
||||||
|
# - for `path == [ "y" ]` we return [ "z" ]
|
||||||
|
# - for `path == [ "x" ]` or `path == [ "y" "z" ]` we return []
|
||||||
|
#
|
||||||
|
# Type: findSubNames :: (Attrs -> Attrs) -> [String] -> [String]
|
||||||
|
findSubNames = take: path:
|
||||||
|
let
|
||||||
|
# define the current path, but nothing more.
|
||||||
|
curLevel = lib.setAttrByPath path {};
|
||||||
|
# `take` will either set:
|
||||||
|
# - { $path = path } => { $path = {} };
|
||||||
|
# - { $path.next = path.next } => { $path = { next = ?; } }
|
||||||
|
# so, index $path into the output of `take`,
|
||||||
|
# and if it has any attrs that means we're interested in those too.
|
||||||
|
nextLevel = lib.getAttrFromPath path (take curLevel);
|
||||||
|
in
|
||||||
|
builtins.attrNames nextLevel;
|
||||||
|
|
||||||
|
# computes a list of all terminal paths that `take` is interested in,
|
||||||
|
# where each path is a list of attr names to descend to reach that terminal.
|
||||||
|
# Type: findTerminalPaths :: (Attrs -> Attrs) -> [String] -> [[String]]
|
||||||
|
findTerminalPaths = take: path:
|
||||||
|
let
|
||||||
|
subNames = findSubNames take path;
|
||||||
|
in if subNames == [] then
|
||||||
|
[ path ]
|
||||||
|
else
|
||||||
|
let
|
||||||
|
terminalsPerChild = builtins.map (name: findTerminalPaths take (path ++ [name])) subNames;
|
||||||
|
in
|
||||||
|
lib.concatLists terminalsPerChild;
|
||||||
|
|
||||||
|
# merges all present values for the provided path
|
||||||
|
# Type: mergeAtPath :: [String] -> [Attrs] -> (lib.mkMerge)
|
||||||
|
mergeAtPath = path: l:
|
||||||
|
let
|
||||||
|
itemsToMerge = builtins.filter (lib.hasAttrByPath path) l;
|
||||||
|
in lib.mkMerge (builtins.map (lib.getAttrFromPath path) itemsToMerge);
|
||||||
|
|
||||||
|
# throw if `item` includes any data not wanted by `take`.
|
||||||
|
# this is recursive: `path` tracks the current location being checked.
|
||||||
|
assertTakesEveryAttr = take: item: path:
|
||||||
|
let
|
||||||
|
takesSubNames = findSubNames take path;
|
||||||
|
itemSubNames = findSubNames (_: item) path;
|
||||||
|
unexpectedNames = lib.subtractLists takesSubNames itemSubNames;
|
||||||
|
takesEverySubAttr = builtins.all (name: assertTakesEveryAttr take item (path ++ [name])) itemSubNames;
|
||||||
|
in
|
||||||
|
if takesSubNames == [] then
|
||||||
|
# this happens when the user takes this whole subtree: i.e. *all* subnames are accepted.
|
||||||
|
true
|
||||||
|
else if unexpectedNames != [] then
|
||||||
|
let
|
||||||
|
p = lib.concatStringsSep "." (path ++ lib.sublist 0 1 unexpectedNames);
|
||||||
|
in
|
||||||
|
throw ''unexpected entry: ${p}''
|
||||||
|
else
|
||||||
|
takesEverySubAttr;
|
||||||
|
}
|
Reference in New Issue
Block a user