How to reuse submodules in a submodule

Currently re-writing a HM module I have, I’m hitting some kind of a roadblock and don’t know how to tackle it. Maybe what I’m trying to do is not possible, which is the reason for this post.

The module has some top-level options and multiple submodules. In addition to that, there is one of the submodules which reuses some of the top-level submodules. This is the culprit.

To simplify, here is a tree view of the module options:

.
├── submodule1
│   ├── option3
│   └── option4
├── submodule2
│   ├── option5
│   └── option6
├── submodule3
│   ├── option7
│   └── option8
├── submodule4
│   ├── submodule2
│   │   ├── option5
│   │   └── option6
│   └── submodule3
│       ├── option7
│       └── option8
├── option1
└── option2

Currently, all submodules (1, 2 and 3) call lambdas which define their options as follows:

{ lib }:
lib.types.submodule { /* Options */ }

They are imported in the top-level like that:

{ lib, config, ... }:

let
  submodule1 = import ./submodule1.nix { inherit lib; };
  submodule2 = import ./submodule2.nix { inherit lib; };
  submodule3 = import ./submodule3.nix { inherit lib; };
in
{
  options = {
    submodule1 = mkOption {
      type = submodule1;
      default = { };
      description = "First submodule";
    };
    submodule2 = mkOption {
      type = submodule2;
      default = { };
      description = "Second submodule";
    };
    submodule3 = mkOption {
      type = submodule3;
      default = { };
      description = "Third submodule";
    };
  };
}

As for submodule4, it’s a bit more complicated and looks like this:

{ lib, submodule2, submodule3 }:
types.submodule {
  options = {
    submodule2 = lib.mkOption {
      type = submodule2;
      default = { };
      description = "Second submodule";
    };
    submodule3 = lib.mkOption {
      type = submodule3;
      default = { };
      description = "Third submodule";
  };
}

It is imported in the top-level in this way:

{ lib, config, ... }:

let
  submodule1 = import ./submodule1.nix { inherit lib; };
  submodule2 = import ./submodule2.nix { inherit lib; };
  submodule3 = import ./submodule3.nix { inherit lib; };
  submodule4 = import ./submodule4.nix {
    inherit lib submodule2 submodule3;
  };
in
{
  options = {
    submodule1 = mkOption {
      type = submodule1;
      default = { };
      description = "First submodule";
    };
    submodule2 = mkOption {
      type = submodule2;
      default = { };
      description = "Second submodule";
    };
    submodule3 = mkOption {
      type = submodule3;
      default = { };
      description = "Third submodule";
    };
    submodule4 = mkOption {
      type = lib.types.listOf submodule4;
      default = { };
      description = "Fourth submodule";
    };
  };
}

Although this is working fine, I’d like for the submodule files to be « real » submodules that I can import using imports rather than import. In the end, the current implementation only uses lambdas which create the submodule types.

The issue I have is that the submodule4 is a list of submodules which happen to have options using top-level submodules definition. Not sure I’m being clear…

While rewriting the submodule4, I noticed that what I’m trying to do might not be possible since if I add submodule2 and submodule3 to submodule4’s imports, they will end up being top-level, right?

{ lib, ... }:

{
  imports = [
    ./submodule2.nix
    ./submodule3.nix
  ];

  options = {
    submodule4 = lib.mkOption {
      type = lib.types.listOf /* what? */;
      default = [ { isDefault = true; } ];
      description = "Fourth submodule";
    };
  }
}

I have a feeling I’m overthinking this and should stick with the current implementation but it somehow feels wrong in the way it currently is implemented.

Any thoughts?