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}