├── .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)
--------------------------------------------------------------------------------