refactor(nix/*): drop yants and consumers, and some more

Change-Id: I96ab5890518c7bb0d4a676adbad20e4c49699b63
This commit is contained in:
Florian Klink 2025-02-24 18:00:35 +07:00
parent 001556aa30
commit cff6575948
33 changed files with 11 additions and 2414 deletions

View file

@ -1,51 +0,0 @@
{ lib, pkgs, depot, ... }:
# Takes a derivation and a list of binary names
# and returns an attribute set of `name -> path`.
# The list can also contain renames in the form of
# `{ use, as }`, which goes `as -> usePath`.
#
# It is usually used to construct an attrset `bins`
# containing all the binaries required in a file,
# similar to a simple import system.
#
# Example:
#
# bins = getBins pkgs.hello [ "hello" ]
# // getBins pkgs.coreutils [ "printf" "ln" "echo" ]
# // getBins pkgs.execline
# [ { use = "if"; as = "execlineIf" } ]
# // getBins pkgs.s6-portable-utils
# [ { use = "s6-test"; as = "test" }
# { use = "s6-cat"; as = "cat" }
# ];
#
# provides
# bins.{hello,printf,ln,echo,execlineIf,test,cat}
#
let
getBins = drv: xs:
let
f = x:
# TODO(Profpatsch): typecheck
let x' = if builtins.isString x then { use = x; as = x; } else x;
in {
name = x'.as;
value = "${lib.getBin drv}/bin/${x'.use}";
};
in
builtins.listToAttrs (builtins.map f xs);
tests = import ./tests.nix {
inherit getBins;
inherit (depot.nix) writeScriptBin;
inherit (depot.nix.runTestsuite) assertEq it runTestsuite;
};
in
{
__functor = _: getBins;
inherit tests;
}

View file

@ -1,40 +0,0 @@
{ writeScriptBin, assertEq, it, runTestsuite, getBins }:
let
drv = writeScriptBin "hello" "its me";
drv2 = writeScriptBin "goodbye" "tschau";
bins = getBins drv [
"hello"
{ use = "hello"; as = "also-hello"; }
]
// getBins drv2 [ "goodbye" ]
;
simple = it "path is equal to the executable name" [
(assertEq "path"
bins.hello
"${drv}/bin/hello")
(assertEq "content"
(builtins.readFile bins.hello)
"its me")
];
useAs = it "use/as can be used to rename attributes" [
(assertEq "path"
bins.also-hello
"${drv}/bin/hello")
];
secondDrv = it "by merging attrsets you can build up bins" [
(assertEq "path"
bins.goodbye
"${drv2}/bin/goodbye")
];
in
runTestsuite "getBins" [
simple
useAs
secondDrv
]

View file

@ -1,192 +0,0 @@
{ lib, depot, ... }:
/*
JSON Merge-Patch for nix
Spec: https://tools.ietf.org/html/rfc7396
An algorithm for changing and removing fields in nested objects.
For example, given the following original document:
{
a = "b";
c = {
d = "e";
f = "g";
}
}
Changing the value of `a` and removing `f` can be achieved by merging the patch
{
a = "z";
c.f = null;
}
which results in
{
a = "z";
c = {
d = "e";
};
}
Pseudo-code:
define MergePatch(Target, Patch):
if Patch is an Object:
if Target is not an Object:
Target = {} # Ignore the contents and set it to an empty Object
for each Name/Value pair in Patch:
if Value is null:
if Name exists in Target:
remove the Name/Value pair from Target
else:
Target[Name] = MergePatch(Target[Name], Value)
return Target
else:
return Patch
*/
let
foldlAttrs = op: init: attrs:
lib.foldl' op init
(lib.mapAttrsToList lib.nameValuePair attrs);
mergePatch = target: patch:
if lib.isAttrs patch
then
let target' = if lib.isAttrs target then target else { };
in foldlAttrs
(acc: patchEl:
if patchEl.value == null
then removeAttrs acc [ patchEl.name ]
else acc // {
${patchEl.name} =
mergePatch
(acc.${patchEl.name} or "unnused")
patchEl.value;
})
target'
patch
else patch;
inherit (depot.nix.runTestsuite)
runTestsuite
it
assertEq
;
tests =
let
# example target from the RFC
testTarget = {
a = "b";
c = {
d = "e";
f = "g";
};
};
# example patch from the RFC
testPatch = {
a = "z";
c.f = null;
};
emptyPatch = it "the empty patch returns the original target" [
(assertEq "id"
(mergePatch testTarget { })
testTarget)
];
nonAttrs = it "one side is a non-attrset value" [
(assertEq "target is a value means the value is replaced by the patch"
(mergePatch 42 testPatch)
(mergePatch { } testPatch))
(assertEq "patch is a value means it replaces target alltogether"
(mergePatch testTarget 42)
42)
];
rfcExamples = it "the examples from the RFC" [
(assertEq "a subset is deleted and overwritten"
(mergePatch testTarget testPatch)
{
a = "z";
c = {
d = "e";
};
})
(assertEq "a more complicated example from the example section"
(mergePatch
{
title = "Goodbye!";
author = {
givenName = "John";
familyName = "Doe";
};
tags = [ "example" "sample" ];
content = "This will be unchanged";
}
{
title = "Hello!";
phoneNumber = "+01-123-456-7890";
author.familyName = null;
tags = [ "example" ];
})
{
title = "Hello!";
phoneNumber = "+01-123-456-7890";
author = {
givenName = "John";
};
tags = [ "example" ];
content = "This will be unchanged";
})
];
rfcTests =
let
r = index: target: patch: res:
(assertEq "test number ${toString index}"
(mergePatch target patch)
res);
in
it "the test suite from the RFC" [
(r 1 { "a" = "b"; } { "a" = "c"; } { "a" = "c"; })
(r 2 { "a" = "b"; } { "b" = "c"; } { "a" = "b"; "b" = "c"; })
(r 3 { "a" = "b"; } { "a" = null; } { })
(r 4 { "a" = "b"; "b" = "c"; }
{ "a" = null; }
{ "b" = "c"; })
(r 5 { "a" = [ "b" ]; } { "a" = "c"; } { "a" = "c"; })
(r 6 { "a" = "c"; } { "a" = [ "b" ]; } { "a" = [ "b" ]; })
(r 7 { "a" = { "b" = "c"; }; }
{ "a" = { "b" = "d"; "c" = null; }; }
{ "a" = { "b" = "d"; }; })
(r 8 { "a" = [{ "b" = "c"; }]; }
{ "a" = [ 1 ]; }
{ "a" = [ 1 ]; })
(r 9 [ "a" "b" ] [ "c" "d" ] [ "c" "d" ])
(r 10 { "a" = "b"; } [ "c" ] [ "c" ])
(r 11 { "a" = "foo"; } null null)
(r 12 { "a" = "foo"; } "bar" "bar")
(r 13 { "e" = null; } { "a" = 1; } { "e" = null; "a" = 1; })
(r 14 [ 1 2 ]
{ "a" = "b"; "c" = null; }
{ "a" = "b"; })
(r 15 { }
{ "a" = { "bb" = { "ccc" = null; }; }; }
{ "a" = { "bb" = { }; }; })
];
in
runTestsuite "mergePatch" [
emptyPatch
nonAttrs
rfcExamples
rfcTests
];
in
{
__functor = _: mergePatch;
inherit tests;
}

View file

@ -1 +0,0 @@
raitobezarius

View file

@ -1,93 +0,0 @@
# nint — Nix INTerpreter
`nint` is a shebang compatible interpreter for nix. It is currently
implemented as a fairly trivial wrapper around `nix-instantiate --eval`.
It allows to run nix expressions as command line tools if they conform
to the following calling convention:
* Every nix script needs to evaluate to a function which takes an
attribute set as its single argument. Ideally a set pattern with
an ellipsis should be used. By default `nint` passes the following
arguments:
* `currentDir`: the current working directory as a nix path
* `argv`: a list of arguments to the invokation including the
program name at `builtins.head argv`.
* Extra arguments can be manually passed as described below.
* The return value must either be
* A string which is rendered to `stdout`.
* An attribute set with the following optional attributes:
* `stdout`: A string that's rendered to `stdout`
* `stderr`: A string that's rendered to `stderr`
* `exit`: A number which is used as an exit code.
If missing, nint always exits with 0 (or equivalent).
## Usage
```
nint [ --arg ARG VALUE … ] script.nix [ ARGS … ]
```
Instead of `--arg`, `--argstr` can also be used. They both work
like the flags of the same name for `nix-instantiate` and may
be specified any number of times as long as they are passed
*before* the nix expression to run.
Below is a shebang which also passes `depot` as an argument
(note the usage of `env -S` to get around the shebang limitation
to two arguments).
```nix
#!/usr/bin/env -S nint --arg depot /path/to/depot
```
## Limitations
* No side effects except for writing to `stdout`.
* Output is not streaming, i. e. even if the output is incrementally
calculated, nothing will be printed until the full output is available.
With plain nix strings we can't do better anyways.
* Limited error handling for the script, no way to set the exit code etc.
Some of these limitations may be possible to address in the future by using
an alternative nix interpreter and a more elaborate calling convention.
## Example
Below is a (very simple) implementation of a `ls(1)`-like program in nix:
```nix
#!/usr/bin/env nint
{ currentDir, argv, ... }:
let
lib = import <nixpkgs/lib>;
dirs =
let
args = builtins.tail argv;
in
if args == []
then [ currentDir ]
else args;
makeAbsolute = p:
if builtins.isPath p
then p
else if builtins.match "^/.*" p != null
then p
else "${toString currentDir}/${p}";
in
lib.concatStringsSep "\n"
(lib.flatten
(builtins.map
(d: (builtins.attrNames (builtins.readDir (makeAbsolute d))))
dirs)) + "\n"
```

View file

@ -1,16 +0,0 @@
{ depot, pkgs, ... }:
let
inherit (depot.nix.writers)
rustSimpleBin
;
in
rustSimpleBin
{
name = "nint";
dependencies = [
depot.third_party.rust-crates.serde_json
];
}
(builtins.readFile ./nint.rs)

View file

@ -1,149 +0,0 @@
extern crate serde_json;
use serde_json::Value;
use std::convert::TryFrom;
use std::ffi::OsString;
use std::io::{stderr, stdout, Error, ErrorKind, Write};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::process::Command;
fn render_nix_string(s: &OsString) -> OsString {
let mut rendered = Vec::new();
rendered.extend(b"\"");
for b in s.as_os_str().as_bytes() {
match char::from(*b) {
'\"' => rendered.extend(b"\\\""),
'\\' => rendered.extend(b"\\\\"),
'$' => rendered.extend(b"\\$"),
_ => rendered.push(*b),
}
}
rendered.extend(b"\"");
OsString::from_vec(rendered)
}
fn render_nix_list(arr: &[OsString]) -> OsString {
let mut rendered = Vec::new();
rendered.extend(b"[ ");
for el in arr {
rendered.extend(render_nix_string(el).as_os_str().as_bytes());
rendered.extend(b" ");
}
rendered.extend(b"]");
OsString::from_vec(rendered)
}
/// Slightly overkill helper macro which takes a `Map<String, Value>` obtained
/// from `Value::Object` and an output name (`stderr` or `stdout`) as an
/// identifier. If a value exists for the given output in the object it gets
/// written to the appropriate output.
macro_rules! handle_set_output {
($map_name:ident, $output_name:ident) => {
match $map_name.get(stringify!($output_name)) {
Some(Value::String(s)) => $output_name().write_all(s.as_bytes()),
Some(_) => Err(Error::new(
ErrorKind::Other,
format!("Attribute {} must be a string!", stringify!($output_name)),
)),
None => Ok(()),
}
};
}
fn main() -> std::io::Result<()> {
let mut nix_args = Vec::new();
let mut args = std::env::args_os().into_iter();
let mut in_args = true;
let mut argv: Vec<OsString> = Vec::new();
// skip argv[0]
args.next();
loop {
let arg = match args.next() {
Some(a) => a,
None => break,
};
if !arg.to_str().map(|s| s.starts_with("-")).unwrap_or(false) {
in_args = false;
}
if in_args {
match (arg.to_str()) {
Some("--arg") | Some("--argstr") => {
nix_args.push(arg);
nix_args.push(args.next().unwrap());
nix_args.push(args.next().unwrap());
Ok(())
}
_ => Err(Error::new(ErrorKind::Other, "unknown argument")),
}?
} else {
argv.push(arg);
}
}
if argv.len() < 1 {
Err(Error::new(ErrorKind::Other, "missing argv"))
} else {
let cd = std::env::current_dir()?.into_os_string();
nix_args.push(OsString::from("--arg"));
nix_args.push(OsString::from("currentDir"));
nix_args.push(cd);
nix_args.push(OsString::from("--arg"));
nix_args.push(OsString::from("argv"));
nix_args.push(render_nix_list(&argv[..]));
nix_args.push(OsString::from("--eval"));
nix_args.push(OsString::from("--strict"));
nix_args.push(OsString::from("--json"));
nix_args.push(argv[0].clone());
let run = Command::new("nix-instantiate").args(nix_args).output()?;
match serde_json::from_slice(&run.stdout[..]) {
Ok(Value::String(s)) => stdout().write_all(s.as_bytes()),
Ok(Value::Object(m)) => {
handle_set_output!(m, stdout)?;
handle_set_output!(m, stderr)?;
match m.get("exit") {
Some(Value::Number(n)) => {
let code = n.as_i64().and_then(|v| i32::try_from(v).ok());
match code {
Some(i) => std::process::exit(i),
None => {
Err(Error::new(ErrorKind::Other, "Attribute exit is not an i32"))
}
}
}
Some(_) => Err(Error::new(ErrorKind::Other, "exit must be a number")),
None => Ok(()),
}
}
Ok(_) => Err(Error::new(
ErrorKind::Other,
"output must be a string or an object",
)),
_ => {
stderr().write_all(&run.stderr[..]);
Err(Error::new(ErrorKind::Other, "internal nix error"))
}
}
}
}

View file

