From 7dc03f8eee5c9f6518f8be81e1722b35c2ca7593 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 21 May 2025 18:11:04 +0200 Subject: [PATCH 1/4] Feat(clanLib): init types {uniqueDeferredSerializableModule} --- lib/default.nix | 2 ++ lib/flake-module.nix | 1 + lib/types/default.nix | 28 ++++++++++++++++++++++++++ lib/types/flake-module.nix | 25 +++++++++++++++++++++++ lib/types/tests.nix | 41 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 lib/types/default.nix create mode 100644 lib/types/flake-module.nix create mode 100644 lib/types/tests.nix diff --git a/lib/default.nix b/lib/default.nix index 7ea118d51..857a8d14b 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -43,6 +43,8 @@ lib.fix (clanLib: { inventory = clanLib.callLib ./inventory { }; modules = clanLib.callLib ./inventory/frontmatter { }; test = clanLib.callLib ./test { }; + # Custom types + types = clanLib.callLib ./types { }; # Plain imports. introspection = import ./introspection { inherit lib; }; diff --git a/lib/flake-module.nix b/lib/flake-module.nix index bb4e91c99..abf55216a 100644 --- a/lib/flake-module.nix +++ b/lib/flake-module.nix @@ -11,6 +11,7 @@ rec { ./introspection/flake-module.nix ./inventory/flake-module.nix ./jsonschema/flake-module.nix + ./types/flake-module.nix ]; flake.clanLib = import ./default.nix { inherit lib inputs self; diff --git a/lib/types/default.nix b/lib/types/default.nix new file mode 100644 index 000000000..868a83541 --- /dev/null +++ b/lib/types/default.nix @@ -0,0 +1,28 @@ +{ lib, ... }: +{ + uniqueDeferredSerializableModule = lib.fix ( + self: + # Essentially the "raw" type, but with a custom name and check + lib.mkOptionType { + name = "deferredModule"; + description = "deferred module that has custom check and merge behavior"; + descriptionClass = "noun"; + # Unfortunately, tryEval doesn't catch JSON errors + check = value: lib.seq (builtins.toJSON value) true; + merge = lib.options.mergeUniqueOption { + message = "------"; + merge = loc: defs: { + imports = map ( + def: lib.setDefaultModuleLocation "${def.file}, via option ${lib.showOption loc}" def.value + ) defs; + }; + }; + functor = { + inherit (self) name; + type = self; + # Non mergable type + binOp = _a: _b: null; + }; + } + ); +} diff --git a/lib/types/flake-module.nix b/lib/types/flake-module.nix new file mode 100644 index 000000000..2b75953c5 --- /dev/null +++ b/lib/types/flake-module.nix @@ -0,0 +1,25 @@ +{ self, inputs, ... }: +{ + perSystem = + { ... }: + let + # Module that contains the tests + # This module adds: + # - legacyPackages..eval-tests-hello-world + # - checks..eval-tests-hello-world + test-types-module = ( + self.clanLib.test.flakeModules.makeEvalChecks { + module = throw ""; + inherit self inputs; + testName = "types"; + tests = ./tests.nix; + # Optional arguments passed to the test + testArgs = { }; + } + ); + in + { + imports = [ test-types-module ]; + legacyPackages.xxx = { }; + }; +} diff --git a/lib/types/tests.nix b/lib/types/tests.nix new file mode 100644 index 000000000..268292356 --- /dev/null +++ b/lib/types/tests.nix @@ -0,0 +1,41 @@ +{ lib, clanLib, ... }: +let + evalSettingsModule = + m: + lib.evalModules { + modules = [ + { + options.foo = lib.mkOption { + type = clanLib.types.uniqueDeferredSerializableModule; + }; + } + m + ]; + }; +in +{ + test_1 = + let + eval = evalSettingsModule { + foo = { }; + }; + in + { + inherit eval; + expr = eval.config.foo; + expected = { + # Foo has imports + # This can only ever be one module due to the type of foo + imports = [ + { + # This is the result of 'setDefaultModuleLocation' + # Which also returns exactly one module + _file = ", via option foo"; + imports = [ + { } + ]; + } + ]; + }; + }; +} From 3951889b747772e35b88047e3a9dce17d5477d49 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 21 May 2025 18:24:17 +0200 Subject: [PATCH 2/4] Feat(settings): use uniqueDeferredSerializableModule for settings --- lib/inventory/build-inventory/interface.nix | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/inventory/build-inventory/interface.nix b/lib/inventory/build-inventory/interface.nix index ed3dd3599..8a4a60474 100644 --- a/lib/inventory/build-inventory/interface.nix +++ b/lib/inventory/build-inventory/interface.nix @@ -1,3 +1,4 @@ +{ clanLib }: { lib, config, @@ -390,9 +391,7 @@ in types.submodule { options.settings = lib.mkOption { default = { }; - # Dont transform the value with `types.deferredModule` here. We need to keep it json serializable - # TODO: We need a custom serializer for deferredModule - type = types.deferredModule; + type = clanLib.types.uniqueDeferredSerializableModule; }; } ); @@ -404,7 +403,7 @@ in }; settings = lib.mkOption { default = { }; - type = types.deferredModule; + type = types.uniqueDeferredSerializableModule; }; }; } From f16cfe68b6fb322bf3819e8d7ecba26bd90189ad Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 21 May 2025 18:54:07 +0200 Subject: [PATCH 3/4] Tests(deferred custom module): add more tests, dissallow nested imports --- lib/types/default.nix | 12 +++++++--- lib/types/tests.nix | 51 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/lib/types/default.nix b/lib/types/default.nix index 868a83541..0073c0d2b 100644 --- a/lib/types/default.nix +++ b/lib/types/default.nix @@ -2,18 +2,24 @@ { uniqueDeferredSerializableModule = lib.fix ( self: + + let + checkDef = loc: def: if def.value ? imports then throw "uniqueDeferredSerializableModule doesn't allow nested imports" else def; + in # Essentially the "raw" type, but with a custom name and check lib.mkOptionType { name = "deferredModule"; - description = "deferred module that has custom check and merge behavior"; + description = "deferred custom module. Must be JSON serializable."; descriptionClass = "noun"; # Unfortunately, tryEval doesn't catch JSON errors - check = value: lib.seq (builtins.toJSON value) true; + check = value: lib.seq (builtins.toJSON value) (lib.isAttrs value); merge = lib.options.mergeUniqueOption { message = "------"; merge = loc: defs: { imports = map ( - def: lib.setDefaultModuleLocation "${def.file}, via option ${lib.showOption loc}" def.value + def: + lib.seq (checkDef loc def) + lib.setDefaultModuleLocation "${def.file}, via option ${lib.showOption loc}" def.value ) defs; }; }; diff --git a/lib/types/tests.nix b/lib/types/tests.nix index 268292356..850ffaf9d 100644 --- a/lib/types/tests.nix +++ b/lib/types/tests.nix @@ -14,7 +14,7 @@ let }; in { - test_1 = + test_simple = let eval = evalSettingsModule { foo = { }; @@ -38,4 +38,53 @@ in ]; }; }; + + test_no_nested_imports = + let + eval = evalSettingsModule { + foo = { + imports = []; + }; + }; + in + { + inherit eval; + expr = eval.config.foo; + expectedError = { + type = "ThrownError"; + message = "*nested imports"; + }; + }; + + test_no_function_modules = + let + eval = evalSettingsModule { + foo = {...}: { + + }; + }; + in + { + inherit eval; + expr = eval.config.foo; + expectedError = { + type = "TypeError"; + message = "cannot convert a function to JSON"; + }; + }; + + test_non_attrs_module = + let + eval = evalSettingsModule { + foo = "foo.nix"; + }; + in + { + inherit eval; + expr = eval.config.foo; + expectedError = { + type = "ThrownError"; + message = ".*foo.* is not of type"; + }; + }; } From c4980d3990969754a9e18eb9debffed915850664 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 21 May 2025 19:00:46 +0200 Subject: [PATCH 4/4] fix(clanLib): propagate clanLib into module apply --- lib/build-clan/default.nix | 3 ++- lib/build-clan/eval-docs.nix | 8 ++++++-- lib/build-clan/flake-module.nix | 1 + lib/build-clan/function-adapter.nix | 2 +- lib/build-clan/interface.nix | 7 ++++++- lib/inventory/build-inventory/interface.nix | 2 +- lib/inventory/default.nix | 2 +- lib/inventory/schemas/default.nix | 4 +++- lib/types/default.nix | 12 +++++++++--- lib/types/tests.nix | 10 ++++++---- 10 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/build-clan/default.nix b/lib/build-clan/default.nix index 02e74aa99..7327814a0 100644 --- a/lib/build-clan/default.nix +++ b/lib/build-clan/default.nix @@ -3,12 +3,13 @@ ## Add any logic to ./module.nix { lib, + clanLib, ... }: { flakePartsModule = { imports = [ - ./interface.nix + (lib.modules.importApply ./interface.nix { inherit clanLib; }) ./module.nix ]; }; diff --git a/lib/build-clan/eval-docs.nix b/lib/build-clan/eval-docs.nix index 5ac1aca7d..584aa3e06 100644 --- a/lib/build-clan/eval-docs.nix +++ b/lib/build-clan/eval-docs.nix @@ -1,9 +1,13 @@ -{ pkgs, lib }: +{ + pkgs, + lib, + clanLib, +}: let eval = lib.evalModules { class = "nixos"; modules = [ - ./interface.nix + (lib.modules.importApply ./interface.nix { inherit clanLib; }) ]; }; evalDocs = pkgs.nixosOptionsDoc { diff --git a/lib/build-clan/flake-module.nix b/lib/build-clan/flake-module.nix index 5cb96520a..a03f618ca 100644 --- a/lib/build-clan/flake-module.nix +++ b/lib/build-clan/flake-module.nix @@ -19,6 +19,7 @@ in let jsonDocs = import ./eval-docs.nix { inherit pkgs lib; + inherit (self) clanLib; }; in { diff --git a/lib/build-clan/function-adapter.nix b/lib/build-clan/function-adapter.nix index aa8b1a601..cb5add50e 100644 --- a/lib/build-clan/function-adapter.nix +++ b/lib/build-clan/function-adapter.nix @@ -18,7 +18,7 @@ module: ; }; modules = [ - ./interface.nix + (lib.modules.importApply ./interface.nix { inherit (clan-core) clanLib; }) module { inherit specialArgs; diff --git a/lib/build-clan/interface.nix b/lib/build-clan/interface.nix index fa4f781bb..3d652a72b 100644 --- a/lib/build-clan/interface.nix +++ b/lib/build-clan/interface.nix @@ -1,3 +1,4 @@ +{ clanLib }: { lib, self, @@ -94,7 +95,11 @@ in }; inventory = lib.mkOption { - type = types.submodule { imports = [ ../inventory/build-inventory/interface.nix ]; }; + type = types.submodule { + imports = [ + (lib.modules.importApply ../inventory/build-inventory/interface.nix { inherit clanLib; }) + ]; + }; description = '' The `Inventory` submodule. diff --git a/lib/inventory/build-inventory/interface.nix b/lib/inventory/build-inventory/interface.nix index 8a4a60474..d1caca220 100644 --- a/lib/inventory/build-inventory/interface.nix +++ b/lib/inventory/build-inventory/interface.nix @@ -403,7 +403,7 @@ in }; settings = lib.mkOption { default = { }; - type = types.uniqueDeferredSerializableModule; + type = clanLib.types.uniqueDeferredSerializableModule; }; }; } diff --git a/lib/inventory/default.nix b/lib/inventory/default.nix index 9b3112f37..66fa02330 100644 --- a/lib/inventory/default.nix +++ b/lib/inventory/default.nix @@ -5,7 +5,7 @@ in { inherit (services) evalClanService mapInstances resolveModule; inherit (import ./build-inventory { inherit lib clanLib; }) buildInventory; - interface = ./build-inventory/interface.nix; + interface = lib.modules.importApply ./build-inventory/interface.nix { inherit clanLib; }; # Returns the list of machine names # { ... } -> [ string ] resolveTags = diff --git a/lib/inventory/schemas/default.nix b/lib/inventory/schemas/default.nix index e014b190a..048a03ae9 100644 --- a/lib/inventory/schemas/default.nix +++ b/lib/inventory/schemas/default.nix @@ -17,7 +17,9 @@ let frontMatterSchema = jsonLib.parseOptions self.clanLib.modules.frontmatterOptions { }; - inventorySchema = jsonLib.parseModule (import ../build-inventory/interface.nix); + inventorySchema = jsonLib.parseModule ( + import ../build-inventory/interface.nix { inherit (self) clanLib; } + ); renderSchema = pkgs.writers.writePython3Bin "render-schema" { flakeIgnore = [ diff --git a/lib/types/default.nix b/lib/types/default.nix index 0073c0d2b..aa00fcac4 100644 --- a/lib/types/default.nix +++ b/lib/types/default.nix @@ -4,7 +4,12 @@ self: let - checkDef = loc: def: if def.value ? imports then throw "uniqueDeferredSerializableModule doesn't allow nested imports" else def; + checkDef = + _loc: def: + if def.value ? imports then + throw "uniqueDeferredSerializableModule doesn't allow nested imports" + else + def; in # Essentially the "raw" type, but with a custom name and check lib.mkOptionType { @@ -18,8 +23,9 @@ merge = loc: defs: { imports = map ( def: - lib.seq (checkDef loc def) - lib.setDefaultModuleLocation "${def.file}, via option ${lib.showOption loc}" def.value + lib.seq (checkDef loc def) lib.setDefaultModuleLocation + "${def.file}, via option ${lib.showOption loc}" + def.value ) defs; }; }; diff --git a/lib/types/tests.nix b/lib/types/tests.nix index 850ffaf9d..ffcf9fb25 100644 --- a/lib/types/tests.nix +++ b/lib/types/tests.nix @@ -43,7 +43,7 @@ in let eval = evalSettingsModule { foo = { - imports = []; + imports = [ ]; }; }; in @@ -53,15 +53,17 @@ in expectedError = { type = "ThrownError"; message = "*nested imports"; - }; + }; }; test_no_function_modules = let eval = evalSettingsModule { - foo = {...}: { + foo = + { ... }: + { - }; + }; }; in {