taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-util/package.json | 3++-
Mpackages/taler-util/src/index.ts | 1+
Apackages/taler-util/src/yaml.test.ts | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-util/src/yaml.ts | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpnpm-lock.yaml | 3+++
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: