UNPKG

typesxml

Version:

Open source XML library written in TypeScript

186 lines 8.64 kB
/******************************************************************************* * Copyright (c) 2023-2026 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/epl-v10.html * * Contributors: * Maxprograms - initial API and implementation *******************************************************************************/ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Readable } from "node:stream"; import { DOMBuilder } from "../DOMBuilder.js"; import { SAXParser } from "../SAXParser.js"; import { jsonFileToXmlFile, jsonObjectToXmlDocument, jsonObjectToXmlFile, jsonStreamToXmlDocument, xmlDocumentToJsonObject, xmlFileToJsonFile, xmlStreamToJsonObject, xmlStringToJsonFile, xmlStringToJsonObject } from "../json/JsonConversion.js"; async function runJsonConversionTests() { const samples = [ '<?xml version="1.0" encoding="UTF-8"?>\n' + '<!--Before root--><!--Second comment-->' + '<!DOCTYPE note [' + '<!ENTITY writer "John Doe">' + '<!ELEMENT note ANY>' + '<!ELEMENT body ANY>' + '<!ELEMENT h:header ANY>' + '<!ELEMENT child ANY>' + '<!ELEMENT empty EMPTY>' + ']>' + '<note xmlns:h="http://example.com/h" category="memo">' + '<h:header><![CDATA[Agenda]]></h:header>' + '<body>Don\'t forget &writer; on Monday<?pi instructions?></body>' + '<empty />' + '</note>', '<?xml version="1.1" encoding="UTF-8" standalone="yes"?>\n' + '<root attr1="value1" attr2="value2">' + 'Text node<child>Inner</child>' + '<child xml:lang="en">More text</child>' + '<![CDATA[raw <content>]]>' + '<?processing data?>' + '<!--After content-->' + '</root>' ]; for (let index = 0; index < samples.length; index++) { const label = `Sample ${index + 1}`; await assertStructuredRoundTrip(samples[index], label); } await assertPlainObjectRoundTrip(); await assertRootInference(); await assertFileConversions(samples[0]); await assertStreamConversions(samples[1]); console.log("JSON conversion tests passed."); } async function assertStructuredRoundTrip(xmlText, label) { const originalDocument = parseXml(xmlText); const simpleJson = xmlStringToJsonObject(xmlText); if (typeof simpleJson !== "object" || simpleJson === null || Array.isArray(simpleJson)) { throw new Error(`Expected simple conversion to yield object JSON for ${label}`); } if ("rootName" in simpleJson || "prolog" in simpleJson || "doctype" in simpleJson) { throw new Error(`Simple conversion leaked document metadata for ${label}`); } const jsonDocument = xmlStringToJsonObject(xmlText, { mode: "roundtrip" }); if (jsonDocument.rootName !== originalDocument.getRoot()?.getName()) { throw new Error(`Root name mismatch for ${label}`); } const rebuiltDocument = jsonObjectToXmlDocument(jsonDocument); if (!originalDocument.equals(rebuiltDocument)) { throw new Error(`XML -> JSON -> XML comparison failed for ${label}`); } if (jsonDocument.prolog && jsonDocument.prolog.length === 0) { throw new Error(`Expected prolog entries for ${label}`); } if (jsonDocument.declaration && jsonDocument.declaration.version === undefined) { throw new Error(`Missing declaration details for ${label}`); } } async function assertPlainObjectRoundTrip() { const source = { library: "painters", books: ["DaVinci", "VanGogh", "Rubens"], prices: [13000, 5000, 20000] }; const document = jsonObjectToXmlDocument(source, "libraryCatalog"); const simpleRoundTrip = xmlDocumentToJsonObject(document); if (typeof simpleRoundTrip !== "object" || simpleRoundTrip === null || Array.isArray(simpleRoundTrip)) { throw new Error("Simple round-trip should yield an object"); } if ("rootName" in simpleRoundTrip) { throw new Error("Simple round-trip should not include root metadata"); } const roundTrip = xmlDocumentToJsonObject(document, { mode: "roundtrip" }); const root = roundTrip.root; if (!root || Array.isArray(root) || typeof root !== "object") { throw new Error("Unexpected JSON structure for plain object round-trip"); } const rootObject = root; if (rootObject.library !== "painters") { throw new Error("Library value mismatch after round-trip"); } const booksEntry = rootObject.books; if (!Array.isArray(booksEntry) || booksEntry.some((item) => typeof item !== "string") || booksEntry.length !== 3) { throw new Error("Books array mismatch after round-trip"); } const pricesEntry = rootObject.prices; if (!Array.isArray(pricesEntry) || pricesEntry.some((entry) => typeof entry !== "string")) { throw new Error("Prices array expected string representations"); } } async function assertRootInference() { const source = { library: { books: ["DaVinci", "VanGogh", "Rubens"] } }; const document = jsonObjectToXmlDocument(source); const root = document.getRoot(); if (!root || root.getName() !== "library") { throw new Error("Expected root element name to be inferred from single property"); } const roundTripJson = xmlDocumentToJsonObject(document); if (typeof roundTripJson !== "object" || roundTripJson === null || Array.isArray(roundTripJson)) { throw new Error("Round-tripped JSON should remain an object"); } if (!("library" in roundTripJson)) { throw new Error("Round-tripped JSON missing inferred property"); } } async function assertFileConversions(xmlText) { const tempDir = mkdtempSync(join(tmpdir(), "typesxml-json-files-")); const xmlInputPath = join(tempDir, "input.xml"); writeFileSync(xmlInputPath, xmlText, "utf8"); const jsonPath = join(tempDir, "output.json"); await xmlFileToJsonFile(xmlInputPath, jsonPath, "utf8", 2, "utf8", { mode: "roundtrip" }); const parsedJson = JSON.parse(readFileSync(jsonPath, "utf8")); const rebuiltDocument = jsonObjectToXmlDocument(parsedJson); const originalDocument = parseXml(xmlText); if (!originalDocument.equals(rebuiltDocument)) { throw new Error("File-based XML -> JSON -> XML comparison failed"); } const rebuiltXmlPath = join(tempDir, "roundtrip.xml"); await jsonFileToXmlFile(jsonPath, rebuiltXmlPath); const roundTripDocument = parseXml(readFileSync(rebuiltXmlPath, "utf8")); if (!originalDocument.equals(roundTripDocument)) { throw new Error("JSON file -> XML file comparison failed"); } const jsonStringPath = join(tempDir, "string.json"); await xmlStringToJsonFile(xmlText, jsonStringPath, { mode: "roundtrip" }); const jsonDocument = JSON.parse(readFileSync(jsonStringPath, "utf8")); await jsonObjectToXmlFile(jsonDocument, join(tempDir, "from-string.xml")); rmSync(tempDir, { recursive: true, force: true }); } async function assertStreamConversions(xmlText) { const xmlDocument = parseXml(xmlText); const xmlStream = Readable.from([xmlText]); const jsonFromStream = await xmlStreamToJsonObject(xmlStream, { mode: "roundtrip" }); const rebuiltFromStream = jsonObjectToXmlDocument(jsonFromStream); if (!xmlDocument.equals(rebuiltFromStream)) { throw new Error("XML stream round-trip failed"); } const jsonText = JSON.stringify(jsonFromStream); const jsonStream = Readable.from([jsonText]); const documentFromJsonStream = await jsonStreamToXmlDocument(jsonStream); if (!xmlDocument.equals(documentFromJsonStream)) { throw new Error("JSON stream round-trip failed"); } } function parseXml(xmlText) { const parser = new SAXParser(); const builder = new DOMBuilder(); builder.initialize(); parser.setContentHandler(builder); parser.setValidating(false); parser.parseString(xmlText); const document = builder.getDocument(); if (!document) { throw new Error("Unable to parse XML text"); } return document; } runJsonConversionTests().catch((error) => { console.error("JSON conversion tests failed:", error); process.exitCode = 1; }); //# sourceMappingURL=JsonConversionTest.js.map