commit 06c3aeca5da2c8904c2a4ae26aa21bf365a669fe
parent 3666f260e5b1dfbd272d0f8042326eadb97e1706
Author: Florian Dold <dold@taler.net>
Date: Tue, 1 Sep 2026 19:47:03 +0200
taler-util: add lightweight YAML serializer
Diffstat:
5 files changed, 128 insertions(+), 1 deletion(-)
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
@@ -81,7 +81,8 @@
"@types/follow-redirects": "^1.14.4",
"@types/node": "^20.19.41",
"esbuild": "^0.28.0",
- "typescript": "^7.0.2"
+ "typescript": "^7.0.2",
+ "yaml": "^2.9.0"
},
"dependencies": {
"@noble/ciphers": "^2.4.0",
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
@@ -68,6 +68,7 @@ export * from "./time.js";
export * from "./timer.js";
export * from "./transaction-test-data.js";
export * from "./url.js";
+export * from "./yaml.js";
// FIXME: remove all this, needs refactor
export * from "./types-donau.js";
diff --git a/packages/taler-util/src/yaml.test.ts b/packages/taler-util/src/yaml.test.ts
@@ -0,0 +1,51 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+*/
+
+import assert from "node:assert";
+import { test } from "node:test";
+import { parse } from "yaml";
+import { stringifyYaml } from "./yaml.js";
+
+test("YAML serializer emits a readable JSON-compatible subset", () => {
+ assert.strictEqual(
+ stringifyYaml({
+ formatVersion: 1,
+ strings: ["true", "with: colon", "line\nbreak", "Grüße"],
+ nested: { emptyObject: {}, emptyArray: [], absent: undefined },
+ numbers: [0, 1.5, Number.NaN],
+ }),
+ `"formatVersion": 1
+"strings":
+ - "true"
+ - "with: colon"
+ - "line\\nbreak"
+ - "Grüße"
+"nested":
+ "emptyObject": {}
+ "emptyArray": []
+"numbers":
+ - 0
+ - 1.5
+ - null
+`,
+ );
+});
+
+test("YAML serializer follows JSON array and top-level semantics", () => {
+ assert.strictEqual(stringifyYaml([undefined, true]), `- null\n- true\n`);
+ assert.throws(() => stringifyYaml(undefined), TypeError);
+ assert.throws(() => stringifyYaml(1n), TypeError);
+});
+
+test("YAML serializer round-trips JSON values with a general parser", () => {
+ const value = {
+ ambiguous: ["null", "yes", "1e3", "2026-09-01", "a: b", "#comment"],
+ nested: [{ key: "value" }, [], {}],
+ unicode: "Grüße 👛",
+ };
+ assert.deepStrictEqual(parse(stringifyYaml(value)), value);
+});
diff --git a/packages/taler-util/src/yaml.ts b/packages/taler-util/src/yaml.ts
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+*/
+
+type JsonScalar = string | number | boolean | null;
+type JsonValue = JsonScalar | JsonValue[] | { [key: string]: JsonValue };
+
+function isNonEmptyCollection(value: JsonValue): boolean {
+ if (Array.isArray(value)) return value.length > 0;
+ return (
+ value !== null && typeof value === "object" && Object.keys(value).length > 0
+ );
+}
+
+function renderYaml(value: JsonValue, indentation: number): string {
+ if (
+ value === null ||
+ typeof value === "string" ||
+ typeof value === "number" ||
+ typeof value === "boolean"
+ ) {
+ return JSON.stringify(value);
+ }
+ if (Array.isArray(value)) {
+ if (value.length === 0) return "[]";
+ return value
+ .map((entry) => {
+ const rendered = renderYaml(entry, indentation + 2);
+ if (isNonEmptyCollection(entry)) {
+ return `${" ".repeat(indentation)}-\n${rendered}`;
+ }
+ return `${" ".repeat(indentation)}- ${rendered}`;
+ })
+ .join("\n");
+ }
+ const entries = Object.entries(value);
+ if (entries.length === 0) return "{}";
+ return entries
+ .map(([key, entry]) => {
+ const prefix = `${" ".repeat(indentation)}${JSON.stringify(key)}:`;
+ const rendered = renderYaml(entry, indentation + 2);
+ if (isNonEmptyCollection(entry)) {
+ return `${prefix}\n${rendered}`;
+ }
+ return `${prefix} ${rendered}`;
+ })
+ .join("\n");
+}
+
+/**
+ * Serialize a JSON-compatible value as a conservative YAML 1.2 subset.
+ *
+ * JSON normalization gives this function exactly JSON's treatment of
+ * `toJSON`, undefined object members, non-finite numbers and array holes.
+ * Double-quoting every string and mapping key avoids YAML's implicit scalar
+ * types without needing a general-purpose YAML implementation.
+ */
+export function stringifyYaml(value: unknown): string {
+ const json = JSON.stringify(value);
+ if (json === undefined) {
+ throw new TypeError("top-level value is not JSON serializable");
+ }
+ return `${renderYaml(JSON.parse(json) as JsonValue, 0)}\n`;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -586,6 +586,9 @@ importers:
typescript:
specifier: ^7.0.2
version: 7.0.2
+ yaml:
+ specifier: ^2.9.0
+ version: 2.9.0
packages/taler-wallet-cli:
dependencies: