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