From f7d6c846f6ae299bb74961e9f7ae519a78465be5 Mon Sep 17 00:00:00 2001 From: Jake Hamilton Date: Mon, 3 Jun 2024 02:57:13 -0700 Subject: [PATCH] chore: initial commit --- lib/README.md | 32 ++- lib/default.test.nix | 107 +++++++++ lib/src/attrs/default.nix | 67 +++--- lib/src/attrs/default.test.nix | 312 ++++++++++++++++++++++++ lib/src/bools/default.nix | 11 + lib/src/bools/default.test.nix | 161 +++++++++++++ lib/src/default.nix | 4 +- lib/src/errors/default.nix | 5 + lib/src/errors/default.test.nix | 3 + lib/src/fp/default.nix | 15 +- lib/src/fp/default.test.nix | 139 +++++++++++ lib/src/generators/default.test.nix | 113 +++++++++ lib/src/importers/default.nix | 2 + lib/src/importers/default.test.nix | 3 + lib/src/lists/default.nix | 34 ++- lib/src/lists/default.test.nix | 174 ++++++++++++++ lib/src/math/default.nix | 8 +- lib/src/math/default.test.nix | 19 ++ lib/src/modules/default.nix | 145 +++++++++--- lib/src/modules/default.test.nix | 352 +++++++++++++++++++++++++++- lib/src/numbers/default.nix | 2 +- lib/src/numbers/default.test.nix | 57 +++++ lib/src/options/default.nix | 22 +- lib/src/options/default.test.nix | 3 + lib/src/packages/default.nix | 5 +- lib/src/packages/default.test.nix | 3 + lib/src/paths/default.test.nix | 3 + lib/src/points/default.nix | 5 +- lib/src/points/default.test.nix | 3 + lib/src/strings/default.test.nix | 3 + lib/src/types/default.nix | 29 ++- lib/src/types/default.test.nix | 3 + lib/src/versions/default.test.nix | 3 + lib/test.sh | 18 ++ 34 files changed, 1760 insertions(+), 105 deletions(-) create mode 100644 lib/default.test.nix create mode 100644 lib/src/bools/default.test.nix create mode 100644 lib/src/errors/default.test.nix create mode 100644 lib/src/fp/default.test.nix create mode 100644 lib/src/generators/default.test.nix create mode 100644 lib/src/importers/default.test.nix create mode 100644 lib/src/lists/default.test.nix create mode 100644 lib/src/math/default.test.nix create mode 100644 lib/src/numbers/default.test.nix create mode 100644 lib/src/options/default.test.nix create mode 100644 lib/src/packages/default.test.nix create mode 100644 lib/src/paths/default.test.nix create mode 100644 lib/src/points/default.test.nix create mode 100644 lib/src/strings/default.test.nix create mode 100644 lib/src/types/default.test.nix create mode 100644 lib/src/versions/default.test.nix create mode 100755 lib/test.sh diff --git a/lib/README.md b/lib/README.md index 7de6e61..b14e1ab 100644 --- a/lib/README.md +++ b/lib/README.md @@ -75,12 +75,40 @@ in } ``` -Successful tests will return `true` while failing test will resolve with `false`. +Successful tests will return `true` while failing test will resolve with `false`. You can run +all tests with the following command: + +```shell +./test.sh +``` + +If you want to run a specific test suite, you can run the command, specifying the directory +to the tests file: + +```shell +./test.sh $namespace +``` + +For example, to run the tests for only `attrs`, use the following command: + +```shell +./test.sh attrs +``` ### Formatting +> **Note:** To keep this flake light and keep its inputs empty we do not include a package +> set which would provide a formatter. Instead please run `nix run nixpkgs#nixfmt-rfc-style` +> until an improved solution is available. + All code in this project must be formatted using the provided formatter in the `flake.nix` -file. You can run this formatter using the command `nix fmt`. +file. You can run this formatter using the command `nix fmt` (not currently available). + +### Code Quality + +In order to keep the project approachable and easy to maintain, certain patterns are not allowed. +In particular, the use of `with` and `rec` are not allowed. Additionally, you should prefer the +fully qualified name of a variable rather than creating intermediate ones using `inherit`. ### Adding Functionality diff --git a/lib/default.test.nix b/lib/default.test.nix new file mode 100644 index 0000000..9b7861e --- /dev/null +++ b/lib/default.test.nix @@ -0,0 +1,107 @@ +let + lib = import ./default.nix; + + root = ./.; + + files = [ + ./src/attrs/default.test.nix + ./src/bools/default.test.nix + ./src/errors/default.test.nix + ./src/fp/default.test.nix + ./src/generators/default.test.nix + ./src/importers/default.test.nix + ./src/lists/default.test.nix + ./src/math/default.test.nix + ./src/modules/default.test.nix + ./src/numbers/default.test.nix + ./src/options/default.test.nix + ./src/packages/default.test.nix + ./src/paths/default.test.nix + ./src/points/default.test.nix + ./src/strings/default.test.nix + ./src/types/default.test.nix + ./src/versions/default.test.nix + ]; + + resolve = file: let + imported = import file; + value = + if builtins.isFunction imported + then imported {inherit lib;} + else imported; + relative = lib.strings.removePrefix (builtins.toString root) (builtins.toString file); + in { + inherit file value; + relative = + if lib.strings.hasPrefix "/" relative + then "." + relative + else relative; + namespace = getNamespace file; + }; + + resolved = builtins.map resolve files; + + getNamespace = path: let + relative = lib.strings.removePrefix (builtins.toString root) (builtins.toString path); + parts = lib.strings.split "/" relative; + in + if builtins.length parts > 2 + then builtins.elemAt parts 2 + else relative; + + results = let + getTests = file: prefix: suite: let + nested = lib.attrs.mapToList (name: value: getTests file (prefix ++ [name]) value) suite; + relative = lib.strings.removePrefix (builtins.toString root) (builtins.toString file); + in + if builtins.isAttrs suite + then builtins.concatLists nested + else [ + { + inherit prefix file; + name = builtins.concatStringsSep " > " prefix; + value = suite; + relative = + if lib.strings.hasPrefix "/" relative + then "." + relative + else relative; + } + ]; + + base = + builtins.map (entry: getTests entry.file [entry.namespace] entry.value) resolved; + in + builtins.concatLists base; + + successes = builtins.filter (test: test.value) results; + failures = builtins.filter (test: !test.value) results; + + total = "${builtins.toString (builtins.length successes)} / ${builtins.toString (builtins.length results)}"; +in + if failures == [] + then let + message = + lib.strings.concatMapSep "\n" + (test: "✅ ${test.name}") + successes; + in '' + SUCCESS (${total}) + + ${message} + '' + else let + successMessage = + lib.strings.concatMapSep "\n" + (test: "✅ ${test.name}") + successes; + failureMessage = + lib.strings.concatMapSep "\n\n" + (test: + "❎ ${test.name}\n" + + " -> ${test.relative}") + failures; + in '' + FAILURE (${total}) + + ${failureMessage} + '' diff --git a/lib/src/attrs/default.nix b/lib/src/attrs/default.nix index 9f1fcd6..4d47802 100644 --- a/lib/src/attrs/default.nix +++ b/lib/src/attrs/default.nix @@ -13,16 +13,18 @@ lib: { mergeRecursiveUntil = predicate: x: y: let process = path: builtins.zipAttrsWith ( - key: values: let - currentPath = path ++ [key]; + name: values: let + currentPath = path ++ [name]; isSingleValue = builtins.length values == 1; isComplete = predicate currentPath (builtins.elemAt values 1) (builtins.elemAt values 0); in - if isSingleValue || isComplete + if isSingleValue then builtins.elemAt values 0 + else if isComplete + then builtins.elemAt values 1 else process currentPath values ); in @@ -43,13 +45,13 @@ lib: { ## ## @type (List String) -> a -> Attrs -> a | b select = path: fallback: target: let - key = builtins.head path; + name = 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 if target ? ${name} + then lib.attrs.select rest fallback target.${name} else fallback; ## Get a value from an attribute set by a path. If the path does not exist, @@ -61,10 +63,12 @@ lib: { error = builtins.throw "Path not found in attribute set: ${pathAsString}"; in if lib.attrs.has path target - then lib.attrs.select path target + then lib.attrs.select path null target else error; - # TODO: Document this. + ## Create a nested attribute set with a value as the leaf node. + ## + ## @type (List String) -> a -> Attrs set = path: value: let length = builtins.length path; process = depth: @@ -80,13 +84,13 @@ lib: { ## ## @type (List String) -> Attrs -> Bool has = path: target: let - key = builtins.head path; + name = builtins.head path; rest = builtins.tail path; in if path == [] then true - else if target ? ${key} - then lib.attrs.has rest target.${key} + else if target ? ${name} + then lib.attrs.has rest target.${name} else false; ## Depending on a given condition, either use the given value or an empty @@ -98,43 +102,46 @@ lib: { then value else {}; - ## Map an attribute set's keys and values to a list. + ## Map an attribute set's names and values to a list. ## ## @type Any a => (String -> Any -> a) -> Attrs -> List a mapToList = f: target: - builtins.map (key: f key target.${key}) (builtins.attrNames target); + builtins.map (name: f name target.${name}) (builtins.attrNames target); - # TODO: Document this. + ## Map an attribute set recursively. Only non-set leaf nodes will be mapped. + ## + ## @type (List String -> Any -> Any) -> Attrs -> Attrs mapRecursive = f: target: lib.attrs.mapRecursiveWhen (lib.fp.const true) f target; - # TODO: Document this. + ## Map an attribute set recursively when a given predicate returns true. + ## Only leaf nodes according to the predicate will be mapped. + ## + ## @type (Attrs -> Bool) -> (List String -> Any -> Any) -> Attrs -> Attrs mapRecursiveWhen = predicate: f: target: let process = path: builtins.mapAttrs ( - key: value: + name: value: if builtins.isAttrs value && predicate value - then process (path ++ [key]) value - else f (path ++ [key]) value + then process (path ++ [name]) value + else f (path ++ [name]) value ); in process [] target; - # TODO: Document this. + ## Filter an attribute set by a given predicate. The filter is only performed + ## on the base level of the attribute set. + ## + ## @type (String -> Any -> Bool) -> Attrs -> Attrs filter = predicate: target: let - keys = builtins.attrNames target; - process = key: let - value = target.${key}; + names = builtins.attrNames target; + process = name: let + value = target.${name}; in - if predicate key value - then [ - { - name = key; - value = value; - } - ] + if predicate name value + then [{inherit name value;}] else []; - valid = builtins.concatMap process keys; + valid = builtins.concatMap process names; in builtins.listToAttrs valid; }; diff --git a/lib/src/attrs/default.test.nix b/lib/src/attrs/default.test.nix index 00be372..301d74d 100644 --- a/lib/src/attrs/default.test.nix +++ b/lib/src/attrs/default.test.nix @@ -1,6 +1,79 @@ let lib = import ./../default.nix; in { + "merge" = { + "merges two shallow sets" = let + expected = { + x = 1; + y = 2; + }; + + actual = lib.attrs.merge {x = 1;} {y = 2;}; + in + expected == actual; + + "overwrites values from the first set" = let + expected = { + x = 2; + }; + actual = lib.attrs.merge {x = 1;} {x = 2;}; + in + actual == expected; + + "does not merge nested sets" = let + expected = { + x.y = 2; + }; + actual = lib.attrs.merge {x.z = 1;} {x.y = 2;}; + in + actual == expected; + }; + + "mergeRecursiveUntil" = { + "merges with predicate" = let + expected = { + x.y.z = 1; + }; + actual = + lib.attrs.mergeRecursiveUntil + (path: x: y: lib.lists.last path == "z") + {x.y.z = 2;} + {x.y.z = 1;}; + in + actual == expected; + + "handles shallow merges" = let + expected = { + x.y.z = 1; + }; + actual = + lib.attrs.mergeRecursiveUntil + (path: x: y: true) + { + x = { + y.z = 2; + + a = false; + }; + } + {x.y.z = 1;}; + in + actual == expected; + }; + + "mergeRecursive" = { + "merges two sets deeply" = let + expected = { + x.y.z = 1; + }; + actual = + lib.attrs.mergeRecursive + {x.y.z = 2;} + {x.y.z = 1;}; + in + actual == expected; + }; + "select" = { "selects a nested value" = let expected = "value"; @@ -13,5 +86,244 @@ in { }; in actual == expected; + + "handles empty path" = let + expected = { + x = { + y = { + z = 1; + }; + }; + }; + actual = + lib.attrs.select + [] + null + { + x = { + y = { + z = 1; + }; + }; + }; + in + actual == expected; + + "handles fallback value" = let + expected = "fallback"; + actual = + lib.attrs.select + ["x" "y" "z"] + expected + {}; + in + actual == expected; + }; + + "selectOrThrow" = { + "selects a nested value" = let + expected = "value"; + actual = + lib.attrs.selectOrThrow + ["x" "y" "z"] + { + x.y.z = expected; + }; + in + actual == expected; + + "handles empty path" = let + expected = { + x = { + y = { + z = 1; + }; + }; + }; + actual = + lib.attrs.selectOrThrow + [] + { + x = { + y = { + z = 1; + }; + }; + }; + in + actual == expected; + + "throws on nonexistent path" = let + actual = + lib.attrs.selectOrThrow + ["x" "y" "z"] + {}; + + evaluated = builtins.tryEval (builtins.deepSeq actual actual); + in + !evaluated.success; + }; + + "set" = { + "creates a nested set" = let + expected = { + x = { + y = { + z = 1; + }; + }; + }; + actual = lib.attrs.set ["x" "y" "z"] 1; + in + actual == expected; + + "handles empty path" = let + expected = 1; + actual = lib.attrs.set [] 1; + in + actual == expected; + }; + + "has" = { + "returns true for a nested value" = let + exists = lib.attrs.has ["x" "y" "z"] {x.y.z = 1;}; + in + exists; + + "returns false for a nonexistent value" = let + exists = lib.attrs.has ["x" "y" "z"] {}; + in + !exists; + + "handles empty path" = let + exists = lib.attrs.has [] {}; + in + exists; + }; + + "when" = { + "returns the value when condition is true" = let + expected = "value"; + actual = lib.attrs.when true expected; + in + actual == expected; + + "returns an empty set when condition is false" = let + expected = {}; + actual = lib.attrs.when false "value"; + in + actual == expected; + }; + + "mapToList" = { + "converts a set to a list" = let + expected = [ + { + name = "x"; + value = 1; + } + { + name = "y"; + value = 2; + } + ]; + actual = + lib.attrs.mapToList + (name: value: {inherit name value;}) + { + x = 1; + y = 2; + }; + in + actual == expected; + }; + + "mapRecursiveWhen" = { + "maps a set recursively" = let + expected = { + x = { + y = { + z = 2; + }; + }; + }; + actual = + lib.attrs.mapRecursiveWhen + (value: true) + (path: value: value + 1) + { + x = { + y = { + z = 1; + }; + }; + }; + in + actual == expected; + + "maps a set given a condition" = let + expected = { + x = { + y = { + z = 1; + }; + }; + }; + actual = + lib.attrs.mapRecursiveWhen + (value: !(value ? z)) + (path: value: + # We map before we get to a non-set value + if builtins.isAttrs value + then value + else value + 1) + { + x = { + y = { + z = 1; + }; + }; + }; + in + actual == expected; + }; + + "mapRecursive" = { + "maps a set recursively" = let + expected = { + x = { + y = { + z = 2; + }; + }; + }; + actual = + lib.attrs.mapRecursive + (path: value: value + 1) + { + x = { + y = { + z = 1; + }; + }; + }; + in + actual == expected; + }; + + "filter" = { + "filters a set" = let + expected = { + y = 2; + }; + actual = + lib.attrs.filter + (name: value: name == "y") + { + x = 1; + y = 2; + }; + in + actual == expected; }; } diff --git a/lib/src/bools/default.nix b/lib/src/bools/default.nix index 8455249..f15cf71 100644 --- a/lib/src/bools/default.nix +++ b/lib/src/bools/default.nix @@ -1,10 +1,21 @@ lib: { bools = { into = { + ## Convert a boolean value into a string. + ## + ## @type Bool -> String string = value: if value then "true" else "false"; + + ## Convert a boolean into either the string "yes" or "no". + ## + ## @type Bool -> String + yesno = value: + if value + then "yes" + else "no"; }; ## Choose between two values based on a condition. When true, the first value diff --git a/lib/src/bools/default.test.nix b/lib/src/bools/default.test.nix new file mode 100644 index 0000000..89f2313 --- /dev/null +++ b/lib/src/bools/default.test.nix @@ -0,0 +1,161 @@ +let + lib = import ./../default.nix; +in { + "into" = { + "string" = { + "handles true" = let + expected = "true"; + actual = lib.bools.into.string true; + in + actual == expected; + + "handles false" = let + expected = "false"; + actual = lib.bools.into.string false; + in + actual == expected; + }; + + "yesno" = { + "handles true" = let + expected = "yes"; + actual = lib.bools.into.yesno true; + in + actual == expected; + "handles false" = let + expected = "no"; + actual = lib.bools.into.yesno false; + in + actual == expected; + }; + }; + + "when" = { + "returns first value when true" = let + expected = "foo"; + actual = lib.bools.when true expected "bar"; + in + actual == expected; + + "returns second value when false" = let + expected = "bar"; + actual = lib.bools.when false "foo" expected; + in + actual == expected; + }; + + "and" = { + "returns true when both are true" = let + expected = true; + actual = lib.bools.and true true; + in + actual == expected; + "returns false when first is false" = let + expected = false; + actual = lib.bools.and false true; + in + actual == expected; + "returns false when second is false" = let + expected = false; + actual = lib.bools.and true false; + in + actual == expected; + "returns false when both are false" = let + expected = false; + actual = lib.bools.and false false; + in + actual == expected; + }; + + "and'" = let + getTrue = _: true; + getFalse = _: false; + in { + "returns true when both are true" = let + expected = true; + actual = lib.bools.and' getTrue getTrue null; + in + actual == expected; + + "returns false when first is false" = let + expected = false; + actual = lib.bools.and' getFalse getTrue null; + in + actual == expected; + + "returns false when second is false" = let + expected = false; + actual = lib.bools.and' getTrue getFalse null; + in + actual == expected; + + "returns false when both are false" = let + expected = false; + actual = lib.bools.and' getFalse getFalse null; + in + actual == expected; + }; + + "or" = { + "returns true when both are true" = let + expected = true; + actual = lib.bools.or true true; + in + actual == expected; + "returns true when first is true" = let + expected = true; + actual = lib.bools.or true false; + in + actual == expected; + "returns true when second is true" = let + expected = true; + actual = lib.bools.or false true; + in + actual == expected; + "returns false when both are false" = let + expected = false; + actual = lib.bools.or false false; + in + actual == expected; + }; + + "or'" = let + getTrue = _: true; + getFalse = _: false; + in { + "returns true when both are true" = let + expected = true; + actual = lib.bools.or' getTrue getTrue null; + in + actual == expected; + "returns true when first is true" = let + expected = true; + actual = lib.bools.or' getTrue getFalse null; + in + actual == expected; + "returns true when second is true" = let + expected = true; + actual = lib.bools.or' getFalse getTrue null; + in + actual == expected; + "returns false when both are false" = let + expected = false; + actual = lib.bools.or' getFalse getFalse null; + in + actual == expected; + }; + + "not" = { + "returns false when true" = let + expected = false; + actual = lib.bools.not true; + in + actual == expected; + + "returns true when false" = let + expected = true; + actual = lib.bools.not false; + in + actual == expected; + }; +} diff --git a/lib/src/default.nix b/lib/src/default.nix index 36e696f..33eb8e4 100644 --- a/lib/src/default.nix +++ b/lib/src/default.nix @@ -40,8 +40,8 @@ let mergeAttrsRecursiveUntil = predicate: x: y: let process = path: builtins.zipAttrsWith ( - key: values: let - currentPath = path ++ [key]; + name: values: let + currentPath = path ++ [name]; isSingleValue = builtins.length values == 1; isComplete = predicate currentPath diff --git a/lib/src/errors/default.nix b/lib/src/errors/default.nix index d024b63..9522f78 100644 --- a/lib/src/errors/default.nix +++ b/lib/src/errors/default.nix @@ -1,5 +1,10 @@ lib: { errors = { + ## Prints a message if the condition is not met. The result of + ## the condition is returned. + ## + ## @notest + ## @type Bool -> String -> Bool trace = condition: message: if condition then true diff --git a/lib/src/errors/default.test.nix b/lib/src/errors/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/errors/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/fp/default.nix b/lib/src/fp/default.nix index 57fd5a2..152160c 100644 --- a/lib/src/fp/default.nix +++ b/lib/src/fp/default.nix @@ -21,7 +21,7 @@ lib: { ## ## @type (List (Any -> Any)) -> Any -> Any pipe = fs: ( - x: builtins.foldl' (value: f: f x) x fs + x: builtins.foldl' (value: f: f value) x fs ); ## Reverse the order of arguments to a function that has two parameters. @@ -37,13 +37,20 @@ lib: { ## @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. + ## Get the arguments of a function or functor. + ## An attribute set is returned with the arguments as keys. The values + ## are `true` when the argument has a default value specified and `false` + ## when it does not. + ## + ## @type Function -> Attrs args = f: if f ? __functor - then f.__args__ or lib.fp.args (f.__functor f) + then f.__args__ or (lib.fp.args (f.__functor f)) else builtins.functionArgs f; - # TODO: Document this. + ## Create a function that is called with only the arguments it specifies. + ## + ## @type Attrs a => (a -> b) -> a -> b withDynamicArgs = f: args: let fArgs = lib.fp.args f; common = builtins.intersectAttrs fArgs args; diff --git a/lib/src/fp/default.test.nix b/lib/src/fp/default.test.nix new file mode 100644 index 0000000..d1d352d --- /dev/null +++ b/lib/src/fp/default.test.nix @@ -0,0 +1,139 @@ +let + lib = import ./../default.nix; +in { + "id" = { + "returns its argument" = let + expected = "foo"; + actual = lib.fp.id expected; + in + actual == expected; + }; + + "const" = { + "creates a function that returns its argument" = let + expected = "foo"; + actual = lib.fp.const expected "bar"; + in + actual == expected; + }; + + "compose" = { + "composes two functions" = let + f = x: x + 1; + g = x: x * 2; + expected = 5; + actual = lib.fp.compose f g 2; + in + actual == expected; + }; + + "pipe" = { + "pipes two functions" = let + f = x: x + 1; + g = x: x * 2; + expected = 5; + actual = lib.fp.pipe [g f] 2; + in + actual == expected; + }; + + "flip2" = { + "flips the arguments of a binary function" = let + f = a: b: a - b; + expected = 1; + actual = lib.fp.flip2 f 1 2; + in + actual == expected; + }; + + "flip3" = { + "flips the arguments of a ternary function" = let + f = a: b: c: a - b - c; + expected = 0; + actual = lib.fp.flip3 f 1 2 3; + in + actual == expected; + }; + + "flip4" = { + "flips the arguments of a quaternary function" = let + f = a: b: c: d: a - b - c - d; + expected = -2; + actual = lib.fp.flip4 f 1 2 3 4; + in + actual == expected; + }; + + "args" = { + "gets a functions attr set arguments" = let + expected = { + x = false; + y = true; + }; + actual = lib.fp.args ({ + x, + y ? null, + }: + null); + in + actual == expected; + + "returns an empty set if the function has no attrs arguments" = let + expected = {}; + actual = lib.fp.args (args: null); + in + actual == expected; + + "supports functors" = let + expected = { + x = false; + y = true; + }; + + actual = lib.fp.args { + __functor = self: { + x, + y ? null, + }: + null; + }; + in + actual == expected; + + "supports cached functor arguments" = let + expected = { + x = false; + y = true; + }; + actual = lib.fp.args { + __args__ = { + x = false; + y = true; + }; + __functor = self: args: + null; + }; + in + actual == expected; + }; + + "withDynamicArgs" = { + "applies a function with dynamic arguments" = let + expected = {x = true;}; + actual = lib.fp.withDynamicArgs (args @ {x}: args) { + x = true; + y = true; + }; + in + actual == expected; + + "applies all arguments if none are specified" = let + expected = { + x = true; + y = true; + }; + actual = lib.fp.withDynamicArgs (args: args) expected; + in + actual == expected; + }; +} diff --git a/lib/src/generators/default.test.nix b/lib/src/generators/default.test.nix new file mode 100644 index 0000000..2ef515e --- /dev/null +++ b/lib/src/generators/default.test.nix @@ -0,0 +1,113 @@ +let + lib = import ./../default.nix; +in { + "withRecursion" = { + "evaluates within a given limit" = let + expected = { + x = 1; + }; + actual = lib.generators.withRecursion {limit = 100;} expected; + in + expected == actual; + + "fails when the limit is reached" = let + expected = { + x = 1; + }; + actual = lib.generators.withRecursion {limit = -1;} expected; + evaluated = builtins.tryEval (builtins.deepSeq actual actual); + in + !evaluated.success; + + "does not fail when throw is disabled" = let + expected = { + x = ""; + }; + actual = + lib.generators.withRecursion { + limit = -1; + throw = false; + } + {x = 1;}; + evaluated = builtins.tryEval (builtins.deepSeq actual actual); + in + evaluated.success + && evaluated.value == expected; + }; + + "pretty" = { + "formats with defaults" = let + expected = '' + { + attrs = { }; + bool = true; + float = 0.0; + function = ; + int = 0; + list = [ ]; + string = "string"; + }''; + actual = lib.generators.pretty {} { + attrs = {}; + bool = true; + float = 0.0; + function = x: x; + int = 0; + list = []; + string = "string"; + # NOTE: We are not testing `path` types because they can return out of store + # values which are not deterministic. + # path = ./.; + }; + in + actual == expected; + + "formats with custom prettifiers" = let + expected = '' + { + attrs = { }; + bool = true; + custom = ; + float = 0.0; + function = ; + int = 0; + list = [ ]; + string = "string"; + }''; + actual = + lib.generators.pretty { + allowCustomPrettifiers = true; + } { + attrs = {}; + bool = true; + float = 0.0; + function = x: x; + int = 0; + list = []; + string = "string"; + custom = { + value = 0; + __pretty__ = value: ""; + }; + }; + in + actual == expected; + + "formats with multiline disabled" = let + expected = "{ attrs = { }; bool = true; float = 0.0; function = ; int = 0; list = [ ]; string = \"string\"; }"; + actual = + lib.generators.pretty { + multiline = false; + } { + attrs = {}; + bool = true; + float = 0.0; + function = x: x; + int = 0; + list = []; + string = "string"; + }; + in + actual == expected; + }; +} diff --git a/lib/src/importers/default.nix b/lib/src/importers/default.nix index 833372d..8fe0967 100644 --- a/lib/src/importers/default.nix +++ b/lib/src/importers/default.nix @@ -2,11 +2,13 @@ lib: { importers = { ## Import a JSON file as a Nix value. ## + ## @notest ## @type Path -> a json = file: builtins.fromJSON (builtins.readFile file); ## Import a TOML file as a Nix value. ## + ## @notest ## @type Path -> a toml = file: builtins.fromTOML (builtins.readFile file); }; diff --git a/lib/src/importers/default.test.nix b/lib/src/importers/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/importers/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/lists/default.nix b/lib/src/lists/default.nix index 8a0f696..1d86b15 100644 --- a/lib/src/lists/default.nix +++ b/lib/src/lists/default.nix @@ -1,7 +1,11 @@ lib: { lists = { from = { - # TODO: Document this. + ## Convert a value to a list. If the value is already a list, + ## it will be returned as-is. If the value is not a list, it + ## will be wrapped in a list. + ## + ## @type a | (List a) -> List a any = value: if builtins.isList value then value @@ -11,7 +15,7 @@ lib: { sort = { ## Perform a natural sort on a list of strings. ## - ## @type List -> List + ## @type List String -> List String natural = list: let vectorize = string: let serialize = part: @@ -27,21 +31,29 @@ lib: { builtins.map (x: builtins.elemAt x 1) (builtins.sort isLess prepared); }; - # TODO: Document this. + ## Map a list using both the index and value of each item. The + ## index starts at 0. + ## + ## @type (Int -> a -> b) -> List a -> List b mapWithIndex = f: list: builtins.genList (i: f i (builtins.elemAt list i)) (builtins.length list); - # TODO: Document this. + ## Map a list using both the index and value of each item. The + ## index starts at 1. + ## + ## @type (Int -> a -> b) -> List a -> List b mapWithIndex1 = f: list: builtins.genList (i: f (i + 1) (builtins.elemAt list i)) (builtins.length list); - ## Compare two lists. + ## Compare two lists using a custom compare function. The compare + ## function is called for each element in the lists that need to + ## be compared. ## - ## @type (a -> b -> Int) -> List a -> List b -> Int + ## @type (a -> b -> -1 | 0 | 1) -> List a -> List b -> Int compare = compare: a: b: let result = compare (builtins.head a) (builtins.head b); in @@ -60,7 +72,7 @@ lib: { ## ## @type List a -> a last = list: - assert lib.assertMsg (list != []) "List cannot be empty"; + assert lib.errors.trace (list != []) "List cannot be empty"; builtins.elemAt list (builtins.length list - 1); ## Slice part of a list to create a new list. @@ -137,7 +149,9 @@ lib: { else [value] else []; - # TODO: Document this. + ## Count the number of items in a list that satisfy a given predicate. + ## + ## @type (a -> Bool) -> List a -> Int count = predicate: list: builtins.foldl' ( total: value: @@ -148,7 +162,9 @@ lib: { 0 list; - # TODO: Document this. + ## Remove duplicate items from a list. + ## + ## @type List -> List unique = list: let filter = result: value: if builtins.elem value result diff --git a/lib/src/lists/default.test.nix b/lib/src/lists/default.test.nix new file mode 100644 index 0000000..a4460e4 --- /dev/null +++ b/lib/src/lists/default.test.nix @@ -0,0 +1,174 @@ +let + lib = import ./../default.nix; +in { + "from" = { + "any" = { + "returns a list containing the value" = let + expected = [1]; + actual = lib.lists.from.any 1; + in + actual == expected; + + "returns the value if the value was already a list" = let + expected = [1]; + actual = lib.lists.from.any expected; + in + actual == expected; + }; + }; + + "sort" = { + "natural" = { + "sorts a list of strings" = let + expected = ["1" "a" "a0" "a1" "b" "c"]; + actual = lib.lists.sort.natural ["c" "a" "b" "a1" "a0" "1"]; + in + actual == expected; + }; + }; + + "mapWithIndex" = { + "maps a list using index 0" = let + expected = ["0: a" "1: b" "2: c"]; + actual = lib.lists.mapWithIndex (i: v: "${builtins.toString i}: ${v}") ["a" "b" "c"]; + in + actual == expected; + }; + + "mapWithIndex1" = { + "maps a list using index 1" = let + expected = ["1: a" "2: b" "3: c"]; + actual = lib.lists.mapWithIndex1 (i: v: "${builtins.toString i}: ${v}") ["a" "b" "c"]; + in + actual == expected; + }; + + "compare" = { + "compares two lists" = { + "returns -1 if the first list is smaller" = let + expected = -1; + actual = lib.lists.compare lib.numbers.compare [1 2 3] [1 2 4]; + in + actual == expected; + "returns 1 if the first list is larger" = let + expected = 1; + actual = lib.lists.compare lib.numbers.compare [1 2 4] [1 2 3]; + in + actual == expected; + "returns 0 if the lists are equal" = let + expected = 0; + actual = lib.lists.compare lib.numbers.compare [1 2 3] [1 2 3]; + in + actual == expected; + }; + }; + + "last" = { + "returns the last element of a list" = let + expected = 3; + actual = lib.lists.last [1 2 3]; + in + actual == expected; + + "fails if the list is empty" = let + actual = lib.lists.last []; + evaluated = builtins.tryEval actual; + in + !evaluated.success; + }; + + "slice" = { + "slices a list" = { + "slices a list from the start" = let + expected = [1 2]; + actual = lib.lists.slice 0 2 [1 2 3]; + in + actual == expected; + "slices a list from the end" = let + expected = [2 3]; + actual = lib.lists.slice 1 3 [1 2 3]; + in + actual == expected; + "slices a list from the middle" = let + expected = [2]; + actual = lib.lists.slice 1 1 [1 2 3]; + in + actual == expected; + }; + }; + + "take" = { + "takes the first n elements" = let + expected = [1 2]; + actual = lib.lists.take 2 [1 2 3]; + in + actual == expected; + }; + + "drop" = { + "drops the first n elements" = let + expected = [3]; + actual = lib.lists.drop 2 [1 2 3]; + in + actual == expected; + }; + + "reverse" = { + "reverses a list" = let + expected = [3 2 1]; + actual = lib.lists.reverse [1 2 3]; + in + actual == expected; + }; + + "intersperse" = { + "intersperses a list with a separator" = let + expected = [1 "-" 2 "-" 3]; + actual = lib.lists.intersperse "-" [1 2 3]; + in + actual == expected; + + "handles lists with less than 2 elements" = let + expected = [1]; + actual = lib.lists.intersperse "-" [1]; + in + actual == expected; + }; + + "range" = { + "returns a range of numbers" = let + expected = [1 2 3 4 5]; + actual = lib.lists.range 1 5; + in + actual == expected; + }; + + "when" = { + "returns the list if the condition is true" = let + expected = [1 2 3]; + actual = lib.lists.when true [1 2 3]; + in + actual == expected; + "returns an empty list if the condition is false" = let + expected = []; + actual = lib.lists.when false [1 2 3]; + in + actual == expected; + }; + + "count" = { + "counts the number of elements in a list" = let + expected = 2; + actual = lib.lists.count (value: value < 3) [1 2 3]; + in + actual == expected; + }; + + "unique" = { + "removes duplicate elements" = let + expected = [1 2 3]; + actual = lib.lists.unique [1 2 3 1 2 3]; + in + actual == expected; + }; +} diff --git a/lib/src/math/default.nix b/lib/src/math/default.nix index c7486e3..45331a3 100644 --- a/lib/src/math/default.nix +++ b/lib/src/math/default.nix @@ -1,12 +1,16 @@ lib: { math = { - # TODO: Document this. + ## Return the smaller of two numbers. + ## + ## @type Int -> Int -> Int min = x: y: if x < y then x else y; - # TODO: Document this. + ## Return the larger of two numbers. + ## + ## @type Int -> Int -> Int max = x: y: if x > y then x diff --git a/lib/src/math/default.test.nix b/lib/src/math/default.test.nix new file mode 100644 index 0000000..c9aa0cd --- /dev/null +++ b/lib/src/math/default.test.nix @@ -0,0 +1,19 @@ +let + lib = import ./../default.nix; +in { + "min" = { + "returns the smaller number" = let + expected = 1; + actual = lib.math.min 1 2; + in + actual == expected; + }; + + "max" = { + "returns the larger number" = let + expected = 2; + actual = lib.math.max 1 2; + in + actual == expected; + }; +} diff --git a/lib/src/modules/default.nix b/lib/src/modules/default.nix index b956596..ffe807a 100644 --- a/lib/src/modules/default.nix +++ b/lib/src/modules/default.nix @@ -3,6 +3,7 @@ lib: { from = { ## Create a module from a JSON file. ## + ## @notest ## @type Path -> Module json = file: { __file__ = file; @@ -11,6 +12,7 @@ lib: { ## Create a module from a TOML file. ## + ## @notest ## @type Path -> Module toml = file: { __file__ = file; @@ -19,7 +21,12 @@ lib: { }; apply = { - # TODO: Document this. + ## Apply custom definitions such as `merge` and `when` to a definition. + ## Note that this function does not perform actiosn like `merge`, but + ## instead pulls out the merge contents to be processed by the module + ## system. + ## + ## @type Definition -> List (Definition | (List Definition)) properties = definition: if lib.types.is "merge" definition then builtins.concatMap lib.modules.apply.properties definition.content @@ -32,7 +39,11 @@ lib: { else [] else [definition]; - # TODO: Document this. + ## Apply overrides for a definition. This uses the priority system + ## to determine which definition to use. The most important (lowest + ## priority) choice will be used. + ## + ## @type List Definition -> { highestPriority :: Int, values :: List (Definition | (List Definition)) } overrides = definitions: let getPriority = definition: if lib.types.is "override" definition.value @@ -64,7 +75,9 @@ lib: { definitions; }; - # TODO: Document this. + ## Apply ordering for prioritized definitions. + ## + ## @type List Definition -> List Definition order = definitions: let normalize = definition: if lib.types.is "order" definition @@ -80,7 +93,10 @@ lib: { in builtins.sort compare normalized; - # TODO: Document this. + ## Normalize the type of an option. This will set a default type if none + ## was provided. + ## + ## @type List String -> Option -> Option fixup = location: option: if option.type.getSubModules or null == null then @@ -95,32 +111,43 @@ lib: { options = []; }; - # TODO: Document this. + ## Invert the structure of `merge`, `when`, and `override` definitions so + ## that they apply to each individual attribute in their respective sets. + ## Note that this function _only_ supports attribute sets within specialized + ## definitions such as `when` and `override`. Other values like lists will + ## throw a type error. + ## + ## @type Definition -> List Definition invert = config: 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)) + (builtins.mapAttrs (name: 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)) + (builtins.mapAttrs (name: value: lib.modules.override config.priority value)) (lib.modules.apply.invert config.content) else [config]; }; validate = { - # TODO: Document this. + ## Check that a module only specifies supported attributes. + ## + ## @type Attrs -> Bool keys = module: let invalid = builtins.removeAttrs module lib.modules.VALID_KEYS; in invalid == {}; }; - # TODO: Document this. + ## Modules only support certain keys at the root level. This list determines + ## the valid attributes that users can supply. + ## + ## @type List String VALID_KEYS = [ "__file__" "__key__" @@ -132,7 +159,10 @@ lib: { "meta" ]; - # TODO: Document this. + ## Normalize a module to a standard structure. All other information will be + ## lost in the conversion. + ## + ## @type String -> String -> Attrs -> Module normalize = file: key: module: let invalid = builtins.removeAttrs module lib.modules.VALID_KEYS; invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid); @@ -159,7 +189,11 @@ lib: { } else builtins.throw "Module `${key}` has unsupported attribute(s): ${invalidKeys}"; - # TODO: Document this. + ## Convert a module that is either a function or an attribute set into + ## a resolved attribute set. If the module was a function then it will + ## be evaluated and the result will be returned. + ## + ## @type String -> Attrs | (Attrs -> Attrs) -> Attrs -> Attrs resolve = key: module: args: let dynamicArgs = builtins.mapAttrs @@ -175,24 +209,38 @@ lib: { then lib.fp.withDynamicArgs module (args // dynamicArgs) else module; - # TODO: Document this. + ## The default priority to set for values that do not have one provided. + ## + ## @type Int 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 + ## @notest + ## @type Int -> a -> Attrs order = priority: value: { __type__ = "order"; inherit priority value; }; orders = { - # TODO: Document this. + ## Order a value before others. + ## + ## @notest + ## @type a -> Attrs before = lib.modules.order 500; - # TODO: Document this. + + ## Use the default ordering for a value. + ## + ## @notest + ## @type a -> Attrs default = lib.modules.order 1000; - # TODO: Document this. + + ## Order a value after others. + ## + ## @notest + ## @type a -> Attrs after = lib.modules.order 1500; }; @@ -201,36 +249,70 @@ lib: { ## @type List Module -> List (String | Path) getFiles = builtins.map (module: module.__file__); - # TODO: Document this. + ## Create a conditional value which is only used when the condition's + ## value is `true`. + ## + ## @notest + ## @type Bool -> a -> When a when = condition: content: { __type__ = "when"; inherit condition content; }; - # TODO: Document this. + ## Merge module attribute sets when evaluated in the module system. + ## + ## @notest + ## @type List Attrs -> Attrs merge = content: { __type__ = "merge"; inherit content; }; - # TODO: Document this. + ## Create a value override which can replace other definitions if it + ## has the higher priority. + ## + ## @notest + ## @type Int -> a -> Attrs override = priority: content: { __type__ = "override"; inherit priority content; }; overrides = { - # TODO: Document this. + ## Create an override used for setting the `default.value` from an + ## option. This uses the lowest priority of all predefined overrides. + ## + ## @notest + ## @type a -> Attrs option = lib.modules.override 1500; - # TODO: Document this. + + ## Create a default override for a value. This uses a very low priority + ## so that it can easily be overridden. + ## + ## @notest + ## @type a -> Attrs default = lib.modules.override 1000; - # TODO: Document this. + + ## Create a high priority override for a value. This is not the highest + ## priority possible, but it will override nearly everything else except + ## for very explicit cases. + ## + ## @notest + ## @type a -> Attrs force = lib.modules.override 50; - # TODO: Document this. + + ## Create a high priority override intended to be used only for VM targets. + ## This allows for forcing certain values even if a user has otherwise + ## specified `lib.modules.overrides.force`. + ## + ## @notest + ## @type a -> Attrs vm = lib.modules.override 10; }; - # TODO: Document this. + ## Combine multiple modules together. + ## + ## @type List String -> List Module -> { matched :: Attrs, unmatched :: List Definition } combine = prefix: modules: let getConfig = module: builtins.map @@ -246,16 +328,15 @@ lib: { modules; process = prefix: options: configs: let - # TODO: Document this. byName = attr: f: modules: builtins.zipAttrsWith - (lib.fp.const builtins.concatLists) + (key: value: builtins.concatLists value) (builtins.map ( module: let subtree = module.${attr}; in if builtins.isAttrs subtree - then builtins.mapAttrs (key: f module) subtree + then builtins.mapAttrs (name: f module) subtree else builtins.throw "Value for `${builtins.concatStringsSep "." prefix} is of type `${builtins.typeOf subtree}` but an attribute set was expected." ) modules); @@ -348,7 +429,7 @@ lib: { matched = builtins.mapAttrs (key: value: value.matched) resultsByName; unmatched = - builtins.mapAttrs (key: value: value.unmatched) resultsByName + builtins.mapAttrs (name: value: value.unmatched) resultsByName // builtins.removeAttrs definitionsByName' (builtins.attrNames matched); in { inherit matched; @@ -374,7 +455,10 @@ lib: { in process prefix modules configs; - # TODO: Document this. + ## Run a set of modules. Custom arguments can also be supplied which will + ## be provided to all modules statically as they are not modifiable. + ## + ## @type { modules? :: List (Attrs | (Attrs -> Attrs)), args? :: Attrs, prefix? :: List String } -> { type :: Module, extend :: lib.modules.run, options :: Attrs, config :: Attrs, __module__ :: Attrs } run = settings @ { modules ? [], args ? {}, @@ -395,7 +479,6 @@ lib: { 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); @@ -532,7 +615,7 @@ lib: { declared = lib.attrs.mapRecursiveWhen (value: !(lib.types.is "option" value)) - (key: value: value.value) + (name: value: value.value) options; freeform = let diff --git a/lib/src/modules/default.test.nix b/lib/src/modules/default.test.nix index 6ce3c10..b92d74b 100644 --- a/lib/src/modules/default.test.nix +++ b/lib/src/modules/default.test.nix @@ -1,7 +1,315 @@ let lib = import ./../default.nix; in { - examples = { + "apply" = { + "properties" = { + "handles normal values" = let + expected = [{}]; + actual = lib.modules.apply.properties {}; + in + actual == expected; + + "handles merge" = let + expected = [{x = 1;} {x = 2;}]; + actual = lib.modules.apply.properties (lib.modules.merge [{x = 1;} {x = 2;}]); + in + actual + == expected; + + "handles when" = let + expected = [[{x = 1;}]]; + actual = lib.modules.apply.properties (lib.modules.when true [{x = 1;}]); + in + actual == expected; + }; + + "overrides" = { + "handles normal values" = let + expected = { + highestPriority = 100; + values = [ + { + value = 1; + } + ]; + }; + actual = lib.modules.apply.overrides [ + { + value = 1; + } + ]; + in + actual == expected; + + "handles overrides" = let + expected = { + highestPriority = 100; + values = [ + {value = "world";} + ]; + }; + actual = lib.modules.apply.overrides [ + {value = "world";} + {value = lib.modules.override 101 "hello";} + ]; + in + actual == expected; + }; + + "order" = { + "handles normal values" = let + expected = [{}]; + actual = lib.modules.apply.order [{}]; + in + actual == expected; + + "handles priority" = let + expected = [ + { + value = 1; + priority = 10; + } + { + value = 3; + priority = 50; + } + {value = 2;} + ]; + actual = lib.modules.apply.order [ + { + value = 1; + priority = 10; + } + {value = 2;} + { + value = 3; + priority = 50; + } + ]; + in + actual == expected; + }; + + "fixup" = { + "sets default type for option" = let + actual = lib.modules.apply.fixup [] ( + lib.options.create {} + ); + in + actual.type.name == "Unspecified"; + }; + + "invert" = { + "inverts merge" = let + expected = [{x = 1;} {x = 2;}]; + actual = + lib.modules.apply.invert (lib.modules.merge [{x = 1;} {x = 2;}]); + in + actual + == expected; + + "inverts when" = let + expected = [ + { + x = { + __type__ = "when"; + condition = true; + content = 1; + }; + y = { + __type__ = "when"; + condition = true; + content = 2; + }; + } + ]; + actual = lib.modules.apply.invert (lib.modules.when true { + x = 1; + y = 2; + }); + in + actual == expected; + + "inverts overrides" = let + expected = [ + { + x = { + __type__ = "override"; + priority = 100; + content = 1; + }; + y = { + __type__ = "override"; + priority = 100; + content = 2; + }; + } + ]; + actual = lib.modules.apply.invert (lib.modules.override 100 { + x = 1; + y = 2; + }); + in + actual == expected; + }; + }; + + "validate" = { + "keys" = { + "handles an empty set" = let + value = lib.modules.validate.keys {}; + in + value; + + "handles a valid module" = let + value = lib.modules.validate.keys { + __file__ = "virtual:aux/example"; + __key__ = "aux/example"; + includes = []; + excludes = []; + options = {}; + config = {}; + freeform = null; + meta = {}; + }; + in + value; + + "handles an invalid module" = let + value = lib.modules.validate.keys { + invalid = null; + }; + in + !value; + }; + }; + + "normalize" = { + "handles an empty set" = let + expected = { + __file__ = "/aux/example.nix"; + __key__ = "example"; + config = {}; + excludes = []; + includes = []; + options = {}; + }; + actual = lib.modules.normalize "/aux/example.nix" "example" {}; + in + actual == expected; + + "handles an example module" = let + expected = { + __file__ = "myfile.nix"; + __key__ = "mykey"; + config = { + x = true; + }; + excludes = []; + includes = []; + options = {}; + }; + actual = lib.modules.normalize "/aux/example.nix" "example" { + __file__ = "myfile.nix"; + __key__ = "mykey"; + config.x = true; + }; + in + actual == expected; + }; + + "resolve" = { + "handles an attribute set" = let + expected = {config.x = 1;}; + actual = lib.modules.resolve "example" {config.x = 1;} {}; + in + actual == expected; + + "handles a function" = let + expected = {config.x = 1;}; + actual = lib.modules.resolve "example" (lib.fp.const {config.x = 1;}) {}; + in + actual == expected; + + "handles a function with arguments" = let + expected = {config.x = 1;}; + actual = lib.modules.resolve "example" (args: {config.x = args.x;}) {x = 1;}; + in + actual == expected; + }; + + "getFiles" = { + "gets the files for a list of modules" = let + expected = ["/aux/example.nix"]; + actual = lib.modules.getFiles [{__file__ = "/aux/example.nix";}]; + in + actual + == expected; + }; + + "combine" = { + "handles empty modules" = let + expected = { + matched = {}; + unmatched = []; + }; + actual = lib.modules.combine [] [ + (lib.modules.normalize "/aux/example.nix" "example" {}) + ]; + in + actual == expected; + + "handles a single module" = let + expected = { + matched = {}; + unmatched = [ + { + __file__ = "/aux/example.nix"; + prefix = ["x"]; + value = 1; + } + ]; + }; + actual = lib.modules.combine [] [ + (lib.modules.normalize "/aux/example.nix" "example" { + config = { + x = 1; + }; + }) + ]; + in + actual == expected; + + "handles multiple modules" = let + unmatched = [ + { + __file__ = "/aux/example2.nix"; + prefix = ["y"]; + value = 2; + } + ]; + actual = lib.modules.combine [] [ + (lib.modules.normalize "/aux/example1.nix" "example2" { + options = { + x = lib.options.create {}; + }; + + config = { + x = 1; + }; + }) + (lib.modules.normalize "/aux/example2.nix" "example2" { + config = { + y = 2; + }; + }) + ]; + in + (actual.unmatched == unmatched) + && actual.matched ? x; + }; + + "run" = { "empty" = let evaluated = lib.modules.run { modules = [ @@ -73,5 +381,47 @@ in { actual = evaluated.config.aux.message; in actual == expected; + + "conditional" = let + expected = "Hello, World!"; + evaluated = lib.modules.run { + modules = [ + { + options.aux = { + message = lib.options.create { + type = lib.types.string; + }; + }; + config = { + aux = { + message = lib.modules.when true expected; + }; + }; + } + ]; + }; + in + evaluated.config.aux.message == expected; + + "conditional list" = let + expected = ["Hello, World!"]; + evaluated = lib.modules.run { + modules = [ + { + options.aux = { + message = lib.options.create { + type = lib.types.list.of lib.types.string; + }; + }; + config = { + aux = { + message = lib.modules.when true expected; + }; + }; + } + ]; + }; + in + evaluated.config.aux.message == expected; }; } diff --git a/lib/src/numbers/default.nix b/lib/src/numbers/default.nix index 84f6965..7921cd1 100644 --- a/lib/src/numbers/default.nix +++ b/lib/src/numbers/default.nix @@ -49,7 +49,7 @@ lib: { then "F" else builtins.throw "Invalid hex digit."; in - lib.strings.concatMapSep + lib.strings.concatMap serialize (lib.numbers.into.base 16 value); }; diff --git a/lib/src/numbers/default.test.nix b/lib/src/numbers/default.test.nix new file mode 100644 index 0000000..45ba2b8 --- /dev/null +++ b/lib/src/numbers/default.test.nix @@ -0,0 +1,57 @@ +let + lib = import ./../default.nix; +in { + "into" = { + "string" = { + "converts an int into a string" = let + expected = "1"; + actual = lib.numbers.into.string 1; + in + actual == expected; + + "converts a float into a string" = let + expected = "1.0"; + actual = lib.numbers.into.string 1.0; + in + actual == expected; + }; + + "base" = { + "converts a number into a given base" = let + expected = [1 0 0]; + actual = lib.numbers.into.base 2 4; + in + actual == expected; + }; + + "hex" = { + "converts a number into a hex string" = let + expected = "64"; + actual = lib.numbers.into.hex 100; + in + (builtins.trace actual) + actual + == expected; + }; + }; + + "compare" = { + "returns -1 when first is less than second" = let + expected = -1; + actual = lib.numbers.compare 1 2; + in + actual == expected; + + "returns 0 when first is equal to second" = let + expected = 0; + actual = lib.numbers.compare 1 1; + in + actual == expected; + + "returns 1 when first is greater than second" = let + expected = 1; + actual = lib.numbers.compare 2 1; + in + actual == expected; + }; +} diff --git a/lib/src/options/default.nix b/lib/src/options/default.nix index 7d56b8d..c6fdd48 100644 --- a/lib/src/options/default.nix +++ b/lib/src/options/default.nix @@ -33,7 +33,9 @@ lib: { # TODO: Improve this error message to show the location and definitions for the option. else builtins.throw "Cannot merge definitions."; - # TODO: Document this. + ## Merge multiple option definitions together. + ## + ## @type Location -> Type -> List Definition definitions = location: type: definitions: let identifier = lib.options.getIdentifier location; resolve = definition: let @@ -81,11 +83,14 @@ lib: { }; }; + ## Merge multiple option declarations together. + ## + ## @type Location -> List Option 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}; + shared = name: option.options ? ${name} && result ? ${name}; typeSet = lib.attrs.when ((shared "type") && isTypeMergeable) { type = mergedType; }; @@ -212,9 +217,12 @@ lib: { ## @type List String -> String getIdentifier = location: let special = [ - "" # attrsOf (submodule {}) - "*" # listOf (submodule {}) - "" # functionTo + # lib.types.attrs.of (lib.types.submodule {}) + "" + # lib.types.list.of (submodule {}) + "*" + # lib.types.function + "" ]; escape = part: if builtins.elem part special @@ -258,7 +266,9 @@ lib: { in lib.strings.concatMap serialize definitions; - # TODO: Document this. + ## Run a set of definitions, calculating the resolved value and associated information. + ## + ## @type Location -> Option -> List Definition -> String & { value :: Any, highestPriority :: Int, isDefined :: Bool, files :: List String, definitions :: List Any, definitionsWithLocations :: List Definition } run = location: option: definitions: let identifier = lib.options.getIdentifier location; diff --git a/lib/src/options/default.test.nix b/lib/src/options/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/options/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/packages/default.nix b/lib/src/packages/default.nix index 94ee27a..da3a089 100644 --- a/lib/src/packages/default.nix +++ b/lib/src/packages/default.nix @@ -12,7 +12,7 @@ lib: { if builtins.stringLength value <= 207 && validate value != null then builtins.unsafeDiscardStringContext value else - lib.fp.pipe value [ + lib.fp.pipe [ # Get rid of string context. This is safe under the assumption that the # resulting string is only used as a derivation name builtins.unsafeDiscardStringContext @@ -36,6 +36,7 @@ lib: { if builtins.stringLength x == 0 then "unknown" else x) - ]; + ] + value; }; } diff --git a/lib/src/packages/default.test.nix b/lib/src/packages/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/packages/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/paths/default.test.nix b/lib/src/paths/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/paths/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/points/default.nix b/lib/src/points/default.nix index f51520b..153fc4b 100644 --- a/lib/src/points/default.nix +++ b/lib/src/points/default.nix @@ -17,10 +17,7 @@ lib: { ## 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 + ## @type (a -> a) -> a & { __unfix__ :: (a -> a) } fix' = f: let x = f x diff --git a/lib/src/points/default.test.nix b/lib/src/points/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/points/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/strings/default.test.nix b/lib/src/strings/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/strings/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/types/default.nix b/lib/src/types/default.nix index f83bdb8..5d5bd28 100644 --- a/lib/src/types/default.nix +++ b/lib/src/types/default.nix @@ -151,7 +151,7 @@ lib: { # TODO: Document this. unspecified = lib.types.create { name = "Unspecified"; - description = "unspecified value"; + description = "unspecified type"; }; # TODO: Document this. @@ -388,20 +388,20 @@ lib: { merge = location: definitions: let normalize = definition: builtins.mapAttrs - (key: value: { + (name: 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; + zipper = name: definitions: + (lib.options.merge.definitions (location ++ [name]) type definitions).optional; filtered = lib.attrs.filter - (key: value: value ? value) + (name: value: value ? value) (builtins.zipAttrsWith zipper normalized); in - builtins.mapAttrs (key: value: value.value) filtered; + builtins.mapAttrs (name: value: value.value) filtered; getSubOptions = prefix: type.getSubOptions (prefix ++ [""]); getSubModules = type.getSubModules; withSubModules = modules: lib.types.attrs.of (type.withSubModules modules); @@ -421,14 +421,14 @@ lib: { merge = location: definitions: let normalize = definition: builtins.mapAttrs - (key: value: { + (name: 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; + zipper = name: definitions: let + merged = lib.options.merge.definitions (location ++ [name]) type definitions; in merged.optional.value or type.fallback.value or merged.merged; in @@ -494,7 +494,14 @@ lib: { j: value: let resolved = lib.options.merge.definitions - (location ++ ["[definition ${builtins.toString i}-entry ${j}]"]); + (location ++ ["[definition ${builtins.toString i}-entry ${j}]"]) + type + [ + { + file = definition.file; + value = value; + } + ]; in resolved.optional ) @@ -503,7 +510,7 @@ lib: { definitions; merged = builtins.concatLists result; filtered = builtins.filter (definition: definition ? value) merged; - values = lib.optiosn.getDefinitionValues filtered; + values = lib.options.getDefinitionValues filtered; in values; getSubOptions = prefix: type.getSubOptions (prefix ++ ["*"]); diff --git a/lib/src/types/default.test.nix b/lib/src/types/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/types/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/src/versions/default.test.nix b/lib/src/versions/default.test.nix new file mode 100644 index 0000000..00ebf8d --- /dev/null +++ b/lib/src/versions/default.test.nix @@ -0,0 +1,3 @@ +let + lib = import ./../default.nix; +in {} diff --git a/lib/test.sh b/lib/test.sh new file mode 100755 index 0000000..b6e1592 --- /dev/null +++ b/lib/test.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p jq + +set -euo pipefail + +namespace=${1:-} + +if [ -z "$namespace" ]; then + nix eval -f ./default.test.nix --show-trace --raw +else + if [ -d "./src/$namespace" ]; then + nix eval -f "./src/$namespace/default.test.nix" --show-trace --json | jq + else + echo "Namespace $namespace not found" + exit 1 + fi +fi +