Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkgs-lib: Implement settings format for EDN #197096

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions nixos/doc/manual/development/settings-options.section.md
Expand Up @@ -168,6 +168,48 @@ have a predefined type and string generator already declared under

: Creates PHP array that contains both indexed and associative values. For example, `lib.mkMixedArray [ "hello" "world" ] { "nix" = "is-great"; }` returns `['hello', 'world', 'nix' => 'is-great']`

`pkgs.formats.edn { keywordTransform ? (kw: "${optionalString (!hasPrefix ":" kw) ":"}${kw}") }`

: A function taking an attribute set with values

`keywordTransform`

: A function to format the names in an attrbute set. The default turns names into EDN keywords.

It returns a set with EDN-specific attributes `type`, `lib`, and
`generate` as specified [below](#pkgs-formats-result).

The `lib` attribute contains functions to be used in settings, for
generating special EDN values:

`mkRaw ednCode`

: Outputs the given string as raw EDN

`mkSet list`

: Outputs the given list as an EDN set, instead of the default
EDN vector

`mkList list`

: Outputs the given list as an EDN list, instead of the default
EDN vector

`mkKeyword string`

: Outputs the given string as an EDN keyword, instead of the default
EDN string

`mkSymbol string`

: Outputs the given string as an EDN symbol, instead of the default
EDN string

`mkTaggedElement tag value`

: Outputs an EDN tagged element

[]{#pkgs-formats-result}
These functions all return an attribute set with these values:

Expand Down
2 changes: 2 additions & 0 deletions pkgs/pkgs-lib/formats.nix
Expand Up @@ -45,6 +45,8 @@ rec {

php = (import ./formats/php/default.nix { inherit lib pkgs; }).format;

edn = import ./formats/edn/default.nix {inherit lib pkgs;};

json = {}: {

type = with lib.types; let
Expand Down
261 changes: 261 additions & 0 deletions pkgs/pkgs-lib/formats/edn/default.nix
@@ -0,0 +1,261 @@
{
lib,
pkgs,
...
}:
/*
For configurations using [EDN].

Since EDN has more types than Nix, we need a way to map Nix types to
more than 1 EDN type. To that end, this format provides its own library,
and its own set of types.

A Nix string could correspond in EDN to a [String], a [Symbol] or a [Keyword].

A Nix array could correspond in EDN to a [List],[Set] or a [Vector].

Some more types exists, like fractions or regexes, but since they are less used,
we can leave the `mkRaw` function as an escape hatch.

[EDN]: <https://github.com/edn-format/edn>
[String]: <https://github.com/edn-format/edn#strings>
[Symbol]: <https://github.com/edn-format/edn#symbols>
[Keyword]: <https://github.com/edn-format/edn#keywords>
[List]: <https://github.com/edn-format/edn#lists>
[Set]: <https://github.com/edn-format/edn#sets>
[Vector]: <https://github.com/edn-format/edn#vectors>
*/
with lib;
{
keywordTransform ? (kw: "${lib.optionalString (!lib.hasPrefix ":" kw) ":"}${kw}"),
formatter ? "${pkgs.zprint}/bin/zprint",
}: let
toEdn = value:
with builtins;
if value == null
then "nil"
else if value == true
then "true"
else if value == false
then "false"
else if isInt value || isFloat value
then toString value
else if isString value
then string value
else if isAttrs value
then attrs value
else if isList value
then vector value
else abort "formats.edn: should never happen (value = ${value})";

escapeEdn = escape ["\\" "#" "\""];
string = value: "\"${escapeEdn value}\"";

attrs = set:
if set ? _ednType
then specialType set
else let
toKVPair = name: value: "${keywordTransform name} ${toEdn value}";
in
"{" + concatStringsSep ", " (mapAttrsToList toKVPair set) + "}";

kv = {
key,
value,
...
}: "${toEdn key} ${toEdn value}";

ednMap = kvs: "{" + concatStringsSep ", " (map kv kvs) + "}";

listContent = values: concatStringsSep ", " (map toEdn values);

vector = values: "[" + (listContent values) + "]";

list = values: "(" + (listContent values) + ")";

set = values: "#{" + (listContent values) + "}";

keyword = value: ":${value}";

symbol = value: "${value}";

taggedElement = {
tag,
value,
}: "#${tag} ${toEdn value}";

specialType = {
value,
_ednType,
} @ st:
if _ednType == "raw"
then value
else if _ednType == "list"
then list value
else if _ednType == "set"
then set value
else if _ednType == "keyword"
then keyword value
else if _ednType == "symbol"
then symbol value
else if _ednType == "taggedElement"
then taggedElement st
else if _ednType == "map"
then ednMap value
else abort "formats.edn: should never happen (_ednType = ${_ednType})";
in {
type = with lib.types; let
valueType =
nullOr
(oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "EDN value";
};
in
valueType;

lib = let
mkRaw = value: {
inherit value;
_ednType = "raw";
};
in {
inherit mkRaw;

/*
Make an Edn set out of a list.
*/
mkSet = value: {
inherit value;
_ednType = "set";
};

/*
Make an Edn list out of a list.
*/
mkList = value: {
inherit value;
_ednType = "list";
};

/*
Make an Edn keyword out of a string.
*/
mkKeyword = value: {
inherit value;
_ednType = "keyword";
};

/*
Make an Edn symbol out of a string.
*/
mkSymbol = value: {
inherit value;
_ednType = "symbol";
};

/*
Make an Edn tagged element.
*/
mkTaggedElement = tag: value: {
inherit tag value;
_ednType = "taggedElement";
};

/*
Make an Edn map out of a list of pairs. This is needed for maps with keys that are not keywords.
*/
mkMap = value: {
inherit value;
_ednType = "map";
};

/*
Make an Edn MapEntry for use with mkMap out of a key and a value
This is needed for keys that are not keywords
*/
mkKV = key: value: {
inherit key value;
_ednType = "kv";
};

/*
Contains Edn types. Every type it exports can also be replaced
by raw Edn code (i.e. every type is `either type rawEdn`).

It also reexports standard types, wrapping them so that they can
also be raw Edn.
*/
types = with lib.types; let
isEdnType = type: x: (x._ednType or "") == type;

rawEdn = mkOptionType {
name = "rawEdn";
description = "raw edn";
check = isEdnType "raw";
};

ednOr = other: either other rawEdn;
in
{
inherit rawEdn ednOr;

list = ednOr (mkOptionType {
name = "ednList";
description = "edn list";
check = isEdnType "list";
});

set = ednOr (mkOptionType {
name = "ednSet";
description = "edn set";
check = isEdnType "set";
});

keyword = ednOr (mkOptionType {
name = "ednKeyword";
description = "edn keyword";
check = isEdnType "keyword";
});

symbol = ednOr (mkOptionType {
name = "ednSymbol";
description = "edn symbol";
check = isEdnType "symbol";
});

taggedElement = ednOr (mkOptionType {
name = "ednTaggedElement";
description = "edn taggedElement";
check = isEdnType "taggedElement";
});

map = ednOr (mkOptionType {
name = "ednMap";
description = "edn map";
check = m: (isEdnType "map" m) && (all (isEdnType "kv") m.kvs);
});
# Wrap standard types, since anything in the Edn configuration
# can be raw Edn
}
// lib.mapAttrs (_name: type: ednOr type) lib.types;
};

generate = name: value: let
edn = toEdn value;
in
runCommand name {
inherit name edn;
passAsFile = ["edn"];
} ''
cat "$ednPath" | ${formatter} > "$out"
'';
}
43 changes: 43 additions & 0 deletions pkgs/pkgs-lib/formats/edn/edn_to_nix.clj
@@ -0,0 +1,43 @@
#!/usr/bin/env bb
(ns edn-to-nix
(:require [clojure.string :as str]
[clojure.edn :as edn]))

(defn kw->str [kw]
(str (when-let [n (namespace kw)] (str n "/")) (name kw)))

(comment
(kw->str :foo)
(kw->str :foo/bar))

(defn edn-to-nix [edn]
(cond (map? edn) (if (every? keyword? (keys edn) )
;; simple map with only keywords keys -> nix attrset
(str "{" (str/join " " (map (fn [[k v]] (str (pr-str (kw->str k)) " = " (edn-to-nix v) ";")) edn)) "}")
;; complex map with non-keyword keys -> mkMap
(str "(mkMap [" (str/join " " (map (fn [[k v]] (str "(mkKV " (edn-to-nix k) " " (edn-to-nix v) ")")) edn)) "])")
)
(vector? edn) (str "[" (str/join " " (map edn-to-nix edn)) "]")
(set? edn) (str "(mkSet [" (str/join " " (map edn-to-nix edn)) "])")
(list? edn) (str "(mkList [" (str/join " " (map edn-to-nix edn)) "])")
(keyword? edn) (str "(mkKeyword \"" (kw->str edn) "\")")
(symbol? edn) (str "(mkSymbol \"" (str edn) "\")")
(nil? edn) "null"
(or (string? edn)
(int? edn)
(float? edn)
(boolean? edn)) (pr-str edn)
:else (str "(mkRaw " (pr-str edn) ")")))

(defn edn-file-to-nix [path]
(-> path
slurp
edn/read-string
edn-to-nix
print))

(defn -main [& args]
(edn-file-to-nix (first args)))

(when (= *file* (System/getProperty "babashka.file"))
(apply -main *command-line-args*))