@bmqube/xmlrpc
Version:
A pure TypeScript XML-RPC client and server. Forked from (https://github.com/baalexander/node-xmlrpc)
192 lines (191 loc) • 6.14 kB
JavaScript
// serializer.mts
import * as xmlBuilder from "xmlbuilder";
import dateFormatter from "./date_formatter.mjs";
import CustomType from "./customtype.mjs";
/**
* Creates the XML for an XML-RPC method call.
*/
export function serializeMethodCall(method, paramsInput, encoding) {
const params = paramsInput ?? [];
const options = { version: "1.0", allowSurrogateChars: true };
if (encoding)
options.encoding = encoding;
const xml = xmlBuilder.create("methodCall", options)
.ele("methodName")
.txt(method)
.up()
.ele("params");
params.forEach((param) => {
serializeValue(param, xml.ele("param"));
});
// Includes the <?xml ...> declaration
return xml.doc().toString();
}
/**
* Creates the XML for an XML-RPC method response.
*/
export function serializeMethodResponse(result) {
const xml = xmlBuilder.create("methodResponse", {
version: "1.0",
allowSurrogateChars: true,
})
.ele("params")
.ele("param");
serializeValue(result, xml);
return xml.doc().toString();
}
/**
* Creates the XML for an XML-RPC fault response.
*/
export function serializeFault(fault) {
const xml = xmlBuilder.create("methodResponse", {
version: "1.0",
allowSurrogateChars: true,
}).ele("fault");
serializeValue(fault, xml);
return xml.doc().toString();
}
function serializeValue(value, xml) {
const stack = [{ value, xml }];
let current = null;
let valueNode = null;
let next = null;
while (stack.length > 0) {
current = stack[stack.length - 1];
if (current.index !== undefined) {
// Iterating a compound
next = getNextItemsFrame(current);
if (next) {
stack.push(next);
}
else {
stack.pop();
}
}
else {
// about to add a new value (compound or simple)
valueNode = current.xml.ele("value");
switch (typeof current.value) {
case "boolean":
appendBoolean(current.value, valueNode);
stack.pop();
break;
case "string":
appendString(current.value, valueNode);
stack.pop();
break;
case "number":
appendNumber(current.value, valueNode);
stack.pop();
break;
case "object":
if (current.value === null) {
valueNode.ele("nil");
stack.pop();
}
else if (current.value instanceof Date) {
appendDatetime(current.value, valueNode);
stack.pop();
}
else if (Buffer.isBuffer(current.value)) {
appendBuffer(current.value, valueNode);
stack.pop();
}
else if (current.value instanceof CustomType) {
current.value.serialize(valueNode);
stack.pop();
}
else {
if (Array.isArray(current.value)) {
current.xml = valueNode.ele("array").ele("data");
}
else {
current.xml = valueNode.ele("struct");
current.keys = Object.keys(current.value);
}
current.index = 0;
next = getNextItemsFrame(current);
if (next) {
stack.push(next);
}
else {
stack.pop();
}
}
break;
default:
// unsupported value types are ignored (parity with original)
stack.pop();
break;
}
}
}
}
function getNextItemsFrame(frame) {
let nextFrame = null;
if (frame.keys) {
if (frame.index < frame.keys.length) {
const key = frame.keys[frame.index++];
const member = frame.xml.ele("member").ele("name");
// original used .text(key) then .up()
(member.text ? member.text(key) : member.txt(key)).up?.();
nextFrame = {
value: frame.value[key],
xml: member, // the JS original attaches next value node at member-level
};
}
}
else if (frame.index < frame.value.length) {
nextFrame = {
value: frame.value[frame.index],
xml: frame.xml,
};
frame.index = frame.index + 1;
}
return nextFrame;
}
function appendBoolean(value, xml) {
xml.ele("boolean").txt(value ? 1 : 0);
}
const illegalChars = /^(?![^<&]*]]>[^<&]*)[^<&]*$/;
function appendString(value, xml) {
if (value.length === 0) {
xml.ele("string");
return;
}
if (!illegalChars.test(value)) {
const str = xml.ele("string");
if (typeof str.d === "function") {
str.d(value);
}
else if (typeof str.cdata === "function") {
str.cdata(value);
}
else {
str.txt(value);
}
return;
}
xml.ele("string").txt(value);
}
function appendNumber(value, xml) {
if (value % 1 === 0) {
xml.ele("int").txt(value);
}
else {
xml.ele("double").txt(value);
}
}
function appendDatetime(value, xml) {
xml.ele("dateTime.iso8601").txt(dateFormatter.encodeIso8601(value));
}
function appendBuffer(value, xml) {
xml.ele("base64").txt(value.toString("base64"));
}
// Default export that mirrors the CommonJS `module.exports = { ... }` style
const Serializer = {
serializeMethodCall,
serializeMethodResponse,
serializeFault,
};
export default Serializer;