Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions checkmate/modules/tests/aspect_file.nix
Original file line number Diff line number Diff line change
@@ -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"
];
};
}
2 changes: 1 addition & 1 deletion nix/flakeModule.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions nix/lib.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
lib:
let
# Type system: aspectsType, aspectSubmodule, providerType
types = import ./types.nix lib;
types = import ./types.nix lib "aspects";
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont like this! revert, see comment about not passing namespace string around.


# Generic transposition utility: parameterized by emit function
transpose =
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion nix/new-scope.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions nix/new.nix
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
57 changes: 40 additions & 17 deletions nix/resolve.nix
Original file line number Diff line number Diff line change
@@ -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 "<anonymous>";
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 "<anonymous>") ];
in
build (filePath class segments) class chain segments provided;

in
resolve
12 changes: 9 additions & 3 deletions nix/types.nix
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Core type system for aspect-oriented configuration

lib:
lib: namespace:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about instead of passing namespace here, and causing API breakage, we use the _file option in the namespace itself.

flake.aspects._file = "flake.aspects";

and this should be propagated to aspects inside of it.

default _file is null, but users can override it per aspect or per sub-namespace (aka provides)

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 {
Expand Down Expand Up @@ -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}";
Expand Down Expand Up @@ -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 (
Expand Down