228 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			228 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
/* Functions that generate widespread file
 | 
						|
 * formats from nix data structures.
 | 
						|
 *
 | 
						|
 * They all follow a similar interface:
 | 
						|
 * generator { config-attrs } data
 | 
						|
 *
 | 
						|
 * `config-attrs` are “holes” in the generators
 | 
						|
 * with sensible default implementations that
 | 
						|
 * can be overwritten. The default implementations
 | 
						|
 * are mostly generators themselves, called with
 | 
						|
 * their respective default values; they can be reused.
 | 
						|
 *
 | 
						|
 * Tests can be found in ./tests.nix
 | 
						|
 * Documentation in the manual, #sec-generators
 | 
						|
 */
 | 
						|
{ lib }:
 | 
						|
with (lib).trivial;
 | 
						|
let
 | 
						|
  libStr = lib.strings;
 | 
						|
  libAttr = lib.attrsets;
 | 
						|
 | 
						|
  inherit (lib) isFunction;
 | 
						|
in
 | 
						|
 | 
						|
rec {
 | 
						|
 | 
						|
  ## -- HELPER FUNCTIONS & DEFAULTS --
 | 
						|
 | 
						|
  /* Convert a value to a sensible default string representation.
 | 
						|
   * The builtin `toString` function has some strange defaults,
 | 
						|
   * suitable for bash scripts but not much else.
 | 
						|
   */
 | 
						|
  mkValueStringDefault = {}: v: with builtins;
 | 
						|
    let err = t: v: abort
 | 
						|
          ("generators.mkValueStringDefault: " +
 | 
						|
           "${t} not supported: ${toPretty {} v}");
 | 
						|
    in   if isInt      v then toString v
 | 
						|
    # we default to not quoting strings
 | 
						|
    else if isString   v then v
 | 
						|
    # isString returns "1", which is not a good default
 | 
						|
    else if true  ==   v then "true"
 | 
						|
    # here it returns to "", which is even less of a good default
 | 
						|
    else if false ==   v then "false"
 | 
						|
    else if null  ==   v then "null"
 | 
						|
    # if you have lists you probably want to replace this
 | 
						|
    else if isList     v then err "lists" v
 | 
						|
    # same as for lists, might want to replace
 | 
						|
    else if isAttrs    v then err "attrsets" v
 | 
						|
    else if isFunction v then err "functions" v
 | 
						|
    else err "this value is" (toString v);
 | 
						|
 | 
						|
 | 
						|
  /* Generate a line of key k and value v, separated by
 | 
						|
   * character sep. If sep appears in k, it is escaped.
 | 
						|
   * Helper for synaxes with different separators.
 | 
						|
   *
 | 
						|
   * mkValueString specifies how values should be formatted.
 | 
						|
   *
 | 
						|
   * mkKeyValueDefault {} ":" "f:oo" "bar"
 | 
						|
   * > "f\:oo:bar"
 | 
						|
   */
 | 
						|
  mkKeyValueDefault = {
 | 
						|
    mkValueString ? mkValueStringDefault {}
 | 
						|
  }: sep: k: v:
 | 
						|
    "${libStr.escape [sep] k}${sep}${mkValueString v}";
 | 
						|
 | 
						|
 | 
						|
  ## -- FILE FORMAT GENERATORS --
 | 
						|
 | 
						|
 | 
						|
  /* Generate a key-value-style config file from an attrset.
 | 
						|
   *
 | 
						|
   * mkKeyValue is the same as in toINI.
 | 
						|
   */
 | 
						|
  toKeyValue = {
 | 
						|
    mkKeyValue ? mkKeyValueDefault {} "="
 | 
						|
  }: attrs:
 | 
						|
    let mkLine = k: v: mkKeyValue k v + "\n";
 | 
						|
    in libStr.concatStrings (libAttr.mapAttrsToList mkLine attrs);
 | 
						|
 | 
						|
 | 
						|
  /* Generate an INI-style config file from an
 | 
						|
   * attrset of sections to an attrset of key-value pairs.
 | 
						|
   *
 | 
						|
   * generators.toINI {} {
 | 
						|
   *   foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
 | 
						|
   *   baz = { "also, integers" = 42; };
 | 
						|
   * }
 | 
						|
   *
 | 
						|
   *> [baz]
 | 
						|
   *> also, integers=42
 | 
						|
   *>
 | 
						|
   *> [foo]
 | 
						|
   *> ciao=bar
 | 
						|
   *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
 | 
						|
   *
 | 
						|
   * The mk* configuration attributes can generically change
 | 
						|
   * the way sections and key-value strings are generated.
 | 
						|
   *
 | 
						|
   * For more examples see the test cases in ./tests.nix.
 | 
						|
   */
 | 
						|
  toINI = {
 | 
						|
    # apply transformations (e.g. escapes) to section names
 | 
						|
    mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
 | 
						|
    # format a setting line from key and value
 | 
						|
    mkKeyValue    ? mkKeyValueDefault {} "="
 | 
						|
  }: attrsOfAttrs:
 | 
						|
    let
 | 
						|
        # map function to string for each key val
 | 
						|
        mapAttrsToStringsSep = sep: mapFn: attrs:
 | 
						|
          libStr.concatStringsSep sep
 | 
						|
            (libAttr.mapAttrsToList mapFn attrs);
 | 
						|
        mkSection = sectName: sectValues: ''
 | 
						|
          [${mkSectionName sectName}]
 | 
						|
        '' + toKeyValue { inherit mkKeyValue; } sectValues;
 | 
						|
    in
 | 
						|
      # map input to ini sections
 | 
						|
      mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
 | 
						|
 | 
						|
 | 
						|
  /* Generates JSON from an arbitrary (non-function) value.
 | 
						|
    * For more information see the documentation of the builtin.
 | 
						|
    */
 | 
						|
  toJSON = {}: builtins.toJSON;
 | 
						|
 | 
						|
 | 
						|
  /* YAML has been a strict superset of JSON since 1.2, so we
 | 
						|
    * use toJSON. Before it only had a few differences referring
 | 
						|
    * to implicit typing rules, so it should work with older
 | 
						|
    * parsers as well.
 | 
						|
    */
 | 
						|
  toYAML = {}@args: toJSON args;
 | 
						|
 | 
						|
 | 
						|
  /* Pretty print a value, akin to `builtins.trace`.
 | 
						|
    * Should probably be a builtin as well.
 | 
						|
    */
 | 
						|
  toPretty = {
 | 
						|
    /* If this option is true, attrsets like { __pretty = fn; val = …; }
 | 
						|
       will use fn to convert val to a pretty printed representation.
 | 
						|
       (This means fn is type Val -> String.) */
 | 
						|
    allowPrettyValues ? false
 | 
						|
  }@args: v: with builtins;
 | 
						|
    let     isPath   = v: typeOf v == "path";
 | 
						|
    in if   isInt      v then toString v
 | 
						|
    else if isFloat    v then "~${toString v}"
 | 
						|
    else if isString   v then ''"${libStr.escape [''"''] v}"''
 | 
						|
    else if true  ==   v then "true"
 | 
						|
    else if false ==   v then "false"
 | 
						|
    else if null  ==   v then "null"
 | 
						|
    else if isPath     v then toString v
 | 
						|
    else if isList     v then "[ "
 | 
						|
        + libStr.concatMapStringsSep " " (toPretty args) v
 | 
						|
      + " ]"
 | 
						|
    else if isAttrs    v then
 | 
						|
      # apply pretty values if allowed
 | 
						|
      if attrNames v == [ "__pretty" "val" ] && allowPrettyValues
 | 
						|
         then v.__pretty v.val
 | 
						|
      # TODO: there is probably a better representation?
 | 
						|
      else if v ? type && v.type == "derivation" then
 | 
						|
        "<δ:${v.name}>"
 | 
						|
        # "<δ:${concatStringsSep "," (builtins.attrNames v)}>"
 | 
						|
      else "{ "
 | 
						|
          + libStr.concatStringsSep " " (libAttr.mapAttrsToList
 | 
						|
              (name: value:
 | 
						|
                "${toPretty args name} = ${toPretty args value};") v)
 | 
						|
        + " }"
 | 
						|
    else if isFunction v then
 | 
						|
      let fna = lib.functionArgs v;
 | 
						|
          showFnas = concatStringsSep "," (libAttr.mapAttrsToList
 | 
						|
                       (name: hasDefVal: if hasDefVal then "(${name})" else name)
 | 
						|
                       fna);
 | 
						|
      in if fna == {}    then "<λ>"
 | 
						|
                         else "<λ:{${showFnas}}>"
 | 
						|
    else abort "generators.toPretty: should never happen (v = ${v})";
 | 
						|
 | 
						|
  # PLIST handling
 | 
						|
  toPlist = {}: v: let
 | 
						|
    isFloat = builtins.isFloat or (x: false);
 | 
						|
    expr = ind: x:  with builtins;
 | 
						|
      if x == null  then "" else
 | 
						|
      if isBool x   then bool ind x else
 | 
						|
      if isInt x    then int ind x else
 | 
						|
      if isString x then str ind x else
 | 
						|
      if isList x   then list ind x else
 | 
						|
      if isAttrs x  then attrs ind x else
 | 
						|
      if isFloat x  then float ind x else
 | 
						|
      abort "generators.toPlist: should never happen (v = ${v})";
 | 
						|
 | 
						|
    literal = ind: x: ind + x;
 | 
						|
 | 
						|
    bool = ind: x: literal ind  (if x then "<true/>" else "<false/>");
 | 
						|
    int = ind: x: literal ind "<integer>${toString x}</integer>";
 | 
						|
    str = ind: x: literal ind "<string>${x}</string>";
 | 
						|
    key = ind: x: literal ind "<key>${x}</key>";
 | 
						|
    float = ind: x: literal ind "<real>${toString x}</real>";
 | 
						|
 | 
						|
    indent = ind: expr "\t${ind}";
 | 
						|
 | 
						|
    item = ind: libStr.concatMapStringsSep "\n" (indent ind);
 | 
						|
 | 
						|
    list = ind: x: libStr.concatStringsSep "\n" [
 | 
						|
      (literal ind "<array>")
 | 
						|
      (item ind x)
 | 
						|
      (literal ind "</array>")
 | 
						|
    ];
 | 
						|
 | 
						|
    attrs = ind: x: libStr.concatStringsSep "\n" [
 | 
						|
      (literal ind "<dict>")
 | 
						|
      (attr ind x)
 | 
						|
      (literal ind "</dict>")
 | 
						|
    ];
 | 
						|
 | 
						|
    attr = let attrFilter = name: value: name != "_module" && value != null;
 | 
						|
    in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList
 | 
						|
      (name: value: lib.optional (attrFilter name value) [
 | 
						|
      (key "\t${ind}" name)
 | 
						|
      (expr "\t${ind}" value)
 | 
						|
    ]) x));
 | 
						|
 | 
						|
  in ''<?xml version="1.0" encoding="UTF-8"?>
 | 
						|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
						|
<plist version="1.0">
 | 
						|
${expr "" v}
 | 
						|
</plist>'';
 | 
						|
 | 
						|
}
 |