~dblsaiko/nix-corelib

9b8acf4e5e54175272e581e9a3fdc572513549c4 — Marco Rebhan 1 year, 6 months ago
Initial commit
9 files changed, 462 insertions(+), 0 deletions(-)

A .editorconfig
A flake.nix
A fmt
A lib/default.nix
A lib/fs.nix
A lib/list.nix
A lib/mod-tree.nix
A lib/set.nix
A lib/string.nix
A  => .editorconfig +5 -0
@@ 1,5 @@
root = true

[*.nix]
indent_size = 2
indent_style = space

A  => flake.nix +5 -0
@@ 1,5 @@
{
  outputs = {self}: {
    lib = import lib/mod-tree.nix ./lib "lib" {};
  };
}

A  => fmt +4 -0
@@ 1,4 @@
#!/usr/bin/env nix-shell
#!nix-shell -i sh -p alejandra

exec alejandra .

A  => lib/default.nix +52 -0
@@ 1,52 @@
{use, ...}: let
  fs = use ./fs.nix;
  list = use ./list.nix;
  set = use ./set.nix;
  string = use ./string.nix;

  # Returns the smaller of `a` and `b`.
  #
  # min :: int -> int -> int
  min = a: b:
    if a < b
    then a
    else b;

  # Bootstraps a tree of modules like this one. The top module gets passed the
  # arguments passed in `args` plus two extra arguments, `use` and one with the
  # name passed to `name`. `use` is a function which loads a submodule by path
  # and evaluates it. This is similar to `import` but automatically passes all
  # the arguments the top module received. With the argument named as `name`,
  # a submodule may refer to other modules in the tree without having to import
  # it itself.
  #
  # This function is similar to nixpkgs's mkDerivation, but does not allow
  # passing extra arguments for each module, and is more designed for nested
  # module trees as opposed to making one big attrset.
  #
  # Arguments:
  #   top  - the root file of the tree
  #   name - the name of the attribute passed as argument to all the modules
  #          referring to the root of the tree
  #   args - the arguments passed in from the outside to the modules
  #
  # Example:
  #     let
  #       util = modTree ./util "util";
  #       util' = util {inherit nixpkgs;};
  #     in
  #       util.string.chars "Hello, World!"
  #
  # modTree :: top: path -> name: str -> args: {...} -> {...}
  modTree = import ./mod-tree.nix;

  # Bootstraps a tree of modules like this one. Like modTree, but the name is
  # inferred as the name of the top file/directory minus its extension.
  #
  # modTree' :: top: path -> args: {...} -> {...}
  modTree' = top: modTree top (fs.fileStem top);
in {
  inherit fs list set string;

  inherit min modTree modTree';
}

A  => lib/fs.nix +52 -0
@@ 1,52 @@
{lib, ...}: let
  inherit (builtins) concatMap readDir;
  inherit (lib.set) groupByValue;
  inherit (lib.list) associateWith dropWhile;
  inherit (lib.string) rsplitOnce;

  walkDir = dir: let
    entries = groupByValue (readDir dir);
  in
    entries.regular
    or []
    ++ concatMap (
      subdir:
        map (entry: "${subdir}/${entry}") (walkDir (dir + "/${subdir}"))
    ) (entries.directory or []);

  dirlistCustom = {filenameFilter ? path: type: true, ...}: dir:
    map (x: dir + "/${x}") (walkDir dir);
  dirlist = dir: dirlistCustom {} dir;

  # for now, only remove "." from the beginning, add more when needed
  sanitizeName = name: dropWhile (ch: ch == ".") name;

  etcTree = dir:
    associateWith (file: {
      source = builtins.path {
        path = dir + "/${file}";
        name = sanitizeName (baseNameOf file);
      };
    }) (walkDir dir);

  fileStem = path: let
    baseName = baseNameOf path;
    parts = rsplitOnce "." baseName;
  in
    if parts == null
    then baseName
    else if parts.left == ""
    then baseName
    else parts.left;

  extension = path: let
    parts = rsplitOnce "." (baseNameOf path);
  in
    if parts == null
    then null
    else if parts.left == ""
    then null
    else parts.right;
in {
  inherit walkDir dirlistCustom dirlist sanitizeName etcTree fileStem extension;
}

