Commit 6a96ddb6 authored by Minijackson's avatar Minijackson Committed by Yt
Browse files

pkgs-lib: Implement settings format for Elixir

parent ea84cd68
Loading
Loading
Loading
Loading
+45 −0
Original line number Diff line number Diff line
@@ -66,6 +66,45 @@ have a predefined type and string generator already declared under
    and returning a set with TOML-specific attributes `type` and
    `generate` as specified [below](#pkgs-formats-result).

`pkgs.formats.elixirConf { elixir ? pkgs.elixir }`

:   A function taking an attribute set with values

    `elixir`

    :   The Elixir package which will be used to format the generated output

    It returns a set with Elixir-Config-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 Elixir values:

    `mkRaw elixirCode`

    :   Outputs the given string as raw Elixir code

    `mkGetEnv { envVariable, fallback ? null }`

    :   Makes the configuration fetch an environment variable at runtime

    `mkAtom atom`

    :   Outputs the given string as an Elixir atom, instead of the default
        Elixir binary string. Note: lowercase atoms still needs to be prefixed
        with `:`

    `mkTuple array`

    :   Outputs the given array as an Elixir tuple, instead of the default
        Elixir list

    `mkMap attrset`

    :   Outputs the given attribute set as an Elixir map, instead of the
        default Elixir keyword list


::: {#pkgs-formats-result}
These functions all return an attribute set with these values:
:::
@@ -74,6 +113,12 @@ These functions all return an attribute set with these values:

:   A module system type representing a value of the format

`lib`

:   Utility functions for convenience, or special interactions with the format.
    This attribute is optional. It may contain inside a `types` attribute
    containing types specific to this format.

`generate` *`filename jsonValue`*

:   A function that can render a value of the format to a file. Returns
+104 −0
Original line number Diff line number Diff line
@@ -137,6 +137,97 @@
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <literal>pkgs.formats.elixirConf { elixir ? pkgs.elixir }</literal>
        </term>
        <listitem>
          <para>
            A function taking an attribute set with values
          </para>
          <variablelist>
            <varlistentry>
              <term>
                <literal>elixir</literal>
              </term>
              <listitem>
                <para>
                  The Elixir package which will be used to format the
                  generated output
                </para>
              </listitem>
            </varlistentry>
          </variablelist>
          <para>
            It returns a set with Elixir-Config-specific attributes
            <literal>type</literal>, <literal>lib</literal>, and
            <literal>generate</literal> as specified
            <link linkend="pkgs-formats-result">below</link>.
          </para>
          <para>
            The <literal>lib</literal> attribute contains functions to
            be used in settings, for generating special Elixir values:
          </para>
          <variablelist>
            <varlistentry>
              <term>
                <literal>mkRaw elixirCode</literal>
              </term>
              <listitem>
                <para>
                  Outputs the given string as raw Elixir code
                </para>
              </listitem>
            </varlistentry>
            <varlistentry>
              <term>
                <literal>mkGetEnv { envVariable, fallback ? null }</literal>
              </term>
              <listitem>
                <para>
                  Makes the configuration fetch an environment variable
                  at runtime
                </para>
              </listitem>
            </varlistentry>
            <varlistentry>
              <term>
                <literal>mkAtom atom</literal>
              </term>
              <listitem>
                <para>
                  Outputs the given string as an Elixir atom, instead of
                  the default Elixir binary string. Note: lowercase
                  atoms still needs to be prefixed with
                  <literal>:</literal>
                </para>
              </listitem>
            </varlistentry>
            <varlistentry>
              <term>
                <literal>mkTuple array</literal>
              </term>
              <listitem>
                <para>
                  Outputs the given array as an Elixir tuple, instead of
                  the default Elixir list
                </para>
              </listitem>
            </varlistentry>
            <varlistentry>
              <term>
                <literal>mkMap attrset</literal>
              </term>
              <listitem>
                <para>
                  Outputs the given attribute set as an Elixir map,
                  instead of the default Elixir keyword list
                </para>
              </listitem>
            </varlistentry>
          </variablelist>
        </listitem>
      </varlistentry>
    </variablelist>
    <para xml:id="pkgs-formats-result">
      These functions all return an attribute set with these values:
@@ -152,6 +243,19 @@
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <literal>lib</literal>
        </term>
        <listitem>
          <para>
            Utility functions for convenience, or special interactions
            with the format. This attribute is optional. It may contain
            inside a <literal>types</literal> attribute containing types
            specific to this format.
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <literal>generate</literal>
+207 −0
Original line number Diff line number Diff line
@@ -14,6 +14,15 @@ rec {
      # The description needs to be overwritten for recursive types
      type = ...;

      # Utility functions for convenience, or special interactions with the
      # format (optional)
      lib = {
        exampleFunction = ...
        # Types specific to the format (optional)
        types = { ... };
        ...
      };

      # generate :: Name -> Value -> Path
      # A function for generating a file with a value of such a type
      generate = ...;
@@ -147,4 +156,202 @@ rec {
    '';

  };

  /* For configurations of Elixir project, like config.exs or runtime.exs

    Most Elixir project are configured using the [Config] Elixir DSL

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

    To be more detailed, a Nix attribute set could correspond in Elixir to a
    [Keyword list] (the more common type), or it could correspond to a [Map].

    A Nix string could correspond in Elixir to a [String] (also called
    "binary"), an [Atom], or a list of chars (usually discouraged).

    A Nix array could correspond in Elixir to a [List] or a [Tuple].

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

    For more information on how to use this format in modules, please refer to
    the Elixir section of the Nixos documentation.

    TODO: special Elixir values doesn't show up nicely in the documentation

    [Config]: <https://hexdocs.pm/elixir/Config.html>
    [Keyword list]: <https://hexdocs.pm/elixir/Keyword.html>
    [Map]: <https://hexdocs.pm/elixir/Map.html>
    [String]: <https://hexdocs.pm/elixir/String.html>
    [Atom]: <https://hexdocs.pm/elixir/Atom.html>
    [List]: <https://hexdocs.pm/elixir/List.html>
    [Tuple]: <https://hexdocs.pm/elixir/Tuple.html>
  */
  elixirConf = { elixir ? pkgs.elixir }:
    with lib; let
      toElixir = 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 list value else
        abort "formats.elixirConf: should never happen (value = ${value})";

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

      attrs = set:
        if set ? _elixirType then specialType set
        else
          let
            toKeyword = name: value: "${name}: ${toElixir value}";
            keywordList = concatStringsSep ", " (mapAttrsToList toKeyword set);
          in
          "[" + keywordList + "]";

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

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

      specialType = { value, _elixirType }:
        if _elixirType == "raw" then value else
        if _elixirType == "atom" then value else
        if _elixirType == "map" then elixirMap value else
        if _elixirType == "tuple" then tuple value else
        abort "formats.elixirConf: should never happen (_elixirType = ${_elixirType})";

      elixirMap = set:
        let
          toEntry = name: value: "${toElixir name} => ${toElixir value}";
          entries = concatStringsSep ", " (mapAttrsToList toEntry set);
        in
        "%{${entries}}";

      tuple = values: "{${listContent values}}";

      toConf = values:
        let
          keyConfig = rootKey: key: value:
            "config ${rootKey}, ${key}, ${toElixir value}";
          keyConfigs = rootKey: values: mapAttrsToList (keyConfig rootKey) values;
          rootConfigs = flatten (mapAttrsToList keyConfigs values);
        in
        ''
          import Config

          ${concatStringsSep "\n" rootConfigs}
        '';
    in
    {
      type = with lib.types; let
        valueType = nullOr
          (oneOf [
            bool
            int
            float
            str
            (attrsOf valueType)
            (listOf valueType)
          ]) // {
          description = "Elixir value";
        };
      in
      attrsOf (attrsOf (valueType));

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

        in
        {
          inherit mkRaw;

          /* Fetch an environment variable at runtime, with optional fallback
          */
          mkGetEnv = { envVariable, fallback ? null }:
            mkRaw "System.get_env(${toElixir envVariable}, ${toElixir fallback})";

          /* Make an Elixir atom.

            Note: lowercase atoms still need to be prefixed by ':'
          */
          mkAtom = value: {
            inherit value;
            _elixirType = "atom";
          };

          /* Make an Elixir tuple out of a list.
          */
          mkTuple = value: {
            inherit value;
            _elixirType = "tuple";
          };

          /* Make an Elixir map out of an attribute set.
          */
          mkMap = value: {
            inherit value;
            _elixirType = "map";
          };

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

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

            rawElixir = mkOptionType {
              name = "rawElixir";
              description = "raw elixir";
              check = isElixirType "raw";
            };

            elixirOr = other: either other rawElixir;
          in
          {
            inherit rawElixir elixirOr;

            atom = elixirOr (mkOptionType {
              name = "elixirAtom";
              description = "elixir atom";
              check = isElixirType "atom";
            });

            tuple = elixirOr (mkOptionType {
              name = "elixirTuple";
              description = "elixir tuple";
              check = isElixirType "tuple";
            });

            map = elixirOr (mkOptionType {
              name = "elixirMap";
              description = "elixir map";
              check = isElixirType "map";
            });
            # Wrap standard types, since anything in the Elixir configuration
            # can be raw Elixir
          } // lib.mapAttrs (_name: type: elixirOr type) lib.types;
        };

      generate = name: value: pkgs.runCommandNoCC name
        {
          value = toConf value;
          passAsFile = [ "value" ];
          nativeBuildInputs = [ elixir ];
        } ''
        cp "$valuePath" "$out"
        mix format "$out"
      '';
    };

}