From 0409563e324e6fb6a99bd81ee210944896df77be Mon Sep 17 00:00:00 2001 From: Jake Hamilton Date: Sat, 1 Jun 2024 04:00:53 -0700 Subject: [PATCH] chore: initial commit --- LICENSE | 21 + README.md | 29 + lib/LICENSE | 23 + lib/README.md | 104 ++++ lib/default.nix | 1 + lib/flake.nix | 7 + lib/src/attrs/default.nix | 141 +++++ lib/src/attrs/default.test.nix | 17 + lib/src/bools/default.nix | 48 ++ lib/src/default.nix | 76 +++ lib/src/errors/default.nix | 8 + lib/src/fp/default.nix | 46 ++ lib/src/generators/default.nix | 144 +++++ lib/src/importers/default.nix | 13 + lib/src/lists/default.nix | 160 ++++++ lib/src/math/default.nix | 15 + lib/src/modules/default.nix | 600 +++++++++++++++++++++ lib/src/modules/default.test.nix | 28 + lib/src/numbers/default.nix | 69 +++ lib/src/options/default.nix | 306 +++++++++++ lib/src/packages/default.nix | 41 ++ lib/src/paths/default.nix | 32 ++ lib/src/points/default.nix | 33 ++ lib/src/strings/alphabet.nix | 59 ++ lib/src/strings/ascii.nix | 97 ++++ lib/src/strings/default.nix | 245 +++++++++ lib/src/types/default.nix | 889 +++++++++++++++++++++++++++++++ lib/src/versions/default.nix | 18 + 28 files changed, 3270 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/LICENSE create mode 100644 lib/README.md create mode 100644 lib/default.nix create mode 100644 lib/flake.nix create mode 100644 lib/src/attrs/default.nix create mode 100644 lib/src/attrs/default.test.nix create mode 100644 lib/src/bools/default.nix create mode 100644 lib/src/default.nix create mode 100644 lib/src/errors/default.nix create mode 100644 lib/src/fp/default.nix create mode 100644 lib/src/generators/default.nix create mode 100644 lib/src/importers/default.nix create mode 100644 lib/src/lists/default.nix create mode 100644 lib/src/math/default.nix create mode 100644 lib/src/modules/default.nix create mode 100644 lib/src/modules/default.test.nix create mode 100644 lib/src/numbers/default.nix create mode 100644 lib/src/options/default.nix create mode 100644 lib/src/packages/default.nix create mode 100644 lib/src/paths/default.nix create mode 100644 lib/src/points/default.nix create mode 100644 lib/src/strings/alphabet.nix create mode 100644 lib/src/strings/ascii.nix create mode 100644 lib/src/strings/default.nix create mode 100644 lib/src/types/default.nix create mode 100644 lib/src/versions/default.nix diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc57b98 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5245074 --- /dev/null +++ b/README.md @@ -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. | diff --git a/lib/LICENSE b/lib/LICENSE new file mode 100644 index 0000000..22c5fd5 --- /dev/null +++ b/lib/LICENSE @@ -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. diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..7de6e61 --- /dev/null +++ b/lib/README.md @@ -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 = ""; + }; + 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? diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..76840e2 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1 @@ +import ./src diff --git a/lib/flake.nix b/lib/flake.nix new file mode 100644 index 0000000..8501377 --- /dev/null +++ b/lib/flake.nix @@ -0,0 +1,7 @@ +{ + description = "A very basic flake"; + + outputs = _: { + lib = import ./src; + }; +} diff --git a/lib/src/attrs/default.nix b/lib/src/attrs/default.nix new file mode 100644 index 0000000..9f1fcd6 --- /dev/null +++ b/lib/src/attrs/default.nix @@ -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; + }; +} diff --git a/lib/src/attrs/default.test.nix b/lib/src/attrs/default.test.nix new file mode 100644 index 0000000..00be372 --- /dev/null +++ b/lib/src/attrs/default.test.nix @@ -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; + }; +} diff --git a/lib/src/bools/default.nix b/lib/src/bools/default.nix new file mode 100644 index 0000000..8455249 --- /dev/null +++ b/lib/src/bools/default.nix @@ -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; + }; +} diff --git a/lib/src/default.nix b/lib/src/default.nix new file mode 100644 index 0000000..36e696f --- /dev/null +++ b/lib/src/default.nix @@ -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 diff --git a/lib/src/errors/default.nix b/lib/src/errors/default.nix new file mode 100644 index 0000000..d024b63 --- /dev/null +++ b/lib/src/errors/default.nix @@ -0,0 +1,8 @@ +lib: { + errors = { + trace = condition: message: + if condition + then true + else builtins.trace message false; + }; +} diff --git a/lib/src/fp/default.nix b/lib/src/fp/default.nix new file mode 100644 index 0000000..1b3bc55 --- /dev/null +++ b/lib/src/fp/default.nix @@ -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; + }; +} diff --git a/lib/src/generators/default.nix b/lib/src/generators/default.nix new file mode 100644 index 0000000..5e0106b --- /dev/null +++ b/lib/src/generators/default.nix @@ -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 "" + 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 "" + else ""; + + 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 "" + 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; + }; +} diff --git a/lib/src/importers/default.nix b/lib/src/importers/default.nix new file mode 100644 index 0000000..833372d --- /dev/null +++ b/lib/src/importers/default.nix @@ -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); + }; +} diff --git a/lib/src/lists/default.nix b/lib/src/lists/default.nix new file mode 100644 index 0000000..8a0f696 --- /dev/null +++ b/lib/src/lists/default.nix @@ -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; + }; +} diff --git a/lib/src/math/default.nix b/lib/src/math/default.nix new file mode 100644 index 0000000..c7486e3 --- /dev/null +++ b/lib/src/math/default.nix @@ -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; + }; +} diff --git a/lib/src/modules/default.nix b/lib/src/modules/default.nix new file mode 100644 index 0000000..e37e0c3 --- /dev/null +++ b/lib/src/modules/default.nix @@ -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 "" "" 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__; + }; + }; +} diff --git a/lib/src/modules/default.test.nix b/lib/src/modules/default.test.nix new file mode 100644 index 0000000..d379070 --- /dev/null +++ b/lib/src/modules/default.test.nix @@ -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; + }; +} diff --git a/lib/src/numbers/default.nix b/lib/src/numbers/default.nix new file mode 100644 index 0000000..84f6965 --- /dev/null +++ b/lib/src/numbers/default.nix @@ -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; + }; +} diff --git a/lib/src/options/default.nix b/lib/src/options/default.nix new file mode 100644 index 0000000..7d56b8d --- /dev/null +++ b/lib/src/options/default.nix @@ -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 = [ + "" # attrsOf (submodule {}) + "*" # listOf (submodule {}) + "" # 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; + }; + }; +} diff --git a/lib/src/packages/default.nix b/lib/src/packages/default.nix new file mode 100644 index 0000000..94ee27a --- /dev/null +++ b/lib/src/packages/default.nix @@ -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) + ]; + }; +} diff --git a/lib/src/paths/default.nix b/lib/src/paths/default.nix new file mode 100644 index 0000000..937d72f --- /dev/null +++ b/lib/src/paths/default.nix @@ -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; + }; + }; +} diff --git a/lib/src/points/default.nix b/lib/src/points/default.nix new file mode 100644 index 0000000..f51520b --- /dev/null +++ b/lib/src/points/default.nix @@ -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; + }; +} diff --git a/lib/src/strings/alphabet.nix b/lib/src/strings/alphabet.nix new file mode 100644 index 0000000..145dc34 --- /dev/null +++ b/lib/src/strings/alphabet.nix @@ -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" + ]; +} diff --git a/lib/src/strings/ascii.nix b/lib/src/strings/ascii.nix new file mode 100644 index 0000000..d7c95a8 --- /dev/null +++ b/lib/src/strings/ascii.nix @@ -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; +} diff --git a/lib/src/strings/default.nix b/lib/src/strings/default.nix new file mode 100644 index 0000000..f70cf82 --- /dev/null +++ b/lib/src/strings/default.nix @@ -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 + ["\"" "'" "<" ">" "&"] + [""" "'" "<" ">" "&"]; + + 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; + }; +} diff --git a/lib/src/types/default.nix b/lib/src/types/default.nix new file mode 100644 index 0000000..f83bdb8 --- /dev/null +++ b/lib/src/types/default.nix @@ -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 ++ [""]; + 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 ++ [""]); + 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 ++ [""]); + 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 ++ [""]) type normalized; + in + merged.merged; + getSubOptions = prefix: type.getSubOptions (prefix ++ [""]); + 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 ""; + } + ] + ++ 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; + }; + }; + }; +} diff --git a/lib/src/versions/default.nix b/lib/src/versions/default.nix new file mode 100644 index 0000000..a50c25e --- /dev/null +++ b/lib/src/versions/default.nix @@ -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; + }; +}