core/pkgs/pkgs-lib/formats/java-properties/default.nix
2024-06-30 09:16:52 +01:00

153 lines
4.7 KiB
Nix

{ 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"
'';
};
}