@ -1,139 +0,0 @@
{ depot, lib, ... }:
let
inherit (depot.nix.runTestsuite)
runTestsuite
it
assertEq
assertThrows
;
tree-ex = depot.nix.readTree {
path = ./test-example;
args = { };
};
example = it "corresponds to the README example" [
(assertEq "third_party attrset"
(lib.isAttrs tree-ex.third_party
&& (! lib.isDerivation tree-ex.third_party))
true)
(assertEq "third_party attrset other attribute"
tree-ex.third_party.favouriteColour
"orange")
(assertEq "rustpkgs attrset aho-corasick"
tree-ex.third_party.rustpkgs.aho-corasick
"aho-corasick")
(assertEq "rustpkgs attrset serde"
tree-ex.third_party.rustpkgs.serde
"serde")
(assertEq "tools cheddear"
"cheddar"
tree-ex.tools.cheddar)
(assertEq "tools roquefort"
tree-ex.tools.roquefort
"roquefort")
];
tree-tl = depot.nix.readTree {
path = ./test-tree-traversal;
args = { };
};
traversal-logic = it "corresponds to the traversal logic in the README" [
(assertEq "skip-tree/a is read"
tree-tl.skip-tree.a
"a is read normally")
(assertEq "skip-tree does not contain b"
(builtins.attrNames tree-tl.skip-tree)
[ "__readTree" "__readTreeChildren" "a" ])
(assertEq "skip-tree children list does not contain b"
tree-tl.skip-tree.__readTreeChildren
[ "a" ])
(assertEq "skip subtree default.nix is read"
tree-tl.skip-subtree.but
"the default.nix is still read")
(assertEq "skip subtree a/default.nix is skipped"
(tree-tl.skip-subtree ? a)
false)
(assertEq "skip subtree b/c.nix is skipped"
(tree-tl.skip-subtree ? b)
false)
(assertEq "skip subtree a/default.nix would be read without .skip-subtree"
(tree-tl.no-skip-subtree.a)
"am I subtree yet?")
(assertEq "skip subtree b/c.nix would be read without .skip-subtree"
(tree-tl.no-skip-subtree.b.c)
"cool")
(assertEq "default.nix attrset is merged with siblings"
tree-tl.default-nix.no
"siblings should be read")
(assertEq "default.nix means sibling isnt read"
(tree-tl.default-nix ? sibling)
false)
(assertEq "default.nix means subdirs are still read and merged into default.nix"
(tree-tl.default-nix.subdir.a)
"but Im picked up")
(assertEq "default.nix can be not an attrset"
tree-tl.default-nix.no-merge
"Im not merged with any children")
(assertEq "default.nix is not an attrset -> children are not merged"
(tree-tl.default-nix.no-merge ? subdir)
false)
(assertEq "default.nix can contain a derivation"
(lib.isDerivation tree-tl.default-nix.can-be-drv)
true)
(assertEq "Even if default.nix is a derivation, children are traversed and merged"
tree-tl.default-nix.can-be-drv.subdir.a
"Picked up through the drv")
(assertEq "default.nix drv is not changed by readTree"
tree-tl.default-nix.can-be-drv
(import ./test-tree-traversal/default-nix/can-be-drv/default.nix { }))
];
# these each call readTree themselves because the throws have to happen inside assertThrows
wrong = it "cannot read these files and will complain" [
(assertThrows "this file is not a function"
(depot.nix.readTree {
path = ./test-wrong-not-a-function;
args = { };
}).not-a-function)
# cant test for that, assertThrows cant catch this error
# (assertThrows "this file is a function but doesnt have dots"
# (depot.nix.readTree {} ./test-wrong-no-dots).no-dots-in-function)
];
read-markers = depot.nix.readTree {
path = ./test-marker;
args = { };
};
assertMarkerByPath = path:
assertEq "${lib.concatStringsSep "." path} is marked correctly"
(lib.getAttrFromPath path read-markers).__readTree
path;
markers = it "marks nodes correctly" [
(assertMarkerByPath [ "directory-marked" ])
(assertMarkerByPath [ "directory-marked" "nested" ])
(assertMarkerByPath [ "file-children" "one" ])
(assertMarkerByPath [ "file-children" "two" ])
(assertEq "nix file children are marked correctly"
read-markers.file-children.__readTreeChildren [ "one" "two" ])
(assertEq "directory children are marked correctly"
read-markers.directory-marked.__readTreeChildren [ "nested" ])
(assertEq "absence of children is marked"
read-markers.directory-marked.nested.__readTreeChildren [ ])
];
in
runTestsuite "readTree" [
example
traversal-logic
wrong
markers
]

View file

@ -1,31 +0,0 @@
{ depot, pkgs, lib, ... }:
let
runExecline = import ./runExecline.nix {
inherit (pkgs) stdenv;
inherit (depot.nix) escapeExecline getBins;
inherit pkgs lib;
};
runExeclineLocal = name: args: execline:
runExecline name
(args // {
derivationArgs = args.derivationArgs or { } // {
preferLocalBuild = true;
allowSubstitutes = false;
};
})
execline;
tests = import ./tests.nix {
inherit runExecline runExeclineLocal;
inherit (depot.nix) getBins writeScript;
inherit (pkgs) stdenv coreutils;
inherit pkgs;
};
in
{
__functor = _: runExecline;
local = runExeclineLocal;
inherit tests;
}

View file

@ -1,122 +0,0 @@
{ pkgs, stdenv, lib, getBins, escapeExecline }:
# runExecline is a primitive building block
# for writing non-kitchen sink builders.
#
# Its conceptually similar to `runCommand`,
# but instead of concatenating bash scripts left
# and right, it actually *uses* the features of
# `derivation`, passing things to `args`
# and making it possible to overwrite the `builder`
# in a sensible manner.
#
# Additionally, it provides a way to pass a nix string
# to `stdin` of the build script.
#
# Similar to //nix/writeExecline, the passed script is
# not a string, but a nested list of nix lists
# representing execline blocks. Escaping is
# done by the implementation, the user can just use
# normal nix strings.
#
# Example:
#
# runExecline "my-drv" { stdin = "hi!"; } [
# "importas" "out" "out"
# # this pipes stdout of s6-cat to $out
# # and s6-cat redirects from stdin to stdout
# "redirfd" "-w" "1" "$out" bins.s6-cat
# ]
#
# which creates a derivation with "hi!" in $out.
#
# See ./tests.nix for more examples.
let
bins = getBins pkgs.execline [
"execlineb"
{ use = "if"; as = "execlineIf"; }
"redirfd"
"importas"
"exec"
]
// getBins pkgs.s6-portable-utils [
"s6-cat"
"s6-grep"
"s6-touch"
"s6-test"
"s6-chmod"
];
in
# TODO: move name into the attrset
name:
{
# a string to pass as stdin to the execline script
stdin ? ""
# a program wrapping the acutal execline invocation;
# should be in Bernstein-chaining style
, builderWrapper ? bins.exec
# additional arguments to pass to the derivation
, derivationArgs ? { }
}:
# the execline script as a nested list of string,
# representing the blocks;
# see docs of `escapeExecline`.
execline:
# those arguments cant be overwritten
assert !derivationArgs ? system;
assert !derivationArgs ? name;
assert !derivationArgs ? builder;
assert !derivationArgs ? args;
derivation (derivationArgs // {
# TODO(Profpatsch): what about cross?
inherit (stdenv) system;
inherit name;
# okay, `builtins.toFile` does not accept strings
# that reference drv outputs. This means we need
# to pass the script and stdin as envvar;
# this might clash with another passed envar,
# so we give it a long & unique name
_runExeclineScript =
let
in escapeExecline execline;
_runExeclineStdin = stdin;
passAsFile = [
"_runExeclineScript"
"_runExeclineStdin"
] ++ derivationArgs.passAsFile or [ ];
# the default, exec acts as identity executable
builder = builderWrapper;
args = [
bins.importas # import script file as $script
"-ui" # drop the envvar afterwards
"script" # substitution name
"_runExeclineScriptPath" # passed script file
bins.importas # do the same for $stdin
"-ui"
"stdin"
"_runExeclineStdinPath"
bins.redirfd # now we
"-r" # read the file
"0" # into the stdin of execlineb
"$stdin" # that was given via stdin
bins.execlineb # the actual invocation
# TODO(Profpatsch): depending on the use-case, -S0 might not be enough
# in all use-cases, then a wrapper for execlineb arguments
# should be added (-P, -S, -s).
"-S0" # set $@ inside the execline script
"-W" # die on syntax error
"$script" # substituted by importas
];
})

View file

@ -1,117 +0,0 @@
{ stdenv
, pkgs
, runExecline
, runExeclineLocal
, getBins
, writeScript
# https://www.mail-archive.com/skaware@list.skarnet.org/msg01256.html
, coreutils
}:
let
bins = getBins coreutils [ "mv" ]
// getBins pkgs.execline [
"execlineb"
{ use = "if"; as = "execlineIf"; }
"redirfd"
"importas"
]
// getBins pkgs.s6-portable-utils [
"s6-chmod"
"s6-grep"
"s6-touch"
"s6-cat"
"s6-test"
];
# execline block of depth 1
block = args: builtins.map (arg: " ${arg}") args ++ [ "" ];
# derivation that tests whether a given line exists
# in the given file. Does not use runExecline, because
# that should be tested after all.
fileHasLine = line: file: derivation {
name = "run-execline-test-file-${file.name}-has-line";
inherit (stdenv) system;
builder = bins.execlineIf;
args =
(block [
bins.redirfd
"-r"
"0"
file # read file to stdin
bins.s6-grep
"-F"
"-q"
line # and grep for the line
])
++ [
# if the block succeeded, touch $out
bins.importas
"-ui"
"out"
"out"
bins.s6-touch
"$out"
];
preferLocalBuild = true;
allowSubstitutes = false;
};
# basic test that touches out
basic = runExeclineLocal "run-execline-test-basic"
{ } [
"importas"
"-ui"
"out"
"out"
"${bins.s6-touch}"
"$out"
];
# whether the stdin argument works as intended
stdin = fileHasLine "foo" (runExeclineLocal "run-execline-test-stdin"
{
stdin = "foo\nbar\nfoo";
} [
"importas"
"-ui"
"out"
"out"
# this pipes stdout of s6-cat to $out
# and s6-cat redirects from stdin to stdout
"redirfd"
"-w"
"1"
"$out"
bins.s6-cat
]);
wrapWithVar = runExeclineLocal "run-execline-test-wrap-with-var"
{
builderWrapper = writeScript "var-wrapper" ''
#!${bins.execlineb} -S0
export myvar myvalue $@
'';
} [
"importas"
"-ui"
"v"
"myvar"
"if"
[ bins.s6-test "myvalue" "=" "$v" ]
"importas"
"out"
"out"
bins.s6-touch
"$out"
];
in
[
basic
stdin
wrapWithVar
]

View file

@ -1,199 +0,0 @@
{ lib, pkgs, depot, ... }:
# Run a nix testsuite.
#
# The tests are simple assertions on the nix level,
# and can use derivation outputs if IfD is enabled.
#
# You build a testsuite by bundling assertions into
# “it”s and then bundling the “it”s into a testsuite.
#
# Running the testsuite will abort evaluation if
# any assertion fails.
#
# Example:
#
# runTestsuite "myFancyTestsuite" [
# (it "does an assertion" [
# (assertEq "42 is equal to 42" "42" "42")
# (assertEq "also 23" 23 23)
# ])
# (it "frmbls the brlbr" [
# (assertEq true false)
# ])
# ]
#
# will fail the second it group because true is not false.
let
inherit (depot.nix.yants)
sum
struct
string
any
defun
list
drv
bool
;
bins = depot.nix.getBins pkgs.coreutils [ "printf" ]
// depot.nix.getBins pkgs.s6-portable-utils [ "s6-touch" "s6-false" "s6-cat" ];
# Returns true if the given expression throws when `deepSeq`-ed
throws = expr:
!(builtins.tryEval (builtins.deepSeq expr { })).success;
# rewrite the builtins.partition result
# to use `ok` and `err` instead of `right` and `wrong`.
partitionTests = pred: xs:
let res = builtins.partition pred xs;
in {
ok = res.right;
err = res.wrong;
};
AssertErrorContext =
sum "AssertErrorContext" {
not-equal = struct "not-equal" {
left = any;
right = any;
};
should-throw = struct "should-throw" {
expr = any;
};
unexpected-throw = struct "unexpected-throw" { };
};
# The result of an assert,
# either its true (yep) or false (nope).
# If it's nope we return an additional context
# attribute which gives details on the failure
# depending on the type of assert performed.
AssertResult =
sum "AssertResult" {
yep = struct "yep" {
test = string;
};
nope = struct "nope" {
test = string;
context = AssertErrorContext;
};
};
# Result of an it. An it is a bunch of asserts
# bundled up with a good description of what is tested.
ItResult =
struct "ItResult" {
it-desc = string;
asserts = list AssertResult;
};
# If the given boolean is true return a positive AssertResult.
# If the given boolean is false return a negative AssertResult
# with the provided AssertErrorContext describing the failure.
#
# This function is intended as a generic assert to implement
# more assert types and is not exposed to the user.
assertBoolContext = defun [ AssertErrorContext string bool AssertResult ]
(context: desc: res:
if res
then { yep = { test = desc; }; }
else {
nope = {
test = desc;
inherit context;
};
});
# assert that left and right values are equal
assertEq = defun [ string any any AssertResult ]
(desc: left: right:
let
context = { not-equal = { inherit left right; }; };
in
assertBoolContext context desc (left == right));
# assert that the expression throws when `deepSeq`-ed
assertThrows = defun [ string any AssertResult ]
(desc: expr:
let
context = { should-throw = { inherit expr; }; };
in
assertBoolContext context desc (throws expr));
# assert that the expression does not throw when `deepSeq`-ed
assertDoesNotThrow = defun [ string any AssertResult ]
(desc: expr:
assertBoolContext { unexpected-throw = { }; } desc (!(throws expr)));
# Annotate a bunch of asserts with a descriptive name
it = desc: asserts: {
it-desc = desc;
inherit asserts;
};
# Run a bunch of its and check whether all asserts are yep.
# If not, abort evaluation with `throw`
# and print the result of the test suite.
#
# Takes a test suite name as first argument.
runTestsuite = defun [ string (list ItResult) drv ]
(name: itResults:
let
goodAss = ass: AssertResult.match ass {
yep = _: true;
nope = _: false;
};
res = partitionTests
(it:
(partitionTests goodAss it.asserts).err == [ ]
)
itResults;
prettyRes = lib.generators.toPretty { } res;
in
if res.err == [ ]
then
depot.nix.runExecline.local "testsuite-${name}-successful" { } [
"importas"
"out"
"out"
# force derivation to rebuild if test case list changes
"ifelse"
[ bins.s6-false ]
[
bins.printf
""
(builtins.hashString "sha512" prettyRes)
]
"if"
[ bins.printf "%s\n" "testsuite ${name} successful!" ]
bins.s6-touch
"$out"
]
else
depot.nix.runExecline.local "testsuite-${name}-failed"
{
stdin = prettyRes + "\n";
} [
"importas"
"out"
"out"
"if"
[ bins.printf "%s\n" "testsuite ${name} failed!" ]
"if"
[ bins.s6-cat ]
"exit"
"1"
]);
in
{
inherit
assertEq
assertThrows
assertDoesNotThrow
it
runTestsuite
;
}

View file

@ -1,110 +0,0 @@
{ depot, ... }:
let
inherit (depot.nix.runTestsuite)
runTestsuite
it
assertEq
;
inherit (depot.nix.stateMonad)
pure
run
join
fmap
bind
get
set
modify
after
for_
getAttr
setAttr
modifyAttr
;
runStateIndependent = run (throw "This should never be evaluated!");
in
runTestsuite "stateMonad" [
(it "behaves correctly independent of state" [
(assertEq "pure" (runStateIndependent (pure 21)) 21)
(assertEq "join pure" (runStateIndependent (join (pure (pure 42)))) 42)
(assertEq "fmap pure" (runStateIndependent (fmap (builtins.mul 2) (pure 21))) 42)
(assertEq "bind pure" (runStateIndependent (bind (pure 12) (x: pure x))) 12)
])
(it "behaves correctly with an integer state" [
(assertEq "get" (run 42 get) 42)
(assertEq "after set get" (run 21 (after (set 42) get)) 42)
(assertEq "after modify get" (run 21 (after (modify (builtins.mul 2)) get)) 42)
(assertEq "fmap get" (run 40 (fmap (builtins.add 2) get)) 42)
(assertEq "stateful sum list"
(run 0 (after
(for_
[
15
12
10
5
]
(x: modify (builtins.add x)))
get))
42)
])
(it "behaves correctly with an attr set state" [
(assertEq "getAttr" (run { foo = 42; } (getAttr "foo")) 42)
(assertEq "after setAttr getAttr"
(run { foo = 21; } (after (setAttr "foo" 42) (getAttr "foo")))
42)
(assertEq "after modifyAttr getAttr"
(run { foo = 10.5; }
(after
(modifyAttr "foo" (builtins.mul 4))
(getAttr "foo")))
42)
(assertEq "fmap getAttr"
(run { foo = 21; } (fmap (builtins.mul 2) (getAttr "foo")))
42)
(assertEq "after setAttr to insert getAttr"
(run { } (after (setAttr "foo" 42) (getAttr "foo")))
42)
(assertEq "insert permutations"
(run
{
a = 2;
b = 3;
c = 5;
}
(after
(bind get
(state:
let
names = builtins.attrNames state;
in
for_ names (name1:
for_ names (name2:
# this is of course a bit silly, but making it more cumbersome
# makes sure the test exercises more of the code.
(bind (getAttr name1)
(value1:
(bind (getAttr name2)
(value2:
setAttr "${name1}_${name2}" (value1 * value2)))))))))
get))
{
a = 2;
b = 3;
c = 5;
a_a = 4;
a_b = 6;
a_c = 10;
b_a = 6;
b_b = 9;
b_c = 15;
c_c = 25;
c_a = 10;
c_b = 15;
}
)
])
]

View file

@ -1,99 +0,0 @@
{ depot, lib, verifyTag, discr, discrDef, match, matchLam }:
let
inherit (depot.nix.runTestsuite)
runTestsuite
assertEq
assertThrows
it
;
isTag-test = it "checks whether something is a tag" [
(assertEq "is Tag"
(verifyTag { foo = "bar"; })
{
isTag = true;
name = "foo";
val = "bar";
errmsg = null;
})
(assertEq "is not Tag"
(removeAttrs (verifyTag { foo = "bar"; baz = 42; }) [ "errmsg" ])
{
isTag = false;
name = null;
val = null;
})
];
discr-test = it "can discr things" [
(assertEq "id"
(discr [
{ a = lib.const true; }
] "x")
{ a = "x"; })
(assertEq "bools here, ints there"
(discr [
{ bool = lib.isBool; }
{ int = lib.isInt; }
] 25)
{ int = 25; })
(assertEq "bools here, ints there 2"
(discr [
{ bool = lib.isBool; }
{ int = lib.isInt; }
]
true)
{ bool = true; })
(assertEq "fallback to default"
(discrDef "def" [
{ bool = lib.isBool; }
{ int = lib.isInt; }
] "foo")
{ def = "foo"; })
(assertThrows "throws failing to match"
(discr [
{ fish = x: x == 42; }
] 21))
];
match-test = it "can match things" [
(assertEq "match example"
(
let
success = { res = 42; };
failure = { err = "no answer"; };
matcher = {
res = i: i + 1;
err = _: 0;
};
in
{
one = match success matcher;
two = match failure matcher;
}
)
{
one = 43;
two = 0;
})
(assertEq "matchLam & pipe"
(lib.pipe { foo = 42; } [
(matchLam {
foo = i: if i < 23 then { small = i; } else { big = i; };
bar = _: { small = 5; };
})
(matchLam {
small = i: "yay it was small";
big = i: "whoo it was big!";
})
])
"whoo it was big!")
];
in
runTestsuite "tag" [
isTag-test
discr-test
match-test
]

View file

@ -1,98 +0,0 @@
{ depot, lib, ... }:
let
inherit (depot.nix.runTestsuite)
runTestsuite
it
assertEq
assertThrows
assertDoesNotThrow
;
inherit (depot.nix.utils)
isDirectory
isRegularFile
isSymlink
storePathName
;
assertUtilsPred = msg: act: exp: [
(assertDoesNotThrow "${msg} does not throw" act)
(assertEq msg (builtins.tryEval act).value exp)
];
pathPredicates = it "judges paths correctly" (lib.flatten [
# isDirectory
(assertUtilsPred "directory isDirectory"
(isDirectory ./directory)
true)
(assertUtilsPred "symlink not isDirectory"
(isDirectory ./symlink-directory)
false)
(assertUtilsPred "file not isDirectory"
(isDirectory ./directory/file)
false)
# isRegularFile
(assertUtilsPred "file isRegularFile"
(isRegularFile ./directory/file)
true)
(assertUtilsPred "symlink not isRegularFile"
(isRegularFile ./symlink-file)
false)
(assertUtilsPred "directory not isRegularFile"
(isRegularFile ./directory)
false)
# isSymlink
(assertUtilsPred "symlink to file isSymlink"
(isSymlink ./symlink-file)
true)
(assertUtilsPred "symlink to directory isSymlink"
(isSymlink ./symlink-directory)
true)
(assertUtilsPred "symlink to symlink isSymlink"
(isSymlink ./symlink-symlink-file)
true)
(assertUtilsPred "symlink to missing file isSymlink"
(isSymlink ./missing)
true)
(assertUtilsPred "directory not isSymlink"
(isSymlink ./directory)
false)
(assertUtilsPred "file not isSymlink"
(isSymlink ./directory/file)
false)
# missing files throw
(assertThrows "isDirectory throws on missing file"
(isDirectory ./does-not-exist))
(assertThrows "isRegularFile throws on missing file"
(isRegularFile ./does-not-exist))
(assertThrows "isSymlink throws on missing file"
(isSymlink ./does-not-exist))
]);
magratheaStorePath =
builtins.unsafeDiscardStringContext depot.tools.magrathea.outPath;
cleanedSource = lib.cleanSource ./.;
storePathNameTests = it "correctly gets the basename of a store path" [
(assertEq "base name of a derivation"
(storePathName depot.tools.magrathea)
depot.tools.magrathea.name)
(assertEq "base name of a store path string"
(storePathName magratheaStorePath)
depot.tools.magrathea.name)
(assertEq "base name of a path within a store path"
(storePathName "${magratheaStorePath}/bin/mg") "mg")
(assertEq "base name of a path"
(storePathName ../default.nix) "default.nix")
(assertEq "base name of a cleanSourced path"
(storePathName cleanedSource)
cleanedSource.name)
];
in
runTestsuite "nix.utils" [
pathPredicates
storePathNameTests
]

View file

@ -1,20 +0,0 @@
{ depot, pkgs, ... }:
{ name, src, deps ? (_: [ ]), emacs ? pkgs.emacs-nox }:
let
inherit (pkgs) emacsPackages emacsPackagesFor;
inherit (builtins) isString toFile;
finalEmacs = (emacsPackagesFor emacs).emacsWithPackages deps;
srcFile =
if isString src
then toFile "${name}.el" src
else src;
in
depot.nix.writeScriptBin name ''
#!/bin/sh
${finalEmacs}/bin/emacs --batch --no-site-file --script ${srcFile} $@
''

View file

@ -1,39 +0,0 @@
{ pkgs, depot, ... }:
# Write an execline script, represented as nested nix lists.
# Everything is escaped correctly.
# https://skarnet.org/software/execline/
# TODO(Profpatsch) upstream into nixpkgs
name:
{
# "var": substitute readNArgs variables and start $@
# from the (readNArgs+1)th argument
# "var-full": substitute readNArgs variables and start $@ from $0
# "env": dont substitute, set # and 0…n environment vaariables, where n=$#
# "none": dont substitute or set any positional arguments
# "env-no-push": like "env", but bypass the push-phase. Not recommended.
argMode ? "var"
, # Number of arguments to be substituted as variables (passed to "var"/"-s" or "var-full"/"-S"
readNArgs ? 0
,
}:
# Nested list of lists of commands.
# Inner lists are translated to execline blocks.
argList:
let
env =
if argMode == "var" then "s${toString readNArgs}"
else if argMode == "var-full" then "S${toString readNArgs}"
else if argMode == "env" then ""
else if argMode == "none" then "P"
else if argMode == "env-no-push" then "p"
else abort ''"${toString argMode}" is not a valid argMode, use one of "var", "var-full", "env", "none", "env-no-push".'';
in
depot.nix.writeScript name ''
#!${pkgs.execline}/bin/execlineb -W${env}
${depot.nix.escapeExecline argList}
''

View file

@ -1,35 +0,0 @@
{ pkgs, depot, ... }:
# Write the given string to $out
# and make it executable.
let
bins = depot.nix.getBins pkgs.s6-portable-utils [
"s6-cat"
"s6-chmod"
];
in
name:
# string of the executable script that is put in $out
script:
depot.nix.runExecline name
{
stdin = script;
derivationArgs = {
preferLocalBuild = true;
allowSubstitutes = false;
};
} [
"importas"
"out"
"out"
# this pipes stdout of s6-cat to $out
# and s6-cat redirects from stdin to stdout
"if"
[ "redirfd" "-w" "1" "$out" bins.s6-cat ]
bins.s6-chmod
"0755"
"$out"
]

View file

@ -1,12 +0,0 @@
{ depot, ... }:
# Like writeScript,
# but put the script into `$out/bin/${name}`.
name:
script:
depot.nix.binify {
exe = (depot.nix.writeScript name script);
inherit name;
}

View file

@ -1,112 +0,0 @@
{ depot, pkgs, lib, ... }:
let
bins = depot.nix.getBins pkgs.s6-portable-utils [ "s6-ln" "s6-ls" "s6-touch" ]
;
linkTo = name: path: depot.nix.runExecline.local name { } [
"importas"
"out"
"out"
bins.s6-ln
"-s"
path
"$out"
];
# Build a rust executable, $out is the executable.
rustSimple = args@{ name, ... }: src:
linkTo name "${rustSimpleBin args src}/bin/${name}";
# Like `rustSimple`, but put the binary in `$out/bin/`.
rustSimpleBin =
{ name
, dependencies ? [ ]
, doCheck ? true
}: src:
(if doCheck then testRustSimple else pkgs.lib.id)
(pkgs.buildRustCrate ({
pname = name;
version = "1.0.0";
crateName = name;
crateBin = [ name ];
dependencies = dependencies;
src = pkgs.runCommandLocal "write-main.rs"
{
src = src;
passAsFile = [ "src" ];
} ''
mkdir -p $out/src/bin
cp "$srcPath" $out/src/bin/${name}.rs
find $out
'';
}));
# Build a rust library, that can be used as dependency to `rustSimple`.
# Wrapper around `pkgs.buildRustCrate`, takes all its arguments.
rustSimpleLib =
{ name
, dependencies ? [ ]
, doCheck ? true
,
}: src:
(if doCheck then testRustSimple else pkgs.lib.id)
(pkgs.buildRustCrate ({
pname = name;
version = "1.0.0";
crateName = name;
dependencies = dependencies;
src = pkgs.runCommandLocal "write-lib.rs"
{
src = src;
passAsFile = [ "src" ];
} ''
mkdir -p $out/src
cp "$srcPath" $out/src/lib.rs
find $out
'';
}));
/* Takes a `buildRustCrate` derivation as an input,
* builds it with `{ buildTests = true; }` and runs
* all tests found in its `tests` dir. If they are
* all successful, `$out` will point to the crate
* built with `{ buildTests = false; }`, otherwise
* it will fail to build.
*
* See also `nix.drvSeqL` which is used to implement
* this behavior.
*/
testRustSimple = rustDrv:
let
crate = buildTests: rustDrv.override { inherit buildTests; };
tests = depot.nix.runExecline.local "${rustDrv.name}-tests-run" { } [
"importas"
"out"
"out"
"if"
[
"pipeline"
[ bins.s6-ls "${crate true}/tests" ]
"forstdin"
"-o0"
"test"
"importas"
"test"
"test"
"${crate true}/tests/$test"
]
bins.s6-touch
"$out"
];
in
depot.nix.drvSeqL [ tests ] (crate false);
in
{
inherit
rustSimple
rustSimpleBin
rustSimpleLib
;
}

View file

@ -1,76 +0,0 @@
{ depot, pkgs, ... }:
let
inherit (depot.nix.writers)
rustSimple
rustSimpleLib
rustSimpleBin
;
inherit (pkgs)
coreutils
;
run = drv: depot.nix.runExecline.local "run-${drv.name}" { } [
"if"
[ drv ]
"importas"
"out"
"out"
"${coreutils}/bin/touch"
"$out"
];
rustTransitiveLib = rustSimpleLib
{
name = "transitive";
} ''
pub fn transitive(s: &str) -> String {
let mut new = s.to_string();
new.push_str(" 1 2 3");
new
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transitive() {
assert_eq!(transitive("foo").as_str(), "foo 1 2 3")
}
}
'';
rustTestLib = rustSimpleLib
{
name = "test_lib";
dependencies = [ rustTransitiveLib ];
} ''
extern crate transitive;
use transitive::{transitive};
pub fn test() -> String {
transitive("test")
}
'';
rustWithLib = run (rustSimple
{
name = "rust-with-lib";
dependencies = [ rustTestLib ];
} ''
extern crate test_lib;
fn main() {
assert_eq!(test_lib::test(), String::from("test 1 2 3"));
}
'');
in
depot.nix.readTree.drvTargets {
inherit
rustTransitiveLib
rustWithLib
;
}

View file

@ -1,88 +0,0 @@
yants
=====
This is a tiny type-checker for data in Nix, written in Nix.
# Features
* Checking of primitive types (`int`, `string` etc.)
* Checking polymorphic types (`option`, `list`, `either`)
* Defining & checking struct/record types
* Defining & matching enum types
* Defining & matching sum types
* Defining function signatures (including curried functions)
* Types are composable! `option string`! `list (either int (option float))`!
* Type errors also compose!
Currently lacking:
* Any kind of inference
* Convenient syntax for attribute-set function signatures
## Primitives & simple polymorphism
![simple](/about/nix/yants/screenshots/simple.png)
## Structs
![structs](/about/nix/yants/screenshots/structs.png)
## Nested structs!
![nested structs](/about/nix/yants/screenshots/nested-structs.png)
## Enums!
![enums](/about/nix/yants/screenshots/enums.png)
## Functions!
![functions](/about/nix/yants/screenshots/functions.png)
# Usage
Yants can be imported from its `default.nix`. A single attribute (`lib`) can be
passed, which will otherwise be imported from `<nixpkgs>`.
TIP: You do not need to clone the entire TVL repository to use Yants!
You can clone just this project through josh: `git clone
https://code.tvl.fyi/depot.git:/nix/yants.git`
Examples for the most common import methods would be:
1. Import into scope with `with`:
```nix
with (import ./default.nix {});
# ... Nix code that uses yants ...
```
2. Import as a named variable:
```nix
let yants = import ./default.nix {};
in yants.string "foo" # or other uses ...
````
3. Overlay into `pkgs.lib`:
```nix
# wherever you import your package set (e.g. from <nixpkgs>):
import <nixpkgs> {
overlays = [
(self: super: {
lib = super.lib // { yants = import ./default.nix { inherit (super) lib; }; };
})
];
}
# yants now lives at lib.yants, besides the other library functions!
```
Please see my [Nix one-pager](https://github.com/tazjin/nix-1p) for more generic
information about the Nix language and what the above constructs mean.
# Stability
The current API of Yants is **not yet** considered stable, but it works fine and
should continue to do so even if used at an older version.
Yants' tests use Nix versions above 2.2 - compatibility with older versions is
not guaranteed.

View file

@ -1,368 +0,0 @@
# Copyright 2019 Google LLC
# SPDX-License-Identifier: Apache-2.0
#
# Provides a "type-system" for Nix that provides various primitive &
# polymorphic types as well as the ability to define & check records.
#
# All types (should) compose as expected.
{ lib ? (import <nixpkgs> { }).lib, ... }:
with builtins; let
prettyPrint = lib.generators.toPretty { };
# typedef' :: struct {
# name = string;
# checkType = function; (a -> result)
# checkToBool = option function; (result -> bool)
# toError = option function; (a -> result -> string)
# def = option any;
# match = option function;
# } -> type
# -> (a -> b)
# -> (b -> bool)
# -> (a -> b -> string)
# -> type
#
# This function creates an attribute set that acts as a type.
#
# It receives a type name, a function that is used to perform a
# check on an arbitrary value, a function that can translate the
# return of that check to a boolean that informs whether the value
# is type-conformant, and a function that can construct error
# messages from the check result.
#
# This function is the low-level primitive used to create types. For
# many cases the higher-level 'typedef' function is more appropriate.
typedef' =
{ name
, checkType
, checkToBool ? (result: result.ok)
, toError ? (_: result: result.err)
, def ? null
, match ? null
}: {
inherit name checkToBool toError;
# check :: a -> bool
#
# This function is used to determine whether a given type is
# conformant.
check = value: checkToBool (checkType value);
# checkType :: a -> struct { ok = bool; err = option string; }
#
# This function checks whether the passed value is type conformant
# and returns an optional type error string otherwise.
inherit checkType;
# __functor :: a -> a
#
# This function checks whether the passed value is type conformant
# and throws an error if it is not.
#
# The name of this function is a special attribute in Nix that
# makes it possible to execute a type attribute set like a normal
# function.
__functor = self: value:
let result = self.checkType value;
in if checkToBool result then value
else throw (toError value result);
};
typeError = type: val:
"expected type '${type}', but value '${prettyPrint val}' is of type '${typeOf val}'";
# typedef :: string -> (a -> bool) -> type
#
# typedef is the simplified version of typedef' which uses a default
# error message constructor.
typedef = name: check: typedef' {
inherit name;
checkType = v:
let res = check v;
in {
ok = res;
} // (lib.optionalAttrs (!res) {
err = typeError name v;
});
};
checkEach = name: t: l: foldl'
(acc: e:
let
res = t.checkType e;
isT = t.checkToBool res;
in
{
ok = acc.ok && isT;
err =
if isT
then acc.err
else acc.err + "${prettyPrint e}: ${t.toError e res}\n";
})
{ ok = true; err = "expected type ${name}, but found:\n"; }
l;
in
lib.fix (self: {
# Primitive types
any = typedef "any" (_: true);
unit = typedef "unit" (v: v == { });
int = typedef "int" isInt;
bool = typedef "bool" isBool;
float = typedef "float" isFloat;
string = typedef "string" isString;
path = typedef "path" (x: typeOf x == "path");
drv = typedef "derivation" (x: isAttrs x && x ? "type" && x.type == "derivation");
function = typedef "function" (x: isFunction x || (isAttrs x && x ? "__functor"
&& isFunction x.__functor));
# Type for types themselves. Useful when defining polymorphic types.
type = typedef "type" (x:
isAttrs x
&& hasAttr "name" x && self.string.check x.name
&& hasAttr "checkType" x && self.function.check x.checkType
&& hasAttr "checkToBool" x && self.function.check x.checkToBool
&& hasAttr "toError" x && self.function.check x.toError
);
# Polymorphic types
option = t: typedef' rec {
name = "option<${t.name}>";
checkType = v:
let res = t.checkType v;
in {
ok = isNull v || (self.type t).checkToBool res;
err = "expected type ${name}, but value does not conform to '${t.name}': "
+ t.toError v res;
};
};
eitherN = tn: typedef "either<${concatStringsSep ", " (map (x: x.name) tn)}>"
(x: any (t: (self.type t).check x) tn);
either = t1: t2: self.eitherN [ t1 t2 ];
list = t: typedef' rec {
name = "list<${t.name}>";
checkType = v:
if isList v
then checkEach name (self.type t) v
else {
ok = false;
err = typeError name v;
};
};
attrs = t: typedef' rec {
name = "attrs<${t.name}>";
checkType = v:
if isAttrs v
then checkEach name (self.type t) (attrValues v)
else {
ok = false;
err = typeError name v;
};
};
# Structs / record types
#
# Checks that all fields match their declared types, no optional
# fields are missing and no unexpected fields occur in the struct.
#
# Anonymous structs are supported (e.g. for nesting) by omitting the
# name.
#
# TODO: Support open records?
struct =
# Struct checking is more involved than the simpler types above.
# To make the actual type definition more readable, several
# helpers are defined below.
let
# checkField checks an individual field of the struct against
# its definition and creates a typecheck result. These results
# are aggregated during the actual checking.
checkField = def: name: value:
let result = def.checkType value; in rec {
ok = def.checkToBool result;
err =
if !ok && isNull value
then "missing required ${def.name} field '${name}'\n"
else "field '${name}': ${def.toError value result}\n";
};
# checkExtraneous determines whether a (closed) struct contains
# any fields that are not part of the definition.
checkExtraneous = def: has: acc:
if (length has) == 0 then acc
else if (hasAttr (head has) def)
then checkExtraneous def (tail has) acc
else
checkExtraneous def (tail has) {
ok = false;
err = acc.err + "unexpected struct field '${head has}'\n";
};
# checkStruct combines all structure checks and creates one
# typecheck result from them
checkStruct = def: value:
let
init = { ok = true; err = ""; };
extraneous = checkExtraneous def (attrNames value) init;
checkedFields = map
(n:
let v = if hasAttr n value then value."${n}" else null;
in checkField def."${n}" n v)
(attrNames def);
combined = foldl'
(acc: res: {
ok = acc.ok && res.ok;
err = if !res.ok then acc.err + res.err else acc.err;
})
init
checkedFields;
in
{
ok = combined.ok && extraneous.ok;
err = combined.err + extraneous.err;
};
struct' = name: def: typedef' {
inherit name def;
checkType = value:
if isAttrs value
then (checkStruct (self.attrs self.type def) value)
else { ok = false; err = typeError name value; };
toError = _: result: "expected '${name}'-struct, but found:\n" + result.err;
};
in
arg: if isString arg then (struct' arg) else (struct' "anon" arg);
# Enums & pattern matching
enum =
let
plain = name: def: typedef' {
inherit name def;
checkType = (x: isString x && elem x def);
checkToBool = x: x;
toError = value: _: "'${prettyPrint value} is not a member of enum ${name}";
};
enum' = name: def: lib.fix (e: (plain name def) // {
match = x: actions: deepSeq (map e (attrNames actions)) (
let
actionKeys = attrNames actions;
missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] def;
in
if (length missing) > 0
then throw "Missing match action for members: ${prettyPrint missing}"
else actions."${e x}"
);
});
in
arg: if isString arg then (enum' arg) else (enum' "anon" arg);
# Sum types
#
# The representation of a sum type is an attribute set with only one
# value, where the key of the value denotes the variant of the type.
sum =
let
plain = name: def: typedef' {
inherit name def;
checkType = (x:
let variant = elemAt (attrNames x) 0;
in if isAttrs x && length (attrNames x) == 1 && hasAttr variant def
then
let
t = def."${variant}";
v = x."${variant}";
res = t.checkType v;
in
if t.checkToBool res
then { ok = true; }
else {
ok = false;
err = "while checking '${name}' variant '${variant}': "
+ t.toError v res;
}
else { ok = false; err = typeError name x; }
);
};
sum' = name: def: lib.fix (s: (plain name def) // {
match = x: actions:
let
variant = deepSeq (s x) (elemAt (attrNames x) 0);
actionKeys = attrNames actions;
defKeys = attrNames def;
missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] defKeys;
in
if (length missing) > 0
then throw "Missing match action for variants: ${prettyPrint missing}"
else actions."${variant}" x."${variant}";
});
in
arg: if isString arg then (sum' arg) else (sum' "anon" arg);
# Typed function definitions
#
# These definitions wrap the supplied function in type-checking
# forms that are evaluated when the function is called.
#
# Note that typed functions themselves are not types and can not be
# used to check values for conformity.
defun =
let
mkFunc = sig: f: {
inherit sig;
__toString = self: foldl' (s: t: "${s} -> ${t.name}")
"λ :: ${(head self.sig).name}"
(tail self.sig);
__functor = _: f;
};
defun' = sig: func:
if length sig > 2
then mkFunc sig (x: defun' (tail sig) (func ((head sig) x)))
else mkFunc sig (x: ((head (tail sig)) (func ((head sig) x))));
in
sig: func:
if length sig < 2
then (throw "Signature must at least have two types (a -> b)")
else defun' sig func;
# Restricting types
#
# `restrict` wraps a type `t`, and uses a predicate `pred` to further
# restrict the values, giving the restriction a descriptive `name`.
#
# First, the wrapped type definition is checked (e.g. int) and then the
# value is checked with the predicate, so the predicate can already
# depend on the value being of the wrapped type.
restrict = name: pred: t:
let restriction = "${t.name}[${name}]"; in typedef' {
name = restriction;
checkType = v:
let res = t.checkType v;
in
if !(t.checkToBool res)
then res
else
let
iok = pred v;
in
if isBool iok then {
ok = iok;
err = "${prettyPrint v} does not conform to restriction '${restriction}'";
} else
# use throw here to avoid spamming the build log
throw "restriction '${restriction}' predicate returned unexpected value '${prettyPrint iok}' instead of boolean";
};
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View file

@ -1,158 +0,0 @@
{ depot, pkgs, ... }:
with depot.nix.yants;
# Note: Derivations are not included in the tests below as they cause
# issues with deepSeq.
let
inherit (depot.nix.runTestsuite)
runTestsuite
it
assertEq
assertThrows
assertDoesNotThrow
;
# this derivation won't throw if evaluated with deepSeq
# unlike most things even remotely related with nixpkgs
trivialDerivation = derivation {
name = "trivial-derivation";
inherit (pkgs.stdenv) system;
builder = "/bin/sh";
args = [ "-c" "echo hello > $out" ];
};
testPrimitives = it "checks that all primitive types match" [
(assertDoesNotThrow "unit type" (unit { }))
(assertDoesNotThrow "int type" (int 15))
(assertDoesNotThrow "bool type" (bool false))
(assertDoesNotThrow "float type" (float 13.37))
(assertDoesNotThrow "string type" (string "Hello!"))
(assertDoesNotThrow "function type" (function (x: x * 2)))
(assertDoesNotThrow "path type" (path /nix))
(assertDoesNotThrow "derivation type" (drv trivialDerivation))
];
testPoly = it "checks that polymorphic types work as intended" [
(assertDoesNotThrow "option type" (option int null))
(assertDoesNotThrow "list type" (list string [ "foo" "bar" ]))
(assertDoesNotThrow "either type" (either int float 42))
];
# Test that structures work as planned.
person = struct "person" {
name = string;
age = int;
contact = option (struct {
email = string;
phone = option string;
});
};
testStruct = it "checks that structures work as intended" [
(assertDoesNotThrow "person struct" (person {
name = "Brynhjulf";
age = 42;
contact.email = "brynhjulf@yants.nix";
}))
];
# Test enum definitions & matching
colour = enum "colour" [ "red" "blue" "green" ];
colourMatcher = {
red = "It is in fact red!";
blue = "It should not be blue!";
green = "It should not be green!";
};
testEnum = it "checks enum definitions and matching" [
(assertEq "enum is matched correctly"
"It is in fact red!"
(colour.match "red" colourMatcher))
(assertThrows "out of bounds enum fails"
(colour.match "alpha" (colourMatcher // {
alpha = "This should never happen";
}))
)
];
# Test sum type definitions
creature = sum "creature" {
human = struct {
name = string;
age = option int;
};
pet = enum "pet" [ "dog" "lizard" "cat" ];
};
some-human = creature {
human = {
name = "Brynhjulf";
age = 42;
};
};
testSum = it "checks sum types definitions and matching" [
(assertDoesNotThrow "creature sum type" some-human)
(assertEq "sum type is matched correctly"
"It's a human named Brynhjulf"
(creature.match some-human {
human = v: "It's a human named ${v.name}";
pet = v: "It's not supposed to be a pet!";
})
)
];
# Test curried function definitions
func = defun [ string int string ]
(name: age: "${name} is ${toString age} years old");
testFunctions = it "checks function definitions" [
(assertDoesNotThrow "function application" (func "Brynhjulf" 42))
];
# Test that all types are types.
assertIsType = name: t:
assertDoesNotThrow "${name} is a type" (type t);
testTypes = it "checks that all types are types" [
(assertIsType "any" any)
(assertIsType "bool" bool)
(assertIsType "drv" drv)
(assertIsType "float" float)
(assertIsType "int" int)
(assertIsType "string" string)
(assertIsType "path" path)
(assertIsType "attrs int" (attrs int))
(assertIsType "eitherN [ ... ]" (eitherN [ int string bool ]))
(assertIsType "either int string" (either int string))
(assertIsType "enum [ ... ]" (enum [ "foo" "bar" ]))
(assertIsType "list string" (list string))
(assertIsType "option int" (option int))
(assertIsType "option (list string)" (option (list string)))
(assertIsType "struct { ... }" (struct { a = int; b = option string; }))
(assertIsType "sum { ... }" (sum { a = int; b = option string; }))
];
testRestrict = it "checks restrict types" [
(assertDoesNotThrow "< 42" ((restrict "< 42" (i: i < 42) int) 25))
(assertDoesNotThrow "list length < 3"
((restrict "not too long" (l: builtins.length l < 3) (list int)) [ 1 2 ]))
(assertDoesNotThrow "list eq 5"
(list (restrict "eq 5" (v: v == 5) any) [ 5 5 5 ]))
];
in
runTestsuite "yants" [
testPrimitives
testPoly
testStruct
testEnum
testSum
testFunctions
testTypes
testRestrict
]