From a75b76e86ea8536c21c86207be83139ef8ab4d9e Mon Sep 17 00:00:00 2001 From: Jake Hamilton Date: Tue, 8 Oct 2024 12:09:38 -0700 Subject: [PATCH] feat: add portable submodule type --- lib/src/types/default.nix | 111 +++++++++++++++++++++++++++++++++ lib/src/types/default.test.nix | 49 ++++++++++++++- 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/lib/src/types/default.nix b/lib/src/types/default.nix index 94a223c..79c02c9 100644 --- a/lib/src/types/default.nix +++ b/lib/src/types/default.nix @@ -916,6 +916,117 @@ lib: { }; }; }; + + ## Create a type from a submodule which can be used in multiple parts of the configuration. + ## + ## @type { module :: Module, name? :: String, description? :: String } -> Attrs + portable = + { name ? "PortableSubmodule" + , description ? "portable submodule" + , module + , + }: + let + normalize = + value: + if builtins.isFunction value || builtins.isList value then + value + else if value ? __modules__ then + value.__modules__ + else + { config = value; }; + + initial = lib.types.create { + name = "PortableSubmoduleInitial"; + description = "initial portable submodule value"; + check = value: builtins.isFunction value || builtins.isAttrs value || builtins.isList value; + merge = + location: definitions: + let + normalized = builtins.map (definition: lib.lists.from.any (normalize definition.value)) definitions; + in + builtins.concatLists normalized; + }; + + base = { config }: { + options = { + __modules__ = lib.options.create { + description = "User specified modules to be evaluated."; + type = lib.types.list.of (initial // { merge = lib.options.merge.one; }); + internal = true; + default.value = [ ]; + }; + + extend = lib.options.create { + description = "Extend the submodule configuration."; + type = lib.types.function lib.types.raw; + internal = true; + writable = false; + default.value = module: + let + normalized = + if builtins.isList module then module + else if builtins.isFunction module || module ? config then + [ module ] + else [{ config = module; }]; + + modules = config.__modules__ ++ normalized; + + result = lib.modules.run { + modules = [ + module + base + { config.__modules__ = modules; } + ] ++ modules; + }; + in + result.config; + }; + }; + }; + + transform = location: value: + let + modules = lib.lists.from.any (normalize value); + + result = lib.modules.run { + prefix = location; + modules = [ + module + base + { config.__modules__ = modules; } + ] ++ modules; + }; + in + result.config; + + final = lib.types.raw // { + merge = location: definitions: + let + modules = lib.lists.flatten ( + builtins.map (definition: normalize definition.value) definitions + ); + + result = lib.modules.run { + prefix = location; + modules = [ + module + base + { config.__modules__ = modules; } + ] ++ modules; + }; + in + result.config; + }; + + type = lib.types.coerceWithLocation initial transform final; + in + type // { + inherit name description; + children = type.children // { + inherit module base; + }; + }; }; deferred = { diff --git a/lib/src/types/default.test.nix b/lib/src/types/default.test.nix index 2c21ad2..04942a6 100644 --- a/lib/src/types/default.test.nix +++ b/lib/src/types/default.test.nix @@ -1,4 +1,51 @@ let lib = import ./../default.nix; in -{ } +{ + "PortableSubmodule" = { + "is portable" = + let + submodule = { config }: { + options = { + primitive = lib.options.create { + type = lib.types.string; + default.value = ""; + }; + + computed = lib.options.create { + type = lib.types.string; + default.value = "computed: ${config.primitive}"; + }; + }; + }; + + option = lib.options.create { + default.value = { }; + type = lib.types.submodules.portable { + module = submodule; + }; + }; + + moduleA = { + options.a = option; + }; + moduleB = { config }: { + options.b = option; + + config.b = config.a; + }; + moduleC = { + config.b.primitive = "custom"; + }; + + result = lib.modules.run { + modules = [ + moduleA + moduleB + moduleC + ]; + }; + in + result.config.b.computed == "computed: custom"; + }; +}