├── doc ├── .gitignore ├── src │ ├── lib │ │ ├── bar.nix │ │ ├── root.nix │ │ ├── rootExpanded.nix │ │ └── index.md │ ├── SUMMARY.md │ ├── examples │ │ ├── default.nix │ │ ├── nixpkgs.nix │ │ └── hello.nix │ ├── modules.md │ ├── concept │ │ ├── callable.nix │ │ └── index.md │ └── motivation.md ├── book.toml ├── default.nix └── package.nix ├── types ├── .envrc ├── .gitignore ├── default.nix ├── shell.nix ├── .github │ └── workflows │ │ └── ci.yml ├── flake.lock ├── flake.nix ├── LICENSE ├── README.md ├── lib.nix ├── types.nix └── tests.nix ├── README.md ├── flake.nix ├── default.nix ├── contrib ├── default.nix └── modules │ ├── nixpkgs │ └── default.nix │ ├── treefmt │ ├── modules │ │ ├── nixfmt.nix │ │ ├── deadnix.nix │ │ └── statix.nix │ └── default.nix │ ├── wrappers │ ├── default.nix │ └── builder.py │ └── write-files │ ├── builder.py │ └── default.nix ├── npins ├── sources.json └── default.nix ├── shell.nix ├── .github └── workflows │ └── nix.yml ├── adios ├── lib │ └── importModules.nix ├── types.nix └── default.nix └── LICENSE /doc/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /types/.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /types/.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adios - A Nix module system 2 | 3 | https://adisbladis.github.io/adios/ 4 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Adios"; 3 | outputs = { ... }: import ./.; 4 | } 5 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | rec { 2 | adios = import ./adios; 3 | adios-contrib = import ./contrib { inherit adios; }; 4 | } 5 | -------------------------------------------------------------------------------- /contrib/default.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | 3 | { 4 | name = "adios-contrib"; 5 | 6 | modules = adios.lib.importModules ./modules; 7 | } 8 | -------------------------------------------------------------------------------- /types/default.nix: -------------------------------------------------------------------------------- 1 | # Previously nixpkgs lib was required for import. 2 | # This retains the same interface. 3 | _: import ./types.nix 4 | -------------------------------------------------------------------------------- /doc/src/lib/bar.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | 3 | { 4 | name = "bar"; 5 | # Other contents omitted 6 | modules = adios.lib.importModules ./.; 7 | } 8 | -------------------------------------------------------------------------------- /doc/src/lib/root.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | 3 | { 4 | name = "root"; 5 | # Other contents omitted 6 | modules = adios.lib.importTree ./.; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /doc/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Motivation](./motivation.md) 4 | - [Concept](./concept/index.md) 5 | - [Modules](./modules.md) 6 | - [Helper functions](./lib/index.md) 7 | -------------------------------------------------------------------------------- /doc/src/examples/default.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | { 3 | modules = { 4 | hello = import ./hello.nix { inherit adios; }; 5 | nixpkgs = import ./nixpkgs.nix { inherit adios; }; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /contrib/modules/nixpkgs/default.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | 3 | { 4 | name = "nixpkgs"; 5 | 6 | options = { 7 | pkgs = { 8 | type = adios.types.attrs; 9 | }; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /doc/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["adisbladis", "llakala"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Adios" 7 | 8 | [preprocessor.cmdrun] 9 | command = "mdbook-cmdrun" 10 | -------------------------------------------------------------------------------- /doc/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | __sources ? import ../npins, 3 | pkgs ? import __sources.nixpkgs { }, 4 | }: 5 | 6 | let 7 | inherit (pkgs) callPackage; 8 | in 9 | { 10 | doc = callPackage ./package.nix { }; 11 | } 12 | -------------------------------------------------------------------------------- /doc/src/modules.md: -------------------------------------------------------------------------------- 1 | # Example modules 2 | 3 | ## Nixpkgs 4 | 5 | ```nix 6 | 7 | ``` 8 | 9 | ## Hello world 10 | 11 | ```nix 12 | 13 | ``` 14 | -------------------------------------------------------------------------------- /doc/src/examples/nixpkgs.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | let 3 | inherit (adios) types; 4 | in 5 | { 6 | options = { 7 | pkgs = { 8 | type = types.attrs; 9 | default = import { }; 10 | }; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /doc/src/lib/rootExpanded.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | 3 | { 4 | name = "root"; 5 | # Other contents omitted 6 | modules = { 7 | foo = import ./foo { inherit adios; }; 8 | bar = import ./bar { inherit adios; }; 9 | }; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /doc/src/concept/callable.nix: -------------------------------------------------------------------------------- 1 | { types }: 2 | { 3 | name = "callable-module"; 4 | 5 | options = { 6 | foo = { 7 | type = types.string; 8 | default = "foo"; 9 | }; 10 | }; 11 | 12 | impl = args: { 13 | # Evaluating someValue.bar will type check args.foo 14 | someValue.bar = args.foo; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /npins/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "pins": { 3 | "nixpkgs": { 4 | "type": "Channel", 5 | "name": "nixpkgs-unstable", 6 | "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre883899.02f2cb8e0feb/nixexprs.tar.xz", 7 | "hash": "0k4n6f873a4ls1mff6wck6z31kglgg8irwc5s3xsprrwbxdv7p58" 8 | } 9 | }, 10 | "version": 5 11 | } 12 | -------------------------------------------------------------------------------- /types/shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | 5 | let 6 | inherit (pkgs) lib; 7 | mkReadme = pkgs.writeShellScriptBin "make-readme" '' 8 | ${lib.getExe' pkgs.nixdoc "nixdoc"} --category types --description "Kororā" --file default.nix | sed s/' {#.*'/""/ > README.md 9 | ''; 10 | in 11 | 12 | pkgs.mkShell { 13 | packages = [ 14 | pkgs.nix-unit 15 | pkgs.nixdoc 16 | mkReadme 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /types/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Nix actions 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - main 9 | 10 | jobs: 11 | nix-unit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: cachix/install-nix-action@v24 16 | with: 17 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 18 | - name: Build shell 19 | run: nix develop -c true 20 | - name: Run tests 21 | run: nix develop -c nix-unit --flake .#libTests 22 | -------------------------------------------------------------------------------- /doc/package.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenvNoCC, 3 | __src ? ../., 4 | mdbook, 5 | mdbook-cmdrun, 6 | }: 7 | 8 | stdenvNoCC.mkDerivation { 9 | pname = "adios-nix-docs-html"; 10 | version = "0.1"; 11 | src = __src; 12 | nativeBuildInputs = [ 13 | mdbook 14 | mdbook-cmdrun 15 | ]; 16 | 17 | dontConfigure = true; 18 | dontFixup = true; 19 | 20 | env.RUST_BACKTRACE = 1; 21 | 22 | buildPhase = '' 23 | runHook preBuild 24 | cd doc 25 | mdbook build 26 | runHook postBuild 27 | ''; 28 | 29 | installPhase = '' 30 | runHook preInstall 31 | mv book $out 32 | runHook postInstall 33 | ''; 34 | } 35 | -------------------------------------------------------------------------------- /contrib/modules/treefmt/modules/nixfmt.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | { 3 | name = "treefmt-nixfmt"; 4 | 5 | options = { 6 | package = { 7 | type = adios.types.derivation; 8 | defaultFunc = { inputs }: inputs."nixpkgs".pkgs.nixfmt; 9 | }; 10 | }; 11 | 12 | inputs = { 13 | "nixpkgs" = { 14 | path = "/nixpkgs"; 15 | }; 16 | }; 17 | 18 | impl = 19 | { options, inputs }: 20 | let 21 | inherit (inputs."nixpkgs") pkgs; 22 | inherit (pkgs) lib; 23 | in 24 | { 25 | name = "nixfmt"; 26 | treefmt = { 27 | command = lib.getExe options.package; 28 | includes = [ "*.nix" ]; 29 | }; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /types/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1754214453, 6 | "narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /doc/src/examples/hello.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | let 3 | inherit (adios) types; 4 | in 5 | { 6 | options = { 7 | enable = { 8 | type = types.bool; 9 | default = false; 10 | }; 11 | 12 | package = { 13 | type = types.derivation; 14 | defaultFunc = { inputs }: inputs."nixpkgs".pkgs.hello; 15 | }; 16 | }; 17 | 18 | inputs = { 19 | nixpkgs = { 20 | path = "/nixpkgs"; 21 | }; 22 | }; 23 | 24 | impl = 25 | { 26 | options, 27 | inputs, 28 | }: 29 | let 30 | inherit (inputs.nixpkgs.pkgs) lib; 31 | in 32 | lib.optionalAttrs options.enable { 33 | packages = [ 34 | options.package 35 | ]; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /contrib/modules/treefmt/modules/deadnix.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | { 3 | name = "treefmt-deadnix"; 4 | 5 | options = { 6 | package = { 7 | type = adios.types.derivation; 8 | defaultFunc = { inputs }: inputs."nixpkgs".pkgs.deadnix; 9 | }; 10 | }; 11 | 12 | inputs = { 13 | "nixpkgs" = { 14 | path = "/nixpkgs"; 15 | }; 16 | }; 17 | 18 | impl = 19 | { options, inputs }: 20 | let 21 | inherit (inputs."nixpkgs") pkgs; 22 | inherit (pkgs) lib; 23 | in 24 | { 25 | name = "deadnix"; 26 | treefmt = { 27 | command = lib.getExe options.package; 28 | options = [ "--edit" ]; 29 | includes = [ "*.nix" ]; 30 | }; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /types/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A simple & fast Nix type system implemented in Nix"; 3 | 4 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 5 | 6 | outputs = 7 | { nixpkgs }: 8 | ( 9 | let 10 | inherit (nixpkgs) lib; 11 | forAllSystems = lib.genAttrs lib.systems.flakeExposed; 12 | in 13 | { 14 | libTests = import ./tests.nix { inherit lib; }; 15 | lib = 16 | let 17 | types = import ./default.nix { inherit lib; }; 18 | in 19 | types 20 | // { 21 | inherit types; 22 | }; 23 | 24 | devShells = forAllSystems ( 25 | system: 26 | let 27 | pkgs = nixpkgs.legacyPackages.${system}; 28 | in 29 | { 30 | default = pkgs.callPackage ./shell.nix { }; 31 | } 32 | ); 33 | } 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /doc/src/motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | Adios aims to be a radically simple alternative to the NixOS module system that solves many of it's design problems. 4 | Modules are contracts that are typed using the [Korora](https://github.com/adisbladis/korora) type system. 5 | 6 | ## NixOS module system problems 7 | 8 | - Lack of flexibility 9 | 10 | NixOS modules aren't reusable outside of a NixOS context. 11 | The goal is to have modules that can be reused just as easily on a MacOS machine as in a Linux development shell. 12 | 13 | - Global namespace 14 | 15 | The NixOS module system is a single global namespace where any module can affect any other module. 16 | 17 | - Resource overhead 18 | 19 | Because of how NixOS modules are evaluated, each evaluation has no memoisation from a previous one. 20 | This has the effect of very high memory usage. 21 | 22 | Adios modules are designed to take advantage of lazy evaluation and memoisation. 23 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import __sources.nixpkgs { }, 3 | __sources ? import ./npins, 4 | }: 5 | 6 | let 7 | inherit (import ./.) adios adios-contrib; 8 | 9 | treefmt = 10 | let 11 | # Load a module definition tree. 12 | # This type checks modules and provides the tree API. 13 | tree = adios adios-contrib; 14 | 15 | # Apply options to tree 16 | eval = tree.eval { 17 | options = { 18 | "/nixpkgs" = { 19 | inherit pkgs; 20 | }; 21 | }; 22 | }; 23 | 24 | # Call treefmt contracts with applied pkgs 25 | treefmt = eval.root.modules.treefmt; 26 | fmts = treefmt.modules; 27 | in 28 | (treefmt { 29 | projectRootFile = "flake.nix"; 30 | formatters = [ 31 | (fmts.nixfmt { }) 32 | (fmts.deadnix { }) 33 | (fmts.statix { }) 34 | ]; 35 | }).package; 36 | 37 | in 38 | pkgs.mkShell { 39 | packages = [ 40 | pkgs.npins 41 | pkgs.nix-unit 42 | treefmt 43 | pkgs.mdbook 44 | pkgs.mdbook-cmdrun 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /types/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adam "adisbladis" Hoese 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Nix 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | jobs: 15 | nix-shell: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: cachix/install-nix-action@v30 19 | with: 20 | nix_path: nixpkgs=channel:nixos-unstable 21 | - uses: actions/checkout@v4.2.2 22 | - name: Run build 23 | run: nix-shell --run true 24 | 25 | build-pages: 26 | if: github.ref == 'refs/heads/master' 27 | runs-on: ubuntu-latest 28 | needs: 29 | - nix-shell 30 | steps: 31 | - uses: cachix/install-nix-action@v30 32 | with: 33 | nix_path: nixpkgs=channel:nixos-unstable 34 | - uses: actions/checkout@v4.2.2 35 | - name: Run build 36 | run: nix-build ./doc -A doc 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: ./result 41 | 42 | deploy-pages: 43 | if: github.ref == 'refs/heads/master' 44 | needs: build-pages 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v4 53 | -------------------------------------------------------------------------------- /doc/src/concept/index.md: -------------------------------------------------------------------------------- 1 | # Concept 2 | 3 | ## Modules 4 | 5 | ### Module definition 6 | 7 | Adios module definitions are plain Nix attribute sets. 8 | 9 | ### Module loading 10 | 11 | The module definition then needs to be _loaded_ by the adios loader function: 12 | ``` nix 13 | adios { 14 | name = "my-module"; 15 | } 16 | ``` 17 | Module loading is responsible for 18 | 19 | - Wrapping the module definition with a type checker 20 | 21 | Module definitions are strictly typed and checked. 22 | 23 | - Wrapping of module definitions `impl` function that provides type checking. 24 | 25 | ### Callable modules 26 | 27 | Callable modules are modules with an `impl` function that takes an attrset with their arguments defined in `options`: 28 | ``` nix 29 | {{#include callable.nix}} 30 | ``` 31 | 32 | Note that module returns are not type checked. 33 | It is expected to pass the return value of a module into another module until you have a value that can be consumed. 34 | 35 | ### Laziness 36 | 37 | Korora does eager evaluation when type checking values. 38 | Adios module type checking however is lazily, with some caveats: 39 | 40 | - Each option, type, test, etc returned by a module are checked on-access 41 | 42 | - When calling a module each passed option is checked lazily 43 | 44 | But defined `struct`'s, `listOf` etc thunks will be forced. 45 | It's best for options definitions to contain a minimal interface to minimize the overhead of eager evaluation. 46 | -------------------------------------------------------------------------------- /doc/src/lib/index.md: -------------------------------------------------------------------------------- 1 | # Helper functions 2 | 3 | ## `importModules` 4 | 5 | Adios comes with a function `importModules`, that will automatically import all the modules in a directory (provided they 6 | follow a certain schema). 7 | 8 | ### Usage 9 | 10 | Given this directory structure: 11 | 12 | ``` 13 | ./modules 14 | ├── default.nix 15 | ├── foo 16 | │   └── default.nix 17 | └── bar 18 | ├── baz 19 | │   └── default.nix 20 | └── default.nix 21 | ``` 22 | 23 | If the root module at `default.nix` is defined like this: 24 | ```nix 25 | {{#include root.nix}} 26 | ``` 27 | 28 | Then `importTree` will generate: 29 | ```nix 30 | {{#include rootExpanded.nix}} 31 | ``` 32 | 33 | Notably, `importModules` is _not_ recursive - the `baz/` module was completely ignored. If the `bar` module wants to 34 | depend on another module defined within its folder, it should import those modules itself, like this: 35 | ```nix 36 | {{#include bar.nix}} 37 | ``` 38 | 39 | ### Limitations 40 | 41 | `importTree` expects all modules to: 42 | - Either be defined as: 43 | - a subfolder, under `$MODULE_NAME/default.nix`. 44 | - a Nix file, under `$MODULE_NAME.nix` (excluding `default.nix`). 45 | - take `{ adios }:` as the file's inputs 46 | - use the same name as the folder it's contained within 47 | 48 | If your module tree doesn't follow this schema, then it's recommended to define your import logic manually. `importTree` 49 | is only a convenience function, and it's okay to not use it if your tree doesn't fit its schema. 50 | -------------------------------------------------------------------------------- /adios/lib/importModules.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | 3 | let 4 | inherit (builtins) 5 | attrNames 6 | pathExists 7 | readDir 8 | concatMap 9 | listToAttrs 10 | match 11 | head 12 | filter 13 | ; 14 | 15 | matchNixFile = match "(.+)\.nix$"; 16 | 17 | moduleArgs = { 18 | inherit adios; 19 | }; 20 | in 21 | rootPath: 22 | let 23 | files = readDir rootPath; 24 | filenames = attrNames files; 25 | 26 | moduleDirs = filter ( 27 | name: files.${name} == "directory" && pathExists "${rootPath}/${name}/default.nix" 28 | ) filenames; 29 | 30 | in 31 | listToAttrs ( 32 | map (name: { 33 | inherit name; 34 | value = import "${rootPath}/${name}/default.nix" moduleArgs; 35 | }) moduleDirs 36 | ) 37 | // listToAttrs ( 38 | concatMap ( 39 | filename: 40 | if filename == "default.nix" || files.${filename} == "directory" then 41 | [ ] 42 | else 43 | ( 44 | let 45 | m = matchNixFile filename; 46 | moduleName = head m; 47 | in 48 | if m == null then 49 | [ ] 50 | else 51 | [ 52 | { 53 | name = 54 | if moduleDirs ? ${moduleName} then 55 | throw '' 56 | Module ${moduleName} was provided by both: 57 | - ${rootPath}/${moduleName}/default.nix 58 | - ${filename} 59 | 60 | This is ambigious. Restructure your code to not have ambigious module names. 61 | '' 62 | else 63 | moduleName; 64 | value = import "${rootPath}/${filename}" moduleArgs; 65 | } 66 | ] 67 | ) 68 | ) filenames 69 | ) 70 | -------------------------------------------------------------------------------- /contrib/modules/treefmt/modules/statix.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | let 3 | inherit (adios) types; 4 | in 5 | { 6 | name = "treefmt-statix"; 7 | 8 | options = { 9 | package = { 10 | type = types.derivation; 11 | defaultFunc = { inputs }: inputs."nixpkgs".pkgs.statix; 12 | }; 13 | 14 | disabled-lints = { 15 | type = types.listOf types.string; 16 | default = [ ]; 17 | }; 18 | }; 19 | 20 | inputs = { 21 | "nixpkgs" = { 22 | path = "/nixpkgs"; 23 | }; 24 | }; 25 | 26 | impl = 27 | { options, inputs }: 28 | { 29 | name = "statix"; 30 | treefmt = { 31 | command = 32 | let 33 | inherit (inputs."nixpkgs") pkgs; 34 | inherit (pkgs) lib; 35 | cmd = lib.getExe options.package; 36 | 37 | # statix requires its configuration file to be named statix.toml exactly 38 | # See: https://github.com/nerdypepper/statix/pull/54 39 | settingsDir = 40 | pkgs.runCommandLocal "statix-config" 41 | { 42 | nativeBuildInputs = [ pkgs.remarshal ]; 43 | value = builtins.toJSON { 44 | disabled = options.disabled-lints; 45 | }; 46 | passAsFile = [ "value" ]; 47 | preferLocalBuild = true; 48 | } 49 | '' 50 | mkdir "$out" 51 | json2toml "$valuePath" "''${out}/statix.toml" 52 | ''; 53 | 54 | in 55 | pkgs.writeShellScript "statix-fix" '' 56 | for file in "''$@"; do 57 | ${cmd} fix --config ${settingsDir}/statix.toml "$file" 58 | done 59 | ''; 60 | includes = [ "*.nix" ]; 61 | }; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /contrib/modules/wrappers/default.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | let 3 | inherit (adios) types; 4 | inherit (types) 5 | optionalAttr 6 | listOf 7 | bool 8 | string 9 | ; 10 | 11 | strings = listOf string; 12 | 13 | stringAttrs = optionalAttr (types.attrsOf types.string); 14 | 15 | prefixedEnv = listOf ( 16 | types.struct "env" { 17 | name = types.string; 18 | value = types.string; 19 | sep = types.string; 20 | } 21 | ); 22 | 23 | in 24 | { 25 | options = { 26 | name = { 27 | type = types.str; 28 | default = "wrapper"; 29 | }; 30 | 31 | paths = { 32 | type = listOf types.derivation; 33 | default = [ ]; 34 | }; 35 | 36 | # set the name of the executed process to NAME 37 | # (if unset or empty, defaults to EXECUTABLE) 38 | argv0.type = string; 39 | 40 | # the executable inherits argv0 from the wrapper. 41 | # (use instead of --argv0 '$0') 42 | inheritArgv0.type = bool; 43 | 44 | # if argv0 doesn't include a / character, resolve it against PATH 45 | resolveArgv0.type = bool; 46 | 47 | # prepend the whitespace-separated list of arguments ARGS to the invocation of the executable 48 | addFlags.type = strings; 49 | # append the whitespace-separated list of arguments ARGS to the invocation of the executable 50 | appendFlags.type = strings; 51 | 52 | # change working directory (use instead of --run "cd DIR") 53 | chdir.type = types.string; 54 | 55 | # add VAR with value VAL to the executable's environment 56 | env.type = stringAttrs; 57 | # like --set, but only adds VAR if not already set in the environment 58 | setDefaultEnv.type = stringAttrs; 59 | # remove VAR from the environment 60 | unsetEnv.type = strings; 61 | # suffix/prefix ENV with VAL, separated by SEP 62 | prefixEnv.type = prefixedEnv; 63 | suffixEnv.type = prefixedEnv; 64 | }; 65 | 66 | inputs = { 67 | "nixpkgs" = { 68 | path = "/nixpkgs"; 69 | }; 70 | }; 71 | 72 | impl = 73 | { inputs, options }: 74 | let 75 | inherit (inputs."nixpkgs") pkgs; 76 | in 77 | { 78 | package = 79 | pkgs.runCommand options.name 80 | { 81 | nativeBuildInputs = [ 82 | pkgs.makeBinaryWrapper 83 | pkgs.python3 84 | pkgs.lndir 85 | ]; 86 | __structuredAttrs = true; 87 | inherit options; 88 | } 89 | '' 90 | source <(python3 ${./builder.py}) 91 | ''; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /contrib/modules/wrappers/builder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from typing import TypedDict, NotRequired 3 | from pathlib import Path 4 | import subprocess 5 | import shlex 6 | import json 7 | import os 8 | 9 | 10 | class PrefixedEnv(TypedDict): 11 | name: str 12 | value: str 13 | sep: str 14 | 15 | 16 | class Options(TypedDict): 17 | name: str 18 | paths: NotRequired[list[str]] 19 | argv0: NotRequired[str] 20 | inheritArgv0: NotRequired[bool] 21 | resolveArgv0: NotRequired[bool] 22 | appendFlas: NotRequired[list[str]] 23 | chdir: NotRequired[str] 24 | env: NotRequired[dict[str, str]] 25 | setDefaultEnv: NotRequired[dict[str, str]] 26 | unsetEnv: NotRequired[list[str]] 27 | prefixEnv: NotRequired[PrefixedEnv] 28 | suffixEnv: NotRequired[PrefixedEnv] 29 | 30 | 31 | def main(): 32 | out = Path(os.environ["out"]) 33 | bin = out.joinpath("bin") 34 | out.mkdir() 35 | 36 | with open(".attrs.json") as fp: 37 | attrs = json.load(fp) 38 | options: Options = attrs["options"] 39 | 40 | mk_wrapper_args: list[str] = [] 41 | 42 | try: 43 | mk_wrapper_args.extend(["--argv0", options["argv0"]]) 44 | except KeyError: 45 | pass 46 | 47 | if options.get("inheritArgv0", False): 48 | mk_wrapper_args.append("--inherit-argv0") 49 | 50 | if options.get("resolveArgv0", False): 51 | mk_wrapper_args.append("--resolve-argv0") 52 | 53 | try: 54 | mk_wrapper_args.extend(options["addFlags"]) 55 | except KeyError: 56 | pass 57 | 58 | try: 59 | mk_wrapper_args.extend(options["appendFlags"]) 60 | except KeyError: 61 | pass 62 | 63 | try: 64 | mk_wrapper_args.extend("--chdir", options["chdir"]) 65 | except KeyError: 66 | pass 67 | 68 | for name, value in options.get("env", { }).items(): 69 | mk_wrapper_args.extend(["--set", name, value]) 70 | 71 | for name, value in options.get("setDefaultEnv", { }).items(): 72 | mk_wrapper_args.extend(["--set-default", name, value]) 73 | 74 | for name in options.get("unsetEnv", []): 75 | mk_wrapper_args.extend(["--unset", name]) 76 | 77 | if "prefixEnv" in options: 78 | pass 79 | 80 | if "suffixEnv" in options: 81 | pass 82 | 83 | for input_path in options.get("paths", []): 84 | path = Path(input_path) 85 | _ = subprocess.check_output(["lndir", str(path), str(out)]) 86 | for bin_file in path.joinpath("bin").iterdir(): 87 | out_bin = bin.joinpath(bin_file.name) 88 | print(shlex.join([ 89 | "wrapProgram", 90 | str(out_bin), 91 | ] + mk_wrapper_args)) 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /adios/types.nix: -------------------------------------------------------------------------------- 1 | { korora }: 2 | 3 | let 4 | inherit (builtins) isString; 5 | inherit (types) 6 | attrsOf 7 | union 8 | struct 9 | optionalAttr 10 | string 11 | never 12 | function 13 | type 14 | any 15 | modules 16 | ; 17 | 18 | neverAttr = optionalAttr never; 19 | 20 | typesT = attrsOf modules.typedef; 21 | 22 | types = korora // { 23 | modules = rec { 24 | typedef = 25 | types.typedef' "typedef" 26 | (union [ 27 | function 28 | type 29 | typesT 30 | ]).verify; 31 | 32 | option = 33 | (struct "option" { 34 | inherit type; 35 | default = optionalAttr any; 36 | defaultFunc = optionalAttr types.function; 37 | description = optionalAttr string; 38 | }).override 39 | { 40 | verify = 41 | option: 42 | if option ? default && option ? defaultFunc then 43 | "'default' & 'defaultFunc' are mutually exclusive" 44 | else 45 | null; 46 | }; 47 | 48 | subOptions = struct "subOptions" { 49 | inherit options; 50 | description = optionalAttr string; 51 | # Make fields used for normal options non-permitted 52 | type = neverAttr; 53 | default = neverAttr; 54 | defaultFunc = neverAttr; 55 | }; 56 | 57 | options = attrsOf (union [ 58 | modules.option 59 | modules.subOptions 60 | ]); 61 | 62 | input = types.option ( 63 | attrsOf ( 64 | struct "input" { 65 | # Note: The lack of a type for an input means no type checking done. 66 | type = optionalAttr type; 67 | # TODO: Narrow permitted chars 68 | path = types.typedef "pathstring" isString; 69 | } 70 | ) 71 | ); 72 | 73 | inputs = attrsOf input; 74 | 75 | lib = types.attrs; 76 | 77 | moduleDef = 78 | (struct "moduleDef" { 79 | name = optionalAttr string; 80 | modules = optionalAttr (attrsOf module); 81 | types = optionalAttr typesT; 82 | impl = optionalAttr function; 83 | options = optionalAttr options; 84 | inputs = optionalAttr inputs; 85 | }).override 86 | { 87 | verify = 88 | self: 89 | (if self ? type && self ? options then "'type' is mutually exclusive with 'options'" else null); 90 | }; 91 | 92 | module = struct "module" { 93 | name = optionalAttr string; 94 | modules = attrsOf module; 95 | types = typesT; 96 | inherit options; 97 | inherit type; 98 | inherit inputs; 99 | __functor = optionalAttr function; 100 | }; 101 | }; 102 | }; 103 | 104 | in 105 | types 106 | -------------------------------------------------------------------------------- /contrib/modules/treefmt/default.nix: -------------------------------------------------------------------------------- 1 | { adios }: 2 | let 3 | inherit (adios) types; 4 | in 5 | { 6 | name = "treefmt"; 7 | 8 | modules = adios.lib.importModules ./modules; 9 | 10 | inputs = { 11 | "nixpkgs" = { 12 | path = "/nixpkgs"; 13 | }; 14 | }; 15 | 16 | options = { 17 | formatters = { 18 | type = types.listOf ( 19 | types.struct "treefmt-formatter" { 20 | name = types.string; 21 | 22 | treefmt = types.struct "treefmt-formatter-config" rec { 23 | command = types.union [ 24 | types.string 25 | types.derivation # Allow setting command using writeShellScript & similar without casting to string 26 | ]; 27 | includes = types.optionalAttr (types.listOf types.string); 28 | excludes = includes; 29 | options = includes; 30 | }; 31 | } 32 | ); 33 | default = [ ]; 34 | }; 35 | 36 | projectRootFile = { 37 | type = types.string; 38 | }; 39 | 40 | package = { 41 | type = types.derivation; 42 | defaultFunc = { inputs }: inputs."nixpkgs".pkgs.treefmt; 43 | }; 44 | 45 | excludes = { 46 | type = types.listOf types.string; 47 | default = [ 48 | # generated lock files i.e. yarn, cargo, nix flakes 49 | "*.lock" 50 | # Files generated by patch 51 | "*.patch" 52 | 53 | # NPM 54 | "package-lock.json" 55 | 56 | # Go 57 | # In theory go mod tidy could format this, but it has other side-effects beyond formatting. 58 | "go.mod" 59 | "go.sum" 60 | 61 | # VCS 62 | ".gitignore" 63 | ".gitmodules" 64 | ".hgignore" 65 | ".svnignore" 66 | ]; 67 | }; 68 | }; 69 | 70 | impl = 71 | { options, inputs }: 72 | let 73 | inherit (inputs."nixpkgs") pkgs; 74 | inherit (pkgs) lib; 75 | inherit (lib) 76 | groupBy 77 | mapAttrs 78 | head 79 | length 80 | throwIf 81 | getExe 82 | ; 83 | 84 | config = { 85 | formatter = mapAttrs ( 86 | name: formatters: 87 | let 88 | def = (head formatters).treefmt; 89 | in 90 | throwIf (length formatters > 1) "treefmt: name collision for formatter '${name}'" { 91 | inherit (def) command; 92 | includes = def.includes or [ ]; 93 | options = def.options or [ ]; 94 | excludes = def.excludes or [ ]; 95 | } 96 | ) (groupBy (fmt: fmt.name) options.formatters); 97 | global.excludes = options.excludes; 98 | }; 99 | 100 | configFile = (pkgs.formats.toml { }).generate "treefmt.toml" config; 101 | 102 | in 103 | { 104 | package = 105 | pkgs.runCommand "treefmt-adios" 106 | { 107 | meta.mainProgram = "treefmt-adios"; 108 | } 109 | '' 110 | mkdir -p $out/bin 111 | 112 | cat > $out/bin/$name << EOF 113 | #!${pkgs.runtimeShell} 114 | set -euo pipefail 115 | unset PRJ_ROOT 116 | exec ${getExe options.package} \ 117 | --config-file=${configFile} \ 118 | --tree-root-file=${options.projectRootFile} \ 119 | "\$@" 120 | EOF 121 | chmod +x $out/bin/$name 122 | 123 | ln -s $out/bin/$name $out/bin/treefmt 124 | ''; 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /contrib/modules/write-files/builder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # pyright: reportAny=false 3 | from typing import TypedDict, NotRequired 4 | from pathlib import Path 5 | import json 6 | import os 7 | 8 | 9 | class SMFHFile(TypedDict): 10 | # Note: Making liberal use of NotRequired 11 | target: NotRequired[str] 12 | type: NotRequired[str] 13 | source: NotRequired[str] 14 | uid: NotRequired[int] 15 | gid: NotRequired[int] 16 | permissions: NotRequired[str] 17 | 18 | 19 | class SMFHManifest(TypedDict): 20 | files: list[SMFHFile] 21 | clobber_by_default: bool 22 | version: int 23 | 24 | 25 | def main(): 26 | # Read input manifest 27 | with open(".attrs.json") as fp: 28 | attrs = json.load(fp) 29 | manifest = attrs["manifest"] 30 | 31 | # Output smfh manifest 32 | files: list[SMFHFile] = [] 33 | 34 | out = Path(os.environ["out"]) 35 | out.mkdir() 36 | 37 | # Write out inlined (text) files to this directory 38 | file_store = out.joinpath("files") 39 | file_store.mkdir() 40 | 41 | for filename, fmeta in manifest["files"].items(): 42 | permissions = fmeta.get("permissions") 43 | 44 | fout: SMFHFile = { 45 | "target": "/".join((manifest["output"], filename)), 46 | "permissions": permissions, 47 | "uid": fmeta.get("uid"), 48 | "gid": fmeta.get("gid"), 49 | } 50 | 51 | # Regular files 52 | has_source = "source" in fmeta 53 | has_text = "text" in fmeta 54 | if has_source or has_text: 55 | fout["type"] = fmeta.get("method", "symlink") 56 | 57 | # Files from a given source 58 | if has_source: 59 | # Recursive files 60 | if fmeta.get("recursive", False): 61 | r_source = fmeta["source"] 62 | for r_root, _, r_files in os.walk(r_source): 63 | for r_file in r_files: 64 | file_rel = r_root.removeprefix(r_source) 65 | if file_rel: 66 | file_rel = "/".join((file_rel, r_file)).removeprefix("/") 67 | else: 68 | file_rel = r_file.removeprefix("/") 69 | files.append( 70 | { 71 | **fout, 72 | "source": "/".join((r_source, file_rel)), 73 | "target": "/".join((fout["target"], file_rel)), 74 | } 75 | ) 76 | 77 | # Regular files 78 | else: 79 | files.append({**fout, "source": fmeta["source"]}) 80 | 81 | # Inline text based files 82 | elif has_text: 83 | file_out = file_store.joinpath(filename) 84 | file_out.parent.mkdir(exist_ok=True, parents=True) 85 | with file_out.open("w") as fp: 86 | fp.write(fmeta["text"]) 87 | if permissions: 88 | os.fchmod(fp.fileno(), int(permissions, base=8)) 89 | fout["source"] = str(file_out) 90 | files.append({**fout, "source": str(file_out)}) 91 | 92 | # Directory 93 | else: 94 | files.append({**fout, "type": "directory"}) 95 | 96 | with out.joinpath("manifest.json").open("w") as fp: 97 | smfh: SMFHManifest = { 98 | "files": files, 99 | "clobber_by_default": False, 100 | "version": 1, 101 | } 102 | json.dump(smfh, fp) 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /contrib/modules/write-files/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | adios, 3 | }: 4 | let 5 | inherit (adios) types; 6 | 7 | self = { 8 | name = "write-files"; 9 | 10 | options = { 11 | name = { 12 | type = types.str; 13 | default = "files"; 14 | }; 15 | files = { 16 | type = types.attrsOf self.types.file; 17 | }; 18 | output = { 19 | type = types.string; 20 | }; 21 | }; 22 | 23 | inputs = { 24 | "nixpkgs" = { 25 | path = "/nixpkgs"; 26 | }; 27 | }; 28 | 29 | types = 30 | let 31 | inherit (types) 32 | union 33 | struct 34 | string 35 | optionalAttr 36 | enum 37 | bool 38 | int 39 | never 40 | ; 41 | optionalStr = optionalAttr string; 42 | optionalInt = optionalAttr int; 43 | optionalBool = optionalAttr bool; 44 | optionalNever = optionalAttr never; 45 | in 46 | { 47 | file = 48 | let 49 | # Common options 50 | common = { 51 | # Base options 52 | permissions = optionalStr; 53 | uid = optionalInt; 54 | gid = optionalInt; 55 | clobber = optionalBool; 56 | 57 | # Invalid unless otherwise specified 58 | text = optionalNever; 59 | source = optionalNever; 60 | recursive = optionalNever; 61 | }; 62 | 63 | # File install method 64 | method = optionalAttr ( 65 | enum "method" [ 66 | # Default when no value is passed 67 | "symlink" 68 | # Implies creating a mutable file that gets overwritten on activation 69 | "copy" 70 | ] 71 | ); 72 | in 73 | types.rename "file" (union [ 74 | # File that is written out into the manifest and written to store and then symlinked/written 75 | # This can be more performant from the Nix evaluation side as it creates fewer derivations 76 | (struct "file-text" ( 77 | common 78 | // { 79 | text = string; 80 | inherit method; 81 | } 82 | )) 83 | 84 | # File that is linked to the exact source path 85 | (struct "file-source" ( 86 | common 87 | // { 88 | source = union [ 89 | types.derivation 90 | types.path 91 | string 92 | ]; 93 | inherit method; 94 | recursive = optionalBool; 95 | } 96 | )) 97 | 98 | # Directory with permissions 99 | (struct "file-directory" common) 100 | ]); 101 | }; 102 | 103 | impl = 104 | { options, inputs }: 105 | let 106 | inherit (inputs.nixpkgs) pkgs; 107 | inherit (pkgs) lib; 108 | 109 | package = pkgs.stdenv.mkDerivation { 110 | inherit (options) name; 111 | nativeBuildInputs = [ 112 | pkgs.python3 113 | ]; 114 | manifest = { 115 | inherit (options) files output; 116 | }; 117 | dontUnpack = true; 118 | dontConfigure = true; 119 | dontBuild = true; 120 | __structuredAttrs = true; 121 | installPhase = '' 122 | runHook preInstall 123 | python3 ${./builder.py} 124 | cat > $out/script < last then [ ] else builtins.genList (n: first + n) (last - first + 1); 18 | 19 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 20 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 21 | 22 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 23 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 24 | concatStrings = builtins.concatStringsSep ""; 25 | 26 | # If the environment variable NPINS_OVERRIDE_${name} is set, then use 27 | # the path directly as opposed to the fetched source. 28 | # (Taken from Niv for compatibility) 29 | mayOverride = 30 | name: path: 31 | let 32 | envVarName = "NPINS_OVERRIDE_${saneName}"; 33 | saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; 34 | ersatz = builtins.getEnv envVarName; 35 | in 36 | if ersatz == "" then 37 | path 38 | else 39 | # this turns the string into an actual Nix path (for both absolute and 40 | # relative paths) 41 | builtins.trace "Overriding path of \"${name}\" with \"${ersatz}\" due to set \"${envVarName}\"" ( 42 | if builtins.substring 0 1 ersatz == "/" then 43 | /. + ersatz 44 | else 45 | /. + builtins.getEnv "PWD" + "/${ersatz}" 46 | ); 47 | 48 | mkSource = 49 | name: spec: 50 | assert spec ? type; 51 | let 52 | path = 53 | if spec.type == "Git" then 54 | mkGitSource spec 55 | else if spec.type == "GitRelease" then 56 | mkGitSource spec 57 | else if spec.type == "PyPi" then 58 | mkPyPiSource spec 59 | else if spec.type == "Channel" then 60 | mkChannelSource spec 61 | else if spec.type == "Tarball" then 62 | mkTarballSource spec 63 | else 64 | builtins.throw "Unknown source type ${spec.type}"; 65 | in 66 | spec // { outPath = mayOverride name path; }; 67 | 68 | mkGitSource = 69 | { 70 | repository, 71 | revision, 72 | url ? null, 73 | submodules, 74 | hash, 75 | ... 76 | }: 77 | assert repository ? type; 78 | # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository 79 | # In the latter case, there we will always be an url to the tarball 80 | if url != null && !submodules then 81 | builtins.fetchTarball { 82 | inherit url; 83 | sha256 = hash; # FIXME: check nix version & use SRI hashes 84 | } 85 | else 86 | let 87 | url = 88 | if repository.type == "Git" then 89 | repository.url 90 | else if repository.type == "GitHub" then 91 | "https://github.com/${repository.owner}/${repository.repo}.git" 92 | else if repository.type == "GitLab" then 93 | "${repository.server}/${repository.repo_path}.git" 94 | else 95 | throw "Unrecognized repository type ${repository.type}"; 96 | urlToName = 97 | url: rev: 98 | let 99 | matched = builtins.match "^.*/([^/]*)(\\.git)?$" url; 100 | 101 | short = builtins.substring 0 7 rev; 102 | 103 | appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; 104 | in 105 | "${if matched == null then "source" else builtins.head matched}${appendShort}"; 106 | name = urlToName url revision; 107 | in 108 | builtins.fetchGit { 109 | rev = revision; 110 | inherit name; 111 | # hash = hash; 112 | inherit url submodules; 113 | }; 114 | 115 | mkPyPiSource = 116 | { url, hash, ... }: 117 | builtins.fetchurl { 118 | inherit url; 119 | sha256 = hash; 120 | }; 121 | 122 | mkChannelSource = 123 | { url, hash, ... }: 124 | builtins.fetchTarball { 125 | inherit url; 126 | sha256 = hash; 127 | }; 128 | 129 | mkTarballSource = 130 | { 131 | url, 132 | locked_url ? url, 133 | hash, 134 | ... 135 | }: 136 | builtins.fetchTarball { 137 | url = locked_url; 138 | sha256 = hash; 139 | }; 140 | in 141 | if version == 5 then 142 | builtins.mapAttrs mkSource data.pins 143 | else 144 | throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" 145 | -------------------------------------------------------------------------------- /types/README.md: -------------------------------------------------------------------------------- 1 | # Kororā 2 | A tiny & fast composable type system for Nix, in Nix. 3 | 4 | Named after the [little penguin](https://www.doc.govt.nz/nature/native-animals/birds/birds-a-z/penguins/little-penguin-korora/). 5 | 6 | # Features 7 | 8 | - Types 9 | - Primitive types (`string`, `int`, etc) 10 | - Polymorphic types (`union`, `attrsOf`, etc) 11 | - Struct types 12 | 13 | # Basic usage 14 | 15 | - Verification 16 | 17 | Basic verification is done with the type function `verify`: 18 | ``` nix 19 | { korora }: 20 | let 21 | t = korora.string; 22 | 23 | value = 1; 24 | 25 | # Error contains the string "Expected type 'string' but value '1' is of type 'int'" 26 | error = t.verify 1; 27 | 28 | in if error != null then throw error else value 29 | ``` 30 | Errors are returned as a string. 31 | On success `null` is returned. 32 | 33 | - Checking (assertions) 34 | 35 | For convenience you can also check a value on-the-fly: 36 | ``` nix 37 | { korora }: 38 | let 39 | t = korora.string; 40 | 41 | value = 1; 42 | 43 | # Same error as previous example, but `check` throws. 44 | value = t.check value value; 45 | 46 | in value 47 | ``` 48 | 49 | On error `check` throws. On success it returns the value that was passed in. 50 | 51 | # Examples 52 | For usage example see [tests.nix](./tests.nix). 53 | 54 | # Reference 55 | 56 | ## `lib.types.typedef` 57 | 58 | Declare a custom type using a bool function 59 | 60 | `name` 61 | 62 | : Name of the type as a string 63 | 64 | 65 | `verify` 66 | 67 | : Verification function returning a bool. 68 | 69 | 70 | ## `lib.types.typedef'` 71 | 72 | Declare a custom type using an option function. 73 | 74 | `name` 75 | 76 | : Name of the type as a string 77 | 78 | 79 | `verify` 80 | 81 | : Verification function returning null on success & a string with error message on error. 82 | 83 | 84 | ## `lib.types.string` 85 | 86 | String 87 | 88 | ## `lib.types.str` 89 | 90 | Type alias for string 91 | 92 | ## `lib.types.any` 93 | 94 | Any 95 | 96 | ## `lib.types.never` 97 | 98 | Never 99 | 100 | ## `lib.types.int` 101 | 102 | Int 103 | 104 | ## `lib.types.float` 105 | 106 | Single precision floating point 107 | 108 | ## `lib.types.number` 109 | 110 | Either an int or a float 111 | 112 | ## `lib.types.bool` 113 | 114 | Bool 115 | 116 | ## `lib.types.attrs` 117 | 118 | Attribute with undefined attribute types 119 | 120 | ## `lib.types.list` 121 | 122 | Attribute with undefined element types 123 | 124 | ## `lib.types.function` 125 | 126 | Function 127 | 128 | ## `lib.types.path` 129 | 130 | Path 131 | 132 | ## `lib.types.derivation` 133 | 134 | Derivation 135 | 136 | ## `lib.types.type` 137 | 138 | Type 139 | 140 | ## `lib.types.option` 141 | 142 | Option 143 | 144 | `t` 145 | 146 | : Null or t 147 | 148 | 149 | ## `lib.types.listOf` 150 | 151 | listOf 152 | 153 | `t` 154 | 155 | : Element type 156 | 157 | 158 | ## `lib.types.attrsOf` 159 | 160 | listOf 161 | 162 | `t` 163 | 164 | : Attribute value type 165 | 166 | 167 | ## `lib.types.union` 168 | 169 | union 170 | 171 | `types` 172 | 173 | : Any of 174 | 175 | 176 | ## `lib.types.intersection` 177 | 178 | intersection 179 | 180 | `types` 181 | 182 | : All of 183 | 184 | 185 | ## `lib.types.rename` 186 | 187 | rename 188 | 189 | Because some polymorphic types such as attrsOf inherits names from it's 190 | sub-types we need to erase the name to not cause infinite recursion. 191 | 192 | #### Example: 193 | ``` nix 194 | myType = types.attrsOf ( 195 | types.rename "eitherType" (types.union [ 196 | types.string 197 | myType 198 | ]) 199 | ); 200 | ``` 201 | 202 | `name` 203 | 204 | : Function argument 205 | 206 | 207 | `type` 208 | 209 | : Function argument 210 | 211 | 212 | ## `lib.types.struct` 213 | 214 | struct 215 | 216 | #### Example 217 | ``` nix 218 | korora.struct "myStruct" { 219 | foo = types.string; 220 | } 221 | ``` 222 | 223 | #### Features 224 | 225 | - Totality 226 | 227 | By default, all attribute names must be present in a struct. It is possible to override this by specifying _totality_. Here is how to do this: 228 | ``` nix 229 | (korora.struct "myStruct" { 230 | foo = types.string; 231 | }).override { total = false; } 232 | ``` 233 | 234 | This means that a `myStruct` struct can have any of the keys omitted. Thus these are valid: 235 | ``` nix 236 | let 237 | s1 = { }; 238 | s2 = { foo = "bar"; } 239 | in ... 240 | ``` 241 | 242 | - Unknown attribute names 243 | 244 | By default, unknown attribute names are allowed. 245 | 246 | It is possible to override this by specifying `unknown`. 247 | ``` nix 248 | (korora.struct "myStruct" { 249 | foo = types.string; 250 | }).override { unknown = false; } 251 | ``` 252 | 253 | This means that 254 | ``` nix 255 | { 256 | foo = "bar"; 257 | baz = "hello"; 258 | } 259 | ``` 260 | is normally valid, but not when `unknown` is set to `false`. 261 | 262 | Because Nix lacks primitive operations to iterate over attribute sets dynamically without 263 | allocation this function allocates one intermediate attribute set per struct verification. 264 | 265 | - Custom invariants 266 | 267 | Custom struct verification functions can be added as such: 268 | ``` nix 269 | (types.struct "testStruct2" { 270 | x = types.int; 271 | y = types.int; 272 | }).override { 273 | verify = v: if v.x + v.y == 2 then "VERBOTEN" else null; 274 | }; 275 | ``` 276 | 277 | #### Function signature 278 | 279 | `name` 280 | 281 | : Name of struct type as a string 282 | 283 | 284 | `members` 285 | 286 | : Attribute set of type definitions. 287 | 288 | 289 | ## `lib.types.optionalAttr` 290 | 291 | optionalAttr 292 | 293 | `t` 294 | 295 | : Function argument 296 | 297 | 298 | ## `lib.types.enum` 299 | 300 | enum 301 | 302 | `name` 303 | 304 | : Name of enum type as a string 305 | 306 | 307 | `elems` 308 | 309 | : List of allowable enum members 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /types/lib.nix: -------------------------------------------------------------------------------- 1 | # Utilities copied from nixpkgs lib https://github.com/NixOS/nixpkgs/tree/master/lib 2 | # This is just enough to provide the feature set used by toPretty which is used to generate error messages. 3 | # No other parts of nixpkgs lib are used in Korora. 4 | 5 | let 6 | inherit (builtins) 7 | isInt 8 | isFloat 9 | isString 10 | filter 11 | isList 12 | split 13 | concatStringsSep 14 | replaceStrings 15 | addErrorContext 16 | length 17 | isFunction 18 | functionArgs 19 | elemAt 20 | isAttrs 21 | attrNames 22 | match 23 | isPath 24 | toJSON 25 | genList 26 | ; 27 | 28 | sublist = 29 | start: count: list: 30 | let 31 | len = length list; 32 | in 33 | genList (n: elemAt list (n + start)) ( 34 | if start >= len then 35 | 0 36 | else if start + count > len then 37 | len - start 38 | else 39 | count 40 | ); 41 | 42 | take = count: sublist 0 count; 43 | 44 | last = 45 | list: 46 | if list == [ ] then 47 | (throw "lists.last: list must not be empty!") 48 | else 49 | elemAt list (length list - 1); 50 | 51 | init = 52 | list: 53 | if list == [ ] then (throw "lists.init: list must not be empty!") else take (length list - 1) list; 54 | 55 | mapAttrsToList = f: attrs: map (name: f name attrs.${name}) (attrNames attrs); 56 | 57 | concatMapStringsSep = 58 | sep: f: list: 59 | concatStringsSep sep (map f list); 60 | 61 | escape = list: replaceStrings list (map (c: "\\${c}") list); 62 | 63 | escapeNixString = s: escape [ "$" ] (toJSON s); 64 | 65 | escapeNixIdentifier = 66 | s: 67 | # Regex from https://github.com/NixOS/nix/blob/d048577909e383439c2549e849c5c2f2016c997e/src/libexpr/lexer.l#L91 68 | if match "[a-zA-Z_][a-zA-Z0-9_'-]*" s != null then s else escapeNixString s; 69 | 70 | in 71 | 72 | { 73 | 74 | /** 75 | Pretty print a value, akin to `builtins.trace`. 76 | 77 | Should probably be a builtin as well. 78 | 79 | The pretty-printed string should be suitable for rendering default values 80 | in the NixOS manual. In particular, it should be as close to a valid Nix expression 81 | as possible. 82 | 83 | # Inputs 84 | 85 | Structured function argument 86 | : allowPrettyValues 87 | : If this option is true, attrsets like { __pretty = fn; val = …; } 88 | will use fn to convert val to a pretty printed representation. 89 | (This means fn is type Val -> String.) 90 | : multiline 91 | : If this option is true, the output is indented with newlines for attribute sets and lists 92 | : indent 93 | : Initial indentation level 94 | 95 | Value 96 | : The value to be pretty printed 97 | */ 98 | toPretty = 99 | { 100 | allowPrettyValues ? false, 101 | multiline ? true, 102 | indent ? "", 103 | }: 104 | let 105 | go = 106 | indent: v: 107 | let 108 | introSpace = if multiline then "\n${indent} " else " "; 109 | outroSpace = if multiline then "\n${indent}" else " "; 110 | in 111 | if isInt v then 112 | toString v 113 | # toString loses precision on floats, so we use toJSON instead. This isn't perfect 114 | # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for 115 | # pretty-printing purposes this is acceptable. 116 | else if isFloat v then 117 | builtins.toJSON v 118 | else if isString v then 119 | let 120 | lines = filter (v: !isList v) (split "\n" v); 121 | escapeSingleline = escape [ 122 | "\\" 123 | "\"" 124 | "\${" 125 | ]; 126 | escapeMultiline = 127 | replaceStrings 128 | [ 129 | "\${" 130 | "''" 131 | ] 132 | [ 133 | "''\${" 134 | "'''" 135 | ]; 136 | singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\""; 137 | multilineResult = 138 | let 139 | escapedLines = map escapeMultiline lines; 140 | # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer" 141 | # indentation level. Otherwise, '' is appended to the last line. 142 | lastLine = last escapedLines; 143 | in 144 | "''" 145 | + introSpace 146 | + concatStringsSep introSpace (init escapedLines) 147 | + (if lastLine == "" then outroSpace else introSpace + lastLine) 148 | + "''"; 149 | in 150 | if multiline && length lines > 1 then multilineResult else singlelineResult 151 | else if true == v then 152 | "true" 153 | else if false == v then 154 | "false" 155 | else if null == v then 156 | "null" 157 | else if isPath v then 158 | toString v 159 | else if isList v then 160 | if v == [ ] then 161 | "[ ]" 162 | else 163 | "[" + introSpace + concatMapStringsSep introSpace (go (indent + " ")) v + outroSpace + "]" 164 | else if isFunction v then 165 | let 166 | fna = functionArgs v; 167 | showFnas = concatStringsSep ", " ( 168 | mapAttrsToList (name: hasDefVal: if hasDefVal then name + "?" else name) fna 169 | ); 170 | in 171 | if fna == { } then "" else "" 172 | else if isAttrs v then 173 | # apply pretty values if allowed 174 | if allowPrettyValues && v ? __pretty && v ? val then 175 | v.__pretty v.val 176 | else if v == { } then 177 | "{ }" 178 | else if v ? type && v.type == "derivation" then 179 | "" 180 | else 181 | "{" 182 | + introSpace 183 | + concatStringsSep introSpace ( 184 | mapAttrsToList ( 185 | name: value: 186 | "${escapeNixIdentifier name} = ${ 187 | addErrorContext "while evaluating an attribute `${name}`" (go (indent + " ") value) 188 | };" 189 | ) v 190 | ) 191 | + outroSpace 192 | + "}" 193 | else 194 | abort "generators.toPretty: should never happen (v = ${v})"; 195 | in 196 | go indent; 197 | 198 | } 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /types/types.nix: -------------------------------------------------------------------------------- 1 | /* 2 | A tiny & fast composable type system for Nix, in Nix. 3 | 4 | Named after the [little penguin](https://www.doc.govt.nz/nature/native-animals/birds/birds-a-z/penguins/little-penguin-korora/). 5 | 6 | # Features 7 | 8 | - Types 9 | - Primitive types (`string`, `int`, etc) 10 | - Polymorphic types (`union`, `attrsOf`, etc) 11 | - Struct types 12 | 13 | # Basic usage 14 | 15 | - Verification 16 | 17 | Basic verification is done with the type function `verify`: 18 | ``` nix 19 | { korora }: 20 | let 21 | t = korora.string; 22 | 23 | value = 1; 24 | 25 | # Error contains the string "Expected type 'string' but value '1' is of type 'int'" 26 | error = t.verify 1; 27 | 28 | in if error != null then throw error else value 29 | ``` 30 | Errors are returned as a string. 31 | On success `null` is returned. 32 | 33 | - Checking (assertions) 34 | 35 | For convenience you can also check a value on-the-fly: 36 | ``` nix 37 | { korora }: 38 | let 39 | t = korora.string; 40 | 41 | value = 1; 42 | 43 | # Same error as previous example, but `check` throws. 44 | value = t.check value value; 45 | 46 | in value 47 | ``` 48 | 49 | On error `check` throws. On success it returns the value that was passed in. 50 | 51 | # Examples 52 | For usage example see [tests.nix](./tests.nix). 53 | 54 | # Reference 55 | */ 56 | let 57 | inherit (builtins) 58 | typeOf 59 | isString 60 | isFunction 61 | isAttrs 62 | isList 63 | all 64 | attrValues 65 | isPath 66 | head 67 | split 68 | concatStringsSep 69 | any 70 | isInt 71 | isFloat 72 | isBool 73 | attrNames 74 | elem 75 | foldl' 76 | elemAt 77 | ; 78 | 79 | isDerivation = value: isAttrs value && (value.type or null == "derivation"); 80 | 81 | optionalElem = cond: e: if cond then [ e ] else [ ]; 82 | 83 | joinKeys = list: concatStringsSep ", " (map (e: "'${e}'") list); 84 | 85 | toPretty = (import ./lib.nix).toPretty { indent = " "; }; 86 | 87 | typeError = name: v: "Expected type '${name}' but value '${toPretty v}' is of type '${typeOf v}'"; 88 | 89 | # Builtin primitive checkers return a bool for indicating errors but we return option 90 | wrapBoolVerify = 91 | name: verify: v: 92 | if verify v then null else typeError name v; 93 | 94 | # Wrap builtins.all to return option, with string on error. 95 | all' = 96 | func: list: 97 | if all (v: func v == null) list then 98 | null 99 | else 100 | # If an error was found, run the checks again to find the first error to return. 101 | ( 102 | let 103 | recurse = 104 | i: 105 | let 106 | v = elemAt list i; 107 | in 108 | if func v != null then func v else recurse (i + 1); 109 | in 110 | recurse 0 111 | ); 112 | 113 | addErrorContext = context: error: if error == null then null else "${context}: ${error}"; 114 | 115 | fix = 116 | f: 117 | let 118 | x = f x; 119 | in 120 | x; 121 | 122 | in 123 | fix (self: { 124 | 125 | # Utility functions 126 | 127 | /** 128 | Declare a custom type using a bool function 129 | */ 130 | typedef = 131 | # Name of the type as a string 132 | name: 133 | # Verification function returning a bool. 134 | verify: 135 | assert isFunction verify; 136 | self.typedef' name (wrapBoolVerify name verify); 137 | 138 | /** 139 | Declare a custom type using an option function. 140 | */ 141 | typedef' = 142 | # Name of the type as a string 143 | name: 144 | # Verification function returning null on success & a string with error message on error. 145 | verify: 146 | assert isFunction verify; 147 | { 148 | inherit name verify; 149 | check = v: v2: if verify v == null then v2 else throw (verify v); 150 | 151 | # The name of the type without polymorphic metadata 152 | __name = head (split "<" name); 153 | }; 154 | 155 | # Primitive types 156 | 157 | /** 158 | String 159 | */ 160 | string = self.typedef "string" isString; 161 | 162 | /** 163 | Type alias for string 164 | */ 165 | str = self.string; 166 | 167 | /** 168 | Any 169 | */ 170 | any = self.typedef' "any" (_: null); 171 | 172 | /** 173 | Never 174 | */ 175 | never = self.typedef "never" (_: false); 176 | 177 | /** 178 | Int 179 | */ 180 | int = self.typedef "int" isInt; 181 | 182 | /** 183 | Single precision floating point 184 | */ 185 | float = self.typedef "float" isFloat; 186 | 187 | /** 188 | Either an int or a float 189 | */ 190 | number = self.typedef "number" (v: isInt v || isFloat v); 191 | 192 | /** 193 | Bool 194 | */ 195 | bool = self.typedef "bool" isBool; 196 | 197 | /** 198 | Attribute with undefined attribute types 199 | */ 200 | attrs = self.typedef "attrs" isAttrs; 201 | 202 | /** 203 | Attribute with undefined element types 204 | */ 205 | list = self.typedef "list" isList; 206 | 207 | /** 208 | Function 209 | */ 210 | function = self.typedef "function" isFunction; 211 | 212 | /** 213 | Path 214 | */ 215 | path = self.typedef "path" isPath; 216 | 217 | /** 218 | Derivation 219 | */ 220 | derivation = self.typedef "derivation" isDerivation; 221 | 222 | /** 223 | Polymorphic types 224 | */ 225 | 226 | /** 227 | Type 228 | */ 229 | type = self.typedef "type" ( 230 | v: isAttrs v && v ? name && isString v.name && v ? verify && isFunction v.verify 231 | ); 232 | 233 | /** 234 | Option 235 | */ 236 | option = 237 | # Null or t 238 | t: 239 | let 240 | name = "option<${t.name}>"; 241 | inherit (t) verify; 242 | withErrorContext = addErrorContext "in ${name}"; 243 | in 244 | self.typedef' name (v: if v == null then null else withErrorContext (verify v)); 245 | 246 | /** 247 | listOf 248 | */ 249 | listOf = 250 | # Element type 251 | t: 252 | let 253 | name = "listOf<${t.name}>"; 254 | inherit (t) verify; 255 | withErrorContext = addErrorContext "in ${name} element"; 256 | in 257 | self.typedef' name (v: if !isList v then typeError name v else withErrorContext (all' verify v)); 258 | 259 | /** 260 | listOf 261 | */ 262 | attrsOf = 263 | # Attribute value type 264 | t: 265 | let 266 | name = "attrsOf<${t.name}>"; 267 | inherit (t) verify; 268 | withErrorContext = addErrorContext "in ${name} value"; 269 | in 270 | self.typedef' name ( 271 | v: if !isAttrs v then typeError name v else withErrorContext (all' verify (attrValues v)) 272 | ); 273 | 274 | /** 275 | union 276 | */ 277 | union = 278 | # Any of 279 | types: 280 | assert isList types; 281 | let 282 | name = "union<${concatStringsSep "," (map (t: t.name) types)}>"; 283 | funcs = map (t: t.verify) types; 284 | in 285 | self.typedef name (v: any (func: func v == null) funcs); 286 | 287 | /** 288 | intersection 289 | */ 290 | intersection = 291 | # All of 292 | types: 293 | assert isList types; 294 | let 295 | name = "intersection<${concatStringsSep "," (map (t: t.name) types)}>"; 296 | funcs = map (t: t.verify) types; 297 | in 298 | self.typedef name (v: all (func: func v == null) funcs); 299 | 300 | /** 301 | rename 302 | 303 | Because some polymorphic types such as attrsOf inherits names from it's 304 | sub-types we need to erase the name to not cause infinite recursion. 305 | 306 | #### Example: 307 | ``` nix 308 | myType = types.attrsOf ( 309 | types.rename "eitherType" (types.union [ 310 | types.string 311 | myType 312 | ]) 313 | ); 314 | ``` 315 | */ 316 | rename = name: type: self.typedef' name type.verify; 317 | 318 | /** 319 | struct 320 | 321 | #### Example 322 | ``` nix 323 | korora.struct "myStruct" { 324 | foo = types.string; 325 | } 326 | ``` 327 | 328 | #### Features 329 | 330 | - Totality 331 | 332 | By default, all attribute names must be present in a struct. It is possible to override this by specifying _totality_. Here is how to do this: 333 | ``` nix 334 | (korora.struct "myStruct" { 335 | foo = types.string; 336 | }).override { total = false; } 337 | ``` 338 | 339 | This means that a `myStruct` struct can have any of the keys omitted. Thus these are valid: 340 | ``` nix 341 | let 342 | s1 = { }; 343 | s2 = { foo = "bar"; } 344 | in ... 345 | ``` 346 | 347 | - Unknown attribute names 348 | 349 | By default, unknown attribute names are allowed. 350 | 351 | It is possible to override this by specifying `unknown`. 352 | ``` nix 353 | (korora.struct "myStruct" { 354 | foo = types.string; 355 | }).override { unknown = false; } 356 | ``` 357 | 358 | This means that 359 | ``` nix 360 | { 361 | foo = "bar"; 362 | baz = "hello"; 363 | } 364 | ``` 365 | is normally valid, but not when `unknown` is set to `false`. 366 | 367 | Because Nix lacks primitive operations to iterate over attribute sets dynamically without 368 | allocation this function allocates one intermediate attribute set per struct verification. 369 | 370 | - Custom invariants 371 | 372 | Custom struct verification functions can be added as such: 373 | ``` nix 374 | (types.struct "testStruct2" { 375 | x = types.int; 376 | y = types.int; 377 | }).override { 378 | verify = v: if v.x + v.y == 2 then "VERBOTEN" else null; 379 | }; 380 | ``` 381 | 382 | #### Function signature 383 | */ 384 | struct = 385 | # Name of struct type as a string 386 | name: 387 | # Attribute set of type definitions. 388 | members: 389 | assert isAttrs members; 390 | let 391 | names = attrNames members; 392 | withErrorContext = addErrorContext "in struct '${name}'"; 393 | 394 | mkStruct' = 395 | { 396 | total ? true, 397 | unknown ? true, 398 | verify ? null, 399 | }: 400 | assert isBool total; 401 | assert isBool unknown; 402 | assert verify != null -> isFunction verify; 403 | let 404 | optionalFuncs = 405 | optionalElem (!unknown) ( 406 | v: 407 | if removeAttrs v names == { } then 408 | null 409 | else 410 | "keys [${joinKeys (attrNames (removeAttrs v names))}] are unrecognized, expected keys are [${joinKeys names}]" 411 | ) 412 | ++ optionalElem (verify != null) verify; 413 | 414 | # Turn member verifications into a list of verification functions with their verify functions 415 | # already looked up & with error contexts already computed. 416 | verifyAttrs = 417 | let 418 | funcs = map ( 419 | attr: 420 | let 421 | memberType = members.${attr}; 422 | inherit (memberType) verify; 423 | withErrorContext = addErrorContext "in member '${attr}'"; 424 | missingMember = "missing member '${attr}'"; 425 | isOptionalAttr = memberType.__name == "optionalAttr"; 426 | in 427 | v: 428 | ( 429 | if v ? ${attr} then 430 | withErrorContext (verify v.${attr}) 431 | else if total && (!isOptionalAttr) then 432 | missingMember 433 | else 434 | null 435 | ) 436 | ) names; 437 | in 438 | v: 439 | if all (func: func v == null) funcs then 440 | null 441 | else 442 | ( 443 | # If an error was found, run the checks again to find the first error to return. 444 | foldl' ( 445 | acc: func: 446 | if acc != null then 447 | acc 448 | else if func v != null then 449 | func v 450 | else 451 | null 452 | ) null funcs 453 | ); 454 | 455 | verify' = 456 | if optionalFuncs == [ ] then 457 | verifyAttrs 458 | else 459 | let 460 | allFuncs = [ verifyAttrs ] ++ optionalFuncs; 461 | in 462 | v: 463 | foldl' ( 464 | acc: func: 465 | if acc != null then 466 | acc 467 | else if func v != null then 468 | func v 469 | else 470 | null 471 | ) null allFuncs; 472 | 473 | in 474 | (self.typedef' name (v: withErrorContext (if !isAttrs v then typeError name v else verify' v))) 475 | // { 476 | override = mkStruct'; 477 | }; 478 | in 479 | mkStruct' { }; 480 | 481 | /** 482 | optionalAttr 483 | */ 484 | optionalAttr = 485 | t: 486 | let 487 | name = "optionalAttr<${t.name}>"; 488 | inherit (t) verify; 489 | withErrorContext = addErrorContext "in ${name}"; 490 | in 491 | self.typedef' name (v: withErrorContext (verify v)); 492 | 493 | /** 494 | enum 495 | */ 496 | enum = 497 | # Name of enum type as a string 498 | name: 499 | # List of allowable enum members 500 | elems: 501 | assert isList elems; 502 | self.typedef' name ( 503 | v: if elem v elems then null else "'${toPretty v}' is not a member of enum '${name}'" 504 | ); 505 | }) 506 | -------------------------------------------------------------------------------- /types/tests.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | lib ? pkgs.lib, 4 | }: 5 | 6 | let 7 | inherit (lib) toUpper substring stringLength; 8 | 9 | types = import ./default.nix { inherit lib; }; 10 | 11 | capitalise = s: toUpper (substring 0 1 s) + (substring 1 (stringLength s) s); 12 | 13 | addCoverage = 14 | public: tests: 15 | ( 16 | assert !tests ? coverage; 17 | tests 18 | // { 19 | coverage = lib.mapAttrs' (n: _v: { 20 | name = "test" + (capitalise n); 21 | value = { 22 | expr = tests ? ${n}; 23 | expected = true; 24 | }; 25 | }) public; 26 | } 27 | ); 28 | 29 | in 30 | lib.fix ( 31 | self: 32 | addCoverage types { 33 | string = { 34 | testInvalid = { 35 | expr = types.str.verify 1; 36 | expected = "Expected type 'string' but value '1' is of type 'int'"; 37 | }; 38 | 39 | testValid = { 40 | expr = types.str.verify "Hello"; 41 | expected = null; 42 | }; 43 | }; 44 | str = self.string; 45 | 46 | # Dummy out for coverage 47 | typedef = { 48 | testValid = { 49 | expr = (types.typedef "testDef" (_: true)).name; 50 | expected = "testDef"; 51 | }; 52 | testInvalidName = { 53 | expr = types.typedef 1 null; 54 | expectedError.type = "AssertionError"; 55 | }; 56 | testInvalidFunc = { 57 | expr = types.typedef "testDef" "x"; 58 | expectedError.type = "AssertionError"; 59 | }; 60 | }; 61 | typedef' = { 62 | testValid = { 63 | expr = (types.typedef' "testDef" (_: true)).name; 64 | expected = "testDef"; 65 | }; 66 | testInvalidName = { 67 | expr = types.typedef' 1 null; 68 | expectedError.type = "AssertionError"; 69 | }; 70 | testInvalidFunc = { 71 | expr = types.typedef' "testDef" "x"; 72 | expectedError.type = "AssertionError"; 73 | }; 74 | }; 75 | 76 | function = { 77 | testInvalid = { 78 | expr = types.function.verify 1; 79 | expected = "Expected type 'function' but value '1' is of type 'int'"; 80 | }; 81 | 82 | testValid = { 83 | expr = types.function.verify (_: null); 84 | expected = null; 85 | }; 86 | }; 87 | 88 | path = { 89 | testInvalid = { 90 | expr = types.path.verify 1; 91 | expected = "Expected type 'path' but value '1' is of type 'int'"; 92 | }; 93 | 94 | testValid = { 95 | expr = types.path.verify ./.; 96 | expected = null; 97 | }; 98 | }; 99 | 100 | derivation = { 101 | testInvalid = { 102 | expr = types.derivation.verify { }; 103 | expected = "Expected type 'derivation' but value '{ }' is of type 'set'"; 104 | }; 105 | 106 | testValid = { 107 | expr = types.derivation.verify ( 108 | builtins.derivation { 109 | name = "test"; 110 | builder = ":"; 111 | system = "fake"; 112 | } 113 | ); 114 | expected = null; 115 | }; 116 | }; 117 | 118 | any = { 119 | testValid = { 120 | expr = types.any.verify (throw "NO U"); # Note: Value not checked 121 | expected = null; 122 | }; 123 | }; 124 | 125 | never = { 126 | testInvalid = { 127 | expr = types.never.verify 1234; 128 | expected = "Expected type 'never' but value '1234' is of type 'int'"; 129 | }; 130 | }; 131 | 132 | int = { 133 | testInvalid = { 134 | expr = types.int.verify "x"; 135 | expected = "Expected type 'int' but value '\"x\"' is of type 'string'"; 136 | }; 137 | 138 | testValid = { 139 | expr = types.int.verify 1; 140 | expected = null; 141 | }; 142 | }; 143 | 144 | float = { 145 | testInvalid = { 146 | expr = types.float.verify "x"; 147 | expected = "Expected type 'float' but value '\"x\"' is of type 'string'"; 148 | }; 149 | 150 | testValid = { 151 | expr = types.float.verify 1.0; 152 | expected = null; 153 | }; 154 | }; 155 | 156 | number = { 157 | testInvalid = { 158 | expr = types.number.verify "x"; 159 | expected = "Expected type 'number' but value '\"x\"' is of type 'string'"; 160 | }; 161 | 162 | testValidInt = { 163 | expr = types.number.verify 1; 164 | expected = null; 165 | }; 166 | 167 | testValidFloat = { 168 | expr = types.number.verify 1.0; 169 | expected = null; 170 | }; 171 | }; 172 | 173 | bool = { 174 | testInvalid = { 175 | expr = types.bool.verify "x"; 176 | expected = "Expected type 'bool' but value '\"x\"' is of type 'string'"; 177 | }; 178 | 179 | testValid = { 180 | expr = types.bool.verify true; 181 | expected = null; 182 | }; 183 | }; 184 | 185 | attrs = { 186 | testInvalid = { 187 | expr = types.attrs.verify "x"; 188 | expected = "Expected type 'attrs' but value '\"x\"' is of type 'string'"; 189 | }; 190 | 191 | testValid = { 192 | expr = types.attrs.verify { }; 193 | expected = null; 194 | }; 195 | }; 196 | 197 | list = { 198 | testInvalid = { 199 | expr = types.list.verify "x"; 200 | expected = "Expected type 'list' but value '\"x\"' is of type 'string'"; 201 | }; 202 | 203 | testValid = { 204 | expr = types.list.verify [ ]; 205 | expected = null; 206 | }; 207 | }; 208 | 209 | listOf = 210 | let 211 | testListOf = types.listOf types.str; 212 | in 213 | { 214 | testValid = { 215 | expr = testListOf.verify [ "hello" ]; 216 | expected = null; 217 | }; 218 | 219 | testInvalidElem = { 220 | expr = testListOf.verify [ 1 ]; 221 | expected = "in listOf element: Expected type 'string' but value '1' is of type 'int'"; 222 | }; 223 | 224 | testInvalidType = { 225 | expr = testListOf.verify 1; 226 | expected = "Expected type 'listOf' but value '1' is of type 'int'"; 227 | }; 228 | }; 229 | 230 | attrsOf = 231 | let 232 | testAttrsOf = types.attrsOf types.str; 233 | in 234 | { 235 | testValid = { 236 | expr = testAttrsOf.verify { 237 | x = "hello"; 238 | }; 239 | expected = null; 240 | }; 241 | 242 | testInvalidElem = { 243 | expr = testAttrsOf.verify { 244 | x = 1; 245 | }; 246 | expected = "in attrsOf value: Expected type 'string' but value '1' is of type 'int'"; 247 | }; 248 | 249 | testInvalidType = { 250 | expr = testAttrsOf.verify 1; 251 | expected = "Expected type 'attrsOf' but value '1' is of type 'int'"; 252 | }; 253 | }; 254 | 255 | union = 256 | let 257 | testUnion = types.union [ types.str ]; 258 | in 259 | { 260 | testValid = { 261 | expr = testUnion.verify "hello"; 262 | expected = null; 263 | }; 264 | 265 | testInvalid = { 266 | expr = testUnion.verify 1; 267 | expected = "Expected type 'union' but value '1' is of type 'int'"; 268 | }; 269 | }; 270 | 271 | intersection = 272 | let 273 | struct1 = types.struct "struct1" { 274 | a = types.str; 275 | }; 276 | 277 | struct2 = types.struct "struct2" { 278 | b = types.str; 279 | }; 280 | 281 | testIntersection = types.intersection [ 282 | struct1 283 | struct2 284 | ]; 285 | in 286 | { 287 | testValid = { 288 | expr = testIntersection.verify { 289 | a = "foo"; 290 | b = "bar"; 291 | }; 292 | expected = null; 293 | }; 294 | 295 | testInvalid = { 296 | expr = testIntersection.verify 1; 297 | expected = "Expected type 'intersection' but value '1' is of type 'int'"; 298 | }; 299 | }; 300 | 301 | type = { 302 | testValid = { 303 | expr = types.type.verify types.string; 304 | expected = null; 305 | }; 306 | 307 | testInvalid = { 308 | expr = types.type.verify { }; 309 | expected = "Expected type 'type' but value '{ }' is of type 'set'"; 310 | }; 311 | }; 312 | 313 | option = 314 | let 315 | testOption = types.option types.str; 316 | in 317 | { 318 | testValidString = { 319 | expr = testOption.verify "hello"; 320 | expected = null; 321 | }; 322 | 323 | testNull = { 324 | expr = testOption.verify null; 325 | expected = null; 326 | }; 327 | 328 | testInvalid = { 329 | expr = testOption.verify 3; 330 | expected = "in option: Expected type 'string' but value '3' is of type 'int'"; 331 | }; 332 | }; 333 | 334 | struct = 335 | let 336 | testStruct = types.struct "testStruct" { 337 | foo = types.string; 338 | }; 339 | 340 | testStruct2 = 341 | (types.struct "testStruct2" { 342 | x = types.int; 343 | y = types.int; 344 | }).override 345 | { 346 | verify = v: if v.x + v.y == 2 then "VERBOTEN" else null; 347 | }; 348 | 349 | testStructNonTotal = testStruct.override { total = false; }; 350 | testStructWithoutUnknown = testStruct.override { unknown = false; }; 351 | 352 | in 353 | { 354 | testValid = { 355 | expr = testStruct.verify { 356 | foo = "bar"; 357 | }; 358 | expected = null; 359 | }; 360 | 361 | testMissingAttr = { 362 | expr = testStruct.verify { }; 363 | expected = "in struct 'testStruct': missing member 'foo'"; 364 | }; 365 | 366 | testNonTotal = { 367 | expr = testStructNonTotal.verify { 368 | foo = "bar"; 369 | unknown = "is allowed"; 370 | }; 371 | expected = null; 372 | }; 373 | 374 | testExtraInvariantCheck = { 375 | expr = testStruct2.verify { 376 | x = 1; 377 | y = 1; 378 | }; 379 | expected = "in struct 'testStruct2': VERBOTEN"; 380 | }; 381 | 382 | testUnknownAttrNotAllowed = { 383 | expr = testStructWithoutUnknown.verify { 384 | foo = "bar"; 385 | bar = "foo"; 386 | }; 387 | expected = "in struct 'testStruct': keys ['bar'] are unrecognized, expected keys are ['foo']"; 388 | }; 389 | 390 | testUnknownAttr = { 391 | expr = testStruct.verify { 392 | foo = "bar"; 393 | bar = "foo"; 394 | }; 395 | expected = null; 396 | }; 397 | 398 | testInvalidType = { 399 | expr = testStruct.verify "bar"; 400 | expected = "in struct 'testStruct': Expected type 'testStruct' but value '\"bar\"' is of type 'string'"; 401 | }; 402 | 403 | testInvalidMember = { 404 | expr = testStruct.verify { 405 | foo = 1; 406 | }; 407 | expected = "in struct 'testStruct': in member 'foo': Expected type 'string' but value '1' is of type 'int'"; 408 | }; 409 | }; 410 | 411 | optionalAttr = 412 | let 413 | testStruct = types.struct "testOptionalAttrStruct" { 414 | foo = types.string; 415 | optionalFoo = types.optionalAttr types.string; 416 | }; 417 | 418 | in 419 | { 420 | testWithOptional = { 421 | expr = testStruct.verify { 422 | foo = "hello"; 423 | optionalFoo = "goodbye"; 424 | }; 425 | expected = null; 426 | }; 427 | 428 | testWithoutOptional = { 429 | expr = testStruct.verify { 430 | foo = "hello"; 431 | }; 432 | expected = null; 433 | }; 434 | 435 | testWithInvalidOptional = { 436 | expr = testStruct.verify { 437 | foo = "hello"; 438 | optionalFoo = 1234; 439 | }; 440 | expected = "in struct 'testOptionalAttrStruct': in member 'optionalFoo': in optionalAttr: Expected type 'string' but value '1234' is of type 'int'"; 441 | }; 442 | }; 443 | 444 | enum = 445 | let 446 | testEnum = types.enum "testEnum" [ 447 | "A" 448 | "B" 449 | "C" 450 | ]; 451 | in 452 | { 453 | testHasElem = { 454 | expr = testEnum.verify "B"; 455 | expected = null; 456 | }; 457 | 458 | testNotHasElem = { 459 | expr = testEnum.verify "nope"; 460 | expected = "'\"nope\"' is not a member of enum 'testEnum'"; 461 | }; 462 | }; 463 | 464 | rename = { 465 | testRename = { 466 | expr = 467 | let 468 | t = types.rename "florp" types.string; 469 | in 470 | { 471 | inherit (t) name; 472 | isFunction = builtins.isFunction t.verify; 473 | }; 474 | expected = { 475 | name = "florp"; 476 | isFunction = true; 477 | }; 478 | }; 479 | }; 480 | 481 | recursiveTypes = { 482 | struct = 483 | let 484 | recursive = types.struct "recursive" { 485 | children = types.optionalAttr (types.attrsOf recursive); 486 | }; 487 | in 488 | { 489 | testOK = { 490 | expr = recursive.verify { 491 | children = { 492 | x = { }; 493 | }; 494 | }; 495 | expected = null; 496 | }; 497 | 498 | testNotOK = { 499 | expr = recursive.check { 500 | children = { 501 | x = "hello"; 502 | }; 503 | } null; 504 | expectedError.type = "ThrownError"; 505 | }; 506 | }; 507 | 508 | attrsOf = 509 | let 510 | # Because attrsOf inherits names from it's sub-types we need to erase the name to not cause infinite recursion. 511 | # This should have it's own exposed function. 512 | type = types.attrsOf ( 513 | types.rename "eitherType" ( 514 | types.union [ 515 | types.string 516 | type 517 | ] 518 | ) 519 | ); 520 | in 521 | { 522 | testOK = { 523 | expr = type.verify { 524 | foo = "bar"; 525 | baz = { 526 | foo = "bar"; 527 | baz = { 528 | foo = "bar"; 529 | }; 530 | }; 531 | }; 532 | expected = null; 533 | }; 534 | 535 | testNotOK = { 536 | expr = type.check { 537 | foo = "bar"; 538 | baz = { 539 | foo = "bar"; 540 | baz = { 541 | foo = "bar"; 542 | int = 1; 543 | }; 544 | }; 545 | } null; 546 | expectedError.type = "ThrownError"; 547 | }; 548 | }; 549 | }; 550 | } 551 | ) 552 | -------------------------------------------------------------------------------- /adios/default.nix: -------------------------------------------------------------------------------- 1 | let 2 | types = import ./types.nix { 3 | korora = import ../types/types.nix; 4 | }; 5 | 6 | # Helper functions for users, accessed through `adios.lib` 7 | lib = { 8 | importModules = import ./lib/importModules.nix { inherit adios; }; 9 | }; 10 | 11 | inherit (builtins) 12 | attrNames 13 | listToAttrs 14 | mapAttrs 15 | concatMap 16 | isAttrs 17 | genericClosure 18 | filter 19 | isString 20 | split 21 | head 22 | tail 23 | foldl' 24 | attrValues 25 | substring 26 | concatStringsSep 27 | intersectAttrs 28 | functionArgs 29 | ; 30 | 31 | optionalAttrs = cond: attrs: if cond then attrs else { }; 32 | 33 | # A coarse grained options type for input validation 34 | optionsType = types.attrsOf types.attrs; 35 | 36 | # Default in error messages when no name is provided 37 | anonymousModuleName = ""; 38 | 39 | # Call a function with only it's supported attributes. 40 | callFunction = fn: attrs: fn (intersectAttrs (functionArgs fn) attrs); 41 | 42 | # Compute options from defaults & provided args 43 | computeOptions = 44 | let 45 | checkOption = 46 | errorPrefix: option: value: 47 | let 48 | err = option.type.verify value; 49 | in 50 | if err != null then (throw "${errorPrefix}: ${err}") else value; 51 | in 52 | # Computed args fixpoint 53 | self: 54 | # Error prefix string 55 | errorPrefix: 56 | # Defined options 57 | options: 58 | # Passed options 59 | args: 60 | listToAttrs ( 61 | concatMap ( 62 | name: 63 | let 64 | option = options.${name}; 65 | errorPrefix' = "${errorPrefix}: in option '${name}'"; 66 | in 67 | # Explicitly passed value 68 | if args ? ${name} then 69 | [ 70 | { 71 | inherit name; 72 | value = checkOption errorPrefix' option args.${name}; 73 | } 74 | ] 75 | # Default value 76 | else if option ? default then 77 | [ 78 | { 79 | inherit name; 80 | value = checkOption errorPrefix' option option.default; 81 | } 82 | ] 83 | # Computed default value 84 | else if option ? defaultFunc then 85 | [ 86 | { 87 | # Compute value with args fixpoint 88 | inherit name; 89 | value = checkOption errorPrefix' option (callFunction option.defaultFunc self); 90 | } 91 | ] 92 | # Compute nested options 93 | else if option ? options then 94 | let 95 | value = computeOptions self errorPrefix' options.${name} (args.${name} or { }); 96 | in 97 | # Only return a value if suboptions actually returned anything 98 | if value != { } then [ { inherit name value; } ] else [ ] 99 | # Nothing passed & no default. Leave unset. 100 | else 101 | [ ] 102 | ) (attrNames options) 103 | ); 104 | 105 | # Lazy typecheck options 106 | checkOptionsType = 107 | errorPrefix: options: 108 | mapAttrs ( 109 | name: option: 110 | if option ? options then 111 | { options = checkOptionsType "${errorPrefix}: in option '${name}'" option.options; } 112 | else 113 | let 114 | err = types.modules.option.verify option; 115 | in 116 | if err != null then throw "${errorPrefix}: in option '${name}': type error: ${err}" else option 117 | ) options; 118 | 119 | # Lazy type check an attrset 120 | checkAttrsOf = 121 | errorPrefix: type: value: 122 | let 123 | err = type.verify value; 124 | in 125 | if err == null then 126 | value 127 | else if isAttrs value then 128 | mapAttrs (name: checkAttrsOf "${errorPrefix}: in attr '${name}'" type) value 129 | else 130 | throw "${errorPrefix}: in attr: ${err}"; 131 | 132 | # Check a single type with error prefix 133 | checkType = 134 | errorPrefix: type: value: 135 | let 136 | err = type.verify value; 137 | in 138 | if err == null then value else throw "${errorPrefix}: ${err}"; 139 | 140 | # Type check a module lazily 141 | loadModule = 142 | def: 143 | let 144 | errorPrefix = 145 | if def ? name then "in module ${types.string.check def.name def.name}" else "in module"; 146 | in 147 | # The loaded module instance 148 | { 149 | options = checkOptionsType "${errorPrefix} options definition" (def.options or { }); 150 | 151 | modules = mapAttrs (_: loadModule) (def.modules or { }); 152 | 153 | lib = checkType "${errorPrefix}: while checking 'lib'" types.modules.lib (def.lib or { }); 154 | 155 | types = checkAttrsOf "${errorPrefix}: while checking 'types'" types.modules.typedef ( 156 | def.types or { } 157 | ); 158 | 159 | inputs = checkAttrsOf "${errorPrefix}: while checking 'inputs'" types.modules.input ( 160 | def.inputs or { } 161 | ); 162 | } 163 | // (optionalAttrs (def ? name) { 164 | name = checkType "${errorPrefix}: while checking 'name'" types.string def.name; 165 | }) 166 | // (optionalAttrs (def ? impl) { 167 | impl = checkType "${errorPrefix}: while checking 'impl'" types.function def.impl; 168 | 169 | # Make contract callable 170 | __functor = 171 | self: 172 | { 173 | options ? { }, 174 | inputs ? { }, 175 | }: 176 | let 177 | args' = computeOptions args' errorPrefix self.options { inherit options inputs; }; 178 | in 179 | callFunction self.impl args'; 180 | }); 181 | 182 | # Merge lhs & rhs recursing into suboptions 183 | mergeOptionsUnchecked = 184 | options: lhs: rhs: 185 | lhs 186 | // rhs 187 | // listToAttrs ( 188 | concatMap ( 189 | optionName: 190 | let 191 | option = options.${optionName}; 192 | in 193 | if option ? options then 194 | [ 195 | { 196 | name = optionName; 197 | value = mergeOptionsUnchecked option.options (lhs.${optionName} or { }) (rhs.${optionName} or { }); 198 | } 199 | ] 200 | else 201 | [ ] 202 | ) (attrNames options) 203 | ); 204 | 205 | # Split string by separator 206 | splitString = sep: s: filter isString (split sep s); 207 | 208 | # Return absolute module path relative to pwd 209 | absModulePath = 210 | pwd: path: toString (if substring 0 1 path == "/" then /. + path else /. + pwd + "/${path}"); 211 | 212 | # Get a module by it's / delimited path 213 | getModule = 214 | module: name: 215 | assert name != ""; 216 | if name == "/" then 217 | module 218 | else 219 | let 220 | tokens = splitString "/" name; 221 | in 222 | # Assert that module input begins with a / 223 | if head tokens != "" then 224 | throw '' 225 | Module path `${name}` didn't start with a slash, when it was expected to. 226 | This likely means you used the incorrect name during the eval stage. 227 | A module path should look something like "/nixpkgs", which refers to `root.modules.nixpkgs`, 228 | and lets us set the options for that module. 229 | '' 230 | else 231 | foldl' ( 232 | module: tok: 233 | if !module.modules ? ${tok} then 234 | throw '' 235 | Module path `${tok}` wasn't a child module of `${module.name or anonymousModuleName}`. 236 | Valid children of `${module.name}`: [${concatStringsSep ", " (attrNames module.modules)}] 237 | '' 238 | else 239 | module.modules.${tok} 240 | ) module (tail tokens); 241 | 242 | # Resolve required module dependencies for defined config options 243 | resolveTree = 244 | scope: moduleNames: 245 | listToAttrs ( 246 | map 247 | (x: { 248 | name = x.key; 249 | value = getModule scope x.key; 250 | }) 251 | (genericClosure { 252 | # Get startSet from passed config 253 | startSet = map (key: { 254 | inherit key; 255 | }) moduleNames; 256 | # Discover module dependencies 257 | operator = 258 | { key }: 259 | map (input: { 260 | key = absModulePath key input.path; 261 | }) (attrValues (getModule scope key).inputs); 262 | }) 263 | ); 264 | 265 | evalModuleTree = 266 | { 267 | # Passed options 268 | options, 269 | # Resolved modules attrset 270 | resolution, 271 | # Previous eval memoisation 272 | memoArgs ? { }, 273 | memoResults ? { }, 274 | }: 275 | rec { 276 | # Computed options/inputs for each module in resolution 277 | args = 278 | mapAttrs (modulePath: module: { 279 | inputs = mapAttrs (_: input: args.${input.path}.options) module.inputs; 280 | options = computeOptions args.${modulePath} "while computing ${modulePath} args" module.options ( 281 | options.${modulePath} or { } 282 | ); 283 | }) resolution 284 | // memoArgs; 285 | 286 | inherit options resolution; 287 | 288 | # Module call results for each callable module in resolution 289 | results = 290 | listToAttrs ( 291 | concatMap ( 292 | modulePath: 293 | let 294 | module = resolution.${modulePath}; 295 | in 296 | if module ? impl then 297 | [ 298 | { 299 | name = modulePath; 300 | value = callFunction module.impl args.${modulePath}; 301 | } 302 | ] 303 | else 304 | [ ] 305 | ) (attrNames resolution) 306 | ) 307 | // memoResults; 308 | }; 309 | 310 | # Apply options to a module tree, returning a new module tree where modules can be called 311 | # with their inputs already wired up & options partially applied. 312 | applyTreeOptions = 313 | { 314 | # Root module 315 | root, 316 | # Passed options 317 | options, 318 | # Attrset of computed args from tree eval context 319 | args, 320 | }: 321 | let 322 | recurse = 323 | # Path to current module as a list of string 324 | modulePath': 325 | # Current module 326 | module: 327 | let 328 | # Create submodule path string 329 | modulePath = "/" + concatStringsSep "/" modulePath'; 330 | 331 | # Module arguments 332 | args' = 333 | # Take args from resolved context if it's available there. 334 | args.${modulePath} or 335 | # fall back to computing 336 | { 337 | inputs = mapAttrs ( 338 | _: input: (getModule tree' (absModulePath modulePath input.path)).args.options 339 | ) module.inputs; 340 | options = computeOptions args' "while computing ${modulePath} args" module.options ( 341 | options.${modulePath} or { } 342 | ); 343 | }; 344 | in 345 | module 346 | // { 347 | args = args'; 348 | # Recurse into child modules 349 | modules = mapAttrs (moduleName: recurse (modulePath' ++ [ moduleName ])) module.modules; 350 | } 351 | // optionalAttrs (module ? impl) { 352 | # Wrap module call with computed args 353 | __functor = 354 | let 355 | passedOptions = options.${modulePath} or { }; 356 | in 357 | self: options: 358 | let 359 | # Concat passed options with options passed to tree eval 360 | options' = mergeOptionsUnchecked self.options passedOptions options; 361 | # Re-compute args fixpoint with passed args 362 | args = { 363 | inherit (self.args) inputs; 364 | options = computeOptions args "while calling ${modulePath}" module.options options'; 365 | }; 366 | in 367 | # Call implementation 368 | self.impl args; 369 | }; 370 | 371 | tree' = recurse [ ] root; 372 | in 373 | tree'; 374 | 375 | mkOverride = 376 | root: prevEval: 377 | { 378 | # Updated options 379 | options ? { }, 380 | # Whether to allow re-resolving 381 | resolve ? true, 382 | }: 383 | optionsType.check options ( 384 | let 385 | # TODO: Filter nulled out options 386 | options' = prevEval.options // options; 387 | 388 | # Names of all modules being updated 389 | moduleNames = attrNames options; 390 | 391 | # Names of all modules being referenced in the new options, but not present 392 | # in the old module resolution. 393 | # If this list is non-empty modules have to be re-resolved. 394 | newModuleNames = filter (name: !prevEval.resolution ? ${name}) moduleNames; 395 | 396 | # Module dependency resolution 397 | resolution = 398 | if newModuleNames != [ ] then 399 | ( 400 | if resolve then 401 | resolveTree root (attrNames options') 402 | else 403 | throw '' 404 | Module overriding caused re-resolving, which is disabled. 405 | Differing modules: ${concatStringsSep " " newModuleNames} 406 | '' 407 | ) 408 | else 409 | prevEval.resolution; 410 | 411 | # Resolve which module options/results needs to be invalidated 412 | diff = 413 | let 414 | resolutionNames = attrNames resolution; 415 | in 416 | map (result: result.key) (genericClosure { 417 | startSet = map (key: { inherit key; }) moduleNames; 418 | operator = 419 | { key }: 420 | concatMap ( 421 | name: if resolution.${name}.inputs ? ${key} then [ { key = name; } ] else [ ] 422 | ) resolutionNames; 423 | }); 424 | 425 | result = { 426 | # Overriden eval context 427 | evalParams = evalModuleTree { 428 | inherit resolution; 429 | options = options'; 430 | memoArgs = removeAttrs prevEval.args diff; 431 | memoResults = removeAttrs prevEval.results diff; 432 | }; 433 | 434 | # Tree context 435 | root = applyTreeOptions { 436 | inherit root; 437 | options = options'; 438 | inherit (result.evalParams) args; 439 | }; 440 | 441 | # Chained override function 442 | override = mkOverride root result.evalParams; 443 | }; 444 | in 445 | result 446 | ); 447 | 448 | # Load a module tree recursively from root module 449 | loadTree = 450 | root: 451 | let 452 | root' = loadModule root; 453 | in 454 | { 455 | root = root'; 456 | 457 | eval = 458 | { 459 | options ? { }, 460 | }: 461 | optionsType.check options ( 462 | let 463 | result = { 464 | # Eval context 465 | evalParams = 466 | let 467 | resolution = resolveTree root' (attrNames options); 468 | in 469 | evalModuleTree { inherit resolution options; }; 470 | 471 | # Tree context 472 | root = applyTreeOptions { 473 | root = root'; 474 | inherit options; 475 | inherit (result.evalParams) args; 476 | }; 477 | 478 | # Chained override function 479 | override = mkOverride root' result.evalParams; 480 | }; 481 | in 482 | result 483 | ); 484 | }; 485 | 486 | adios = 487 | (loadModule { 488 | name = "adios"; 489 | inherit types lib; 490 | }) 491 | // { 492 | # Overwrite default functor with one that _does not_ do type checking. 493 | # `load` does it's own type checking. 494 | __functor = _: loadTree; 495 | }; 496 | 497 | in 498 | adios 499 | --------------------------------------------------------------------------------