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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions lib/asserts.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{ lib }:

let
inherit (lib.lists)
all
foldl'
;
in

rec {

/* Throw if pred is false, else return pred.
Expand Down Expand Up @@ -50,4 +57,36 @@ rec {
lib.generators.toPretty {} xs}, but is: ${
lib.generators.toPretty {} val}";

/*
Assert that a condition holds for all elements of a list. If it doesn't,
the first element violating the condition and its index will be given to
construct an error message.

Type:
assertAll :: (a -> Bool) -> [a] -> (Int -> a -> String) -> Bool
Copy link
Member Author

@infinisil infinisil Feb 7, 2023

Choose a reason for hiding this comment

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

Design decisions:

  1. Should it be called with named arguments instead? E.g.

    # assertAll :: {
    #   condition :: a -> Bool;
    #   list :: [a];
    #   message :: Int -> a -> String;
    # } -> Bool
    assert assertAll {
      condition = n: n != 2;
      list = [ 1 3 5 ];
      message = i: n: "Index ${toString i}, value ${toString n} failed.";
    };

    RFC 135 also goes in that direction

  2. Should the message have access to not only the first, but all elements that don't satisfy the condition? The module system has such a use case:

    nixpkgs/lib/modules.nix

    Lines 765 to 767 in 12053de

    if all (def: type.check def.value) defsFinal then type.merge loc defsFinal
    else let allInvalid = filter (def: ! type.check def.value) defsFinal;
    in throw "A definition for option `${showOption loc}' is not of type `${type.description}'. Definition values:${showDefs allInvalid}"

    How should this look like? Maybe

    # assertAll :: {
    #   condition :: a -> Bool;
    #   list :: [a];
    #   message :: [{ index :: Int; value :: a; }] -> String;
    # } -> Bool
    assert assertAll {
      condition = n: n != 2;
      list = [ 1 2 3 ];
      message = failingElements: "Index ${(head failingElements).index}, value ${(head failingElements).value} failed.";

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Yes, and to make it like RFC 135, it should be
assertAll = {success, message}: list: ...
  1. Sounds good. Possibly with an index attached? Needs more thinking and discussion.


Example:
assert assertAll (n: n != 2) [ 1 3 5 ] (i: n: "Number ${toString n} at index ${toString i} is two!")
=> true
assert assertAll (n: n != 2) [ 1 2 3 ] (i: n: "Number ${toString n} at index ${toString i} is two!")
=> error: Number 2 at index 1 is two!
*/
assertAll =
# The condition to check on the list elements
cond:
# The list whose elements to check the condition on
list:
# In case an element violates the condition, this function gets called
# with the element index and value, it should return an error message string
msg:
# Fast successful path
if all cond list
then true
# Slower unsuccessful path
else foldl' (i: elem:
if cond elem
then i + 1
else throw (msg i elem)
) 0 list;

}
2 changes: 1 addition & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ let
inherit (self.types) isType setType defaultTypeMerge defaultFunctor
isOptionType mkOptionType;
inherit (self.asserts)
assertMsg assertOneOf;
assertMsg assertOneOf assertAll;
inherit (self.debug) addErrorContextToAttrs traceIf traceVal traceValFn
traceXMLVal traceXMLValMarked traceSeq traceSeqN traceValSeq
traceValSeqFn traceValSeqN traceValSeqNFn traceFnSeqN traceShowVal
Expand Down
11 changes: 11 additions & 0 deletions lib/tests/misc.nix
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,17 @@ runTests {
expected = false;
};

# ASSERTS

testAssertAllExample1 = {
expr = asserts.assertAll (n: n != 2) [ 1 3 5 ] (abort "unused");
expected = true;
};
testAssertAllExample2 = {
expr = (builtins.tryEval (asserts.assertAll (n: n != 2) [ 1 2 3 ] (i: n:
throw "Number ${toString n} at index ${toString i} is two!"))).success;
expected = false;
};

# ATTRSETS

Expand Down