1{ lib, pkgs }:
2let
3 inherit (lib) types;
4 inherit (types) attrsOf oneOf coercedTo str bool int float package;
5in
6{
7 javaProperties = { comment ? "Generated with Nix", boolToString ? lib.boolToString }: {
8
9 # Design note:
10 # A nested representation of inevitably leads to bad UX:
11 # 1. keys like "a.b" must be disallowed, or
12 # the addition of options in a freeformType module
13 # become breaking changes
14 # 2. adding a value for "a" after "a"."b" was already
15 # defined leads to a somewhat hard to understand
16 # Nix error, because that's not something you can
17 # do with attrset syntax. Workaround: "a"."", but
18 # that's too little too late. Another workaround:
19 # mkMerge [ { a = ...; } { a.b = ...; } ].
20 #
21 # Choosing a non-nested representation does mean that
22 # we sacrifice the ability to override at the (conceptual)
23 # hierarchical levels, _if_ an application exhibits those.
24 #
25 # Some apps just use periods instead of spaces in an odd
26 # mix of attempted categorization and natural language,
27 # with no meaningful hierarchy.
28 #
29 # We _can_ choose to support hierarchical config files
30 # via nested attrsets, but the module author should
31 # make sure that problem (2) does not occur.
32 type = let
33 elemType =
34 oneOf ([
35 # `package` isn't generalized to `path` because path values
36 # are ambiguous. Are they host path strings (toString /foo/bar)
37 # or should they be added to the store? ("${/foo/bar}")
38 # The user must decide.
39 (coercedTo package toString str)
40
41 (coercedTo bool boolToString str)
42 (coercedTo int toString str)
43 (coercedTo float toString str)
44 ])
45 // { description = "string, package, bool, int or float"; };
46 in attrsOf elemType;
47
48 generate = name: value:
49 pkgs.runCommandLocal name
50 {
51 # Requirements
52 # ============
53 #
54 # 1. Strings in Nix carry over to the same
55 # strings in Java => need proper escapes
56 # 2. Generate files quickly
57 # - A JVM would have to match the app's
58 # JVM to avoid build closure bloat
59 # - Even then, JVM startup would slow
60 # down config generation.
61 #
62 #
63 # Implementation
64 # ==============
65 #
66 # Escaping has two steps
67 #
68 # 1. jq
69 # Escape known separators, in order not
70 # to break up the keys and values.
71 # This handles typical whitespace correctly,
72 # but may produce garbage for other control
73 # characters.
74 #
75 # 2. iconv
76 # Escape >ascii code points to java escapes,
77 # as .properties files are supposed to be
78 # encoded in ISO 8859-1. It's an old format.
79 # UTF-8 behavior may exist in some apps and
80 # libraries, but we can't rely on this in
81 # general.
82
83 passAsFile = [ "value" ];
84 value = builtins.toJSON value;
85 nativeBuildInputs = [
86 pkgs.jq
87 pkgs.libiconvReal
88 ];
89
90 jqCode =
91 let
92 main = ''
93 to_entries
94 | .[]
95 | "\(
96 .key
97 | ${commonEscapes}
98 | gsub(" "; "\\ ")
99 | gsub("="; "\\=")
100 ) = \(
101 .value
102 | ${commonEscapes}
103 | gsub("^ "; "\\ ")
104 | gsub("\\n "; "\n\\ ")
105 )"
106 '';
107 # Most escapes are equal for both keys and values.
108 commonEscapes = ''
109 gsub("\\\\"; "\\\\")
110 | gsub("\\n"; "\\n\\\n")
111 | gsub("#"; "\\#")
112 | gsub("!"; "\\!")
113 | gsub("\\t"; "\\t")
114 | gsub("\r"; "\\r")
115 '';
116 in
117 main;
118
119 inputEncoding = "UTF-8";
120
121 inherit comment;
122
123 } ''
124 (
125 echo "$comment" | while read -r ln; do echo "# $ln"; done
126 echo
127 jq -r --arg hash '#' "$jqCode" "$valuePath" \
128 | iconv --from-code "$inputEncoding" --to-code JAVA \
129 ) > "$out"
130 '';
131 };
132}