Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion nix/lib/entities/_types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,81 @@ let
# Recursive merge without forcing leaf values. Unlike lib.types.anything this
# does not inspect values deeply (no mapAttrsRecursiveCond), avoiding infinite
# recursion when values reference other options (e.g. den.aspects).
# Concatenates lists rather than overwriting them, which `lib.recursiveUpdate`
# does because it treats a list as an opaque leaf. Two files each writing
# `den.hosts.<h>.includes` therefore kept one list and dropped the other in
# silence, while an attrset key under the same two definitions merged
# normally (measured: `users.alice` and `users.bob` both survive).
#
# Concatenation is the module system's own rule for a list-valued option
# (`listOf` merges by `concatLists`) and den's rule at the aspect tier
# (`aspectContentType`'s `deepMerge`), so this makes the entity registry
# agree with both rather than introduce a third behaviour. It matters most
# for the collection keys: `includes`, `excludes` and `classes` are the
# list-valued keys an entity carries, and all three accumulate everywhere
# else they appear.
# A value the module system would have merged by equality had the key been
# declared. Functions and derivations are excluded: `==` on two functions is
# always false, so comparing them would refuse two identical definitions.
isPlainScalar =
v:
builtins.elem (builtins.typeOf v) [
"string"
"int"
"bool"
"float"
"null"
];

# Values named by type, with the value itself only where rendering it is
# safe. `builtins.toJSON` on a derivation or a function throws, and one side
# of a conflict can be either, so a message that always rendered both would
# fail while reporting a failure.
show =
v: if isPlainScalar v then "`${builtins.toJSON v}`" else "a value of type ${builtins.typeOf v}";

deepMergeAttrs = lib.mkOptionType {
name = "deepMergeAttrs";
description = "recursively merged attribute set";
check = builtins.isAttrs;
merge = _loc: defs: builtins.foldl' (acc: def: lib.recursiveUpdate acc def.value) { } defs;
merge =
_loc: defs:
let
merge2 =
a: b:
a
// builtins.mapAttrs (
bk: bv:
if !(a ? ${bk}) then
bv
else if builtins.isAttrs a.${bk} && builtins.isAttrs bv then
merge2 a.${bk} bv
else if builtins.isList a.${bk} && builtins.isList bv then
# `bv` first: defs reach this merge in reverse declaration order,
# so `a` holds the LATER definition. Measured, not assumed, and
# it is the same ordering that makes the scalar arm below read as
# first-declaration-wins.
bv ++ a.${bk}
else if
# Either side a plain scalar means the two are not both mergeable

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good catch!

# shapes, so reaching here with different values is a genuine
# conflict: two scalars that differ, or a type mismatch such as a
# list against a string. Both silently resolved to one definition
# before this.
(isPlainScalar a.${bk} || isPlainScalar bv) && a.${bk} != bv
then
throw ''
den: conflicting definitions for `${bk}` on an entity.

${show bv} and ${show a.${bk}} were both defined, and neither is a shape the other merges with. Attribute sets merge and lists concatenate; everything else has to agree.

Remove one definition, or give the two a key each.
''
else
bv
) b;
in
builtins.foldl' (acc: def: merge2 acc def.value) { } defs;
};

# Single shared production run: imports + per-scope path set from ONE fx.handle.
Expand Down
74 changes: 28 additions & 46 deletions templates/ci/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading