Commit 26744f7
Changed files (2)
modules
modules/users/schema.nix
@@ -27,6 +27,16 @@
};
default = {};
};
+ system = lib.mkOption {
+ type = lib.types.submodule {
+ options = {
+ settings = lib.mkOption {
+ type = lib.types.anything;
+ };
+ };
+ };
+ default = {};
+ };
};
})
);
modules/mk-settings.nix
@@ -0,0 +1,129 @@
+# Per-aspect settings surface for hosts and users.
+#
+# den.aspects.<name>.settings.host = { <opt> = lib.mkOption ...; };
+# den.aspects.<name>.settings.user = { <opt> = lib.mkOption ...; };
+#
+# Collected into the schemas as typed options:
+#
+# den.schema.host.options.settings.<aspect-path>.<option>
+# den.schema.user.options.system.settings.<aspect-path>.<option>
+#
+# Hosts/users provide values at the same path on their entities, and
+# aspects read them from their context. User values merge the shared
+# `den.users.<username>` registry first, as lowest-priority definitions.
+{
+ config,
+ lib,
+ ...
+}: let
+ # Pipeline-special keys; skip when walking for settings declarations.
+ structuralKeys = [
+ "excludes"
+ "includes"
+ "meta"
+ "name"
+ "provides"
+ ];
+
+ # Collect { prefix, host, user } for aspect nodes declaring settings.
+ # Bare-function aspects cannot carry a settings attr and are skipped.
+ collect = prefix: node:
+ if !(builtins.isAttrs node)
+ then []
+ else let
+ settings = let
+ s = node.settings or null;
+ in
+ if builtins.isAttrs s
+ then s
+ else null;
+ hasHost = builtins.isAttrs (settings.host or null);
+ hasUser = builtins.isAttrs (settings.user or null);
+ mine = lib.optional (hasHost || hasUser) {
+ inherit prefix;
+ host = lib.optionalAttrs hasHost settings.host;
+ user = lib.optionalAttrs hasUser settings.user;
+ };
+ children = lib.concatMap (name: collect (prefix ++ [name]) node.${name}) (
+ builtins.filter (name: !(builtins.elem name structuralKeys) && name != "settings") (
+ builtins.attrNames node
+ )
+ );
+ in
+ mine ++ children;
+
+ collected = collect [] config.den.aspects;
+
+ # Build nested option declarations, grouping shared path prefixes.
+ mkOptions = nodes: let
+ grouped = lib.groupBy (n: builtins.head n.path) nodes;
+ in
+ lib.mapAttrs (
+ name: group:
+ if lib.all (n: builtins.length n.path == 1) group
+ then
+ lib.mkOption {
+ type = lib.types.submodule {
+ options = (builtins.head group).decls;
+ };
+ default = {};
+ description = "Settings for aspect `${name}'.";
+ }
+ else if !(lib.all (n: builtins.length n.path > 1) group)
+ then throw "aspect settings: aspect `${name}' is both a settings leaf and a container"
+ else mkOptions (map (n: n // {path = lib.drop 1 n.path;}) group)
+ )
+ grouped;
+
+ hostNodes = map (n: {
+ path = n.prefix;
+ decls = n.host;
+ }) (lib.filter (n: n.host != {}) collected);
+
+ userNodes = map (n: {
+ path = n.prefix;
+ decls = n.user;
+ }) (lib.filter (n: n.user != {}) collected);
+
+ userSettingsType = lib.types.submodule {
+ # The shared registry and hosts also carry raw user-system values
+ freeformType = lib.types.attrsOf lib.types.anything;
+ options.settings = mkOptions userNodes;
+ };
+
+ # Merge the shared `den.users.<username>` values as lowest-priority defs.
+ registryAware = inner:
+ lib.mkOptionType {
+ name = "den-shared-user-system-settings";
+ check = v: builtins.isAttrs v;
+ merge = loc: defs: let
+ username = lib.elemAt loc (builtins.length loc - 2);
+ shared = config.den.users.${username}.system or {};
+ in
+ inner.merge loc ([
+ {
+ file = "<den.users>";
+ value = shared;
+ }
+ ]
+ ++ defs);
+ };
+in {
+ config = {
+ den.reservedKeys = ["settings"];
+
+ den.schema.host.options.settings = lib.mkOption {
+ type = lib.types.submodule {
+ options = mkOptions hostNodes;
+ };
+ default = {};
+ description = "Host-provided settings for den aspects.";
+ };
+
+ den.schema.user.options.system = lib.mkOption {
+ type = registryAware userSettingsType;
+ default = {};
+ description = "User-provided settings for den aspects.";
+ };
+ };
+}