chore: initial commit

This commit is contained in:
Jake Hamilton 2024-06-03 02:57:13 -07:00
parent b7456195bd
commit f7d6c846f6
Signed by untrusted user: jakehamilton
GPG key ID: 9762169A1B35EA68
34 changed files with 1760 additions and 105 deletions

View file

@ -75,12 +75,40 @@ in
} }
``` ```
Successful tests will return `true` while failing test will resolve with `false`. Successful tests will return `true` while failing test will resolve with `false`. You can run
all tests with the following command:
```shell
./test.sh
```
If you want to run a specific test suite, you can run the command, specifying the directory
to the tests file:
```shell
./test.sh $namespace
```
For example, to run the tests for only `attrs`, use the following command:
```shell
./test.sh attrs
```
### Formatting ### Formatting
> **Note:** To keep this flake light and keep its inputs empty we do not include a package
> set which would provide a formatter. Instead please run `nix run nixpkgs#nixfmt-rfc-style`
> until an improved solution is available.
All code in this project must be formatted using the provided formatter in the `flake.nix` All code in this project must be formatted using the provided formatter in the `flake.nix`
file. You can run this formatter using the command `nix fmt`. file. You can run this formatter using the command `nix fmt` (not currently available).
### Code Quality
In order to keep the project approachable and easy to maintain, certain patterns are not allowed.
In particular, the use of `with` and `rec` are not allowed. Additionally, you should prefer the
fully qualified name of a variable rather than creating intermediate ones using `inherit`.
### Adding Functionality ### Adding Functionality

107
lib/default.test.nix Normal file
View file

@ -0,0 +1,107 @@
let
lib = import ./default.nix;
root = ./.;
files = [
./src/attrs/default.test.nix
./src/bools/default.test.nix
./src/errors/default.test.nix
./src/fp/default.test.nix
./src/generators/default.test.nix
./src/importers/default.test.nix
./src/lists/default.test.nix
./src/math/default.test.nix
./src/modules/default.test.nix
./src/numbers/default.test.nix
./src/options/default.test.nix
./src/packages/default.test.nix
./src/paths/default.test.nix
./src/points/default.test.nix
./src/strings/default.test.nix
./src/types/default.test.nix
./src/versions/default.test.nix
];
resolve = file: let
imported = import file;
value =
if builtins.isFunction imported
then imported {inherit lib;}
else imported;
relative = lib.strings.removePrefix (builtins.toString root) (builtins.toString file);
in {
inherit file value;
relative =
if lib.strings.hasPrefix "/" relative
then "." + relative
else relative;
namespace = getNamespace file;
};
resolved = builtins.map resolve files;
getNamespace = path: let
relative = lib.strings.removePrefix (builtins.toString root) (builtins.toString path);
parts = lib.strings.split "/" relative;
in
if builtins.length parts > 2
then builtins.elemAt parts 2
else relative;
results = let
getTests = file: prefix: suite: let
nested = lib.attrs.mapToList (name: value: getTests file (prefix ++ [name]) value) suite;
relative = lib.strings.removePrefix (builtins.toString root) (builtins.toString file);
in
if builtins.isAttrs suite
then builtins.concatLists nested
else [
{
inherit prefix file;
name = builtins.concatStringsSep " > " prefix;
value = suite;
relative =
if lib.strings.hasPrefix "/" relative
then "." + relative
else relative;
}
];
base =
builtins.map (entry: getTests entry.file [entry.namespace] entry.value) resolved;
in
builtins.concatLists base;
successes = builtins.filter (test: test.value) results;
failures = builtins.filter (test: !test.value) results;
total = "${builtins.toString (builtins.length successes)} / ${builtins.toString (builtins.length results)}";
in
if failures == []
then let
message =
lib.strings.concatMapSep "\n"
(test: " ${test.name}")
successes;
in ''
SUCCESS (${total})
${message}
''
else let
successMessage =
lib.strings.concatMapSep "\n"
(test: " ${test.name}")
successes;
failureMessage =
lib.strings.concatMapSep "\n\n"
(test:
" ${test.name}\n"
+ " -> ${test.relative}")
failures;
in ''
FAILURE (${total})
${failureMessage}
''

View file

@ -13,16 +13,18 @@ lib: {
mergeRecursiveUntil = predicate: x: y: let mergeRecursiveUntil = predicate: x: y: let
process = path: process = path:
builtins.zipAttrsWith ( builtins.zipAttrsWith (
key: values: let name: values: let
currentPath = path ++ [key]; currentPath = path ++ [name];
isSingleValue = builtins.length values == 1; isSingleValue = builtins.length values == 1;
isComplete = isComplete =
predicate currentPath predicate currentPath
(builtins.elemAt values 1) (builtins.elemAt values 1)
(builtins.elemAt values 0); (builtins.elemAt values 0);
in in
if isSingleValue || isComplete if isSingleValue
then builtins.elemAt values 0 then builtins.elemAt values 0
else if isComplete
then builtins.elemAt values 1
else process currentPath values else process currentPath values
); );
in in
@ -43,13 +45,13 @@ lib: {
## ##
## @type (List String) -> a -> Attrs -> a | b ## @type (List String) -> a -> Attrs -> a | b
select = path: fallback: target: let select = path: fallback: target: let
key = builtins.head path; name = builtins.head path;
rest = builtins.tail path; rest = builtins.tail path;
in in
if path == [] if path == []
then target then target
else if target ? ${key} else if target ? ${name}
then lib.attrs.select rest fallback target.${key} then lib.attrs.select rest fallback target.${name}
else fallback; else fallback;
## Get a value from an attribute set by a path. If the path does not exist, ## Get a value from an attribute set by a path. If the path does not exist,
@ -61,10 +63,12 @@ lib: {
error = builtins.throw "Path not found in attribute set: ${pathAsString}"; error = builtins.throw "Path not found in attribute set: ${pathAsString}";
in in
if lib.attrs.has path target if lib.attrs.has path target
then lib.attrs.select path target then lib.attrs.select path null target
else error; else error;
# TODO: Document this. ## Create a nested attribute set with a value as the leaf node.
##
## @type (List String) -> a -> Attrs
set = path: value: let set = path: value: let
length = builtins.length path; length = builtins.length path;
process = depth: process = depth:
@ -80,13 +84,13 @@ lib: {
## ##
## @type (List String) -> Attrs -> Bool ## @type (List String) -> Attrs -> Bool
has = path: target: let has = path: target: let
key = builtins.head path; name = builtins.head path;
rest = builtins.tail path; rest = builtins.tail path;
in in
if path == [] if path == []
then true then true
else if target ? ${key} else if target ? ${name}
then lib.attrs.has rest target.${key} then lib.attrs.has rest target.${name}
else false; else false;
## Depending on a given condition, either use the given value or an empty ## Depending on a given condition, either use the given value or an empty
@ -98,43 +102,46 @@ lib: {
then value then value
else {}; else {};
## Map an attribute set's keys and values to a list. ## Map an attribute set's names and values to a list.
## ##
## @type Any a => (String -> Any -> a) -> Attrs -> List a ## @type Any a => (String -> Any -> a) -> Attrs -> List a
mapToList = f: target: mapToList = f: target:
builtins.map (key: f key target.${key}) (builtins.attrNames target); builtins.map (name: f name target.${name}) (builtins.attrNames target);
# TODO: Document this. ## Map an attribute set recursively. Only non-set leaf nodes will be mapped.
##
## @type (List String -> Any -> Any) -> Attrs -> Attrs
mapRecursive = f: target: mapRecursive = f: target:
lib.attrs.mapRecursiveWhen (lib.fp.const true) f target; lib.attrs.mapRecursiveWhen (lib.fp.const true) f target;
# TODO: Document this. ## Map an attribute set recursively when a given predicate returns true.
## Only leaf nodes according to the predicate will be mapped.
##
## @type (Attrs -> Bool) -> (List String -> Any -> Any) -> Attrs -> Attrs
mapRecursiveWhen = predicate: f: target: let mapRecursiveWhen = predicate: f: target: let
process = path: process = path:
builtins.mapAttrs ( builtins.mapAttrs (
key: value: name: value:
if builtins.isAttrs value && predicate value if builtins.isAttrs value && predicate value
then process (path ++ [key]) value then process (path ++ [name]) value
else f (path ++ [key]) value else f (path ++ [name]) value
); );
in in
process [] target; process [] target;
# TODO: Document this. ## Filter an attribute set by a given predicate. The filter is only performed
## on the base level of the attribute set.
##
## @type (String -> Any -> Bool) -> Attrs -> Attrs
filter = predicate: target: let filter = predicate: target: let
keys = builtins.attrNames target; names = builtins.attrNames target;
process = key: let process = name: let
value = target.${key}; value = target.${name};
in in
if predicate key value if predicate name value
then [ then [{inherit name value;}]
{
name = key;
value = value;
}
]
else []; else [];
valid = builtins.concatMap process keys; valid = builtins.concatMap process names;
in in
builtins.listToAttrs valid; builtins.listToAttrs valid;
}; };

View file

@ -1,6 +1,79 @@
let let
lib = import ./../default.nix; lib = import ./../default.nix;
in { in {
"merge" = {
"merges two shallow sets" = let
expected = {
x = 1;
y = 2;
};
actual = lib.attrs.merge {x = 1;} {y = 2;};
in
expected == actual;
"overwrites values from the first set" = let
expected = {
x = 2;
};
actual = lib.attrs.merge {x = 1;} {x = 2;};
in
actual == expected;
"does not merge nested sets" = let
expected = {
x.y = 2;
};
actual = lib.attrs.merge {x.z = 1;} {x.y = 2;};
in
actual == expected;
};
"mergeRecursiveUntil" = {
"merges with predicate" = let
expected = {
x.y.z = 1;
};
actual =
lib.attrs.mergeRecursiveUntil
(path: x: y: lib.lists.last path == "z")
{x.y.z = 2;}
{x.y.z = 1;};
in
actual == expected;
"handles shallow merges" = let
expected = {
x.y.z = 1;
};
actual =
lib.attrs.mergeRecursiveUntil
(path: x: y: true)
{
x = {
y.z = 2;
a = false;
};
}
{x.y.z = 1;};
in
actual == expected;
};
"mergeRecursive" = {
"merges two sets deeply" = let
expected = {
x.y.z = 1;
};
actual =
lib.attrs.mergeRecursive
{x.y.z = 2;}
{x.y.z = 1;};
in
actual == expected;
};
"select" = { "select" = {
"selects a nested value" = let "selects a nested value" = let
expected = "value"; expected = "value";
@ -13,5 +86,244 @@ in {
}; };
in in
actual == expected; actual == expected;
"handles empty path" = let
expected = {
x = {
y = {
z = 1;
};
};
};
actual =
lib.attrs.select
[]
null
{
x = {
y = {
z = 1;
};
};
};
in
actual == expected;
"handles fallback value" = let
expected = "fallback";
actual =
lib.attrs.select
["x" "y" "z"]
expected
{};
in
actual == expected;
};
"selectOrThrow" = {
"selects a nested value" = let
expected = "value";
actual =
lib.attrs.selectOrThrow
["x" "y" "z"]
{
x.y.z = expected;
};
in
actual == expected;
"handles empty path" = let
expected = {
x = {
y = {
z = 1;
};
};
};
actual =
lib.attrs.selectOrThrow
[]
{
x = {
y = {
z = 1;
};
};
};
in
actual == expected;
"throws on nonexistent path" = let
actual =
lib.attrs.selectOrThrow
["x" "y" "z"]
{};
evaluated = builtins.tryEval (builtins.deepSeq actual actual);
in
!evaluated.success;
};
"set" = {
"creates a nested set" = let
expected = {
x = {
y = {
z = 1;
};
};
};
actual = lib.attrs.set ["x" "y" "z"] 1;
in
actual == expected;
"handles empty path" = let
expected = 1;
actual = lib.attrs.set [] 1;
in
actual == expected;
};
"has" = {
"returns true for a nested value" = let
exists = lib.attrs.has ["x" "y" "z"] {x.y.z = 1;};
in
exists;
"returns false for a nonexistent value" = let
exists = lib.attrs.has ["x" "y" "z"] {};
in
!exists;
"handles empty path" = let
exists = lib.attrs.has [] {};
in
exists;
};
"when" = {
"returns the value when condition is true" = let
expected = "value";
actual = lib.attrs.when true expected;
in
actual == expected;
"returns an empty set when condition is false" = let
expected = {};
actual = lib.attrs.when false "value";
in
actual == expected;
};
"mapToList" = {
"converts a set to a list" = let
expected = [
{
name = "x";
value = 1;
}
{
name = "y";
value = 2;
}
];
actual =
lib.attrs.mapToList
(name: value: {inherit name value;})
{
x = 1;
y = 2;
};
in
actual == expected;
};
"mapRecursiveWhen" = {
"maps a set recursively" = let
expected = {
x = {
y = {
z = 2;
};
};
};
actual =
lib.attrs.mapRecursiveWhen
(value: true)
(path: value: value + 1)
{
x = {
y = {
z = 1;
};
};
};
in
actual == expected;
"maps a set given a condition" = let
expected = {
x = {
y = {
z = 1;
};
};
};
actual =
lib.attrs.mapRecursiveWhen
(value: !(value ? z))
(path: value:
# We map before we get to a non-set value
if builtins.isAttrs value
then value
else value + 1)
{
x = {
y = {
z = 1;
};
};
};
in
actual == expected;
};
"mapRecursive" = {
"maps a set recursively" = let
expected = {
x = {
y = {
z = 2;
};
};
};
actual =
lib.attrs.mapRecursive
(path: value: value + 1)
{
x = {
y = {
z = 1;
};
};
};
in
actual == expected;
};
"filter" = {
"filters a set" = let
expected = {
y = 2;
};
actual =
lib.attrs.filter
(name: value: name == "y")
{
x = 1;
y = 2;
};
in
actual == expected;
}; };
} }

View file

@ -1,10 +1,21 @@
lib: { lib: {
bools = { bools = {
into = { into = {
## Convert a boolean value into a string.
##
## @type Bool -> String
string = value: string = value:
if value if value
then "true" then "true"
else "false"; else "false";
## Convert a boolean into either the string "yes" or "no".
##
## @type Bool -> String
yesno = value:
if value
then "yes"
else "no";
}; };
## Choose between two values based on a condition. When true, the first value ## Choose between two values based on a condition. When true, the first value

View file

@ -0,0 +1,161 @@
let
lib = import ./../default.nix;
in {
"into" = {
"string" = {
"handles true" = let
expected = "true";
actual = lib.bools.into.string true;
in
actual == expected;
"handles false" = let
expected = "false";
actual = lib.bools.into.string false;
in
actual == expected;
};
"yesno" = {
"handles true" = let
expected = "yes";
actual = lib.bools.into.yesno true;
in
actual == expected;
"handles false" = let
expected = "no";
actual = lib.bools.into.yesno false;
in
actual == expected;
};
};
"when" = {
"returns first value when true" = let
expected = "foo";
actual = lib.bools.when true expected "bar";
in
actual == expected;
"returns second value when false" = let
expected = "bar";
actual = lib.bools.when false "foo" expected;
in
actual == expected;
};
"and" = {
"returns true when both are true" = let
expected = true;
actual = lib.bools.and true true;
in
actual == expected;
"returns false when first is false" = let
expected = false;
actual = lib.bools.and false true;
in
actual == expected;
"returns false when second is false" = let
expected = false;
actual = lib.bools.and true false;
in
actual == expected;
"returns false when both are false" = let
expected = false;
actual = lib.bools.and false false;
in
actual == expected;
};
"and'" = let
getTrue = _: true;
getFalse = _: false;
in {
"returns true when both are true" = let
expected = true;
actual = lib.bools.and' getTrue getTrue null;
in
actual == expected;
"returns false when first is false" = let
expected = false;
actual = lib.bools.and' getFalse getTrue null;
in
actual == expected;
"returns false when second is false" = let
expected = false;
actual = lib.bools.and' getTrue getFalse null;
in
actual == expected;
"returns false when both are false" = let
expected = false;
actual = lib.bools.and' getFalse getFalse null;
in
actual == expected;
};
"or" = {
"returns true when both are true" = let
expected = true;
actual = lib.bools.or true true;
in
actual == expected;
"returns true when first is true" = let
expected = true;
actual = lib.bools.or true false;
in
actual == expected;
"returns true when second is true" = let
expected = true;
actual = lib.bools.or false true;
in
actual == expected;
"returns false when both are false" = let
expected = false;
actual = lib.bools.or false false;
in
actual == expected;
};
"or'" = let
getTrue = _: true;
getFalse = _: false;
in {
"returns true when both are true" = let
expected = true;
actual = lib.bools.or' getTrue getTrue null;
in
actual == expected;
"returns true when first is true" = let
expected = true;
actual = lib.bools.or' getTrue getFalse null;
in
actual == expected;
"returns true when second is true" = let
expected = true;
actual = lib.bools.or' getFalse getTrue null;
in
actual == expected;
"returns false when both are false" = let
expected = false;
actual = lib.bools.or' getFalse getFalse null;
in
actual == expected;
};
"not" = {
"returns false when true" = let
expected = false;
actual = lib.bools.not true;
in
actual == expected;
"returns true when false" = let
expected = true;
actual = lib.bools.not false;
in
actual == expected;
};
}

View file

@ -40,8 +40,8 @@ let
mergeAttrsRecursiveUntil = predicate: x: y: let mergeAttrsRecursiveUntil = predicate: x: y: let
process = path: process = path:
builtins.zipAttrsWith ( builtins.zipAttrsWith (
key: values: let name: values: let
currentPath = path ++ [key]; currentPath = path ++ [name];
isSingleValue = builtins.length values == 1; isSingleValue = builtins.length values == 1;
isComplete = isComplete =
predicate currentPath predicate currentPath

View file

@ -1,5 +1,10 @@
lib: { lib: {
errors = { errors = {
## Prints a message if the condition is not met. The result of
## the condition is returned.
##
## @notest
## @type Bool -> String -> Bool
trace = condition: message: trace = condition: message:
if condition if condition
then true then true

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -21,7 +21,7 @@ lib: {
## ##
## @type (List (Any -> Any)) -> Any -> Any ## @type (List (Any -> Any)) -> Any -> Any
pipe = fs: ( pipe = fs: (
x: builtins.foldl' (value: f: f x) x fs x: builtins.foldl' (value: f: f value) x fs
); );
## Reverse the order of arguments to a function that has two parameters. ## Reverse the order of arguments to a function that has two parameters.
@ -37,13 +37,20 @@ lib: {
## @type (a -> b -> c -> d -> e) -> d -> c -> b -> a -> e ## @type (a -> b -> c -> d -> e) -> d -> c -> b -> a -> e
flip4 = f: a: b: c: d: f d c b a; flip4 = f: a: b: c: d: f d c b a;
# TODO: Document this. ## Get the arguments of a function or functor.
## An attribute set is returned with the arguments as keys. The values
## are `true` when the argument has a default value specified and `false`
## when it does not.
##
## @type Function -> Attrs
args = f: args = f:
if f ? __functor if f ? __functor
then f.__args__ or lib.fp.args (f.__functor f) then f.__args__ or (lib.fp.args (f.__functor f))
else builtins.functionArgs f; else builtins.functionArgs f;
# TODO: Document this. ## Create a function that is called with only the arguments it specifies.
##
## @type Attrs a => (a -> b) -> a -> b
withDynamicArgs = f: args: let withDynamicArgs = f: args: let
fArgs = lib.fp.args f; fArgs = lib.fp.args f;
common = builtins.intersectAttrs fArgs args; common = builtins.intersectAttrs fArgs args;

139
lib/src/fp/default.test.nix Normal file
View file

@ -0,0 +1,139 @@
let
lib = import ./../default.nix;
in {
"id" = {
"returns its argument" = let
expected = "foo";
actual = lib.fp.id expected;
in
actual == expected;
};
"const" = {
"creates a function that returns its argument" = let
expected = "foo";
actual = lib.fp.const expected "bar";
in
actual == expected;
};
"compose" = {
"composes two functions" = let
f = x: x + 1;
g = x: x * 2;
expected = 5;
actual = lib.fp.compose f g 2;
in
actual == expected;
};
"pipe" = {
"pipes two functions" = let
f = x: x + 1;
g = x: x * 2;
expected = 5;
actual = lib.fp.pipe [g f] 2;
in
actual == expected;
};
"flip2" = {
"flips the arguments of a binary function" = let
f = a: b: a - b;
expected = 1;
actual = lib.fp.flip2 f 1 2;
in
actual == expected;
};
"flip3" = {
"flips the arguments of a ternary function" = let
f = a: b: c: a - b - c;
expected = 0;
actual = lib.fp.flip3 f 1 2 3;
in
actual == expected;
};
"flip4" = {
"flips the arguments of a quaternary function" = let
f = a: b: c: d: a - b - c - d;
expected = -2;
actual = lib.fp.flip4 f 1 2 3 4;
in
actual == expected;
};
"args" = {
"gets a functions attr set arguments" = let
expected = {
x = false;
y = true;
};
actual = lib.fp.args ({
x,
y ? null,
}:
null);
in
actual == expected;
"returns an empty set if the function has no attrs arguments" = let
expected = {};
actual = lib.fp.args (args: null);
in
actual == expected;
"supports functors" = let
expected = {
x = false;
y = true;
};
actual = lib.fp.args {
__functor = self: {
x,
y ? null,
}:
null;
};
in
actual == expected;
"supports cached functor arguments" = let
expected = {
x = false;
y = true;
};
actual = lib.fp.args {
__args__ = {
x = false;
y = true;
};
__functor = self: args:
null;
};
in
actual == expected;
};
"withDynamicArgs" = {
"applies a function with dynamic arguments" = let
expected = {x = true;};
actual = lib.fp.withDynamicArgs (args @ {x}: args) {
x = true;
y = true;
};
in
actual == expected;
"applies all arguments if none are specified" = let
expected = {
x = true;
y = true;
};
actual = lib.fp.withDynamicArgs (args: args) expected;
in
actual == expected;
};
}

View file

@ -0,0 +1,113 @@
let
lib = import ./../default.nix;
in {
"withRecursion" = {
"evaluates within a given limit" = let
expected = {
x = 1;
};
actual = lib.generators.withRecursion {limit = 100;} expected;
in
expected == actual;
"fails when the limit is reached" = let
expected = {
x = 1;
};
actual = lib.generators.withRecursion {limit = -1;} expected;
evaluated = builtins.tryEval (builtins.deepSeq actual actual);
in
!evaluated.success;
"does not fail when throw is disabled" = let
expected = {
x = "<unevaluated>";
};
actual =
lib.generators.withRecursion {
limit = -1;
throw = false;
}
{x = 1;};
evaluated = builtins.tryEval (builtins.deepSeq actual actual);
in
evaluated.success
&& evaluated.value == expected;
};
"pretty" = {
"formats with defaults" = let
expected = ''
{
attrs = { };
bool = true;
float = 0.0;
function = <function>;
int = 0;
list = [ ];
string = "string";
}'';
actual = lib.generators.pretty {} {
attrs = {};
bool = true;
float = 0.0;
function = x: x;
int = 0;
list = [];
string = "string";
# NOTE: We are not testing `path` types because they can return out of store
# values which are not deterministic.
# path = ./.;
};
in
actual == expected;
"formats with custom prettifiers" = let
expected = ''
{
attrs = { };
bool = true;
custom = <custom>;
float = 0.0;
function = <function>;
int = 0;
list = [ ];
string = "string";
}'';
actual =
lib.generators.pretty {
allowCustomPrettifiers = true;
} {
attrs = {};
bool = true;
float = 0.0;
function = x: x;
int = 0;
list = [];
string = "string";
custom = {
value = 0;
__pretty__ = value: "<custom>";
};
};
in
actual == expected;
"formats with multiline disabled" = let
expected = "{ attrs = { }; bool = true; float = 0.0; function = <function>; int = 0; list = [ ]; string = \"string\"; }";
actual =
lib.generators.pretty {
multiline = false;
} {
attrs = {};
bool = true;
float = 0.0;
function = x: x;
int = 0;
list = [];
string = "string";
};
in
actual == expected;
};
}

View file

@ -2,11 +2,13 @@ lib: {
importers = { importers = {
## Import a JSON file as a Nix value. ## Import a JSON file as a Nix value.
## ##
## @notest
## @type Path -> a ## @type Path -> a
json = file: builtins.fromJSON (builtins.readFile file); json = file: builtins.fromJSON (builtins.readFile file);
## Import a TOML file as a Nix value. ## Import a TOML file as a Nix value.
## ##
## @notest
## @type Path -> a ## @type Path -> a
toml = file: builtins.fromTOML (builtins.readFile file); toml = file: builtins.fromTOML (builtins.readFile file);
}; };

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -1,7 +1,11 @@
lib: { lib: {
lists = { lists = {
from = { from = {
# TODO: Document this. ## Convert a value to a list. If the value is already a list,
## it will be returned as-is. If the value is not a list, it
## will be wrapped in a list.
##
## @type a | (List a) -> List a
any = value: any = value:
if builtins.isList value if builtins.isList value
then value then value
@ -11,7 +15,7 @@ lib: {
sort = { sort = {
## Perform a natural sort on a list of strings. ## Perform a natural sort on a list of strings.
## ##
## @type List -> List ## @type List String -> List String
natural = list: let natural = list: let
vectorize = string: let vectorize = string: let
serialize = part: serialize = part:
@ -27,21 +31,29 @@ lib: {
builtins.map (x: builtins.elemAt x 1) (builtins.sort isLess prepared); builtins.map (x: builtins.elemAt x 1) (builtins.sort isLess prepared);
}; };
# TODO: Document this. ## Map a list using both the index and value of each item. The
## index starts at 0.
##
## @type (Int -> a -> b) -> List a -> List b
mapWithIndex = f: list: mapWithIndex = f: list:
builtins.genList builtins.genList
(i: f i (builtins.elemAt list i)) (i: f i (builtins.elemAt list i))
(builtins.length list); (builtins.length list);
# TODO: Document this. ## Map a list using both the index and value of each item. The
## index starts at 1.
##
## @type (Int -> a -> b) -> List a -> List b
mapWithIndex1 = f: list: mapWithIndex1 = f: list:
builtins.genList builtins.genList
(i: f (i + 1) (builtins.elemAt list i)) (i: f (i + 1) (builtins.elemAt list i))
(builtins.length list); (builtins.length list);
## Compare two lists. ## Compare two lists using a custom compare function. The compare
## function is called for each element in the lists that need to
## be compared.
## ##
## @type (a -> b -> Int) -> List a -> List b -> Int ## @type (a -> b -> -1 | 0 | 1) -> List a -> List b -> Int
compare = compare: a: b: let compare = compare: a: b: let
result = compare (builtins.head a) (builtins.head b); result = compare (builtins.head a) (builtins.head b);
in in
@ -60,7 +72,7 @@ lib: {
## ##
## @type List a -> a ## @type List a -> a
last = list: last = list:
assert lib.assertMsg (list != []) "List cannot be empty"; assert lib.errors.trace (list != []) "List cannot be empty";
builtins.elemAt list (builtins.length list - 1); builtins.elemAt list (builtins.length list - 1);
## Slice part of a list to create a new list. ## Slice part of a list to create a new list.
@ -137,7 +149,9 @@ lib: {
else [value] else [value]
else []; else [];
# TODO: Document this. ## Count the number of items in a list that satisfy a given predicate.
##
## @type (a -> Bool) -> List a -> Int
count = predicate: list: count = predicate: list:
builtins.foldl' ( builtins.foldl' (
total: value: total: value:
@ -148,7 +162,9 @@ lib: {
0 0
list; list;
# TODO: Document this. ## Remove duplicate items from a list.
##
## @type List -> List
unique = list: let unique = list: let
filter = result: value: filter = result: value:
if builtins.elem value result if builtins.elem value result

View file

@ -0,0 +1,174 @@
let
lib = import ./../default.nix;
in {
"from" = {
"any" = {
"returns a list containing the value" = let
expected = [1];
actual = lib.lists.from.any 1;
in
actual == expected;
"returns the value if the value was already a list" = let
expected = [1];
actual = lib.lists.from.any expected;
in
actual == expected;
};
};
"sort" = {
"natural" = {
"sorts a list of strings" = let
expected = ["1" "a" "a0" "a1" "b" "c"];
actual = lib.lists.sort.natural ["c" "a" "b" "a1" "a0" "1"];
in
actual == expected;
};
};
"mapWithIndex" = {
"maps a list using index 0" = let
expected = ["0: a" "1: b" "2: c"];
actual = lib.lists.mapWithIndex (i: v: "${builtins.toString i}: ${v}") ["a" "b" "c"];
in
actual == expected;
};
"mapWithIndex1" = {
"maps a list using index 1" = let
expected = ["1: a" "2: b" "3: c"];
actual = lib.lists.mapWithIndex1 (i: v: "${builtins.toString i}: ${v}") ["a" "b" "c"];
in
actual == expected;
};
"compare" = {
"compares two lists" = {
"returns -1 if the first list is smaller" = let
expected = -1;
actual = lib.lists.compare lib.numbers.compare [1 2 3] [1 2 4];
in
actual == expected;
"returns 1 if the first list is larger" = let
expected = 1;
actual = lib.lists.compare lib.numbers.compare [1 2 4] [1 2 3];
in
actual == expected;
"returns 0 if the lists are equal" = let
expected = 0;
actual = lib.lists.compare lib.numbers.compare [1 2 3] [1 2 3];
in
actual == expected;
};
};
"last" = {
"returns the last element of a list" = let
expected = 3;
actual = lib.lists.last [1 2 3];
in
actual == expected;
"fails if the list is empty" = let
actual = lib.lists.last [];
evaluated = builtins.tryEval actual;
in
!evaluated.success;
};
"slice" = {
"slices a list" = {
"slices a list from the start" = let
expected = [1 2];
actual = lib.lists.slice 0 2 [1 2 3];
in
actual == expected;
"slices a list from the end" = let
expected = [2 3];
actual = lib.lists.slice 1 3 [1 2 3];
in
actual == expected;
"slices a list from the middle" = let
expected = [2];
actual = lib.lists.slice 1 1 [1 2 3];
in
actual == expected;
};
};
"take" = {
"takes the first n elements" = let
expected = [1 2];
actual = lib.lists.take 2 [1 2 3];
in
actual == expected;
};
"drop" = {
"drops the first n elements" = let
expected = [3];
actual = lib.lists.drop 2 [1 2 3];
in
actual == expected;
};
"reverse" = {
"reverses a list" = let
expected = [3 2 1];
actual = lib.lists.reverse [1 2 3];
in
actual == expected;
};
"intersperse" = {
"intersperses a list with a separator" = let
expected = [1 "-" 2 "-" 3];
actual = lib.lists.intersperse "-" [1 2 3];
in
actual == expected;
"handles lists with less than 2 elements" = let
expected = [1];
actual = lib.lists.intersperse "-" [1];
in
actual == expected;
};
"range" = {
"returns a range of numbers" = let
expected = [1 2 3 4 5];
actual = lib.lists.range 1 5;
in
actual == expected;
};
"when" = {
"returns the list if the condition is true" = let
expected = [1 2 3];
actual = lib.lists.when true [1 2 3];
in
actual == expected;
"returns an empty list if the condition is false" = let
expected = [];
actual = lib.lists.when false [1 2 3];
in
actual == expected;
};
"count" = {
"counts the number of elements in a list" = let
expected = 2;
actual = lib.lists.count (value: value < 3) [1 2 3];
in
actual == expected;
};
"unique" = {
"removes duplicate elements" = let
expected = [1 2 3];
actual = lib.lists.unique [1 2 3 1 2 3];
in
actual == expected;
};
}

View file

@ -1,12 +1,16 @@
lib: { lib: {
math = { math = {
# TODO: Document this. ## Return the smaller of two numbers.
##
## @type Int -> Int -> Int
min = x: y: min = x: y:
if x < y if x < y
then x then x
else y; else y;
# TODO: Document this. ## Return the larger of two numbers.
##
## @type Int -> Int -> Int
max = x: y: max = x: y:
if x > y if x > y
then x then x

View file

@ -0,0 +1,19 @@
let
lib = import ./../default.nix;
in {
"min" = {
"returns the smaller number" = let
expected = 1;
actual = lib.math.min 1 2;
in
actual == expected;
};
"max" = {
"returns the larger number" = let
expected = 2;
actual = lib.math.max 1 2;
in
actual == expected;
};
}

View file

@ -3,6 +3,7 @@ lib: {
from = { from = {
## Create a module from a JSON file. ## Create a module from a JSON file.
## ##
## @notest
## @type Path -> Module ## @type Path -> Module
json = file: { json = file: {
__file__ = file; __file__ = file;
@ -11,6 +12,7 @@ lib: {
## Create a module from a TOML file. ## Create a module from a TOML file.
## ##
## @notest
## @type Path -> Module ## @type Path -> Module
toml = file: { toml = file: {
__file__ = file; __file__ = file;
@ -19,7 +21,12 @@ lib: {
}; };
apply = { apply = {
# TODO: Document this. ## Apply custom definitions such as `merge` and `when` to a definition.
## Note that this function does not perform actiosn like `merge`, but
## instead pulls out the merge contents to be processed by the module
## system.
##
## @type Definition -> List (Definition | (List Definition))
properties = definition: properties = definition:
if lib.types.is "merge" definition if lib.types.is "merge" definition
then builtins.concatMap lib.modules.apply.properties definition.content then builtins.concatMap lib.modules.apply.properties definition.content
@ -32,7 +39,11 @@ lib: {
else [] else []
else [definition]; else [definition];
# TODO: Document this. ## Apply overrides for a definition. This uses the priority system
## to determine which definition to use. The most important (lowest
## priority) choice will be used.
##
## @type List Definition -> { highestPriority :: Int, values :: List (Definition | (List Definition)) }
overrides = definitions: let overrides = definitions: let
getPriority = definition: getPriority = definition:
if lib.types.is "override" definition.value if lib.types.is "override" definition.value
@ -64,7 +75,9 @@ lib: {
definitions; definitions;
}; };
# TODO: Document this. ## Apply ordering for prioritized definitions.
##
## @type List Definition -> List Definition
order = definitions: let order = definitions: let
normalize = definition: normalize = definition:
if lib.types.is "order" definition if lib.types.is "order" definition
@ -80,7 +93,10 @@ lib: {
in in
builtins.sort compare normalized; builtins.sort compare normalized;
# TODO: Document this. ## Normalize the type of an option. This will set a default type if none
## was provided.
##
## @type List String -> Option -> Option
fixup = location: option: fixup = location: option:
if option.type.getSubModules or null == null if option.type.getSubModules or null == null
then then
@ -95,32 +111,43 @@ lib: {
options = []; options = [];
}; };
# TODO: Document this. ## Invert the structure of `merge`, `when`, and `override` definitions so
## that they apply to each individual attribute in their respective sets.
## Note that this function _only_ supports attribute sets within specialized
## definitions such as `when` and `override`. Other values like lists will
## throw a type error.
##
## @type Definition -> List Definition
invert = config: invert = config:
if lib.types.is "merge" config if lib.types.is "merge" config
then builtins.concatMap lib.modules.apply.invert config.content then builtins.concatMap lib.modules.apply.invert config.content
else if lib.types.is "when" config else if lib.types.is "when" config
then then
builtins.map builtins.map
(builtins.mapAttrs (key: value: lib.modules.when config.condition value)) (builtins.mapAttrs (name: value: lib.modules.when config.condition value))
(lib.modules.apply.invert config.content) (lib.modules.apply.invert config.content)
else if lib.types.is "override" config else if lib.types.is "override" config
then then
builtins.map builtins.map
(builtins.mapAttrs (key: value: lib.modules.override config.priority value)) (builtins.mapAttrs (name: value: lib.modules.override config.priority value))
(lib.modules.apply.invert config.content) (lib.modules.apply.invert config.content)
else [config]; else [config];
}; };
validate = { validate = {
# TODO: Document this. ## Check that a module only specifies supported attributes.
##
## @type Attrs -> Bool
keys = module: let keys = module: let
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS; invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
in in
invalid == {}; invalid == {};
}; };
# TODO: Document this. ## Modules only support certain keys at the root level. This list determines
## the valid attributes that users can supply.
##
## @type List String
VALID_KEYS = [ VALID_KEYS = [
"__file__" "__file__"
"__key__" "__key__"
@ -132,7 +159,10 @@ lib: {
"meta" "meta"
]; ];
# TODO: Document this. ## Normalize a module to a standard structure. All other information will be
## lost in the conversion.
##
## @type String -> String -> Attrs -> Module
normalize = file: key: module: let normalize = file: key: module: let
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS; invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid); invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
@ -159,7 +189,11 @@ lib: {
} }
else builtins.throw "Module `${key}` has unsupported attribute(s): ${invalidKeys}"; else builtins.throw "Module `${key}` has unsupported attribute(s): ${invalidKeys}";
# TODO: Document this. ## Convert a module that is either a function or an attribute set into
## a resolved attribute set. If the module was a function then it will
## be evaluated and the result will be returned.
##
## @type String -> Attrs | (Attrs -> Attrs) -> Attrs -> Attrs
resolve = key: module: args: let resolve = key: module: args: let
dynamicArgs = dynamicArgs =
builtins.mapAttrs builtins.mapAttrs
@ -175,24 +209,38 @@ lib: {
then lib.fp.withDynamicArgs module (args // dynamicArgs) then lib.fp.withDynamicArgs module (args // dynamicArgs)
else module; else module;
# TODO: Document this. ## The default priority to set for values that do not have one provided.
##
## @type Int
DEFAULT_PRIORITY = 100; DEFAULT_PRIORITY = 100;
## Allow for sorting the values provided to a module by priority. The ## Allow for sorting the values provided to a module by priority. The
## most important value will be used. ## most important value will be used.
## ##
## @type Int -> a -> Priority a ## @notest
## @type Int -> a -> Attrs
order = priority: value: { order = priority: value: {
__type__ = "order"; __type__ = "order";
inherit priority value; inherit priority value;
}; };
orders = { orders = {
# TODO: Document this. ## Order a value before others.
##
## @notest
## @type a -> Attrs
before = lib.modules.order 500; before = lib.modules.order 500;
# TODO: Document this.
## Use the default ordering for a value.
##
## @notest
## @type a -> Attrs
default = lib.modules.order 1000; default = lib.modules.order 1000;
# TODO: Document this.
## Order a value after others.
##
## @notest
## @type a -> Attrs
after = lib.modules.order 1500; after = lib.modules.order 1500;
}; };
@ -201,36 +249,70 @@ lib: {
## @type List Module -> List (String | Path) ## @type List Module -> List (String | Path)
getFiles = builtins.map (module: module.__file__); getFiles = builtins.map (module: module.__file__);
# TODO: Document this. ## Create a conditional value which is only used when the condition's
## value is `true`.
##
## @notest
## @type Bool -> a -> When a
when = condition: content: { when = condition: content: {
__type__ = "when"; __type__ = "when";
inherit condition content; inherit condition content;
}; };
# TODO: Document this. ## Merge module attribute sets when evaluated in the module system.
##
## @notest
## @type List Attrs -> Attrs
merge = content: { merge = content: {
__type__ = "merge"; __type__ = "merge";
inherit content; inherit content;
}; };
# TODO: Document this. ## Create a value override which can replace other definitions if it
## has the higher priority.
##
## @notest
## @type Int -> a -> Attrs
override = priority: content: { override = priority: content: {
__type__ = "override"; __type__ = "override";
inherit priority content; inherit priority content;
}; };
overrides = { overrides = {
# TODO: Document this. ## Create an override used for setting the `default.value` from an
## option. This uses the lowest priority of all predefined overrides.
##
## @notest
## @type a -> Attrs
option = lib.modules.override 1500; option = lib.modules.override 1500;
# TODO: Document this.
## Create a default override for a value. This uses a very low priority
## so that it can easily be overridden.
##
## @notest
## @type a -> Attrs
default = lib.modules.override 1000; default = lib.modules.override 1000;
# TODO: Document this.
## Create a high priority override for a value. This is not the highest
## priority possible, but it will override nearly everything else except
## for very explicit cases.
##
## @notest
## @type a -> Attrs
force = lib.modules.override 50; force = lib.modules.override 50;
# TODO: Document this.
## Create a high priority override intended to be used only for VM targets.
## This allows for forcing certain values even if a user has otherwise
## specified `lib.modules.overrides.force`.
##
## @notest
## @type a -> Attrs
vm = lib.modules.override 10; vm = lib.modules.override 10;
}; };
# TODO: Document this. ## Combine multiple modules together.
##
## @type List String -> List Module -> { matched :: Attrs, unmatched :: List Definition }
combine = prefix: modules: let combine = prefix: modules: let
getConfig = module: getConfig = module:
builtins.map builtins.map
@ -246,16 +328,15 @@ lib: {
modules; modules;
process = prefix: options: configs: let process = prefix: options: configs: let
# TODO: Document this.
byName = attr: f: modules: byName = attr: f: modules:
builtins.zipAttrsWith builtins.zipAttrsWith
(lib.fp.const builtins.concatLists) (key: value: builtins.concatLists value)
(builtins.map ( (builtins.map (
module: let module: let
subtree = module.${attr}; subtree = module.${attr};
in in
if builtins.isAttrs subtree if builtins.isAttrs subtree
then builtins.mapAttrs (key: f module) subtree then builtins.mapAttrs (name: f module) subtree
else builtins.throw "Value for `${builtins.concatStringsSep "." prefix} is of type `${builtins.typeOf subtree}` but an attribute set was expected." else builtins.throw "Value for `${builtins.concatStringsSep "." prefix} is of type `${builtins.typeOf subtree}` but an attribute set was expected."
) )
modules); modules);
@ -348,7 +429,7 @@ lib: {
matched = builtins.mapAttrs (key: value: value.matched) resultsByName; matched = builtins.mapAttrs (key: value: value.matched) resultsByName;
unmatched = unmatched =
builtins.mapAttrs (key: value: value.unmatched) resultsByName builtins.mapAttrs (name: value: value.unmatched) resultsByName
// builtins.removeAttrs definitionsByName' (builtins.attrNames matched); // builtins.removeAttrs definitionsByName' (builtins.attrNames matched);
in { in {
inherit matched; inherit matched;
@ -374,7 +455,10 @@ lib: {
in in
process prefix modules configs; process prefix modules configs;
# TODO: Document this. ## Run a set of modules. Custom arguments can also be supplied which will
## be provided to all modules statically as they are not modifiable.
##
## @type { modules? :: List (Attrs | (Attrs -> Attrs)), args? :: Attrs, prefix? :: List String } -> { type :: Module, extend :: lib.modules.run, options :: Attrs, config :: Attrs, __module__ :: Attrs }
run = settings @ { run = settings @ {
modules ? [], modules ? [],
args ? {}, args ? {},
@ -395,7 +479,6 @@ lib: {
prefix = extensions.prefix or settings.prefix or []; prefix = extensions.prefix or settings.prefix or [];
}; };
# TODO: Document this.
collect = let collect = let
load = args: file: key: module: let load = args: file: key: module: let
moduleFromValue = lib.modules.normalize file key (lib.modules.resolve key module args); moduleFromValue = lib.modules.normalize file key (lib.modules.resolve key module args);
@ -532,7 +615,7 @@ lib: {
declared = declared =
lib.attrs.mapRecursiveWhen lib.attrs.mapRecursiveWhen
(value: !(lib.types.is "option" value)) (value: !(lib.types.is "option" value))
(key: value: value.value) (name: value: value.value)
options; options;
freeform = let freeform = let

View file

@ -1,7 +1,315 @@
let let
lib = import ./../default.nix; lib = import ./../default.nix;
in { in {
examples = { "apply" = {
"properties" = {
"handles normal values" = let
expected = [{}];
actual = lib.modules.apply.properties {};
in
actual == expected;
"handles merge" = let
expected = [{x = 1;} {x = 2;}];
actual = lib.modules.apply.properties (lib.modules.merge [{x = 1;} {x = 2;}]);
in
actual
== expected;
"handles when" = let
expected = [[{x = 1;}]];
actual = lib.modules.apply.properties (lib.modules.when true [{x = 1;}]);
in
actual == expected;
};
"overrides" = {
"handles normal values" = let
expected = {
highestPriority = 100;
values = [
{
value = 1;
}
];
};
actual = lib.modules.apply.overrides [
{
value = 1;
}
];
in
actual == expected;
"handles overrides" = let
expected = {
highestPriority = 100;
values = [
{value = "world";}
];
};
actual = lib.modules.apply.overrides [
{value = "world";}
{value = lib.modules.override 101 "hello";}
];
in
actual == expected;
};
"order" = {
"handles normal values" = let
expected = [{}];
actual = lib.modules.apply.order [{}];
in
actual == expected;
"handles priority" = let
expected = [
{
value = 1;
priority = 10;
}
{
value = 3;
priority = 50;
}
{value = 2;}
];
actual = lib.modules.apply.order [
{
value = 1;
priority = 10;
}
{value = 2;}
{
value = 3;
priority = 50;
}
];
in
actual == expected;
};
"fixup" = {
"sets default type for option" = let
actual = lib.modules.apply.fixup [] (
lib.options.create {}
);
in
actual.type.name == "Unspecified";
};
"invert" = {
"inverts merge" = let
expected = [{x = 1;} {x = 2;}];
actual =
lib.modules.apply.invert (lib.modules.merge [{x = 1;} {x = 2;}]);
in
actual
== expected;
"inverts when" = let
expected = [
{
x = {
__type__ = "when";
condition = true;
content = 1;
};
y = {
__type__ = "when";
condition = true;
content = 2;
};
}
];
actual = lib.modules.apply.invert (lib.modules.when true {
x = 1;
y = 2;
});
in
actual == expected;
"inverts overrides" = let
expected = [
{
x = {
__type__ = "override";
priority = 100;
content = 1;
};
y = {
__type__ = "override";
priority = 100;
content = 2;
};
}
];
actual = lib.modules.apply.invert (lib.modules.override 100 {
x = 1;
y = 2;
});
in
actual == expected;
};
};
"validate" = {
"keys" = {
"handles an empty set" = let
value = lib.modules.validate.keys {};
in
value;
"handles a valid module" = let
value = lib.modules.validate.keys {
__file__ = "virtual:aux/example";
__key__ = "aux/example";
includes = [];
excludes = [];
options = {};
config = {};
freeform = null;
meta = {};
};
in
value;
"handles an invalid module" = let
value = lib.modules.validate.keys {
invalid = null;
};
in
!value;
};
};
"normalize" = {
"handles an empty set" = let
expected = {
__file__ = "/aux/example.nix";
__key__ = "example";
config = {};
excludes = [];
includes = [];
options = {};
};
actual = lib.modules.normalize "/aux/example.nix" "example" {};
in
actual == expected;
"handles an example module" = let
expected = {
__file__ = "myfile.nix";
__key__ = "mykey";
config = {
x = true;
};
excludes = [];
includes = [];
options = {};
};
actual = lib.modules.normalize "/aux/example.nix" "example" {
__file__ = "myfile.nix";
__key__ = "mykey";
config.x = true;
};
in
actual == expected;
};
"resolve" = {
"handles an attribute set" = let
expected = {config.x = 1;};
actual = lib.modules.resolve "example" {config.x = 1;} {};
in
actual == expected;
"handles a function" = let
expected = {config.x = 1;};
actual = lib.modules.resolve "example" (lib.fp.const {config.x = 1;}) {};
in
actual == expected;
"handles a function with arguments" = let
expected = {config.x = 1;};
actual = lib.modules.resolve "example" (args: {config.x = args.x;}) {x = 1;};
in
actual == expected;
};
"getFiles" = {
"gets the files for a list of modules" = let
expected = ["/aux/example.nix"];
actual = lib.modules.getFiles [{__file__ = "/aux/example.nix";}];
in
actual
== expected;
};
"combine" = {
"handles empty modules" = let
expected = {
matched = {};
unmatched = [];
};
actual = lib.modules.combine [] [
(lib.modules.normalize "/aux/example.nix" "example" {})
];
in
actual == expected;
"handles a single module" = let
expected = {
matched = {};
unmatched = [
{
__file__ = "/aux/example.nix";
prefix = ["x"];
value = 1;
}
];
};
actual = lib.modules.combine [] [
(lib.modules.normalize "/aux/example.nix" "example" {
config = {
x = 1;
};
})
];
in
actual == expected;
"handles multiple modules" = let
unmatched = [
{
__file__ = "/aux/example2.nix";
prefix = ["y"];
value = 2;
}
];
actual = lib.modules.combine [] [
(lib.modules.normalize "/aux/example1.nix" "example2" {
options = {
x = lib.options.create {};
};
config = {
x = 1;
};
})
(lib.modules.normalize "/aux/example2.nix" "example2" {
config = {
y = 2;
};
})
];
in
(actual.unmatched == unmatched)
&& actual.matched ? x;
};
"run" = {
"empty" = let "empty" = let
evaluated = lib.modules.run { evaluated = lib.modules.run {
modules = [ modules = [
@ -73,5 +381,47 @@ in {
actual = evaluated.config.aux.message; actual = evaluated.config.aux.message;
in in
actual == expected; actual == expected;
"conditional" = let
expected = "Hello, World!";
evaluated = lib.modules.run {
modules = [
{
options.aux = {
message = lib.options.create {
type = lib.types.string;
};
};
config = {
aux = {
message = lib.modules.when true expected;
};
};
}
];
};
in
evaluated.config.aux.message == expected;
"conditional list" = let
expected = ["Hello, World!"];
evaluated = lib.modules.run {
modules = [
{
options.aux = {
message = lib.options.create {
type = lib.types.list.of lib.types.string;
};
};
config = {
aux = {
message = lib.modules.when true expected;
};
};
}
];
};
in
evaluated.config.aux.message == expected;
}; };
} }

View file

@ -49,7 +49,7 @@ lib: {
then "F" then "F"
else builtins.throw "Invalid hex digit."; else builtins.throw "Invalid hex digit.";
in in
lib.strings.concatMapSep lib.strings.concatMap
serialize serialize
(lib.numbers.into.base 16 value); (lib.numbers.into.base 16 value);
}; };

View file

@ -0,0 +1,57 @@
let
lib = import ./../default.nix;
in {
"into" = {
"string" = {
"converts an int into a string" = let
expected = "1";
actual = lib.numbers.into.string 1;
in
actual == expected;
"converts a float into a string" = let
expected = "1.0";
actual = lib.numbers.into.string 1.0;
in
actual == expected;
};
"base" = {
"converts a number into a given base" = let
expected = [1 0 0];
actual = lib.numbers.into.base 2 4;
in
actual == expected;
};
"hex" = {
"converts a number into a hex string" = let
expected = "64";
actual = lib.numbers.into.hex 100;
in
(builtins.trace actual)
actual
== expected;
};
};
"compare" = {
"returns -1 when first is less than second" = let
expected = -1;
actual = lib.numbers.compare 1 2;
in
actual == expected;
"returns 0 when first is equal to second" = let
expected = 0;
actual = lib.numbers.compare 1 1;
in
actual == expected;
"returns 1 when first is greater than second" = let
expected = 1;
actual = lib.numbers.compare 2 1;
in
actual == expected;
};
}

View file

@ -33,7 +33,9 @@ lib: {
# TODO: Improve this error message to show the location and definitions for the option. # TODO: Improve this error message to show the location and definitions for the option.
else builtins.throw "Cannot merge definitions."; else builtins.throw "Cannot merge definitions.";
# TODO: Document this. ## Merge multiple option definitions together.
##
## @type Location -> Type -> List Definition
definitions = location: type: definitions: let definitions = location: type: definitions: let
identifier = lib.options.getIdentifier location; identifier = lib.options.getIdentifier location;
resolve = definition: let resolve = definition: let
@ -81,11 +83,14 @@ lib: {
}; };
}; };
## Merge multiple option declarations together.
##
## @type Location -> List Option
declarations = location: options: let declarations = location: options: let
merge = result: option: let merge = result: option: let
mergedType = result.type.mergeType option.options.type.functor; mergedType = result.type.mergeType option.options.type.functor;
isTypeMergeable = mergedType != null; isTypeMergeable = mergedType != null;
shared = key: option.options ? ${key} && result ? ${key}; shared = name: option.options ? ${name} && result ? ${name};
typeSet = lib.attrs.when ((shared "type") && isTypeMergeable) { typeSet = lib.attrs.when ((shared "type") && isTypeMergeable) {
type = mergedType; type = mergedType;
}; };
@ -212,9 +217,12 @@ lib: {
## @type List String -> String ## @type List String -> String
getIdentifier = location: let getIdentifier = location: let
special = [ special = [
"<name>" # attrsOf (submodule {}) # lib.types.attrs.of (lib.types.submodule {})
"*" # listOf (submodule {}) "<name>"
"<function body>" # functionTo # lib.types.list.of (submodule {})
"*"
# lib.types.function
"<function body>"
]; ];
escape = part: escape = part:
if builtins.elem part special if builtins.elem part special
@ -258,7 +266,9 @@ lib: {
in in
lib.strings.concatMap serialize definitions; lib.strings.concatMap serialize definitions;
# TODO: Document this. ## Run a set of definitions, calculating the resolved value and associated information.
##
## @type Location -> Option -> List Definition -> String & { value :: Any, highestPriority :: Int, isDefined :: Bool, files :: List String, definitions :: List Any, definitionsWithLocations :: List Definition }
run = location: option: definitions: let run = location: option: definitions: let
identifier = lib.options.getIdentifier location; identifier = lib.options.getIdentifier location;

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -12,7 +12,7 @@ lib: {
if builtins.stringLength value <= 207 && validate value != null if builtins.stringLength value <= 207 && validate value != null
then builtins.unsafeDiscardStringContext value then builtins.unsafeDiscardStringContext value
else else
lib.fp.pipe value [ lib.fp.pipe [
# Get rid of string context. This is safe under the assumption that the # Get rid of string context. This is safe under the assumption that the
# resulting string is only used as a derivation name # resulting string is only used as a derivation name
builtins.unsafeDiscardStringContext builtins.unsafeDiscardStringContext
@ -36,6 +36,7 @@ lib: {
if builtins.stringLength x == 0 if builtins.stringLength x == 0
then "unknown" then "unknown"
else x) else x)
]; ]
value;
}; };
} }

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -17,10 +17,7 @@ lib: {
## definitions. Unlike `fix`, the resulting value is also given a `__unfix__` ## definitions. Unlike `fix`, the resulting value is also given a `__unfix__`
## attribute that is set to the original function passed to `fix'`. ## attribute that is set to the original function passed to `fix'`.
## ##
## FIXME: The below type annotation should include a mention of the `__unfix__` ## @type (a -> a) -> a & { __unfix__ :: (a -> a) }
## value.
##
## @type (a -> a) -> a
fix' = f: let fix' = f: let
x = x =
f x f x

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -151,7 +151,7 @@ lib: {
# TODO: Document this. # TODO: Document this.
unspecified = lib.types.create { unspecified = lib.types.create {
name = "Unspecified"; name = "Unspecified";
description = "unspecified value"; description = "unspecified type";
}; };
# TODO: Document this. # TODO: Document this.
@ -388,20 +388,20 @@ lib: {
merge = location: definitions: let merge = location: definitions: let
normalize = definition: normalize = definition:
builtins.mapAttrs builtins.mapAttrs
(key: value: { (name: value: {
__file__ = definition.__file__; __file__ = definition.__file__;
value = value; value = value;
}) })
definition.value; definition.value;
normalized = builtins.map normalize definitions; normalized = builtins.map normalize definitions;
zipper = key: definitions: zipper = name: definitions:
(lib.options.merge.definitions (location ++ [key]) type definitions).optional; (lib.options.merge.definitions (location ++ [name]) type definitions).optional;
filtered = filtered =
lib.attrs.filter lib.attrs.filter
(key: value: value ? value) (name: value: value ? value)
(builtins.zipAttrsWith zipper normalized); (builtins.zipAttrsWith zipper normalized);
in in
builtins.mapAttrs (key: value: value.value) filtered; builtins.mapAttrs (name: value: value.value) filtered;
getSubOptions = prefix: type.getSubOptions (prefix ++ ["<name>"]); getSubOptions = prefix: type.getSubOptions (prefix ++ ["<name>"]);
getSubModules = type.getSubModules; getSubModules = type.getSubModules;
withSubModules = modules: lib.types.attrs.of (type.withSubModules modules); withSubModules = modules: lib.types.attrs.of (type.withSubModules modules);
@ -421,14 +421,14 @@ lib: {
merge = location: definitions: let merge = location: definitions: let
normalize = definition: normalize = definition:
builtins.mapAttrs builtins.mapAttrs
(key: value: { (name: value: {
__file__ = definition.__file__; __file__ = definition.__file__;
value = value; value = value;
}) })
definition.value; definition.value;
normalized = builtins.map normalize definitions; normalized = builtins.map normalize definitions;
zipper = key: definitions: let zipper = name: definitions: let
merged = lib.options.merge.definitions (location ++ [key]) type definitions; merged = lib.options.merge.definitions (location ++ [name]) type definitions;
in in
merged.optional.value or type.fallback.value or merged.merged; merged.optional.value or type.fallback.value or merged.merged;
in in
@ -494,7 +494,14 @@ lib: {
j: value: let j: value: let
resolved = resolved =
lib.options.merge.definitions lib.options.merge.definitions
(location ++ ["[definition ${builtins.toString i}-entry ${j}]"]); (location ++ ["[definition ${builtins.toString i}-entry ${j}]"])
type
[
{
file = definition.file;
value = value;
}
];
in in
resolved.optional resolved.optional
) )
@ -503,7 +510,7 @@ lib: {
definitions; definitions;
merged = builtins.concatLists result; merged = builtins.concatLists result;
filtered = builtins.filter (definition: definition ? value) merged; filtered = builtins.filter (definition: definition ? value) merged;
values = lib.optiosn.getDefinitionValues filtered; values = lib.options.getDefinitionValues filtered;
in in
values; values;
getSubOptions = prefix: type.getSubOptions (prefix ++ ["*"]); getSubOptions = prefix: type.getSubOptions (prefix ++ ["*"]);

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

View file

@ -0,0 +1,3 @@
let
lib = import ./../default.nix;
in {}

18
lib/test.sh Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p jq
set -euo pipefail
namespace=${1:-}
if [ -z "$namespace" ]; then
nix eval -f ./default.test.nix --show-trace --raw
else
if [ -d "./src/$namespace" ]; then
nix eval -f "./src/$namespace/default.test.nix" --show-trace --json | jq
else
echo "Namespace $namespace not found"
exit 1
fi
fi