Introduction
Nixago is a flake library for generating configuration files. It's primarily geared towards reducing the clutter at the root of your repository added by various development tools (i.e., formatters, linters, etc.). However, Nixago is flexible enough to generate configuration files for most scenarios.
Nixago is designed to be used in tandem with a nix shell to dynamically
create and manage configuration files in your existing development environments.
Define the configurations in the flake.nix
file at the root of your
repository, and Nixago will automatically generate shell hooks for managing the
files.
Quick Start
Add Nixago as an input to your flake.nix
:
{
inputs = {
# ...
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
nixago.url = "github:nix-community/nixago";
nixago.inputs.nixpkgs.follows = "nixpkgs";
# ...
};
}
Generate a Configuration
Nixago offers various engines which can be used for
transforming input data into an output file. Nixago will default to the
nix engine that utilizes pkgs.formats
from nixpkgs.
let
data = {
"field1" = "value1";
"field2" = true;
};
in
nixago.lib.make {
inherit data;
output = "config.json";
format = "json";
engine = nixago.engines.nix { }; # Optional as this is the default value
}
The result of this invocation will be an attribute set with two attributes:
configFile
: A derivation for building the configuration file.shellHook
: A shell hook for managing the file.
Building the derivation produces a file with the following output:
{
"field1": "value1",
"field2": true
}
The make
function takes an attribute set that supports the options defined in
the request module. Please refer to the module definition for all of the
available options.
Using the Shell Hook
The generated shell hook will link the generated configuration file to one of two places:
- If
$PRJ_ROOT
is defined, the file will be linked to$PRJ_ROOT/{output}
whereoutput
is the relative path defined in the call tomake
. - If
$PRJ_ROOT
is not defined, the file will be linked to./{output}
, where the relative path is determined by where the Nix CLI was invoked.
For example, if $PRJ_ROOT
is set to /home/user/code/myprj
and the output is
specified as configs/config.json
then the file will be linked to
/home/user/code/myprj/configs/config.json
.
The shell hook is designed to be integrated into a development shell:
{
# ...
devShells = {
default = pkgs.mkShell {
shellHook = (nixago.lib.make config).shellHook;
};
};
# ...
}
This will ensure the file is generated and linked when you enter the shell. The behavior of the hook can be modified via the available options.
Extending Nixago
An additional repository is available that provides extensions for Nixago. These simplify the process of generating configuration files for common development tools.
Engines
Nixago provides various engines which can be used for changing how the configuration file is being generated. If you would like to suggest a new engine, please open an issue.
CUE
This engine provides an interface for generating configuration files using the CUE language and its associated CLI tool. It allows combining the user-defined input with one or more CUE files and generating a file in the designated output format.
For more information on the design of CUE, see this doc. A good place to start learning the fundamentals is the Cuetorials website.
Concepts
Merging
When using CUE, you will typically define one or more CUE files that will be merged into the desired output. For example, if we had the below CUE file:
name: string
name: "John Doe"
We could then export it and get the following result:
$ cue export file.cue
{
"name": "John Doe"
}
When CUE is presented with multiple files, they are all merged into one singular output:
name: string
name: "John Doe"
$ cue export file1.cue file2.cue
{
"name": "John Doe"
}
Combining with JSON
CUE is a superset of JSON, so it's possible to merge JSON and CUE files. Using our previous example, this time with JSON:
name: string
{
"name": "John Doe"
}
cue export file1.cue file2.json
{
"name": "John Doe"
}
We get the same result because our JSON file and the previously defined CUE file are functionally equivalent.
Constraints
Performing input validation is a natural benefit when using the CUE language. We
used a constraint earlier when we constrained the name
field to a string.
Continuing with our previous examples, if we modified the JSON structure to be
the following:
{
"name": 42
}
Then if we attempted to export both files, we would receive an error:
name: conflicting values 42 and string (mismatched types int and string)
CUE is informing us that we constrained name
to a string
, and yet we passed
in an int (42) with our JSON data structure. This is a type mismatch and results
in an error.
Exporting Modified Structures
A common use case is transforming an incoming structure into a modified outgoing structure. Take the following input structure:
{
"first_name": "john",
"last_name": "doe",
"address": "123 Lane",
"city": "Springfield",
"state": "OR"
}
We only want to export the name and a combined address:
import "strings"
first_name: string
last_name: string
address: string
city: string
state: string
result: {
name: strings.ToTitle(strings.Join([first_name, last_name], " "))
full_address: "\(address)\n\(city), \(state)"
}
We can then ask CUE to only render our result
expression:
$ cue export -e result file1.cue file2.json
{
"name": "John Doe",
"full_address": "123 Lane\nSpringfield, OR"
}
This allows ingesting one data structure and exporting another. As shown in the example, we can perform many permutations on the incoming data using CUE's standard library and interpolation.
Usage
Argument | Required | Description |
---|---|---|
files | Yes | A list of file paths to pass to CUE |
preHook | No | A shell hook to execute before CUE is invoked |
postHook | No | A shell hook to execute after CUE is invoked |
flags | No | Additional flags to pass to CUE |
cue | No | The cue package to use |
jq | No | The jq package to use |
The CUE engine builds off the concepts above. It takes a single required
argument (files
) which is the list of files to pass to CUE. In addition to
these files, the engine will also convert the incoming configuration data into
JSON and pass it as a file to CUE. The result is that CUE will evaluate the
given files
as well as the incoming configuration data. As noted earlier, CUE
will merge all of these together into one final output.
In addition to the files
argument, pre and post-hooks can be provided that
execute before and after CUE is invoked respectively. Finally, additional flags
can be specified to further control the execution of the CUE CLI tool.
Nix Engine
The nix engine uses the set of functions provided by pkgs.formats
. You can
see the source here.
Concepts
The basic outline of an entry in pkgs.formats
is as follows:
{
format = {}: {
generate = name: value: pkgs.runCommand {...};
};
}
Where format
is the name of the supported format (i.e., json
).
Note that the format
entry is a function that takes a single attribute set. In
some cases, this is an empty set, but in other cases, the set contains
additional options for configuring the format.
Usage
When using the nix
engine from Nixago, one can pass attributes to the set with
the following:
{
engine = nixago.lib.engines.nix { opt1 = "value1"; };
}
The provided set will be passed to the format
argument before calling its
generate
function.
When calling make
, the specified value for the format
attribute will be used
to determine which entry in pkgs.formats
to invoke. Thus, specifying an
unsupported format will result in an assertion error. Refer to the nixpkgs
source code to determine the currently supported output formats.
Contributing
This chapter covers the details required to contribute to Nixago. Before submitting a PR, please ensure you review this chapter thoroughly. Nixago has an opinionated design and expects a specific structure to work correctly. Understanding this structure ahead of time will make development more accessible and increase the chance of PRs going through without issues.
Design
When contributing to Nixago, it's essential to understand the general design principles that guide its development. This section provides details on how the library is structured and introduces the basic concepts required to contribute effectively.
Overview
%%{ init : { "theme" : "dark", "flowchart" : { "curve" : "linear" }}}%% %% flowchart TD flake{{User's flake.nix}} make(Call to make) engine(Call to engine) flake -- request ---> make make <-- request ---> engine make -- result --> flake
Nixago strives to meet the Unix philosophy of "do one thing and do it well." For Nixago, this is generating and managing configuration files. Anything that falls outside of this scope, or risks complicating internal structures, should be moved to an external project.
The interface for Nixago is purposefully simple: all interactions work around the request module. This module defines all required and optional fields necessary to generate and manage a configuration file. Extending this interface should not be the default choice, as unmitigated changes can result in muddying the usefulness of the interface it provides.
Nixago promises two outputs: a derivation for building the specified configuration file and a shell hook that manages the file locally. Nixago hands control over to the user regarding how the configuration should be built and how the hook should handle it.
Engines
There are many ways to generate a configuration file. The only restraint that Nixago imposes is that the input data must be a valid Nix expression. The entity that translates this Nix expression into a derivation is called an engine.
The purpose of an engine is to receive and process a request. The request's processing depends on the engine and its underlying tools. The only expectation that Nixago has is that a derivation is returned.
For example, the cue
engine uses the CUE CLI tool to process a list of
CUE files from the user, along with the user's input, to create a derivation
that produces the desired configuration file. This modular design allows Nixago
to be easily extended to support existing infrastructure.