From 16c08793abec512fc7f3a613712217000f6e4a8b Mon Sep 17 00:00:00 2001 From: Victor Borja Date: Sat, 28 Feb 2026 08:42:16 -0600 Subject: [PATCH] allow setting aspect._file or generate one for better module location. --- checkmate/modules/tests/aspect_file.nix | 220 ++++++++++++++++++++++++ nix/flakeModule.nix | 2 +- nix/lib.nix | 4 +- nix/new-scope.nix | 2 +- nix/new.nix | 4 +- nix/resolve.nix | 57 ++++-- nix/types.nix | 12 +- 7 files changed, 275 insertions(+), 26 deletions(-) create mode 100644 checkmate/modules/tests/aspect_file.nix diff --git a/checkmate/modules/tests/aspect_file.nix b/checkmate/modules/tests/aspect_file.nix new file mode 100644 index 0000000..9675a41 --- /dev/null +++ b/checkmate/modules/tests/aspect_file.nix @@ -0,0 +1,220 @@ +{ lib, mkFlake, ... }: +{ + flake.tests."test _file on simple aspect" = + let + flake = mkFlake { + flake.aspects.aspectOne.classOne.bar = [ "x" ]; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + in + { + expr = mod._file; + expected = "flake.aspects:aspectOne.classOne"; + }; + + flake.tests."test _file overriden" = + let + flake = mkFlake { + flake.aspects.aspectOne = { + _file = "foo"; + classOne.bar = [ "x" ]; + }; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + in + { + expr = mod._file; + expected = "foo"; + }; + + flake.tests."test _file respects custom aspect name" = + let + flake = mkFlake { + flake.aspects.aspectOne = { + name = "my-one"; + classOne.bar = [ "x" ]; + }; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + in + { + expr = mod._file; + expected = "flake.aspects:my-one.classOne"; + }; + + flake.tests."test _file on named include has index" = + let + flake = mkFlake { + flake.aspects = + { aspects, ... }: + { + aspectOne = { + classOne.bar = [ "one" ]; + includes = [ aspects.aspectTwo ]; + }; + aspectTwo.classOne.bar = [ "two" ]; + }; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + include0 = lib.elemAt mod.imports 1; + in + { + expr = include0._file; + expected = "flake.aspects:aspectOne.includes[0].aspectTwo.classOne"; + }; + + flake.tests."test _file on second include has index 1" = + let + flake = mkFlake { + flake.aspects = + { aspects, ... }: + { + aspectOne = { + classOne.bar = [ "one" ]; + includes = [ + aspects.aspectTwo + aspects.aspectThree + ]; + }; + aspectTwo.classOne.bar = [ "two" ]; + aspectThree.classOne.bar = [ "three" ]; + }; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + include1 = lib.elemAt mod.imports 2; + in + { + expr = include1._file; + expected = "flake.aspects:aspectOne.includes[1].aspectThree.classOne"; + }; + + flake.tests."test _file on class config wrapper matches root" = + let + flake = mkFlake { + flake.aspects.aspectOne.classOne.bar = [ "x" ]; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + classConfigWrapper = lib.head mod.imports; + in + { + expr = classConfigWrapper._file; + expected = "flake.aspects:aspectOne.classOne"; + }; + + flake.tests."test _file on anonymous function include is not function body" = + let + flake = mkFlake { + flake.aspects.aspectOne = { + classOne.bar = [ "x" ]; + includes = [ + ( + { ... }: + { + classOne.bar = [ "from-fn" ]; + } + ) + ]; + }; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + include0 = lib.elemAt mod.imports 1; + in + { + expr = lib.hasPrefix "flake.aspects:aspectOne.includes[0]." include0._file; + expected = true; + }; + + flake.tests."test _file on deeply nested includes" = + let + flake = mkFlake { + flake.aspects = + { aspects, ... }: + { + aspectOne = { + classOne.bar = [ "one" ]; + includes = [ aspects.aspectTwo ]; + }; + aspectTwo = { + classOne.bar = [ "two" ]; + includes = [ aspects.aspectThree ]; + }; + aspectThree.classOne.bar = [ "three" ]; + }; + }; + modOne = flake.aspects.aspectOne.resolve { class = "classOne"; }; + modTwo = lib.elemAt modOne.imports 1; + modThree = lib.elemAt modTwo.imports 1; + in + { + expr = [ + modOne._file + modTwo._file + modThree._file + ]; + expected = [ + "flake.aspects:aspectOne.classOne" + "flake.aspects:aspectOne.includes[0].aspectTwo.classOne" + "flake.aspects:aspectOne.includes[0].aspectTwo.includes[0].aspectThree.classOne" + ]; + }; + + flake.tests."test _file on provides include" = + let + flake = mkFlake { + flake.aspects = + { aspects, ... }: + { + aspectOne = { + classOne.bar = [ "one" ]; + includes = [ aspects.aspectTwo.provides.helper ]; + }; + aspectTwo.provides.helper = { + name = "helper"; + classOne.bar = [ "help" ]; + }; + }; + }; + mod = flake.aspects.aspectOne.resolve { class = "classOne"; }; + include0 = lib.elemAt mod.imports 1; + in + { + expr = include0._file; + expected = "flake.aspects:aspectOne.includes[0].helper.classOne"; + }; + + flake.tests."test _file on deeply nested provides" = + let + flake = mkFlake { + flake.aspects = + { aspects, ... }: + { + aspectOne = { + classOne.bar = [ "one" ]; + includes = [ aspects.aspectTwo.provides.helper ]; + }; + aspectTwo.provides.helper = { + name = "helper"; + classOne.bar = [ "help" ]; + includes = [ aspects.aspectThree.provides.sub ]; + }; + aspectThree.provides.sub = { + name = "sub"; + classOne.bar = [ "sub" ]; + }; + }; + }; + modOne = flake.aspects.aspectOne.resolve { class = "classOne"; }; + helper = lib.elemAt modOne.imports 1; + sub = lib.elemAt helper.imports 1; + in + { + expr = [ + helper._file + sub._file + ]; + expected = [ + "flake.aspects:aspectOne.includes[0].helper.classOne" + "flake.aspects:aspectOne.includes[0].helper.includes[0].sub.classOne" + ]; + }; +} diff --git a/nix/flakeModule.nix b/nix/flakeModule.nix index 196892f..9f24675 100644 --- a/nix/flakeModule.nix +++ b/nix/flakeModule.nix @@ -7,7 +7,7 @@ ... }: # Invoke new() factory to create flake.aspects and flake.modules -import ./new.nix lib (option: transposed: { +import ./new.nix lib "flake.aspects" (option: transposed: { # User-facing aspects input options.flake.aspects = option; diff --git a/nix/lib.nix b/nix/lib.nix index 58b03fc..4443bf4 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -3,7 +3,7 @@ lib: let # Type system: aspectsType, aspectSubmodule, providerType - types = import ./types.nix lib; + types = import ./types.nix lib "aspects"; # Generic transposition utility: parameterized by emit function transpose = @@ -18,7 +18,7 @@ let # Dynamic class forwarding into submodules forward = import ./forward.nix lib; - # Low-level scope factory: parameterized by callback + # Low-level scope factory: parameterized by namespace and callback new = import ./new.nix lib; # High-level named scope factory diff --git a/nix/new-scope.nix b/nix/new-scope.nix index 60b0608..a0c3f7b 100644 --- a/nix/new-scope.nix +++ b/nix/new-scope.nix @@ -3,7 +3,7 @@ new: name: { config, lib, ... }: # Invoke new() to create ${name}.aspects and ${name}.modules -new (option: transposed: { +new "${name}.aspects" (option: transposed: { options.${name} = { # User-facing aspects input aspects = option; diff --git a/nix/new.nix b/nix/new.nix index 3899e2f..12b7f32 100644 --- a/nix/new.nix +++ b/nix/new.nix @@ -1,12 +1,12 @@ # Low-level aspect scope factory # Creates aspect integration via callback pattern for maximum flexibility -lib: cb: cfg: +lib: namespace: cb: cfg: let # Import aspects transposer: validates and transposes aspect config aspects = import ./aspects.nix lib cfg; # Import type system for aspect validation - types = import ./types.nix lib; + types = import ./types.nix lib namespace; # Create aspects input option option = lib.mkOption { diff --git a/nix/resolve.nix b/nix/resolve.nix index cab0660..1e2a56a 100644 --- a/nix/resolve.nix +++ b/nix/resolve.nix @@ -1,28 +1,51 @@ # Core aspect resolution algorithm # Resolves aspect definitions into nixpkgs modules with dependency resolution -lib: +lib: namespace: let - # Process a single provider: invoke with context and resolve - include = - class: aspect-chain: provider: + filePath = class: segments: "${namespace}:${lib.concatStringsSep "." segments}.${class}"; + + build = + file: class: chain: segments: provided: let - provided = provider { inherit aspect-chain class; }; + fileAttr = if provided ? _file then provided._file else null; + computedFile = if fileAttr == null then file else fileAttr; in - resolve class aspect-chain provided; + { + _file = computedFile; + imports = lib.flatten [ + { + _file = computedFile; + imports = [ (provided.${class} or { }) ]; + } + (lib.imap0 (includeAt class chain segments) (provided.includes or [ ])) + ]; + }; - # Main resolution: extract class config and recursively resolve includes - resolve = class: aspect-chain: provided: { - imports = - let - config = provided.${class} or { }; - includes = provided.includes or [ ]; - in - lib.flatten [ - config - (lib.map (include class (aspect-chain ++ [ provided ])) includes) + includeAt = + class: chain: segments: idx: provider: + let + provided = provider { + aspect-chain = chain; + inherit class; + }; + chain' = chain ++ [ provided ]; + name = provided.name or ""; + segments' = segments ++ [ + "includes[${toString idx}]" + name ]; - }; + file = filePath class segments'; + in + build file class chain' segments' provided; + + resolve = + class: aspect-chain: provided: + let + chain = aspect-chain ++ [ provided ]; + segments = [ (provided.name or "") ]; + in + build (filePath class segments) class chain segments provided; in resolve diff --git a/nix/types.nix b/nix/types.nix index 7687391..405cf34 100644 --- a/nix/types.nix +++ b/nix/types.nix @@ -1,8 +1,8 @@ # Core type system for aspect-oriented configuration -lib: +lib: namespace: let - resolve = import ./resolve.nix lib; + resolve = import ./resolve.nix lib namespace; # Type for computed values that only exist during evaluation ignoredType = lib.types.mkOptionType { @@ -73,6 +73,12 @@ let type = lib.types.str; }; + _file = lib.mkOption { + description = "Aspect file location"; + default = null; + type = lib.types.nullOr lib.types.str; + }; + description = lib.mkOption { description = "Aspect description"; default = "Aspect ${name}"; @@ -102,7 +108,7 @@ let visible = false; description = "Functor to default provider"; type = lib.types.functionTo providerType; - default = aspect: { class, aspect-chain }: if true || (class aspect-chain) then aspect else aspect; + default = aspect: { class, aspect-chain }: if true then aspect else class aspect-chain; }; modules = mkInternal "resolved modules from this aspect" ignoredType (