├── .envrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── lib ├── default.nix └── eval.nix └── tests ├── json ├── data.input ├── default.nix ├── expected.json └── json.cue ├── pre-commit ├── default.nix ├── expected.yml └── pre-commit.cue └── text ├── default.nix ├── expected.txt └── text.cue /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | paths-ignore: 4 | - '**.md' 5 | push: 6 | branches: 7 | - master 8 | paths-ignore: 9 | - '**.md' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | format: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: cachix/install-nix-action@v17 18 | - run: nix develop -c nixpkgs-fmt --check $(git ls-files **/*.nix) 19 | test: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: cachix/install-nix-action@v17 24 | - run: nix flake check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joshua Gilman 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nix-cue 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | > Validate and generate configuration files using [Nix][1] and [Cue][2]. 14 | 15 | ## Features 16 | 17 | - Specify configuration data using native Nix syntax 18 | - Validate configuration data using the language features from [Cue][2] 19 | - Generate configuration files in any of the [supported formats][3] 20 | - All artifacts are placed in the Nix store 21 | 22 | ## Usage 23 | 24 | Add the flake as an input: 25 | 26 | ```nix 27 | { #.... 28 | inputs = { 29 | # ... 30 | nix-cue.url = "github:jmgilman/nix-cue"; 31 | }; 32 | } 33 | ``` 34 | 35 | The flake provides a single function: `nix-cue.lib.${system}.eval`. The function 36 | takes a few common parameters, for example: 37 | 38 | ```nix 39 | { # ... 40 | configFile = nix-cue.lib.${system}.eval { 41 | inherit pkgs; 42 | inputFiles = [ ./pre-commit.cue ]; # Input files to pass to `cue eval` 43 | outputFile = ".pre-commit-config.yaml"; # Output file to put in Nix store 44 | data = { 45 | # Concrete data to pass to `cue eval` 46 | }; 47 | }; 48 | } 49 | ``` 50 | 51 | The full path to the output file in the Nix store will be returned (in the above 52 | case, we are storing it in `configFile`). The `data` parameter is optional and 53 | is used to pass a Nix set as concrete input to `cue`. The expression is 54 | converted to JSON and added as an additional input file. 55 | 56 | `cue` determines the output format by examining the output file extension. If 57 | you need to output a specific format without a matching file extension, pass the 58 | `output` flag with the desired format. 59 | 60 | Flags can be passed to `cue eval` by appending them to the function arguments. 61 | The format is `{ flag_name = flag_value; }`. For example, to force `cue` to 62 | output JSON regardless of the output file extension: 63 | 64 | ```nix 65 | { # ... 66 | configFile = nix-cue.lib.${system}.eval { 67 | # ... 68 | output = "json"; # Equivalent to --output "json" 69 | # ... 70 | }; 71 | } 72 | ``` 73 | 74 | ## Example 75 | 76 | As an example, we can validate and generate a configuration file for 77 | [pre-commit][4]. The first step is to define a cue file: 78 | 79 | ```cue 80 | #Config: { 81 | default_install_hook_types?: [...string] 82 | default_language_version?: [string]: string 83 | default_stages?: [...string] 84 | files?: string 85 | exclude?: string 86 | fail_fast?: bool 87 | minimum_pre_commit_version?: string 88 | repos: [...#Repo] 89 | } 90 | 91 | #Hook: { 92 | additional_dependencies?: [...string] 93 | alias?: string 94 | always_run?: bool 95 | args?: [...string] 96 | exclude?: string 97 | exclude_types?: [...string] 98 | files?: string 99 | id: string 100 | language_version?: string 101 | log_file?: string 102 | name?: string 103 | stages?: [...string] 104 | types?: [...string] 105 | types_or?: [...string] 106 | verbose?: bool 107 | } 108 | 109 | #Repo: { 110 | repo: string 111 | rev?: string 112 | if repo != "local" { 113 | rev: string 114 | } 115 | hooks: [...#Hook] 116 | } 117 | 118 | { 119 | #Config 120 | } 121 | ``` 122 | 123 | This validates against the schema described in the [official docs][5]. With the 124 | Cue file, we can now define our input data and generate the YAML configuration: 125 | 126 | ```nix 127 | { 128 | inputs = { 129 | nixpkgs.url = "github:nixos/nixpkgs"; 130 | flake-utils.url = "github:numtide/flake-utils"; 131 | nix-cue.url = "github:jmgilman/nix-cue"; 132 | }; 133 | 134 | outputs = { self, nixpkgs, flake-utils, nix-cue }: 135 | flake-utils.lib.eachDefaultSystem (system: 136 | let 137 | pkgs = import nixpkgs { inherit system; }; 138 | 139 | # Define our pre-commit configuration 140 | config = { 141 | repos = [ 142 | { 143 | repo = "https://github.com/test/repo"; 144 | rev = "1.0"; 145 | hooks = [ 146 | { 147 | id = "my-hook"; 148 | } 149 | ]; 150 | } 151 | ]; 152 | }; 153 | 154 | # Validate the configuration and generate the output file 155 | configFile = nix-cue.lib.${system}.eval { 156 | inherit pkgs; 157 | inputFiles = [ ./pre-commit.cue ]; 158 | outputFile = ".pre-commit-config.yaml"; 159 | data = config; 160 | }; 161 | in 162 | { 163 | lib = { 164 | mkConfig = import ./lib/pre-commit.nix; 165 | }; 166 | 167 | devShell = pkgs.mkShell { 168 | shellHook = '' 169 | # Link the store output to our local directory 170 | unlink .pre-commit-config.yaml 171 | ln -s ${configFile} .pre-commit-config.yaml 172 | ''; 173 | }; 174 | } 175 | ); 176 | } 177 | ``` 178 | 179 | Running `nix develop` with the above flake will generate a 180 | `.pre-commit-config.yaml` file in the store using the configuration given in 181 | `config` and then link it to the local directory via the `shellHook`: 182 | 183 | ```yaml 184 | repos: 185 | - hooks: 186 | - id: my-hook 187 | repo: https://github.com/test/repo 188 | rev: "1.0" 189 | ``` 190 | 191 | You can see more examples in the [tests](./tests) folder. 192 | 193 | ## Testing 194 | 195 | Tests can be run with: 196 | 197 | ```shell 198 | nix flake check 199 | ``` 200 | 201 | ## Contributing 202 | 203 | Check out the [issues][6] for items needing attention or submit your own and 204 | then: 205 | 206 | 1. Fork the repo () 207 | 2. Create your feature branch (git checkout -b feature/fooBar) 208 | 3. Commit your changes (git commit -am 'Add some fooBar') 209 | 4. Push to the branch (git push origin feature/fooBar) 210 | 5. Create a new Pull Request 211 | 212 | [1]: https://nixos.org/ 213 | [2]: https://cuelang.org/ 214 | [3]: https://cuelang.org/docs/integrations/ 215 | [4]: https://pre-commit.com/ 216 | [5]: https://pre-commit.com/#adding-pre-commit-plugins-to-your-project 217 | [6]: https://github.com/jmgilman/nix-cue/issues 218 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1652776076, 6 | "narHash": "sha256-gzTw/v1vj4dOVbpBSJX4J0DwUR6LIyXo7/SuuTJp1kM=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "04c1b180862888302ddfb2e3ad9eaa63afc60cf8", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1653491992, 21 | "narHash": "sha256-5gG0DeKiqt1jArgwkVvzxPtga96HtTDZAlBnpmYhLcU=", 22 | "owner": "nixos", 23 | "repo": "nixpkgs", 24 | "rev": "5e880240cddf43a67012b8331bfcd2d3a784b1a4", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "nixos", 29 | "repo": "nixpkgs", 30 | "type": "github" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | } 39 | }, 40 | "root": "root", 41 | "version": 7 42 | } 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | }; 6 | 7 | outputs = { self, nixpkgs, flake-utils }: 8 | flake-utils.lib.eachDefaultSystem (system: 9 | let 10 | pkgs = import nixpkgs { inherit system; }; 11 | lib = import ./lib; 12 | in 13 | { 14 | inherit lib; 15 | 16 | checks = { 17 | json = pkgs.callPackage ./tests/json { inherit pkgs lib; }; 18 | pre-commit = pkgs.callPackage ./tests/pre-commit { inherit pkgs lib; }; 19 | text = pkgs.callPackage ./tests/text { inherit pkgs lib; }; 20 | }; 21 | 22 | devShells.default = pkgs.mkShell { 23 | packages = [ 24 | pkgs.cue 25 | pkgs.nixpkgs-fmt 26 | ]; 27 | }; 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | eval = import ./eval.nix; 3 | } 4 | -------------------------------------------------------------------------------- /lib/eval.nix: -------------------------------------------------------------------------------- 1 | { pkgs, inputFiles, outputFile, data ? { }, cue ? pkgs.cue, ... }@args: 2 | with pkgs.lib; 3 | let 4 | json = optionalString (data != { }) (builtins.toJSON data); 5 | 6 | defaultFlags = { 7 | outfile = "$out"; 8 | }; 9 | extraFlags = removeAttrs args [ "pkgs" "inputFiles" "outputFile" "data" "cue" ]; 10 | 11 | allFlags = defaultFlags // extraFlags; 12 | allInputs = inputFiles ++ optionals (json != "") [ "json: $jsonPath" ]; 13 | 14 | flagsToString = name: value: if (builtins.isBool value) then "--${name}" else ''--${name} "${value}"''; 15 | flagStr = builtins.concatStringsSep " " (attrValues (mapAttrs flagsToString allFlags)); 16 | inputStr = builtins.concatStringsSep " " allInputs; 17 | cueEvalCmd = "cue eval ${flagStr} ${inputStr}"; 18 | 19 | result = pkgs.runCommand outputFile 20 | ({ 21 | inherit json; 22 | buildInputs = [ cue ]; 23 | passAsFile = [ "json" ]; 24 | } // optionalAttrs (json != "") { inherit json; passAsFile = [ "json" ]; }) 25 | '' 26 | echo "nix-cue: Rendering output..." 27 | ${cueEvalCmd} 28 | ''; 29 | in 30 | result 31 | 32 | # { pkgs, inputs, output, data ? { }, cue ? pkgs.cue, ... }@flags: 33 | # with pkgs.lib; 34 | # let 35 | # # Converts flags to strings 36 | # flagsToString = name: value: if (builtins.isBool value) then "--${name}" else ''--${name} "${value}"''; 37 | 38 | # # It's common to set the input in a cue file to a named value (i.e. data: _) 39 | # # The wrap parameter is a helper to easily wrap the input to this named value 40 | # dataJSON = optionalString (data != { }) (builtins.toJSON data); 41 | # # json = if wrap != "" then builtins.toJSON { "${wrap}" = input; } else builtins.toJSON input; 42 | 43 | # allFlags = { 44 | # force = true; 45 | # out = output; 46 | # outfile = "$out"; 47 | # } // flags; 48 | 49 | # allInputs = inputs ++ optional (dataJSON != "") [ "json:$jsonPath" ]; 50 | 51 | # flagStr = builtins.concatStringsSep " " (attrValues (mapAttrs flagsToString allFlags)); 52 | # inputStr = builtins.concatStringsSep " " (builtins.map (f: ''"${f}"'') allInputs); 53 | # cueEvalCmd = "cue eval ${flagStr} ${inputStr}"; 54 | 55 | # result = pkgs.runCommand "cue.output" 56 | # { 57 | # buildInputs = [ cue ]; 58 | # } // optionalAttrs (dataJSON != "") { json = dataJSON; passAsFile = [ "json" ]; } 59 | # '' 60 | # echo "nix-cue: Rendering output..." 61 | # ${cueEvalCmd} 62 | # ''; 63 | # in 64 | # result 65 | -------------------------------------------------------------------------------- /tests/json/data.input: -------------------------------------------------------------------------------- 1 | { 2 | "param1": "test", 3 | "param2": 100, 4 | "param3": { 5 | "subparam1": "test1" 6 | } 7 | } -------------------------------------------------------------------------------- /tests/json/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | let 3 | output = lib.eval 4 | { 5 | inherit pkgs; 6 | inputFiles = [ ./json.cue ]; 7 | outputFile = "out.json"; 8 | data = { 9 | param1 = "test"; 10 | param2 = 100; 11 | param3 = { 12 | "subparam1" = "test1"; 13 | }; 14 | }; 15 | }; 16 | 17 | result = pkgs.runCommand "test.json" 18 | { } 19 | '' 20 | cmp "${./expected.json}" "${output}" 21 | touch $out 22 | ''; 23 | in 24 | result 25 | -------------------------------------------------------------------------------- /tests/json/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "param1": "test", 3 | "param2": 100, 4 | "param3": { 5 | "subparam1": "test1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/json/json.cue: -------------------------------------------------------------------------------- 1 | #Config: { 2 | param1: string 3 | param2: int 4 | param3: [string]: string 5 | } 6 | 7 | { 8 | #Config 9 | } -------------------------------------------------------------------------------- /tests/pre-commit/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | let 3 | output = lib.eval 4 | { 5 | inherit pkgs; 6 | inputFiles = [ ./pre-commit.cue ]; 7 | outputFile = ".pre-commit-config.yaml"; 8 | data = { 9 | repos = [ 10 | { 11 | repo = "https://github.com/test/repo"; 12 | rev = "1.0"; 13 | hooks = [ 14 | { 15 | id = "my-hook"; 16 | } 17 | ]; 18 | } 19 | ]; 20 | }; 21 | }; 22 | 23 | result = pkgs.runCommand "test.pre-commit" 24 | { } 25 | '' 26 | cat "${output}" 27 | cmp "${./expected.yml}" "${output}" 28 | touch $out 29 | ''; 30 | in 31 | result 32 | -------------------------------------------------------------------------------- /tests/pre-commit/expected.yml: -------------------------------------------------------------------------------- 1 | repos: 2 | - hooks: 3 | - id: my-hook 4 | repo: https://github.com/test/repo 5 | rev: "1.0" 6 | -------------------------------------------------------------------------------- /tests/pre-commit/pre-commit.cue: -------------------------------------------------------------------------------- 1 | #Config: { 2 | default_install_hook_types?: [...string] 3 | default_language_version?: [string]: string 4 | default_stages?: [...string] 5 | files?: string 6 | exclude?: string 7 | fail_fast?: bool 8 | minimum_pre_commit_version?: string 9 | repos: [...#Repo] 10 | } 11 | 12 | #Hook: { 13 | additional_dependencies?: [...string] 14 | alias?: string 15 | always_run?: bool 16 | args?: [...string] 17 | exclude?: string 18 | exclude_types?: [...string] 19 | files?: string 20 | id: string 21 | language_version?: string 22 | log_file?: string 23 | name?: string 24 | stages?: [...string] 25 | types?: [...string] 26 | types_or?: [...string] 27 | verbose?: bool 28 | } 29 | 30 | #Repo: { 31 | repo: string 32 | rev?: string 33 | if repo != "local" { 34 | rev: string 35 | } 36 | hooks: [...#Hook] 37 | } 38 | 39 | { 40 | #Config 41 | } -------------------------------------------------------------------------------- /tests/text/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | let 3 | output = lib.eval 4 | { 5 | inherit pkgs; 6 | inputFiles = [ ./text.cue ]; 7 | outputFile = "test.txt"; 8 | expression = "rendered"; 9 | data = { 10 | data = { 11 | param1 = "test"; 12 | param2 = 100; 13 | param3 = { 14 | "subparam1" = "test1"; 15 | }; 16 | }; 17 | }; 18 | }; 19 | 20 | result = pkgs.runCommand "test.text" 21 | { } 22 | '' 23 | cmp "${./expected.txt}" "${output}" 24 | touch $out 25 | ''; 26 | in 27 | result 28 | -------------------------------------------------------------------------------- /tests/text/expected.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/text/text.cue: -------------------------------------------------------------------------------- 1 | import "text/template" 2 | 3 | #Config: { 4 | param1: string 5 | param2: int 6 | param3: [string]: string 7 | } 8 | 9 | data: #Config 10 | tmpl: "{{ .param1 }}" 11 | rendered: template.Execute(tmpl, data) --------------------------------------------------------------------------------