Modular, context-aware and aspect-oriented dendritic Nix configurations. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ den.oeiuwq.com
configurations den dendritic nix aspect oriented
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(core): add den.lib.strict for disabling freeform types (#428)

## Summary

Adds new a new lib module `den.lib.strict` which when imported into a
submodule type (usually via den.schema) disables the freeform type which
is prone to subtle bugs with scoping and mistyping problems.

It works out of the box with `den.schema.host` and `den.schema.user`,
but the types of aspects and flake didn't import a schema type so I
added those too.

I've also added support for asserting errors with `denTest`

authored by

Dylan R. Johnston and committed by
GitHub
2c16b8ce cbb9190c

+288 -15
+1
nix/default.nix
··· 6 6 flakeModules.default = ./flakeModule.nix; 7 7 flakeModules.dendritic = ./dendritic.nix; 8 8 flakeModules.denTest = ./denTest.nix; 9 + flakeModules.strict = ./strict.nix; 9 10 10 11 templates = { 11 12 default.path = ../templates/default;
+55 -4
nix/denTest.nix
··· 9 9 }: 10 10 let 11 11 # isolated test, prevent polution between tests. 12 - denTest = module: { 13 - inherit ((evalDen module).config) expr expected; 14 - }; 12 + denTest = 13 + module: 14 + let 15 + config = (evalDen module).config; 16 + in 17 + { 18 + expr = config.expr; 19 + } 20 + // lib.optionalAttrs (!(config.expected ? undefined)) { 21 + expected = config.expected; 22 + } 23 + // lib.optionalAttrs (!(config.expectedError ? undefined)) { 24 + expectedError = config.expectedError; 25 + }; 15 26 16 27 # emulate fake-parts only for self and nixpkgs. 17 28 withSystem = ··· 43 54 testModule = { 44 55 imports = [ inputs.den.flakeModule ]; 45 56 options.expr = lib.mkOption { }; 46 - options.expected = lib.mkOption { }; 57 + options.expected = lib.mkOption { default.undefined = { }; }; 58 + options.expectedError = 59 + let 60 + # lib.types.submodule doesn't work well in types.either or types.oneOf because it lazily evaluates keys 61 + # so we need to strictly check the keys with an additional check 62 + strictSubmodule = module: lib.types.addCheck (lib.types.submodule module) (strictKeys module); 63 + 64 + strictKeys = 65 + module: attrs: 66 + lib.pipe module [ 67 + (x: x.options) 68 + (lib.attrNames) 69 + (lib.lists.all (key: attrs ? ${key})) 70 + ]; 71 + 72 + type = lib.mkOption { 73 + type = lib.types.enum [ 74 + "RestrictedPathError" 75 + "MissingArgumentError" 76 + "UndefinedVarError" 77 + "TypeError" 78 + "Abort" 79 + "ThrownError" 80 + "AssertionError" 81 + "ParseError" 82 + "EvalError" 83 + ]; 84 + }; 85 + 86 + msg = lib.mkOption { type = lib.types.str; }; 87 + 88 + undefined = lib.mkOption { default.undefined = { }; }; 89 + 90 + in 91 + lib.mkOption { 92 + type = lib.types.oneOf [ 93 + (strictSubmodule { options = { inherit undefined; }; }) 94 + (strictSubmodule { options = { inherit type msg; }; }) 95 + ]; 96 + default.undefined = { }; 97 + }; 47 98 config = { 48 99 den.schema.user.classes = lib.mkDefault [ "homeManager" ]; 49 100 den.default.nixos.system.stateVersion = lib.mkDefault "25.11";
+13 -7
nix/flakeOutputs.nix
··· 50 50 "packages" 51 51 "apps" 52 52 "checks" 53 + "tests" 53 54 "legacyPackages" 54 55 ]; 55 56 ··· 60 61 }; 61 62 62 63 manySubmodule = 63 - lib: 64 + lib: imports: 64 65 lib.types.submodule { 66 + inherit imports; 67 + 65 68 freeformType = lib.types.lazyAttrsOf lib.types.unspecified; 66 69 }; 67 70 ··· 70 73 lib.mkOption { 71 74 default = { }; 72 75 defaultText = lib.literalExpression "{ }"; 73 - type = lib.types.lazyAttrsOf (manySubmodule lib); 76 + type = lib.types.lazyAttrsOf (manySubmodule lib [ ]); 74 77 }; 75 78 76 79 flakeOut = ··· 78 81 lib.mkOption { 79 82 default = { }; 80 83 defaultText = lib.literalExpression "{ }"; 81 - type = (manySubmodule lib); 84 + type = (manySubmodule lib [ ]); 82 85 }; 83 86 84 87 flakeTop = 85 - lib: 88 + { lib, den }: 86 89 lib.mkOption { 87 90 default = { }; 88 91 defaultText = lib.literalExpression "{ }"; 89 - type = (manySubmodule lib); 92 + type = (manySubmodule lib [ (den.schema.flake or { }) ]); 90 93 }; 91 94 92 95 flakeBased = builtins.listToAttrs ( ··· 114 117 all.includes = builtins.attrValues (flakeBased // systemBased); 115 118 116 119 flake = 117 - { lib, ... }: 120 + { lib, config, ... }: 118 121 { 119 - options.flake = flakeTop lib; 122 + options.flake = flakeTop { 123 + inherit lib; 124 + inherit (config) den; 125 + }; 120 126 }; 121 127 122 128 in
+4 -1
nix/lib/aspects/types.nix
··· 80 80 { name, config, ... }: 81 81 { 82 82 freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; 83 - imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ]; 83 + imports = [ 84 + (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) 85 + (den.schema.aspect or { }) 86 + ]; 84 87 85 88 options = { 86 89 name = lib.mkOption {
+1
nix/lib/default.nix
··· 32 32 statics = ./statics.nix; 33 33 take = ./take.nix; 34 34 lastFunctionTo = ./last-function-to.nix; 35 + strict = ./strict.nix; 35 36 }; 36 37 in 37 38 den-lib
+31
nix/lib/strict.nix
··· 1 + { lib, ... }: 2 + { 3 + _module.freeformType = lib.mkOptionType { 4 + name = "strict type"; 5 + typeMerge = outer: { 6 + merge = 7 + path: decls: 8 + ( 9 + let 10 + decl = lib.pipe decls [ 11 + lib.head 12 + (lib.getAttr "value") 13 + lib.attrsToList 14 + lib.head 15 + ]; 16 + 17 + kind = if (lib.head path) == "flake" then "flake" else lib.elemAt path 1; 18 + in 19 + throw '' 20 + STRICT MODE 21 + 22 + Attempted to set the option "${decl.name}" in "${lib.join "." path}" but no explicit definition exists. If this wasn't a mistake, disable STRICT mode or configure an option. e.g. 23 + 24 + den.schema.${kind}.options.${decl.name} = lib.mkOption { ... }; 25 + 26 + See https://documentation.example 27 + '' 28 + ); 29 + }; 30 + }; 31 + }
+8
nix/strict.nix
··· 1 + { den, ... }: 2 + { 3 + den.schema.host = den.lib.strict; 4 + den.schema.user = den.lib.strict; 5 + den.schema.aspect = den.lib.strict; 6 + den.schema.home = den.lib.strict; 7 + den.schema.flake = den.lib.strict; 8 + }
+161
templates/ci/modules/features/strict.nix
··· 1 + { denTest, lib, ... }: 2 + { 3 + flake.tests.strict-mode = { 4 + test-relaxed-mode = denTest ( 5 + { den, ... }: 6 + { 7 + den.hosts.x86_64-linux.igloo = { 8 + users.tux = { }; 9 + 10 + arbitrary = "value"; 11 + }; 12 + 13 + expr = den.hosts.x86_64-linux.igloo.arbitrary; 14 + expected = "value"; 15 + } 16 + ); 17 + 18 + test-strict-mode-host = denTest ( 19 + { den, ... }: 20 + { 21 + den.schema.host = den.lib.strict; 22 + 23 + den.hosts.x86_64-linux.igloo = { 24 + users.tux = { }; 25 + arbitrary = "value"; 26 + }; 27 + 28 + expr = den.hosts.x86_64-linux.igloo.arbitrary; 29 + expectedError = { 30 + type = "ThrownError"; 31 + msg = "Attempted to set the option \"arbitrary\" in \"den.hosts.x86_64-linux.igloo\""; 32 + }; 33 + } 34 + ); 35 + 36 + test-strict-mode-user = denTest ( 37 + { den, ... }: 38 + { 39 + den.schema.user = den.lib.strict; 40 + 41 + den.hosts.x86_64-linux.igloo.users.tux.arbitrary = "value"; 42 + 43 + expr = den.hosts.x86_64-linux.igloo.users.tux.arbitrary; 44 + expectedError = { 45 + type = "ThrownError"; 46 + msg = "Attempted to set the option \"arbitrary\" in \"den.hosts.x86_64-linux.igloo.users.tux\""; 47 + }; 48 + } 49 + ); 50 + 51 + test-strict-mode-aspect = denTest ( 52 + { den, ... }: 53 + { 54 + den.hosts.x86_64-linux.igloo.users.tux = { }; 55 + 56 + den.schema.aspect = den.lib.strict; 57 + den.aspects.igloo.arbitrary = { }; 58 + 59 + expr = den.aspects.igloo.arbitrary; 60 + expectedError = { 61 + type = "ThrownError"; 62 + msg = "Attempted to set the option \"arbitrary\" in \"den.aspects.igloo\""; 63 + }; 64 + } 65 + ); 66 + 67 + test-strict-mode-flake = denTest ( 68 + { den, config, ... }: 69 + { 70 + den.schema.flake = den.lib.strict; 71 + flake.arbitray = "value"; 72 + 73 + expr = config.flake.arbitray; 74 + expectedError = { 75 + type = "ThrownError"; 76 + msg = "Attempted to set the option \"arbitray\" in \"flake\""; 77 + }; 78 + } 79 + ); 80 + 81 + test-strict-mode-flake-customisable = denTest ( 82 + { den, config, ... }: 83 + { 84 + den.schema.flake.imports = [ 85 + den.lib.strict 86 + { 87 + options.arbitrary = lib.mkOption { 88 + type = lib.types.str; 89 + }; 90 + } 91 + ]; 92 + flake.arbitrary = "value"; 93 + 94 + expr = config.flake.arbitrary; 95 + expected = "value"; 96 + } 97 + ); 98 + 99 + flakeModule.strict = { 100 + test-host = denTest ( 101 + { inputs, den, ... }: 102 + { 103 + imports = [ inputs.den.flakeModules.strict ]; 104 + 105 + den.hosts.x86_64-linux.igloo.arbitrary = "value"; 106 + 107 + expr = den.hosts.x86_64-linux.igloo.arbitrary; 108 + expectedError = { 109 + type = "ThrownError"; 110 + msg = "Attempted to set the option \"arbitrary\" in \"den.hosts.x86_64-linux.igloo\""; 111 + }; 112 + } 113 + ); 114 + 115 + test-user = denTest ( 116 + { inputs, den, ... }: 117 + { 118 + imports = [ inputs.den.flakeModules.strict ]; 119 + 120 + den.hosts.x86_64-linux.igloo.users.tux.arbitrary = "value"; 121 + 122 + expr = den.hosts.x86_64-linux.igloo.users.tux.arbitrary; 123 + expectedError = { 124 + type = "ThrownError"; 125 + msg = "Attempted to set the option \"arbitrary\" in \"den.hosts.x86_64-linux.igloo.users.tux\""; 126 + }; 127 + } 128 + ); 129 + 130 + test-aspect = denTest ( 131 + { inputs, den, ... }: 132 + { 133 + imports = [ inputs.den.flakeModules.strict ]; 134 + 135 + den.aspects.test.arbitrary = "value"; 136 + 137 + expr = den.aspects.test.arbitrary; 138 + expectedError = { 139 + type = "ThrownError"; 140 + msg = "Attempted to set the option \"arbitrary\" in \"den.aspects.test\""; 141 + }; 142 + } 143 + ); 144 + 145 + test-flake = denTest ( 146 + { inputs, config, ... }: 147 + { 148 + imports = [ inputs.den.flakeModules.strict ]; 149 + 150 + flake.arbitrary = "value"; 151 + 152 + expr = config.flake.arbitrary; 153 + expectedError = { 154 + type = "ThrownError"; 155 + msg = "Attempted to set the option \"arbitrary\" in \"flake\""; 156 + }; 157 + } 158 + ); 159 + }; 160 + }; 161 + }
+14 -3
templates/ci/modules/test-support/eval-den.nix
··· 1 - { inputs, denTest, ... }: 1 + { 2 + inputs, 3 + denTest, 4 + lib, 5 + ... 6 + }: 2 7 { 3 8 imports = [ 4 9 inputs.den.flakeModules.denTest 5 - inputs.den.flakeOutputs.flake 10 + inputs.den.flakeOutputs.tests 6 11 ]; 7 - flake = { 12 + 13 + options.flake = { 14 + denTest = lib.mkOption { }; 15 + den = lib.mkOption { }; 16 + }; 17 + 18 + config.flake = { 8 19 inherit denTest; 9 20 den = 10 21 (denTest (