updateManyAttrsByPath
lib.attrsets.updateManyAttrsByPath
Update or set specific paths of an attribute set.
Takes a list of updates to apply and an attribute set to apply them to,
and returns the attribute set with the updates applied. Updates are
represented as { path = ...; update = ...; }
values, where path
is a
list of strings representing the attribute path that should be updated,
and update
is a function that takes the old value at that attribute path
as an argument and returns the new
value it should be.
Properties:
-
Updates to deeper attribute paths are applied before updates to more shallow attribute paths
-
Multiple updates to the same attribute path are applied in the order they appear in the update list
-
If any but the last
path
element leads into a value that is not an attribute set, an error is thrown -
If there is an update for an attribute path that doesn't exist, accessing the argument in the update function causes an error, but intermediate attribute sets are implicitly created as needed
Type
updateManyAttrsByPath :: [{ path :: [String]; update :: (Any -> Any); }] -> AttrSet -> AttrSet
Examples
lib.attrsets.updateManyAttrsByPath
usage example
updateManyAttrsByPath [
{
path = [ "a" "b" ];
update = old: { d = old.c; };
}
{
path = [ "a" "b" "c" ];
update = old: old + 1;
}
{
path = [ "x" "y" ];
update = old: "xy";
}
] { a.b.c = 0; }
=> { a = { b = { d = 1; }; }; x = { y = "xy"; }; }
Noogle detected
Implementation
The following is the current implementation of this function.
updateManyAttrsByPath =
let
# When recursing into attributes, instead of updating the `path` of each
# update using `tail`, which needs to allocate an entirely new list,
# we just pass a prefix length to use and make sure to only look at the
# path without the prefix length, so that we can reuse the original list
# entries.
go =
prefixLength: hasValue: value: updates:
let
# Splits updates into ones on this level (split.right)
# And ones on levels further down (split.wrong)
split = partition (el: length el.path == prefixLength) updates;
# Groups updates on further down levels into the attributes they modify
nested = groupBy (el: elemAt el.path prefixLength) split.wrong;
# Applies only nested modification to the input value
withNestedMods =
# Return the value directly if we don't have any nested modifications
if split.wrong == [ ] then
if hasValue then
value
else
# Throw an error if there is no value. This `head` call here is
# safe, but only in this branch since `go` could only be called
# with `hasValue == false` for nested updates, in which case
# it's also always called with at least one update
let
updatePath = (head split.right).path;
in
throw (
"updateManyAttrsByPath: Path '${showAttrPath updatePath}' does "
+ "not exist in the given value, but the first update to this "
+ "path tries to access the existing value."
)
else
# If there are nested modifications, try to apply them to the value
if !hasValue then
# But if we don't have a value, just use an empty attribute set
# as the value, but simplify the code a bit
mapAttrs (name: go (prefixLength + 1) false null) nested
else if isAttrs value then
# If we do have a value and it's an attribute set, override it
# with the nested modifications
value // mapAttrs (name: go (prefixLength + 1) (value ? ${name}) value.${name}) nested
else
# However if it's not an attribute set, we can't apply the nested
# modifications, throw an error
let
updatePath = (head split.wrong).path;
in
throw (
"updateManyAttrsByPath: Path '${showAttrPath updatePath}' needs to "
+ "be updated, but path '${showAttrPath (take prefixLength updatePath)}' "
+ "of the given value is not an attribute set, so we can't "
+ "update an attribute inside of it."
);
# We get the final result by applying all the updates on this level
# after having applied all the nested updates
# We use foldl instead of foldl' so that in case of multiple updates,
# intermediate values aren't evaluated if not needed
in
foldl (acc: el: el.update acc) withNestedMods split.right;
in
updates: value: go 0 true value updates;