chore: initial commit

This commit is contained in:
Jake Hamilton 2024-06-01 04:00:53 -07:00
commit 0409563e32
Signed by untrusted user: jakehamilton
GPG key ID: 9762169A1B35EA68
28 changed files with 3270 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License Copyright (c) 2024 Aux Contributors
Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

29
README.md Normal file
View file

@ -0,0 +1,29 @@
# Aux Labs
Welcome to Aux Labs! Complimentary beakers and companion
cubes are available in the gift shop.
The Aux Laboratory is a place for experimentation. Here you will find novel
solutions for problems in the Aux world. These experiments are not intended
to be used by anyone yet due to their highly unstable nature. However, we
have decided to publish them here together so that members of the community
may collaborate.
> **Note**: This repository is a part of Aux's early ad-hoc structure. In the
> future we will be moving to a standardized Aux Enhancement Proposal (AEP)
> format.
## Experiment Phases
| Phase | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Idea | An idea exists to solve a problem we currently have with Aux. Send a pull request to this repository creating a new directory for your experiment. The directory should contain a `README.md` file explaining the purpose of the experiment. |
| Iteration | Work on the experiment is done to solve for unknowns and come up with a good solution for the original problem. It may also be helpful to collaborate with others for feedback. |
| Proposal | The experiment has been satisfactorily completed and is ready to be considered for official adoption by the project. Discussion with the relevant Special Interest Group should take place to handle the transition of the project out of the lab and into its own repository. |
| Adoption | The experiment has been adopted and is now a part of Aux! The experiment should be moved to its own repository and the original experiment directory should be deleted |
## Experiments
| Name | Phase | Description |
| ---------------- | ----- | -------------------------------------------------------- |
| [Aux Lib](./lib) | Idea | A library of common functions used in the Aux ecosystem. |

23
lib/LICENSE Normal file
View file

@ -0,0 +1,23 @@
MIT License
Copyright (c) 2003-2023 Eelco Dolstra and the Nixpkgs/NixOS contributors
Copyright (c) 2024 Aux Contributors
Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

104
lib/README.md Normal file
View file

@ -0,0 +1,104 @@
# Aux Lib
Aux Lib is intended to be a replacement for NixPkg's `lib` with stronger constraints around naming,
organization, and inclusion of functions. In addition to replacing library functions, Aux Lib also
defines a revamped version of the NixOS Module system intended to make it easier, more approachable,
and intuitive.
## Usage
The library can be imported both with and without Nix Flakes. To import the library using Nix Flakes,
add this repository as an input.
```nix
inputs.lib.url = "github:auxolotl/labs?dir=lib";
```
To import the library without using Nix Flakes, you will need to use `fetchTarball` and import the
library entrypoint.
```nix
let
labs = builtins.fetchTarball {
url = "https://github.com/auxolotl/labs/archive/main.tar.gz";
sha256 = "<sha256>";
};
lib = import "${labs}/lib";
in
# ...
```
## Development
To contribute to the project, we accept pull requests to this repository. Please see the following
sections for information on the appropriate practices and required steps for working on Aux Lib.
### Documentation
We want our code to survive in the world, but without proper documentation that won't happen. In
order to not lose knowledge and also make it easier for others to begin participating in the
project we require that every function have an appropriate documentation comment. The format for
these comments is as follows:
```nix
let
## This is a description of what the function does. Any necessary information can be added
## here. After this point comes a gap before a Hindley-Milner style type signature. Note
## that these types are not actually checked, but serve as a helpful addition for the user
## in addition to being provided in generated documentation.
##
## @type Int -> String
func = x: builtins.toString x;
in
# ...
```
### Testing
All functions that are added to the project should include tests. The test suites are located
next to their implementation in files ending in `.test.nix`. These tests should ensure that
the library behaves as expected. The typical structure of these test suites is:
```nix
let
lib = import ./../default.nix;
in
{
"my function" = {
"test 1" = let
expected = 1;
input = {};
actual = lib.myFunction input;
in
actual == expected;
};
}
```
Successful tests will return `true` while failing test will resolve with `false`.
### Formatting
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`.
### Adding Functionality
Before adding new features to the library, submit an issue or talk the idea over with one or
more of the project maintainers. We want to make sure that the library does not become bloated
with tools that aren't used. Some features may be better handled in a separate project. If you
do get the go-ahead to begin working on your feature, please place it in the library structure
similarly to how existing features are. For example, things dealing with strings should go in
`src/strings/default.nix`.
Additionally, you should prefer to group things in attribute sets for like-functionality. More
broad categories such as `strings` and `lists` are helpful, but scoped groups for things like
`into`, `from`, and `validate` also make the library more discoverable. Having all of the
different parts of the library mirroring this organizational structure makes building intuition
for working with the library much easier. To know when to group new things, consider the
following:
- Would your function name be multiple words like `fromString`?
- Are there multiple variants of this function?
- Would it be easier to find in a group?
- Would grouping help avoid name collisions or confusion?

1
lib/default.nix Normal file
View file

@ -0,0 +1 @@
import ./src

7
lib/flake.nix Normal file
View file

@ -0,0 +1,7 @@
{
description = "A very basic flake";
outputs = _: {
lib = import ./src;
};
}

141
lib/src/attrs/default.nix Normal file
View file

@ -0,0 +1,141 @@
lib: {
attrs = {
## Merge two attribute sets at the base level.
##
## @type Attrs a b c => a -> b -> c
merge = x: y: x // y;
## Merge two attribute sets recursively until a given predicate returns true.
## Any values that are _not_ attribute sets will be overridden with the value
## from `y` first if it exists and then `x` otherwise.
##
## @type Attrs a b c => (String -> Any -> Any -> Bool) -> a -> b -> c
mergeRecursiveUntil = predicate: x: y: let
process = path:
builtins.zipAttrsWith (
key: values: let
currentPath = path ++ [key];
isSingleValue = builtins.length values == 1;
isComplete =
predicate currentPath
(builtins.elemAt values 1)
(builtins.elemAt values 0);
in
if isSingleValue || isComplete
then builtins.elemAt values 0
else process currentPath values
);
in
process [] [x y];
## Merge two attribute sets recursively. Any values that are _not_ attribute sets
## will be overridden with the value from `y` first if it exists and then `x`
## otherwise.
##
## @type Attrs a b c => a -> b -> c
mergeRecursive =
lib.attrs.mergeRecursiveUntil
(path: x: y:
!(builtins.isAttrs x && builtins.isAttrs y));
## Get a value from an attribute set by a path. If the path does not exist,
## a fallback value will be returned instead.
##
## @type (List String) -> a -> Attrs -> a | b
select = path: fallback: target: let
key = builtins.head path;
rest = builtins.tail path;
in
if path == []
then target
else if target ? ${key}
then lib.attrs.select rest fallback target.${key}
else fallback;
## Get a value from an attribute set by a path. If the path does not exist,
## an error will be thrown.
##
## @type (List String) -> Attrs -> a
selectOrThrow = path: target: let
pathAsString = builtins.concatStringsSep "." path;
error = builtins.throw "Path not found in attribute set: ${pathAsString}";
in
if lib.attrs.has path target
then lib.attrs.select path target
else error;
# TODO: Document this.
set = path: value: let
length = builtins.length path;
process = depth:
if depth == length
then value
else {
${builtins.elemAt path depth} = process (depth + 1);
};
in
process 0;
## Check if a path exists in an attribute set.
##
## @type (List String) -> Attrs -> Bool
has = path: target: let
key = builtins.head path;
rest = builtins.tail path;
in
if path == []
then true
else if target ? ${key}
then lib.attrs.has rest target.${key}
else false;
## Depending on a given condition, either use the given value or an empty
## attribute set.
##
## @type Attrs a b => Bool -> a -> a | b
when = condition: value:
if condition
then value
else {};
## Map an attribute set's keys and values to a list.
##
## @type Any a => (String -> Any -> a) -> Attrs -> List a
mapToList = f: target:
builtins.map (key: f key target.${key}) (builtins.attrNames target);
# TODO: Document this.
mapRecursive = f: target:
lib.attrs.mapRecursiveWhen (lib.fp.const true) f target;
# TODO: Document this.
mapRecursiveWhen = predicate: f: target: let
process = path:
builtins.mapAttrs (
key: value:
if builtins.isAttrs value && predicate value
then process (path ++ [key]) value
else f (path ++ [key]) value
);
in
process [] target;
# TODO: Document this.
filter = predicate: target: let
keys = builtins.attrNames target;
process = key: let
value = target.${key};
in
if predicate key value
then [
{
name = key;
value = value;
}
]
else [];
valid = builtins.concatMap process keys;
in
builtins.listToAttrs valid;
};
}

View file

@ -0,0 +1,17 @@
let
lib = import ./../default.nix;
in {
"select" = {
"selects a nested value" = let
expected = "value";
actual =
lib.attrs.select
["x" "y" "z"]
null
{
x.y.z = expected;
};
in
actual == expected;
};
}

48
lib/src/bools/default.nix Normal file
View file

@ -0,0 +1,48 @@
lib: {
bools = {
into = {
string = value:
if value
then "true"
else "false";
};
## Choose between two values based on a condition. When true, the first value
## is returned, otherwise the second value is returned.
##
## @type Bool -> a -> b -> a | b
when = condition: x: y:
if condition
then x
else y;
## Perform a logical AND operation on two values.
##
## @type Bool -> Bool -> Bool
and = a: b: a && b;
## Perform a logical AND operation on two functions being applied to a value.
##
## @type (a -> Bool) -> (a -> Bool) -> a -> Bool
and' = f: g: (
x: (f x) && (g x)
);
## Perform a logical OR operation on two values.
##
## @type Bool -> Bool -> Bool
or = a: b: a || b;
## Perform a logical OR operation on two functions being applied to a value.
##
## @type (a -> Bool) -> (a -> Bool) -> a -> Bool
or' = f: g: (
x: (f x) || (g x)
);
## Perform a logical NOT operation on a value.
##
## @type Bool -> Bool
not = a: !a;
};
}

76
lib/src/default.nix Normal file
View file

@ -0,0 +1,76 @@
let
files = [
./attrs
./bools
./errors
./fp
./generators
./importers
./lists
./math
./modules
./numbers
./options
./packages
./paths
./points
./strings
./types
./versions
];
libs = builtins.map (f: import f) files;
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows
## us to define recursive functions without worrying about the order of their
## definitions.
##
## @type (a -> a) -> a
fix = f: let
x = f x;
in
x;
## Merge two attribute sets recursively until a given predicate returns true.
## Any values that are _not_ attribute sets will be overridden with the value
## from `y` first if it exists and then `x` otherwise.
##
## @type Attrs a b c => (String -> Any -> Any -> Bool) -> a -> b -> c
mergeAttrsRecursiveUntil = predicate: x: y: let
process = path:
builtins.zipAttrsWith (
key: values: let
currentPath = path ++ [key];
isSingleValue = builtins.length values == 1;
isComplete =
predicate currentPath
(builtins.elemAt values 1)
(builtins.elemAt values 0);
in
if isSingleValue || isComplete
then builtins.elemAt values 0
else process currentPath values
);
in
process [] [x y];
## Merge two attribute sets recursively. Any values that are _not_ attribute sets
## will be overridden with the value from `y` first if it exists and then `x`
## otherwise.
##
## @type Attrs a b c => a -> b -> c
mergeAttrsRecursive =
mergeAttrsRecursiveUntil
(path: x: y:
!(builtins.isAttrs x && builtins.isAttrs y));
lib = fix (
self: let
merge = acc: create:
mergeAttrsRecursive acc (create self);
in
builtins.foldl' merge {} libs
);
in
lib

View file

@ -0,0 +1,8 @@
lib: {
errors = {
trace = condition: message:
if condition
then true
else builtins.trace message false;
};
}

46
lib/src/fp/default.nix Normal file
View file

@ -0,0 +1,46 @@
lib: {
fp = {
## A function that returns its argument.
##
## @type a -> a
id = x: x;
## Create a function that ignores its argument and returns a constant value.
##
## @type a -> b -> a
const = x: (_: x);
## Compose two functions to produce a new function that applies them both
## from right to left.
##
## @type Function f g => (b -> c) -> (a -> b) -> a -> c
compose = f: g: (x: f (g x));
## Process a value with a series of functions. Functions are applied in the
## order they are provided.
##
## @type (List (Any -> Any)) -> Any -> Any
pipe = fs: (
x: builtins.foldl' (value: f: f x) x fs
);
## Reverse the order of arguments to a function that has two parameters.
##
## @type (a -> b -> c) -> b -> a -> c
flip2 = f: a: b: f b a;
## Reverse the order of arguments to a function that has three parameters.
##
## @type (a -> b -> c -> d) -> c -> b -> a -> d
flip3 = f: a: b: c: f c b a;
## Reverse the order of arguments to a function that has four parameters.
##
## @type (a -> b -> c -> d -> e) -> d -> c -> b -> a -> e
flip4 = f: a: b: c: d: f d c b a;
# TODO: Document this.
args = f:
if f ? __functor
then f.__args__ or lib.fp.args (f.__functor f)
else builtins.functionArgs f;
};
}

View file

@ -0,0 +1,144 @@
lib: {
generators = {
## Limit evaluation of a valud to a certain depth.
##
## @type { limit? :: Int | Null, throw? :: Bool } -> a -> a
withRecursion = {
limit ? null,
throw ? true,
}:
assert builtins.isInt limit; let
special = [
"__functor"
"__functionArgs"
"__toString"
"__pretty__"
];
attr = next: name:
if builtins.elem name special
then lib.fp.id
else next;
transform = depth:
if limit != null && depth > limit
then
if throw
then builtins.throw "Exceeded maximum eval-depth limit of ${builtins.toString limit} while trying to evaluate with `lib.generators.withRecursion'!"
else lib.fp.const "<unevaluated>"
else lib.fp.id;
process = depth: value: let
next = x: process (depth + 1) (transform (depth + 1) x);
in
if builtins.isAttrs value
then builtins.mapAttrs (attr next) value
else if builtins.isList value
then builtins.map next value
else transform (depth + 1) value;
in
process 0;
## Create a pretty printer for Nix values.
##
## @type { indent? :: String, multiline? :: Bool, allowCustomPrettifiers? :: Bool } -> a -> string
pretty = {
indent ? "",
multiline ? true,
allowCustomPrettifiers ? false,
}: let
process = indent: value: let
prefix =
if multiline
then "\n${indent} "
else " ";
suffix =
if multiline
then "\n${indent}"
else " ";
prettyNull = "null";
prettyNumber = lib.numbers.into.string value;
prettyBool = lib.bools.into.string value;
prettyPath = builtins.toString value;
prettyString = let
lines = builtins.filter (x: !builtins.isList x) (builtins.split "\n" value);
escapeSingleline = lib.strings.escape.any ["\\" "\"" "\${"];
escapeMultiline = builtins.replaceStrings ["\${" "''"] ["''\${" "'''"];
singlelineResult = "\"" + lib.strings.concatMapSep "\\n" escapeSingleline lines + "\"";
multilineResult = let
escapedLines = builtins.map escapeMultiline lines;
# The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
# indentation level. Otherwise, '' is appended to the last line.
lastLine = lib.last escapedLines;
contents = builtins.concatStringsSep prefix (lib.lists.init escapedLines);
contentsSuffix =
if lastLine == ""
then suffix
else prefix + lastLine;
in
"''"
+ prefix
+ contents
+ contentsSuffix
+ "''";
in
if multiline && builtins.length lines > 1
then multilineResult
else singlelineResult;
prettyList = let
contents = lib.strings.concatMapSep prefix (process (indent + " ")) value;
in
if builtins.length value == 0
then "[ ]"
else "[${prefix}${contents}${suffix}]";
prettyFunction = let
args = lib.fp.args value;
markArgOptional = name: default:
if default
then name + "?"
else name;
argsWithDefaults = lib.attrs.mapToList markArgOptional args;
serializedArgs = builtins.concatStringsSep ", " argsWithDefaults;
in
if args == {}
then "<function>"
else "<function, args: {${serializedArgs}}>";
prettyAttrs = let
contents = builtins.concatStringsSep prefix (lib.attrs.mapToList
(name: value: "${lib.strings.escape.nix.identifier name} = ${
builtins.addErrorContext "while evaluating an attribute `${name}`"
(process (indent + " ") value)
};")
value);
in
if allowCustomPrettifiers && value ? __pretty__ && value ? value
then value.__pretty__ value.value
else if value == {}
then "{ }"
else if lib.packages.isDerivation value
then "<derivation ${value.name or "???"}>"
else "{${prefix}${contents}${suffix}}";
in
if null == value
then prettyNull
else if builtins.isInt value || builtins.isFloat value
then prettyNumber
else if builtins.isBool value
then prettyBool
else if builtins.isString value
then prettyString
else if builtins.isPath value
then prettyPath
else if builtins.isList value
then prettyList
else if builtins.isFunction value
then prettyFunction
else if builtins.isAttrs value
then prettyAttrs
else builtins.abort "lib.generators.pretty: should never happen (value = ${value})";
in
process indent;
};
}

View file

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

160
lib/src/lists/default.nix Normal file
View file

@ -0,0 +1,160 @@
lib: {
lists = {
from = {
# TODO: Document this.
any = value:
if builtins.isList value
then value
else [value];
};
sort = {
## Perform a natural sort on a list of strings.
##
## @type List -> List
natural = list: let
vectorize = string: let
serialize = part:
if builtins.isList part
then lib.strings.into.int (builtins.head part)
else part;
parts = lib.strings.split "(0|[1-9][0-9]*)" string;
in
builtins.map serialize parts;
prepared = builtins.map (value: [(vectorize value) value]) list;
isLess = a: b: (lib.lists.compare lib.numbers.compare (builtins.head a) (builtins.head b)) < 0;
in
builtins.map (x: builtins.elemAt x 1) (builtins.sort isLess prepared);
};
# TODO: Document this.
mapWithIndex = f: list:
builtins.genList
(i: f i (builtins.elemAt list i))
(builtins.length list);
# TODO: Document this.
mapWithIndex1 = f: list:
builtins.genList
(i: f (i + 1) (builtins.elemAt list i))
(builtins.length list);
## Compare two lists.
##
## @type (a -> b -> Int) -> List a -> List b -> Int
compare = compare: a: b: let
result = compare (builtins.head a) (builtins.head b);
in
if a == []
then
if b == []
then 0
else -1
else if b == []
then 1
else if result == 0
then lib.lists.compare compare (builtins.tail a) (builtins.tail b)
else result;
## Get the last element of a list.
##
## @type List a -> a
last = list:
assert lib.assertMsg (list != []) "List cannot be empty";
builtins.elemAt list (builtins.length list - 1);
## Slice part of a list to create a new list.
##
## @type Int -> Int -> List -> List
slice = start: count: list: let
listLength = builtins.length list;
resultLength =
if start >= listLength
then 0
else if start + count > listLength
then listLength - start
else count;
in
builtins.genList
(i: builtins.elemAt list (start + i))
resultLength;
## Take the first n elements of a list.
##
## @type Int -> List -> List
take = lib.lists.slice 0;
## Drop the first n elements of a list.
##
## @type Int -> List -> List
drop = count: list: let
listLength = builtins.length list;
in
lib.lists.slice count listLength list;
## Reverse a list.
##
## @type List -> List
reverse = list: let
length = builtins.length list;
create = i: builtins.elemAt list (length - i - 1);
in
builtins.genList create length;
## Interleave a list with a separator.
##
## @type Separator -> List -> List
intersperse = separator: list: let
length = builtins.length list;
in
if length < 2
then list
else
builtins.tail (
builtins.concatMap
(part: [separator part])
list
);
## Create a list of integers from a starting number to an ending
## number. This *includes* the ending number as well.
##
## @type Int -> Int -> List
range = start: end:
if start > end
then []
else builtins.genList (i: start + i) (end - start + 1);
## Depending on a given condition, either use the given value (as
## a list) or an empty list.
##
## @type Attrs a b => Bool -> a -> a | b
when = condition: value:
if condition
then
if builtins.isList value
then value
else [value]
else [];
# TODO: Document this.
count = predicate: list:
builtins.foldl' (
total: value:
if predicate value
then total + 1
else total
)
0
list;
# TODO: Document this.
unique = list: let
filter = result: value:
if builtins.elem value result
then result
else result ++ [value];
in
builtins.foldl' filter [] list;
};
}

15
lib/src/math/default.nix Normal file
View file

@ -0,0 +1,15 @@
lib: {
math = {
# TODO: Document this.
min = x: y:
if x < y
then x
else y;
# TODO: Document this.
max = x: y:
if x > y
then x
else y;
};
}

600
lib/src/modules/default.nix Normal file
View file

@ -0,0 +1,600 @@
lib: {
modules = {
from = {
## Create a module from a JSON file.
##
## @type Path -> Module
json = file: {
__file__ = file;
config = lib.importers.json file;
};
## Create a module from a TOML file.
##
## @type Path -> Module
toml = file: {
__file__ = file;
config = lib.importers.toml file;
};
};
apply = {
# TODO: Document this.
properties = definition:
if lib.types.is "merge" definition
then builtins.concatMap lib.modules.apply.properties definition.content
else if lib.types.is "when" definition
then
if !(builtins.isBool definition.condition)
then builtins.throw "lib.modules.when called with a non-boolean condition"
else if definition.condition
then lib.modules.apply.properties definition.content
else []
else [definition];
# TODO: Document this.
overrides = definitions: let
getPriority = definition:
if lib.types.is "override" definition.value
then definition.value.priority
else lib.modules.DEFAULT_PRIORITY;
normalize = definition:
if lib.types.is "override" definition.value
then
definition
// {
value = definition.value.content;
}
else definition;
highestPriority =
builtins.foldl'
(priority: definition: lib.math.min priority (getPriority definition))
9999
definitions;
in {
inherit highestPriority;
values =
builtins.concatMap
(
definition:
if getPriority definition == highestPriority
then [(normalize definition)]
else []
)
definitions;
};
# TODO: Document this.
order = definitions: let
normalize = definition:
if lib.types.is "order" definition
then
definition
// {
value = definition.value.content;
priority = definition.value.priority;
}
else definition;
normalized = builtins.map normalize definitions;
compare = a: b: (a.priority or lib.modules.DEFAULT_PRIORITY) < (b.priority or lib.modules.DEFAULT_PRIORITY);
in
builtins.sort compare normalized;
# TODO: Document this.
fixup = location: option:
if option.type.getSubModules or null == null
then
option
// {
type = option.type or lib.types.unspecified;
}
else
option
// {
type = option.type.withSubModules option.options;
options = [];
};
# TODO: Document this.
invert = config:
if lib.types.is "merge" config
then builtins.concatMap lib.modules.apply.invert config.content
else if lib.types.is "when" config
then
builtins.map
(builtins.mapAttrs (key: value: lib.modules.when config.condition value))
(lib.modules.apply.invert config.content)
else if lib.types.is "override" config
then
builtins.map
(builtins.mapAttrs (key: value: lib.modules.override config.priority value))
(lib.modules.apply.invert config.content)
else [config];
};
validate = {
# TODO: Document this.
keys = module: let
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
in
invalid == {};
};
# TODO: Document this.
VALID_KEYS = [
"__file__"
"__key__"
"includes"
"excludes"
"options"
"config"
"freeform"
"meta"
];
# TODO: Document this.
normalize = file: key: module: let
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
in
if lib.modules.validate.keys module
then {
__file__ = builtins.toString module.__file__ or file;
__key__ = builtins.toString module.__key__ or key;
includes = module.includes or [];
excludes = module.excludes or [];
options = module.options or {};
config = let
base = module.config or {};
withMeta = config:
if module ? meta
then lib.modules.merge [config {meta = module.meta;}]
else config;
withFreeform = config:
if module ? freeform
then lib.modules.merge [config {__module__.freeform = module.freeform;}]
else config;
in
withFreeform (withMeta base);
}
else builtins.throw "Module `${key}` has unsupported attribute(s): ${invalidKeys}";
# TODO: Document this.
resolve = key: module: args: let
dynamicArgs =
builtins.mapAttrs
(
name: value:
builtins.addErrorContext
"while evaluating the module argument `${name}` in `${key}`"
(args.${name} or args.config.__module__.args.dynamic.${name})
)
(lib.fp.args module);
in
if builtins.isFunction module
then module (args // dynamicArgs)
else module;
# TODO: Document this.
DEFAULT_PRIORITY = 100;
## Allow for sorting the values provided to a module by priority. The
## most important value will be used.
##
## @type Int -> a -> Priority a
order = priority: value: {
__type__ = "order";
inherit priority value;
};
orders = {
# TODO: Document this.
before = lib.modules.order 500;
# TODO: Document this.
default = lib.modules.order 1000;
# TODO: Document this.
after = lib.modules.order 1500;
};
## Extract a list of files from a list of modules.
##
## @type List Module -> List (String | Path)
getFiles = builtins.map (module: module.__file__);
# TODO: Document this.
when = condition: content: {
__type__ = "when";
inherit condition content;
};
# TODO: Document this.
merge = content: {
__type__ = "merge";
inherit content;
};
# TODO: Document this.
override = priority: content: {
__type__ = "override";
inherit priority content;
};
overrides = {
# TODO: Document this.
option = lib.modules.override 1500;
# TODO: Document this.
default = lib.modules.override 1000;
# TODO: Document this.
force = lib.modules.override 50;
# TODO: Document this.
vm = lib.modules.override 10;
};
# TODO: Document this.
combine = prefix: modules: let
getConfig = module:
builtins.map
(config: {
__file__ = module.__file__;
inherit config;
})
(lib.modules.apply.invert module.config);
configs =
builtins.concatMap
getConfig
modules;
process = prefix: options: configs: let
# TODO: Document this.
byName = attr: f: modules:
builtins.zipAttrsWith
(lib.fp.const builtins.concatLists)
(builtins.map (
module: let
subtree = module.${attr};
in
if builtins.isAttrs subtree
then builtins.mapAttrs (key: f module) subtree
else builtins.throw "Value for `${builtins.concatStringsSep "." prefix} is of type `${builtins.typeOf subtree}` but an attribute set was expected."
)
modules);
declarationsByName =
byName
"options"
(module: option: [
{
__file__ = module.__file__;
options = option;
}
])
options;
definitionsByName =
byName
"config"
(
module: value:
builtins.map
(config: {
__file__ = module.__file__;
inherit config;
})
(lib.modules.apply.invert value)
)
configs;
definitionsByName' =
byName
"config"
(module: value: [
{
__file__ = module.__file__;
inherit value;
}
])
configs;
getOptionFromDeclaration = declaration:
if lib.types.is "option" declaration.options
then declaration
else
declaration
// {
options = lib.options.create {
type = lib.types.submodule [{options = declaration.options;}];
};
};
resultsByName =
builtins.mapAttrs
(
name: declarations: let
location = prefix ++ [name];
definitions = definitionsByName.${name} or [];
definitions' = definitionsByName'.${name} or [];
optionDeclarations =
builtins.filter
(declaration: lib.types.is "option" declaration.options)
declarations;
in
if builtins.length optionDeclarations == builtins.length declarations
then let
option =
lib.modules.apply.fixup
location
(lib.options.merge.declarations location declarations);
in {
matched = lib.options.run location option definitions';
unmatched = [];
}
else if optionDeclarations != []
then
if builtins.all (declaration: declaration.options.type.name == "Submodule") optionDeclarations
then let
option =
lib.modules.apply.fixup location
(lib.options.merge.declarations location (builtins.map getOptionFromDeclaration declarations));
in {
matched = lib.options.run location option definitions';
unmatched = [];
}
else builtins.throw "The option `${lib.options.getIdentifier location}` in module `${(builtins.head optionDeclarations).__file__}` does not support nested options."
else process location declarations definitions
)
declarationsByName;
matched = builtins.mapAttrs (key: value: value.matched) resultsByName;
unmatched =
builtins.mapAttrs (key: value: value.unmatched) resultsByName
// builtins.removeAttrs definitionsByName' (builtins.attrNames matched);
in {
inherit matched;
unmatched =
if configs == []
then []
else
builtins.concatLists (
lib.attrs.mapToList
(
name: definitions:
builtins.map (definition:
definition
// {
prefix = [name] ++ (definition.prefix or []);
})
definitions
)
unmatched
);
};
in
process prefix modules configs;
# TODO: Document this.
run = settings @ {
modules ? [],
args ? {},
prefix ? [],
}: let
type = lib.types.submodules.of {
inherit modules args;
};
extend = extensions @ {
modules ? [],
args ? {},
prefix ? [],
}:
lib.modules.run {
modules = settings.modules ++ extensions.modules;
args = (settings.args or {}) // extensions.args;
prefix = extensions.prefix or settings.prefix or [];
};
# TODO: Document this.
collect = let
load = args: file: key: module: let
moduleFromValue = lib.modules.normalize file key (lib.modules.resolve key module args);
moduleFromPath =
lib.modules.normalize
(builtins.toString module)
(builtins.toString module)
(lib.modules.resolve (builtins.toString module) (import module) args);
in
if builtins.isAttrs module || builtins.isFunction module
then moduleFromValue
else if builtins.isString module || builtins.isPath module
then moduleFromPath
else builtins.throw "The provided module must be either an attribute set, function, or path but got ${builtins.typeOf module}";
normalize = parentFile: parentKey: modules: args: let
normalized =
lib.lists.mapWithIndex1
(
i: value: let
module = load args parentFile "${parentKey}:unknown-${builtins.toString i}" value;
tree = normalize module.__file__ module.__key__ module.includes args;
in {
inherit module;
key = module.__key__;
modules = tree.modules;
excludes = module.excludes ++ tree.excludes;
}
)
modules;
in {
modules = normalized;
excludes = builtins.concatLists (builtins.catAttrs "excludes" normalized);
};
withExclusions = path: {
modules,
excludes,
}: let
getKey = module:
if builtins.isString module && (builtins.substring 0 1 module != "/")
then (builtins.toString path) + "/" + module
else builtins.toString module;
excludedKeys = builtins.map getKey excludes;
removeExcludes =
builtins.filter
(value: !(builtins.elem value.key excludedKeys));
in
builtins.map
(value: value.module)
(builtins.genericClosure {
startSet = removeExcludes modules;
operator = value: removeExcludes value.modules;
});
process = path: modules: args:
withExclusions path (normalize "<unknown>" "" modules args);
in
process;
internal = {
__file__ = "virtual:aux/internal";
__key__ = "virtual:aux/internal";
options = {
__module__ = {
args = {
static = lib.options.create {
type = lib.types.attrs.lazy lib.types.raw;
writable = false;
internal = false;
description = "Static arguments provided to lib.modules.run which cannot be changed.";
};
dynamic = lib.options.create {
type = lib.types.attrs.lazy lib.types.raw;
${
if prefix == []
then null
else "internal"
} =
true;
visible = false;
description = "Additional arguments pased to each module.";
};
};
check = lib.options.create {
type = lib.types.bool;
default.value = true;
internal = true;
description = "Whether to perform checks on option definitions.";
};
freeform = lib.options.create {
type = lib.types.nullish lib.types.option;
default.value = null;
internal = true;
description = "If set, all options that don't have a declared type will be merged using this type.";
};
};
};
config = {
__module__ = {
args = {
static = args;
dynamic = {
meta = {
inherit extend type;
};
};
};
};
};
};
merged = let
collected =
collect
(args.path or "")
(modules ++ [internal])
({inherit lib options config;} // args);
in
lib.modules.combine prefix (lib.lists.reverse collected);
options = merged.matched;
config = let
declared =
lib.attrs.mapRecursiveWhen
(value: !(lib.types.is "option" value))
(key: value: value.value)
options;
freeform = let
definitions =
builtins.map
(definition: {
__file__ = definition.__file__;
value = lib.attrs.set definition.prefix definition.value;
})
merged.unmatched;
in
if definitions == []
then {}
else declared.__module__.freeform.merge prefix definitions;
in
if declared.__module__.freeform == null
then declared
else
lib.attrs.mergeRecursive
freeform
declared;
checked =
if config.__module__.check && config.__module__.freeform == null && merged.unmatched != []
then let
first = builtins.head merged.unmatched;
identifier = lib.options.getIdentifier (prefix ++ first.prefix);
definitions =
builtins.addErrorContext "while evaluating the error message for definitions of non-existent option `${identifier}`"
(
builtins.addErrorContext "while evaluating a definition from `${first.__file__}`"
(lib.options.getDefinitions [first])
);
message = "The option `${identifier}` does not exist. Definitions:${definitions}";
in
if builtins.attrNames options == ["__module__"]
then
if lib.options.getIdentifier prefix == ""
then
builtins.throw ''
${message}
You are trying to declare options in `config` rather than `options`.
''
else
builtins.throw ''
${message}
There are no options defined in `${lib.options.getIdentifier prefix}`.
Are you sure you declared your options correctly?
''
else builtins.throw message
else null;
withCheck = builtins.seq checked;
in {
inherit type extend;
options = withCheck options;
config = withCheck (builtins.removeAttrs config ["__module__"]);
__module__ = withCheck config.__module__;
};
};
}

View file

@ -0,0 +1,28 @@
let
lib = import ./../default.nix;
in {
examples = {
"empty" = let
expected = "Hello, World!";
evaluated = lib.modules.run {
modules = [
{
options.aux = {
message = lib.options.create {
type = lib.types.string;
};
};
config = {
aux.message = "Hello, World!";
};
}
];
};
actual = evaluated.config.aux.message;
in
actual == expected;
};
}

View file

@ -0,0 +1,69 @@
lib: {
numbers = {
into = {
## Convert a number into a string.
##
## @type Int | Float -> String
string = value:
if builtins.isInt value
then builtins.toString value
else builtins.toJSON value;
## Convert a number into a list of digits in the given base.
##
## @type Int -> Int -> List Int
base = base: target: let
process = value: let
r = value - ((value / base) * base);
q = (value - r) / base;
in
if value < base
then [value]
else [r] ++ process q;
result = process target;
in
assert lib.errors.trace (builtins.isInt base) "Base must be an integer.";
assert lib.errors.trace (builtins.isInt target) "Target must be an integer.";
assert lib.errors.trace (base >= 2) "Base must be at least 2.";
assert lib.errors.trace (target >= 0) "Target cannot be negative.";
lib.lists.reverse result;
## Convert a number into a hexadecimal string.
##
## @type Int -> String
hex = value: let
serialize = part:
if part < 10
then builtins.toString part
else if part == 10
then "A"
else if part == 11
then "B"
else if part == 12
then "C"
else if part == 13
then "D"
else if part == 14
then "E"
else if part == 15
then "F"
else builtins.throw "Invalid hex digit.";
in
lib.strings.concatMapSep
serialize
(lib.numbers.into.base 16 value);
};
## Compare two numbers. When the first number is less than the second, -1
## is returned. When the first number is greater than the second, 1 is
## returned. When the numbers are equal, 0 is returned.
##
## @type Int -> Int -> Int
compare = a: b:
if a < b
then -1
else if a > b
then 1
else 0;
};
}

306
lib/src/options/default.nix Normal file
View file

@ -0,0 +1,306 @@
lib: {
options = {
merge = {
## Merge a list of option definitions into a single value.
##
## @type Location -> List Definition -> Any
default = location: definitions: let
values = lib.options.getDefinitionValues definitions;
first = builtins.elemAt values 0;
mergedFunctions = x:
lib.options.mergeDefault
location
(builtins.map (f: f x) values);
mergedLists = builtins.concatLists values;
mergedAttrs = builtins.foldl' lib.attrs.merge {} values;
mergedBools = builtins.any lib.bools.or false values;
mergedStrings = lib.strings.concat values;
in
if builtins.length values == 1
then builtins.elemAt values 0
else if builtins.all builtins.isFunction values
then mergedFunctions
else if builtins.all builtins.isList values
then mergedLists
else if builtins.all builtins.isAttrs values
then mergedAttrs
else if builtins.all builtins.isBool values
then mergedBools
else if builtins.all lib.strings.isString values
then mergedStrings
else if builtins.all builtins.isInt values && builtins.all (x: x == first) values
then first
# TODO: Improve this error message to show the location and definitions for the option.
else builtins.throw "Cannot merge definitions.";
# TODO: Document this.
definitions = location: type: definitions: let
identifier = lib.options.getIdentifier location;
resolve = definition: let
properties =
builtins.addErrorContext
"while evaluating definitions from `${definition.__file__}`:"
(lib.modules.apply.properties definition.value);
normalize = value: {
__file__ = definition.__file__;
inherit value;
};
in
builtins.map normalize properties;
resolved = builtins.concatMap resolve definitions;
overridden = lib.modules.apply.overrides resolved;
values =
if builtins.any (definition: lib.types.is "order" definition.value) overridden.values
then lib.modules.apply.order overridden.values
else overridden.values;
isDefined = values != [];
invalid = builtins.filter (definition: !(type.check definition.value)) values;
merged =
if isDefined
then
if builtins.all (definition: type.check definition.value) values
then type.merge location values
else builtins.throw "A definition for `${identifier}` is not of type `${type.description}`. Definition values:${lib.options.getDefinitions invalid}"
else builtins.throw "The option `${identifier}` is used but not defined.";
optional =
if isDefined
then {value = merged;}
else {};
in {
inherit isDefined values merged optional;
raw = {
inherit values;
inherit (overridden) highestPriority;
};
};
declarations = location: options: let
merge = result: option: let
mergedType = result.type.mergeType option.options.type.functor;
isTypeMergeable = mergedType != null;
shared = key: option.options ? ${key} && result ? ${key};
typeSet = lib.attrs.when ((shared "type") && isTypeMergeable) {
type = mergedType;
};
files = builtins.map lib.modules.getFiles result.declarations;
serializedFiles = builtins.concatStringsSep " and " files;
getSubModules = option.options.type.getSubModules or null;
submodules =
if getSubModules != null
then
builtins.map
(module: {
__file__ = option.__file__;
includes = [module];
})
getSubModules
++ result.options
else result.options;
in
if shared "default" || shared "example" || shared "description" || shared "apply" || (shared "type" && !isTypeMergeable)
then builtins.throw "The option `${lib.options.getIdentifier location}` in `${option.__file__}` is already declared in ${serializedFiles}"
else
option.options
// result
// {
declarations = result.declarations ++ [option.__file__];
options = submodules;
}
// typeSet;
in
builtins.foldl'
merge
{
inherit location;
declarations = [];
options = [];
}
options;
## Merge an option, only supporting a single unique definition.
##
## @type String -> Location -> List Definition -> Any
unique = message: location: definitions: let
identifier = lib.options.getIdentifier location;
total = builtins.length definitions;
first = builtins.elemAt definitions 0;
in
if total == 1
then first.value
else if total == 0
then builtins.throw "Cannot merge unused option `${identifier}`.\n${message}"
else builtins.throw "The option `${identifier}` is defined multiple times, but must be unique.\n${message}\nDefinitions:${lib.options.getDefinitions definitions}";
## Merge a single instance of an option.
##
## @type Location -> List Definition -> Any
one = lib.options.merge.unique "";
equal = location: definitions: let
identifier = lib.options.getIdentifier location;
first = builtins.elemAt definitions 0;
rest = builtins.tail definitions;
merge = x: y:
if x != y
then builtins.throw "The option `${identifier}` has conflicting definitions:${lib.options.getDefinitions definitions}"
else x;
merged = builtins.foldl' merge first rest;
in
if builtins.length definitions == 0
then builtins.throw "Cannot merge unused option `${identifier}`."
else if builtins.length definitions == 1
then first.value
else merged.value;
};
## Check whether a value is an option.
##
## @type Attrs -> Bool
is = lib.types.is "option";
## Create an option.
##
## @type { type? :: String | Null, apply? :: (a -> b) | Null, default? :: { value :: a, text :: String }, example? :: String | Null, visible? :: Bool | Null, internal? :: Bool | Null, writable? :: Bool | Null, description? :: String | Null } -> Option a
create = settings @ {
type ? lib.types.unspecified,
apply ? null,
default ? {},
example ? null,
visible ? null,
internal ? null,
writable ? null,
description ? null,
}: {
__type__ = "option";
inherit type apply default example visible internal writable description;
};
## Create a sink option.
##
## @type @alias lib.options.create
sink = settings: let
defaults = {
internal = true;
visible = false;
default = false;
description = "A sink option for unused definitions";
type = lib.types.create {
name = "sink";
check = lib.fp.const true;
merge = lib.fp.const (lib.fp.const false);
};
apply = value: builtins.throw "Cannot read the value of a Sink option.";
};
in
lib.options.create (defaults // settings);
## Get the definition values from a list of options definitions.
##
## @type List Definition -> Any
getDefinitionValues = definitions:
builtins.map (definition: definition.value) definitions;
## Convert a list of option identifiers into a single identifier.
##
## @type List String -> String
getIdentifier = location: let
special = [
"<name>" # attrsOf (submodule {})
"*" # listOf (submodule {})
"<function body>" # functionTo
];
escape = part:
if builtins.elem part special
then part
else lib.strings.escape.nix.identifier part;
in
lib.strings.concatMapSep "." escape location;
## Get a string message of the definitions for an option.
##
## @type List Definition -> String
getDefinitions = definitions: let
serialize = definition: let
valueWithRecursionLimit =
lib.generators.withRecursion {
limit = 10;
throw = false;
}
definition.value;
eval = builtins.tryEval (
lib.generators.pretty {}
valueWithRecursionLimit
);
lines = lib.strings.split "\n" eval.value;
linesLength = builtins.length lines;
firstFiveLines = lib.lists.take 5 lines;
ellipsis = lib.lists.when (linesLength > 5) "...";
value = builtins.concatStringsSep "\n " (firstFiveLines ++ ellipsis);
result =
if ! eval.success
then ""
else if linesLength > 1
then ":\n " + value
else ": " + value;
in "\n- In `${definition.__file__}`${result}";
in
lib.strings.concatMap serialize definitions;
# TODO: Document this.
run = location: option: definitions: let
identifier = lib.options.getIdentifier location;
definitionsWithDefault =
if option ? default && option.default ? value
then
[
{
__file__ = builtins.head option.declarations;
value = lib.modules.overrides.option option.default.value;
}
]
++ definitions
else definitions;
merged =
if option.writable or null == false && builtins.length definitionsWithDefault > 1
then let
separatedDefinitions = builtins.map (definition:
definition
// {
value = (lib.options.merge.definitions location option.type [definition]).merged;
})
definitionsWithDefault;
in
builtins.throw "The option `${identifier}` is not writable, but is set more than once:${lib.options.getDefinitions separatedDefinitions}"
else lib.options.merge.definitions location option.type definitionsWithDefault;
value =
if option.apply or null != null
then option.apply merged.merged
else merged.merged;
in
option
// {
value = builtins.addErrorContext "while evaluating the option `${identifier}`:" value;
highestPriority = merged.raw.highestPriority;
isDefined = merged.isDefined;
files = builtins.map (definition: definition.__file__) merged.values;
definitions = lib.options.getDefinitionValues merged.values;
definitionsWithLocations = merged.values;
__toString = identifier;
};
};
}

View file

@ -0,0 +1,41 @@
lib: {
packages = {
# TODO: Document this.
isDerivation = value: value.type or null == "derivation";
# TODO: Document this.
sanitizeDerivationName = let
validate = builtins.match "[[:alnum:]+_?=-][[:alnum:]+._?=-]*";
in
value:
# First detect the common case of already valid strings, to speed those up
if builtins.stringLength value <= 207 && validate value != null
then builtins.unsafeDiscardStringContext value
else
lib.fp.pipe value [
# Get rid of string context. This is safe under the assumption that the
# resulting string is only used as a derivation name
builtins.unsafeDiscardStringContext
# Strip all leading "."
(x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0)
(lib.strings.split "[^[:alnum:]+._?=-]+")
# Replace invalid character ranges with a "-"
(lib.strings.concatMap (x:
if builtins.isList x
then "-"
else x))
# Limit to 211 characters (minus 4 chars for ".drv")
(x: builtins.substring (lib.math.max (builtins.stringLength x - 207) 0) (-1) x)
# If the result is empty, replace it with "unknown"
(x:
if builtins.stringLength x == 0
then "unknown"
else x)
];
};
}

32
lib/src/paths/default.nix Normal file
View file

@ -0,0 +1,32 @@
lib: {
paths = {
into = {
# TODO: Document this
drv = value: let
path = builtins.storePath value;
result = {
type = "derivation";
name =
lib.packages.sanitizeDerivationName
(builtins.substring 33 (-1) (builtins.baseNameOf path));
outPath = path;
outputs = ["out"];
outputName = "out";
out = result;
};
in
result;
};
validate = {
# TODO: Document this.
store = value:
if lib.strings.stringifiable value
then
builtins.substring 0 1 (builtins.toString value)
== "/"
&& builtins.dirOf (builtins.toString value) == builtins.storeDir
else false;
};
};
}

View file

@ -0,0 +1,33 @@
lib: {
points = {
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows
## us to define recursive functions without worrying about the order of their
## definitions.
##
## @type (a -> a) -> a
fix = f: let
x = f x;
in
x;
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows
## us to define recursive functions without worrying about the order of their
## definitions. Unlike `fix`, the resulting value is also given a `__unfix__`
## attribute that is set to the original function passed to `fix'`.
##
## FIXME: The below type annotation should include a mention of the `__unfix__`
## value.
##
## @type (a -> a) -> a
fix' = f: let
x =
f x
// {
__unfix__ = f;
};
in
x;
};
}

View file

@ -0,0 +1,59 @@
{
upper = [
"A"
"B"
"C"
"D"
"E"
"F"
"G"
"H"
"I"
"J"
"K"
"L"
"M"
"N"
"O"
"P"
"Q"
"R"
"S"
"T"
"U"
"V"
"W"
"X"
"Y"
"Z"
];
lower = [
"a"
"b"
"c"
"d"
"e"
"f"
"g"
"h"
"i"
"j"
"k"
"l"
"m"
"n"
"o"
"p"
"q"
"r"
"s"
"t"
"u"
"v"
"w"
"x"
"y"
"z"
];
}

97
lib/src/strings/ascii.nix Normal file
View file

@ -0,0 +1,97 @@
{
" " = 32;
"!" = 33;
"\"" = 34;
"#" = 35;
"$" = 36;
"%" = 37;
"&" = 38;
"'" = 39;
"(" = 40;
")" = 41;
"*" = 42;
"+" = 43;
"," = 44;
"-" = 45;
"." = 46;
"/" = 47;
"0" = 48;
"1" = 49;
"2" = 50;
"3" = 51;
"4" = 52;
"5" = 53;
"6" = 54;
"7" = 55;
"8" = 56;
"9" = 57;
":" = 58;
";" = 59;
"<" = 60;
"=" = 61;
">" = 62;
"?" = 63;
"@" = 64;
"A" = 65;
"B" = 66;
"C" = 67;
"D" = 68;
"E" = 69;
"F" = 70;
"G" = 71;
"H" = 72;
"I" = 73;
"J" = 74;
"K" = 75;
"L" = 76;
"M" = 77;
"N" = 78;
"O" = 79;
"P" = 80;
"Q" = 81;
"R" = 82;
"S" = 83;
"T" = 84;
"U" = 85;
"V" = 86;
"W" = 87;
"X" = 88;
"Y" = 89;
"Z" = 90;
"[" = 91;
"\\" = 92;
"]" = 93;
"^" = 94;
"_" = 95;
"`" = 96;
"a" = 97;
"b" = 98;
"c" = 99;
"d" = 100;
"e" = 101;
"f" = 102;
"g" = 103;
"h" = 104;
"i" = 105;
"j" = 106;
"k" = 107;
"l" = 108;
"m" = 109;
"n" = 110;
"o" = 111;
"p" = 112;
"q" = 113;
"r" = 114;
"s" = 115;
"t" = 116;
"u" = 117;
"v" = 118;
"w" = 119;
"x" = 120;
"y" = 121;
"z" = 122;
"{" = 123;
"|" = 124;
"}" = 125;
"~" = 126;
}

245
lib/src/strings/default.nix Normal file
View file

@ -0,0 +1,245 @@
lib: {
strings = {
into = {
## Convert a character to an integer.
##
## @type String -> Integer
int = char: builtins.getAttr char lib.strings.ascii;
## Convert a string into a list of characters.
##
## @type String -> List String
chars = value: let
range = lib.lists.range 0 (builtins.stringLength value - 1);
pick = index: builtins.substring index 1 value;
in
builtins.map pick range;
shell = {
## Convert a value into a shell variable.
##
## @type String -> Any -> String
var = name: target: let
baseVar = "${name}=${lib.strings.escape.shell.arg target}";
listVar = "declare -a ${name}=(${lib.strings.escape.shell.args target})";
attrsVar = "declare -A ${name}=(${
builtins.concatStringsSep " " (lib.attrs.mapToList
(k: v: "[${lib.strings.escape.shell.arg k}]=${lib.strings.escape.shell.arg v}")
target)
})";
in
assert lib.errors.trace (lib.strings.validate.posix name) "Invalid shell variable name: ${name}";
if builtins.isAttrs target && !lib.strings.validate.stringifiable target
then attrsVar
else if builtins.isList target
then listVar
else baseVar;
## Create shell variables for a map of values.
##
## @type Attrs -> String
vars = target:
builtins.concatStringsSep
"\n"
(lib.attrs.mapToList lib.strings.into.shell.var target);
};
};
escape = {
## Escape parts of a string.
##
## @type List String -> String -> String
any = patterns: source: let
escaped = builtins.map (x: "\\${x}") patterns;
replacer = builtins.replaceStrings patterns escaped;
in
replacer source;
## Escape a given set of characters in a string using their
## ASCII code prefixed with "\x".
##
## @type List String -> String
c = list: let
serialize = char: let
hex = lib.numbers.into.hex (lib.strings.into.int char);
in "\\x${lib.strings.lower hex}";
in
builtins.replaceStrings list (builtins.map serialize list);
nix = {
## Escape a string of Nix code.
##
## @type String -> String
value = value: lib.strings.escape.any ["$"] (builtins.toJSON value);
## Escape a string for use as a Nix identifier.
##
## @type String -> String
identifier = value:
if builtins.match "[a-zA-Z_][a-zA-Z0-9_'-]*" value != null
then value
else lib.strings.escape.nix.value value;
};
## Escape a string for use in a regular expression.
##
## @type String -> String
regex = lib.strings.escape.any (lib.strings.into.chars "\\[{()^$?*+|.");
## Escape a string for use in XML.
##
## @type String -> String
xml =
builtins.replaceStrings
["\"" "'" "<" ">" "&"]
["&quot;" "&apos;" "&lt;" "&gt;" "&amp;"];
shell = {
## Escape a string for use as a shell argument.
##
## @type String -> String
arg = value: "'${builtins.replaceStrings ["'"] ["'\\''"] (builtins.toString value)}'";
## Escape multiple strings for use as shell arguments.
##
## @type List String -> String
args = lib.strings.concatMapSep " " lib.strings.escape.shell.arg;
};
};
validate = {
## Check if a string is a valid POSIX identifier.
##
## @type String -> Bool
posix = name: builtins.match "[a-zA-Z_][a-zA-Z0-9_]*" name != null;
## Check if a value can be used as a string.
##
## @type Any -> Bool
stringifiable = value:
builtins.isString value
|| builtins.isPath value
|| value ? outPath
|| value ? __toString;
# TODO: Document this.
empty = value:
builtins.match "[ \t\n]*" value != null;
};
ascii = import ./ascii.nix;
alphabet = import ./alphabet.nix;
## Concatenate a list of strings together.
##
## @type List String -> String
concat = builtins.concatStringsSep "";
## Concatenate and map a list of strings together.
##
## @type List String -> String
concatMap = lib.strings.concatMapSep "";
## Concatenate and map a list of strings together with a separator.
##
## @type String -> (a -> String) -> List a -> String
concatMapSep = separator: f: list:
builtins.concatStringsSep separator (builtins.map f list);
## Change a string to uppercase.
##
## @type String -> String
upper = builtins.replaceStrings lib.strings.alphabet.lower lib.strings.alphabet.upper;
## Change a string to lowercase.
##
## @type String -> String
lower = builtins.replaceStrings lib.strings.alphabet.upper lib.strings.alphabet.lower;
## Add the context of one string to another.
##
## @type String -> String -> String
withContext = context: value: builtins.substring 0 0 context + value;
## Split a string by a separator.
##
## @type String -> String -> List String
split = separator: value: let
escaped = lib.strings.escape.regex (builtins.toString separator);
raw = builtins.split escaped (builtins.toString value);
parts = builtins.filter builtins.isString raw;
in
builtins.map (lib.strings.withContext value) parts;
## Check if a string starts with a given prefix.
##
## @type String -> String -> Bool
hasPrefix = prefix: value: let
text = builtins.substring 0 (builtins.stringLength prefix) value;
in
text == prefix;
## Check if a string ends with a given suffix.
##
## @type String -> String -> Bool
hasSuffix = suffix: value: let
valueLength = builtins.stringLength value;
suffixLength = builtins.stringLength suffix;
text = builtins.substring (valueLength - suffixLength) valueLength value;
in
(valueLength >= suffixLength)
&& text == suffix;
## Check if a string contains a given infix.
##
## @type String -> String -> Bool
hasInfix = infix: value:
builtins.match ".*${lib.strings.escape.regex infix}.*" "${value}" != null;
## Remove a prefix from a string if it exists.
##
## @type String -> String -> String
removePrefix = prefix: value: let
prefixLength = builtins.stringLength prefix;
valueLength = builtins.stringLength value;
in
if lib.strings.hasPrefix prefix value
then builtins.substring prefixLength (valueLength - prefixLength) value
else value;
## Remove a suffix from a string if it exists.
##
## @type String -> String -> String
removeSuffix = suffix: value: let
suffixLength = builtins.stringLength suffix;
valueLength = builtins.stringLength value;
in
if lib.strings.hasSuffix suffix value
then builtins.substring 0 (valueLength - suffixLength) value
else value;
## Pad the start of a string with a character until it reaches
## a given length.
##
## @type Integer -> String -> String
padStart = length: char: value: let
valueLength = builtins.stringLength value;
padding = builtins.genList (_: char) (length - valueLength);
in
if valueLength < length
then (builtins.concatStringsSep "" padding) + value
else value;
## Pad the end of a string with a character until it reaches
## a given length.
##
## @type Integer -> String -> String
padEnd = length: char: value: let
valueLength = builtins.stringLength value;
padding = builtins.genList (_: char) (length - valueLength);
in
if valueLength < length
then value + (builtins.concatStringsSep "" padding)
else value;
};
}

889
lib/src/types/default.nix Normal file
View file

@ -0,0 +1,889 @@
lib: {
types = {
## Determine whether a given value is a certain type. Note that this is *not*
## the same as primitive Nix types. Types created with `lib.type` are attribute
## sets with a `__type__` symbol.
##
## @type String -> Attrs -> Bool
is = name: value:
value.__type__ or null == name;
# TODO: Document this.
set = name: value:
value
// {
__type__ = name;
};
# TODO: Document this.
functor = name: {
inherit name;
type =
lib.attrs.select
(lib.strings.split "." name)
null
lib.types;
wrapped = null;
payload = null;
merge = null;
};
# TODO: Document this.
merge = f: g: let
wrapped = f.wrapped.mergeType g.wrapped.functor;
payload = f.merge f.payload g.payload;
in
if f.name != g.name
then null
else if f.wrapped == null && g.wrapped == null && f.payload == null && g.payload == null
then f.type
else if f.wrapped != null && g.wrapped != null && wrapped != null
then f.type wrapped
else if f.payload != null && g.payload != null && payload != null
then f.type payload
else null;
# TODO: Document this.
create = settings @ {
name,
description ? name,
fallback ? {},
check ? lib.fp.const true,
merge ? lib.options.merge.default,
functor ? lib.types.functor name,
mergeType ? lib.types.merge functor,
getSubOptions ? lib.fp.const {},
getSubModules ? null,
withSubModules ? lib.fp.const null,
children ? {},
}: {
__type__ = "type";
inherit
name
description
fallback
check
merge
functor
mergeType
getSubOptions
getSubModules
withSubModules
children
;
};
# TODO: Document this.
withCheck = type: check:
type
// {
check = value: type.check value && check value;
};
# TODO: Document this.
raw = lib.types.create {
name = "Raw";
description = "raw value";
check = lib.fp.const true;
merge = lib.options.merge.one;
};
# TODO: Document this.
any = lib.types.create {
name = "Any";
description = "any";
check = lib.fp.const true;
merge = location: definitions: let
identifier = lib.options.getIdentifier location;
first = builtins.elemAt definitions 0;
files = builtins.map lib.modules.getFiles definitions;
serializedFiles = builtins.concatStringsSep " and " files;
getType = value:
if builtins.isAttrs value && lib.strings.validate.stringifiable value
then "StringifiableAttrs"
else builtins.typeOf value;
commonType =
builtins.foldl' (
type: definition:
if getType definition.value != type
then builtins.throw "The option `${identifier}` has conflicting definitions in ${files}"
else type
) (getType first.value)
definitions;
mergeStringifiableAttrs = lib.options.merge.one;
mergeSet = (lib.types.attrs.of lib.types.any).merge;
mergeList =
if builtins.length definitions > 1
then builtins.throw "The option `${identifier}` has conflicting definitions in ${files}"
else (lib.types.list.of lib.types.any).merge;
mergeLambda = location: definitions: x: let
resolvedLocation = location ++ ["<function body>"];
resolvedDefinitions =
builtins.map (definition: {
__file__ = definition.__file__;
value = definition.value x;
})
definitions;
in
lib.types.any.merge resolvedLocation resolvedDefinitions;
merge =
if commonType == "set"
then mergeSet
else if commonType == "list"
then mergeList
else if commonType == "StringifiableAttrs"
then mergeStringifiableAttrs
else if commonType == "lambda"
then mergeLambda
else lib.options.merge.equal;
in
merge location definitions;
};
# TODO: Document this.
unspecified = lib.types.create {
name = "Unspecified";
description = "unspecified value";
};
# TODO: Document this.
bool = lib.types.create {
name = "Bool";
description = "boolean";
check = builtins.isBool;
merge = lib.options.merge.equal;
};
# TODO: Document this.
int = lib.types.create {
name = "Int";
description = "signed integer";
check = builtins.isInt;
merge = lib.options.merge.equal;
};
ints = let
description = start: end: "${builtins.toString start} and ${builtins.toString end} (inclusive)";
# TODO: Document this.
between = start: end:
assert lib.errors.trace (start <= end) "lib.types.ints.between start must be less than or equal to end";
lib.types.withCheck
lib.types.int
(value: value >= start && value <= end)
// {
name = "IntBetween";
description = "integer between ${description start end}";
};
# TODO: Document this.
sign = bits: range: let
start = 0 - (range / 2);
end = range / 2 - 1;
in
between start end
// {
name = "IntSigned${builtins.toString bits}";
description = "${builtins.toString bits} bit signed integer between ${description start end}";
};
# TODO: Document this.
unsign = bits: range: let
start = 0;
end = range - 1;
in
between start end
// {
name = "IntUnsigned${builtins.toString bits}";
description = "${builtins.toString bits} bit unsigned integer between ${description start end}";
};
in {
# TODO: Document this.
inherit between;
# TODO: Document this.
positive =
lib.types.withCheck
lib.types.int
(value: value > 0)
// {
name = "IntPositive";
description = "positive integer";
};
# TODO: Document this.
unsigned =
lib.types.withCheck
lib.types.int
(value: value >= 0)
// {
name = "IntUnsigned";
description = "unsigned integer";
};
# TODO: Document this.
u8 = unsign 8 256;
# TODO: Document this.
u16 = unsign 16 65536;
# TODO: Document this.
u32 = unsign 32 4294967296;
# u64 = unsign 64 18446744073709551616;
# TODO: Document this.
s8 = sign 8 256;
# TODO: Document this.
s16 = sign 16 65536;
# TODO: Document this.
s32 = sign 32 4294967296;
};
# TODO: Document this.
float = lib.types.create {
name = "Float";
description = "floating point number";
check = builtins.isFloat;
merge = lib.options.merge.equal;
};
# TODO: Document this.
number = lib.types.either lib.types.int lib.types.float;
# TODO: Document this.
numbers = let
description = start: end: "${builtins.toString start} and ${builtins.toString end} (inclusive)";
# TODO: Document this.
between = start: end:
assert lib.errors.trace (start <= end) "lib.types.numbers.between start must be less than or equal to end";
lib.types.withCheck
lib.types.number
(value: value >= start && value <= end)
// {
name = "NumberBetween";
description = "numbereger between ${description start end}";
};
in {
# TODO: Document this.
inherit between;
# TODO: Document this.
positive =
lib.types.withCheck
lib.types.int
(value: value > 0)
// {
name = "NumberPositive";
description = "positive number";
};
# TODO: Document this.
positiveOrZero =
lib.types.withCheck
lib.types.int
(value: value >= 0)
// {
name = "NumberPositiveOrZero";
description = "number that is zero or greater";
};
};
# TODO: Document this.
port = lib.types.ints.u16;
# TODO: Document this.
string = lib.types.create {
name = "String";
description = "string";
check = builtins.isString;
merge = lib.options.merge.equal;
};
strings = {
# TODO: Document this.
required = lib.types.create {
name = "StringNonEmpty";
description = "non-empty string";
check = value: lib.types.string.check value && !(lib.strings.validate.empty value);
merge = lib.options.merge.equal;
};
# TODO: Document this.
matching = pattern:
lib.types.create {
name = "StringMatching ${pattern}";
description = "string matching the pattern ${pattern}";
check = value: lib.types.string.check value && builtins.match pattern value != null;
merge = lib.options.merge.equal;
};
# TODO: Document this.
concat = separator:
lib.types.create {
name = "StringConcat";
description =
if separator == ""
then "concatenated string"
else "string concatenated with ${builtins.toJSON separator}";
check = value: lib.types.string.check value;
merge = location: definitions:
builtins.concatStringsSep
separator
(lib.options.getDefinitionValues definitions);
functor =
lib.types.functor "strings.concat"
// {
payload = separator;
merge = x: y:
if x == y
then x
else null;
};
};
# TODO: Document this.
line = let
matcher = lib.types.strings.matching "[^\n\r]*\n?";
in
lib.types.create {
name = "StringLine";
description = "single line string with an optional new line at the end";
check = matcher.check;
merge = location: definitions:
lib.strings.removeSuffix
"\n"
(matcher.merge location definitions);
};
# TODO: Document this.
lines = lib.types.strings.concat "\n";
};
attrs = {
# TODO: Document this.
any = lib.types.create {
name = "Attrs";
description = "attribute set";
fallback = {value = {};};
check = builtins.isAttrs;
merge = location: definitions:
builtins.foldl'
(result: definition: result // definition.value)
{}
definitions;
};
# TODO: Document this.
of = type:
lib.types.create {
name = "AttrsOf";
description = "AttrsOf (${type.name})";
fallback = {value = {};};
check = builtins.isAttrs;
merge = location: definitions: let
normalize = definition:
builtins.mapAttrs
(key: value: {
__file__ = definition.__file__;
value = value;
})
definition.value;
normalized = builtins.map normalize definitions;
zipper = key: definitions:
(lib.options.merge.definitions (location ++ [key]) type definitions).optional;
filtered =
lib.attrs.filter
(key: value: value ? value)
(builtins.zipAttrsWith zipper normalized);
in
builtins.mapAttrs (key: value: value.value) filtered;
getSubOptions = prefix: type.getSubOptions (prefix ++ ["<name>"]);
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.attrs.of (type.withSubModules modules);
functor = lib.types.functor "attrs.of" // {wrapped = type;};
children = {
element = type;
};
};
# TODO: Document this.
lazy = type:
lib.types.create {
name = "LazyAttrsOf";
description = "LazyAttrsOf (${type.name})";
fallback = {value = {};};
check = builtins.isAttrs;
merge = location: definitions: let
normalize = definition:
builtins.mapAttrs
(key: value: {
__file__ = definition.__file__;
value = value;
})
definition.value;
normalized = builtins.map normalize definitions;
zipper = key: definitions: let
merged = lib.options.merge.definitions (location ++ [key]) type definitions;
in
merged.optional.value or type.fallback.value or merged.merged;
in
builtins.zipAttrsWith zipper normalized;
getSubOptions = prefix: type.getSubOptions (prefix ++ ["<name>"]);
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.attrs.lazy (type.withSubModules modules);
functor = lib.types.functor "attrs.lazy" // {wrapped = type;};
children = {
element = type;
};
};
};
# TODO: Document this.
package = lib.types.create {
name = "Package";
description = "package";
check = value: lib.packages.isDerivation value || lib.paths.validate.store value;
merge = location: definitions: let
merged = lib.options.merge.one location definitions;
in
if builtins.isPath merged || (builtins.isString merged && !(builtins.hasContext merged))
then lib.paths.into.drv merged
else merged;
};
packages = {
# TODO: Document this.
shell =
lib.types.package
// {
check = value: lib.packages.isDerivation && builtins.hasAttr "shellPath" value;
};
};
# TODO: Document this.
path = lib.types.create {
name = "Path";
description = "path";
check = value:
lib.strings.validate.stringifiable value
&& builtins.substring 0 1 (builtins.toString value) == "/";
merge = lib.options.merge.equal;
};
list = {
# TODO: Document this.
any = lib.types.list.of lib.types.any;
# TODO: Document this.
of = type:
lib.types.create {
name = "ListOf";
description = "ListOf (${type.name})";
fallback = {value = [];};
check = builtins.isList;
merge = location: definitions: let
result =
lib.lists.mapWithIndex1 (
i: definition:
lib.lists.mapWithIndex1 (
j: value: let
resolved =
lib.options.merge.definitions
(location ++ ["[definition ${builtins.toString i}-entry ${j}]"]);
in
resolved.optional
)
definition.value
)
definitions;
merged = builtins.concatLists result;
filtered = builtins.filter (definition: definition ? value) merged;
values = lib.optiosn.getDefinitionValues filtered;
in
values;
getSubOptions = prefix: type.getSubOptions (prefix ++ ["*"]);
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.list.of (type.withSubModules modules);
functor = lib.types.functor "list.of" // {wrapped = type;};
children = {
element = type;
};
};
# TODO: Document this.
required = type:
lib.types.withCheck
(lib.types.list.of type)
(value: value != [])
// {
description = "non-empty list of ${type.description}";
fallback = {};
};
};
# TODO: Document this.
unique = message: type:
lib.types.create {
name = "Unique";
description = type.description;
fallback = type.fallback;
check = type.check;
merge = lib.options.merge.unique message;
getSubOptions = type.getSubOptions;
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.unique message (type.withSubModules modules);
functor = lib.types.functor "unique" // {wrapped = type;};
children = {
element = type;
};
};
# TODO: Document this.
# Like unique, but does not merge.
single = type:
lib.types.create {
name = "Single";
description = type.description;
fallback = type.fallback;
check = type.check;
merge = lib.options.merge.one;
getSubOptions = type.getSubOptions;
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.single (type.withSubModules modules);
functor = lib.types.functor "unique" // {wrapped = type;};
children = {
element = type;
};
};
# TODO: Document this.
nullish = type:
lib.types.create {
name = "Nullish";
description = "null or ${type.description}";
fallback = {value = null;};
check = value: value == null || type.check value;
merge = location: definitions: let
identifier = lib.options.getIdentifier location;
files = builtins.map lib.modules.getFiles definitions;
serializedFiles = builtins.concatStringsSep " and " files;
totalNulls = lib.lists.count (definition: definition == null) definitions;
in
if totalNulls == builtins.length definitions
then null
else if totalNulls != 0
then builtins.throw "The option `${identifier}` is defined as both null and not null in ${serializedFiles}"
else type.merge location definitions;
getSubOptions = type.getSubOptions;
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.nullish (type.withSubModules modules);
functor = lib.types.functor "nullish" // {wrapped = type;};
children = {
element = type;
};
};
# TODO: Document this.
function = type:
lib.types.create {
name = "Function";
description = "function that returns ${type.description}";
check = builtins.isFunction;
merge = location: definitions: args: let
normalize = definition: {
__file__ = definition.__file__;
value = definition.value args;
};
normalized = builtins.map normalize definitions;
merged = lib.options.merge.definitions (location ++ ["<function body>"]) type normalized;
in
merged.merged;
getSubOptions = prefix: type.getSubOptions (prefix ++ ["<function body>"]);
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.function (type.withSubModules modules);
functor = lib.types.functor "function" // {wrapped = type;};
children = {
element = type;
};
};
# TODO: Document this.
submodule = modules:
lib.types.submodules.of {
modules = lib.lists.from.any modules;
};
submodules = {
# TODO: Document this.
of = settings @ {
modules,
args ? {},
description ? null,
}: let
getModules = builtins.map (
definition: {
__file__ = definition.__file__;
includes = [definition.value];
}
);
base = lib.modules.run {
inherit args;
modules =
[
{
options.__module__.args.name = lib.options.create {
type = lib.types.string;
};
config.__module__.args.name = lib.modules.overrides.default "<name>";
}
]
++ modules;
};
freeform = base.__module__.freeform;
name = "Submodule";
in
lib.types.create {
inherit name;
description =
if description != null
then description
else freeform.description or name;
fallback = {value = {};};
check = value: builtins.isAttrs value || builtins.isFunction value || lib.types.path.check value;
merge = location: definitions: let
result = base.extend {
modules =
[{config.__module__.args.name = lib.lists.last location;}]
++ getModules definitions;
};
in
result.config;
getSubOptions = prefix: let
result = base.extend {inherit prefix;};
in
result.options
// lib.attrs.when (freeform != null) {
__freeformOptions__ = freeform.getSubOptions prefix;
};
getSubModules = modules;
withSubModules = modules:
lib.types.submodules.of {
inherit args description modules;
};
children = lib.attrs.when (freeform != null) {
inherit freeform;
};
functor =
lib.types.functor "submodule"
// {
type = lib.types.submodules.of;
payload = {
inherit modules args description;
};
merge = x: y: {
modules = x.modules ++ y.modules;
args = let
intersection = builtins.intersectAttrs x.args y.args;
in
if intersection == {}
then x.args // y.args
else builtins.throw "A submodule option is declared multiple times with the same args: ${builtins.toString (builtins.attrNames intersection)}";
description =
if x.description == null
then y.description
else if y.description == null
then x.description
else if x.description == y.description
then x.description
else builtins.throw "A submodule description is declared multiple times with conflicting values";
};
};
};
};
deferred = {
# TODO: Document this.
default = lib.types.deferred.of {
modules = [];
};
# TODO: Document this.
of = settings @ {modules}: let
submodule = lib.types.submodule modules;
in
lib.types.create {
name = "Deferred";
description = "module";
check = value: builtins.isAttrs value || builtins.isFunction value || lib.types.path.check value;
merge = location: definitions: {
includes =
modules
++ builtins.map
(definition: {
__file__ = "${definition.__file__}; via ${lib.options.getIdentifier location}";
includes = [definition.value];
})
definitions;
};
getSubOptions = submodule.getSubOptions;
getSubModules = submodule.getSubModules;
withSubModules = modules:
lib.types.deferred.of {
modules = modules;
};
functor =
lib.types.functor "deferred.of"
// {
type = lib.types.deferred.of;
payload = {inherit modules;};
merge = x: y: {
modules = x.modules ++ y.modules;
};
};
};
};
# TODO: Document this.
option = lib.types.create {
name = "Option";
description = "option";
check = lib.types.is "option";
merge = location: definitions: let
first = builtins.elemAt definitions 0;
modules =
builtins.map (definition: {
__file__ = definition.__file__;
options = lib.options.create {
type = definition.value;
};
})
definitions;
merged = lib.modules.fixup location (lib.options.merge.declarations location modules);
in
if builtins.length definitions == 1
then first.value
else merged.type;
};
# TODO: Document this.
enum = values: let
serialize = value:
if builtins.isString value
then ''"${value}"''
else if builtins.isInt value
then builtins.toString value
else if builtins.isBool value
then lib.bools.into.string value
else ''<${builtins.typeOf value}>'';
in
lib.types.create {
name = "Enum";
description =
if values == []
then "empty enum"
else if builtins.length values == 1
then "value ${serialize (builtins.elemAt values 0)} (singular enum)"
else "one of ${lib.strings.concatMapSep ", " serialize values}";
check = value: builtins.elem value values;
merge = lib.options.merge.equal;
functor =
lib.types.functor "enum"
// {
payload = values;
merge = x: y: lib.lists.unique (x ++ y);
};
};
# TODO: Document this.
either = left: right: let
name = "Either";
functor =
lib.types.functor name
// {
wrapped = [left right];
};
in
lib.types.create {
inherit name functor;
description = "${left.description} or ${right.description}";
check = value: left.check value || right.check value;
merge = location: definitions: let
values = lib.options.getDefinitionValues definitions;
isLeft = builtins.all left.check values;
isRight = builtins.all right.check values;
in
if isLeft
then left.merge location definitions
else if isRight
then right.merge location definitions
else lib.options.merge.one location definitions;
mergeType = f: let
mergedLeft = left.mergeType (builtins.elemAt f.wrapped 0).functor;
mergedRight = right.mergeType (builtins.elemAt f.wrapped 1).functor;
in
if (f.name == name) && (mergedLeft != null) && (mergedRight != null)
then functor.type mergedLeft mergedRight
else null;
children = {
inherit left right;
};
};
# TODO: Document this.
one = types: let
first = builtins.elemAt types 0;
rest = lib.lists.tail types;
in
if types == []
then builtins.throw "lib.types.one must be given at least one type"
else builtins.foldl' lib.types.either first rest;
# TODO: Document this.
coerce = initial: transform: final: let
in
if initial.getSubModules != null
then builtins.throw "lib.types.coerce's first argument may not have submodules, but got ${initial.description}"
else
lib.types.create {
name = "Coerce";
description = "${initial.description} that is transformed to ${final.description}";
fallback = final.fallback;
check = value: final.check value || (initial.check value && final.check (transform value));
merge = location: definitions: let
process = value:
if initial.check value
then transform value
else value;
normalize = definition:
definition
// {
value = process definition.value;
};
normalized = builtins.map normalize definitions;
in
final.merge location normalized;
getSubOptions = final.getSubOptions;
getSubModules = final.getSubModules;
withSubModules = modules:
lib.types.coerce
initial
transform
(final.withSubModules modules);
mergeType = x: y: null;
functor = lib.types.functor "coerce" // {wrapped = final;};
children = {
inherit initial final;
};
};
};
}

View file

@ -0,0 +1,18 @@
lib: {
versions = {
## Check if a version is greater than another.
##
## @type String -> String -> Bool
gt = first: second: builtins.compareVersions first second == -1;
## Check if a version is less than another.
##
## @type String -> String -> Bool
lt = first: second: builtins.compareVersions first second == 1;
## Check if a version is equal to another.
##
## @type String -> String -> Bool
eq = first: second: builtins.compareVersions first second == 0;
};
}