A  => lib/list.nix +182 -0
@@ 1,182 @@
{lib, ...}: let
  inherit (builtins) any elemAt foldl' genList length listToAttrs;
  inherit (lib) min;
  inherit (lib.set) mapNames;

  # Returns a list containing the numbers from `from` up to excluding `to`.
  #
  # range :: int -> int -> [int]
  range = from: to: genList (x: x + from) (to - from);

  # Zips together `l1` and `l2` into one list containing attrsets with the n-th
  # element of `l1` and the n-th element of `l2` in the n-th element of the
  # resulting list. If one list is shorter than the other, then the resulting
  # list will have as many elements as the shorter one and the elements from the
  # other list are discarded.
  #
  # Example:
  #   zip [1 2] [3 4] == [{left = 1; right = 3;} {left = 2; right = 4;}]
  #
  # zip :: [a] -> [b] -> [{left: a; right: b;}]
  zip = l1: l2:
    map (idx: {
      left = elemAt l1 idx;
      right = elemAt l2 idx;
    })
    (range 0 (min (length l1) (length l2)));

  # Takes a list, returning a new list with each element of the list accompanied
  # by its position in the list.
  #
  # Example:
  #   enumerate ["a" "b"] == [{idx = 0; value = "a";} {idx = 1; value = "b";}]
  #
  # enumerate: [a] -> [{idx: int; value: a;}]
  enumerate = list:
    map (el:
      mapNames (name:
        {
          "left" = "idx";
          "right" = "value";
        }
        .${name})
      el)
    (zip (range 0 (length list)) list);

  # Takes a list and create a set with entries determined by toName and toValue.
  #
  # associate :: (a -> str) -> (a -> b) -> [a] -> {*: b;}
  associate = toName: toValue: list:
    listToAttrs (map (el: {
        name = toName el;
        value = toValue el;
      })
      list);

  # Takes a list, returning a new attrset with the entries in the list becoming
  # attribute names, and their values determined by `f`.
  #
  # associateWith :: (str -> a) -> [str] -> {*: a;}
  associateWith = f: list: associate (el: el) f list;

  # Takes a list, returning a new attrset with the entries in the list becoming
  # attribute values, and their names determined by `f`.
  #
  # associateBy :: (a -> str) -> [a] -> {*: a;}
  associateBy = f: list: associate f (el: el) list;

  # Finds an element in `list` matching the predicate `f` and returns it. If no
  # element is found, returns `null`.
  #
  # find :: (a -> bool) -> [a] -> a?
  find = f: list: let
    findFrom = idx:
      if idx >= (length list)
      then null
      else let
        current = elemAt list idx;
      in
        if f current
        then current
        else findFrom (idx + 1);
  in
    findFrom 0;

  # Finds an element in `list` matching the predicate `f` and returns it. If no
  # element is found, returns `null`. Traverses the list in reverse order.
  #
  # rfind :: (a -> bool) -> [a] -> a?
  rfind = f: list: find f (reverse list);

  # Finds the index of the first element in `list` matching the predicate `f`.
  # If no such element is found, returns `null`.
  #
  # position :: (a -> bool) -> [a] -> int?
  position = f: list: let
    result = find ({
      idx,
      value,
    }:
      f value) (enumerate list);
  in
    if result == null
    then null
    else result.idx;

  # Finds the index of the last element in `list` matching the predicate `f`.
  # If no such element is found, returns `null`.
  #
  # rposition :: (a -> bool) -> [a] -> int?
  rposition = f: list: let
    result = rfind ({
      idx,
      value,
    }:
      f value) (enumerate list);
  in
    if result == null
    then null
    else result.idx;

  # Returns `list` in reverse order.
  #
  # reverse :: [a] -> [a]
  reverse = list:
    map (idx: elemAt list idx) (genList
      (x: length list - x - 1) (length list));

  # Returns whether `list` contains `el`.
  #
  # contains :: a -> [a] -> bool
  contains = el: list: any (x: el == x) list;

  # Returns `list` with all duplicate elements removed. Each element will keep
  # its first position in the list.
  #
  # distinct :: [a] -> [a]
  distinct = list:
    foldl' (
      acc: el:
        if contains el acc
        then acc
        else acc ++ [el]
    ) []
    list;

  # Returns list with all duplicate elements removed, as determined by `f`
  # returning the same value for two values in the list. Each element will keep
  # its first position in the list.
  #
  # distinctBy :: (a -> b) -> [a] -> a
  distinctBy = f: list:
    (foldl' (
        acc @ {
          out,
          keys,
        }: el:
          if contains (f el) keys
          then acc
          else {
            out = out ++ [el];
            keys = keys ++ [(f el)];
          }
      ) {
        out = [];
        keys = [];
      }
      list)
    .out;

  # Concatenates all lists in `list` together.
  #
  # flatten :: [[a]] -> [a]
  flatten = list: foldl' (acc: el: acc ++ el) [] list;

  # Maps each element of `list` to a list, concatenating all the resulting lists
  # together.
  #
  # flatMap :: (a -> [b]) -> [a] -> [b]
  flatMap = f: list: flatten (map f list);
in {
  inherit range zip enumerate associate associateWith associateBy find rfind position rposition reverse contains distinct distinctBy flatten flatMap;
}

