From a17ac0ff2bc0b86f7750817c0e9a20000433ab0a Mon Sep 17 00:00:00 2001
From: Jake Hamilton <jake.hamilton@hey.com>
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 c224023..3d8f32f 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 = "<default>";
+            };
+
+            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";
+  };
+}