{ lib, pkgs }: let inherit (lib) types; inherit (types) attrsOf oneOf coercedTo str bool int float package; in { javaProperties = { comment ? "Generated with Nix", boolToString ? lib.boolToString }: { # Design note: # A nested representation of inevitably leads to bad UX: # 1. keys like "a.b" must be disallowed, or # the addition of options in a freeformType module # become breaking changes # 2. adding a value for "a" after "a"."b" was already # defined leads to a somewhat hard to understand # Nix error, because that's not something you can # do with attrset syntax. Workaround: "a"."", but # that's too little too late. Another workaround: # mkMerge [ { a = ...; } { a.b = ...; } ]. # # Choosing a non-nested representation does mean that # we sacrifice the ability to override at the (conceptual) # hierarchical levels, _if_ an application exhibits those. # # Some apps just use periods instead of spaces in an odd # mix of attempted categorization and natural language, # with no meaningful hierarchy. # # We _can_ choose to support hierarchical config files # via nested attrsets, but the module author should # make sure that problem (2) does not occur. type = let elemType = oneOf ([ # `package` isn't generalized to `path` because path values # are ambiguous. Are they host path strings (toString /foo/bar) # or should they be added to the store? ("${/foo/bar}") # The user must decide. (coercedTo package toString str) (coercedTo bool boolToString str) (coercedTo int toString str) (coercedTo float toString str) ]) // { description = "string, package, bool, int or float"; }; in attrsOf elemType; generate = name: value: pkgs.runCommandLocal name { # Requirements # ============ # # 1. Strings in Nix carry over to the same # strings in Java => need proper escapes # 2. Generate files quickly # - A JVM would have to match the app's # JVM to avoid build closure bloat # - Even then, JVM startup would slow # down config generation. # # # Implementation # ============== # # Escaping has two steps # # 1. jq # Escape known separators, in order not # to break up the keys and values. # This handles typical whitespace correctly, # but may produce garbage for other control # characters. # # 2. iconv # Escape >ascii code points to java escapes, # as .properties files are supposed to be # encoded in ISO 8859-1. It's an old format. # UTF-8 behavior may exist in some apps and # libraries, but we can't rely on this in # general. passAsFile = [ "value" ]; value = builtins.toJSON value; nativeBuildInputs = [ pkgs.jq pkgs.libiconvReal ]; jqCode = let main = '' to_entries | .[] | "\( .key | ${commonEscapes} | gsub(" "; "\\ ") | gsub("="; "\\=") ) = \( .value | ${commonEscapes} | gsub("^ "; "\\ ") | gsub("\\n "; "\n\\ ") )" ''; # Most escapes are equal for both keys and values. commonEscapes = '' gsub("\\\\"; "\\\\") | gsub("\\n"; "\\n\\\n") | gsub("#"; "\\#") | gsub("!"; "\\!") | gsub("\\t"; "\\t") | gsub("\r"; "\\r") ''; in main; inputEncoding = "UTF-8"; inherit comment; } '' ( echo "$comment" | while read -r ln; do echo "# $ln"; done echo jq -r --arg hash '#' "$jqCode" "$valuePath" \ | iconv --from-code "$inputEncoding" --to-code JAVA \ ) > "$out" ''; }; }