A  => lib/mod-tree.nix +26 -0
@@ 1,26 @@
top: name: args: let
  inherit (builtins) attrNames filter foldl' functionArgs listToAttrs stringLength substring;

  use = path: let
    fn = import path;
    inputs = functionArgs fn;
    neededInputs = filter (el: !inputs.${el}) (attrNames inputs);
    missingInputs = filter (el: !(args' ? ${el})) neededInputs;
    missingInputs' = let
      x = foldl' (acc: a: acc + "'${a}', ") "" missingInputs;
    in
      substring 0 (stringLength x - 2) x;
  in
    if missingInputs == []
    then fn args'
    else throw "missing inputs to ${name}: ${missingInputs'}";

  args' =
    args
    // {
      inherit use;
      ${name} = top';
    };
  top' = use top;
in
  top'

A  => lib/set.nix +62 -0
@@ 1,62 @@
{lib, ...}: let
  inherit (builtins) attrNames attrValues filter foldl' hasAttr mapAttrs;
  inherit (lib.list) associate;

  # Groups attribute names of the set by their values.
  #
  # Example:
  #     groupByValue { a = "x"; b = "x"; c = "y"; d = "z"; } ==
  #       { "x" = ["a" "b"]; "y" = ["c"]; "z" = ["d"]; }
  #
  # groupByValue :: {*: str;} -> {*: [str];}
  groupByValue = attrs:
    foldl' (a: b: a // b) {} (
      map (
        value: {${value} = filter (el: attrs.${el} == value) (attrNames attrs);}
      ) (attrValues attrs)
    );

  # Maps the names in an attrset, retaining the values as before. If `f`
  # produces the same name for two attributes, one attribute is discarded.
  #
  # mapNames :: (str -> str) -> {...} -> {...}
  mapNames = f: set:
    associate (name: f name) (name: set.${name}) (attrNames set);

  # Maps the values in an attrset, retaining the names as before.
  #
  # mapValues :: (a -> b) -> {*: a;} -> {*: b;}
  mapValues = f: mapAttrs (_: f);

  # Filters `set` so that it will only contain attributes for which the
  # predicate `f` returns true.
  #
  # filterAttrs :: (str -> a -> bool) -> {*: a;}
  filterAttrs = f: set:
    removeAttrs set (filter (name: !f name set.${name}) (attrNames set));

  # Filters `set` so that it will only contain names for which the predicate `f`
  # returns true.
  #
  # filterNames :: (str -> bool) -> {*: a;}
  filterNames = f: set:
    filterAttrs (name: value: f name) set;

  # Filters `set` so that it will only contain values for which the predicate
  # `f` returns true.
  #
  # filterValues :: (a -> bool) -> {*: a;}
  filterValues = f: set:
    filterAttrs (name: value: f value) set;

  # Returns a list with the value of the attribute `name` if it exists in `set`,
  # otherwise returns the empty list.
  #
  # optional :: str -> {*: a;} -> [a]
  optional = name: set:
    if hasAttr name set
    then [set.${name}]
    else [];
in {
  inherit groupByValue mapNames mapValues filterAttrs filterNames filterValues optional;
}

A  => lib/string.nix +74 -0
@@ 1,74 @@
{lib, ...}: let
  inherit (builtins) foldl' genList stringLength substring;
  inherit (lib) list;

  # Returns windows of length `len` in `str`. Each window is a substring of
  # `len` characters, each element of the returned list starting one character
  # to the right of the previous one.
  #
  # Example:
  #   windows 3 "apple" == ["app" "ppl" "ple"]
  #
  # windows :: int -> str -> [str]
  windows = len: str:
    genList (idx: substring idx len str) (stringLength str - len + 1);

  # Returns a list containing each character in `s`.
  #
  # chars :: s: str -> [str]
  chars = windows 1;

  # Returns the index of the first character of the first occurrence of the
  # substring `pat` in `str`. If `pat` is not contained in `str`, returns null.
  #
  # position :: str -> str -> int?
  position = pat: str: list.position (el: el == pat) (windows (stringLength pat) str);

  # Returns the index of the first character of the last occurrence of the
  # substring `pat` in `str`. If `pat` is not contained in `str`, returns null.
  #
  # rposition :: str -> str -> int?
  rposition = pat: str: list.rposition (el: el == pat) (windows (stringLength pat) str);

  _splitOnce = position: pat: str: let
    index = position pat str;
  in
    if index == null
    then null
    else {
      left = substring 0 index str;
      right = substring (index + stringLength pat) (stringLength str) str;
    };

  # Splits `str` into two parts delimited by the first occurrence of `pat`.
  #
  # Example:
  #     splitOnce "://" "http://localhost" == {left = "http"; right = "localhost";}
  #
  # splitOnce :: pat: str -> str: str -> {left: str; right: str;}?
  splitOnce = _splitOnce position;

  # Splits `str` into two parts delimited by the last occurrence of `pat`.
  #
  # splitOnce :: str -> str -> {left: str; right: str;}?
  rsplitOnce = _splitOnce rposition;

  # Removes characters from the beginning of `str` while `f` is `true` for the
  # remaining string. The first string for which `f` returns `false` will be
  # returned.
  #
  # dropWhile :: (str -> bool) -> str -> str
  dropWhile = f: str:
    if stringLength str == 0
    then str
    else if f (substring 0 1 str)
    then dropWhile f (substring 1 (stringLength str - 1) str)
    else str;

  # Returns `n` copies of `str` concatenated together.
  #
  # repeat :: int -> str -> str
  repeat = n: str: foldl' (acc: a: acc + a) "" (genList (_: str) n);
in {
  inherit windows chars position rposition splitOnce rsplitOnce dropWhile repeat;
}