xml-class-transformer
Version:
Fluently parse XML into beautiful JS/TS classes and serialize them. GoLang's encoding/xml alternative for JS/TS world.
291 lines (281 loc) • 10.9 kB
JavaScript
/*!
* xml-class-transformer v0.1.1
* (c) Edgar Pogosyan
* Released under the MIT License.
*/
;
Object.defineProperty(exports, '__esModule', { value: true });
var xmljs = require('xml-js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var xmljs__default = /*#__PURE__*/_interopDefaultLegacy(xmljs);
class ClassMetadataRegistry {
constructor() {
this.registry = new Map();
}
setEntityOptions(clazz, opts) {
const metadata = this.registry.get(clazz);
if (metadata) {
metadata.entity = opts;
}
else {
this.registry.set(clazz, {
entity: opts,
properties: new Map(),
});
}
}
setPropertyOptions(clazz, propertyKey, opts) {
const metadata = this.getOrCreate(clazz);
if (opts.name) {
for (const [searchingPropKey, searchingOpts] of metadata.properties) {
if (searchingOpts.name === opts.name) {
throw new Error(`xml-class-transformer: can't use XML element name defined in { name: ${JSON.stringify(opts.name)} } for ${clazz.name}#${propertyKey} since it's already used for ${clazz.name}#${searchingPropKey}. Change it to something else.`);
}
}
}
metadata.properties.set(propertyKey, opts);
}
getOrCreate(clazz) {
const existing = this.registry.get(clazz);
if (existing) {
return existing;
}
else {
const newMetadatas = {
entity: {},
properties: new Map(),
};
this.registry.set(clazz, newMetadatas);
return newMetadatas;
}
}
get(clazz) {
return this.registry.get(clazz);
}
}
const registry = new ClassMetadataRegistry();
function xmlToClass(xml, _class) {
var _a;
const parsed = xmljs__default["default"].xml2js(xml, {
compact: false,
alwaysArray: true,
});
const firstElement = (_a = parsed.elements) === null || _a === void 0 ? void 0 : _a[0];
if (!firstElement) {
throw new Error('No elements found in xml.');
}
return xmlToClassInternal(firstElement, _class);
}
function xmlToClassInternal(element, _class) {
var _a;
if ([String, Number, Boolean].includes(_class)) {
let value = undefined;
const text = (_a = getTextForElem(element)) === null || _a === void 0 ? void 0 : _a.toString();
if (_class === String) {
value = text;
}
else if (_class === Number) {
value = text ? parseInt(text, 10) : undefined;
}
else if (_class === Boolean) {
value = text ? text === 'true' : undefined;
}
return value;
}
const metadatas = registry.get(_class);
if (!metadatas) {
throw new Error('Unknown class ' + _class);
}
const inst = new _class();
metadatas.properties.forEach((metadata, key) => {
var _a, _b, _c, _d;
if (metadata.attr) {
if (!metadata.name) {
throw new Error(`No name is specified for attribute ${key}. Specify it with @XmlProperty({ name: '...' }) decorator.`);
}
const attr = (_a = element.attributes) === null || _a === void 0 ? void 0 : _a[metadata.name];
if (attr) {
inst[key] = attr;
}
else {
inst[key] = undefined;
}
}
else if (metadata.chardata) {
inst[key] = xmlToClassInternal(element, metadata.type);
}
else if (metadata.array) {
if (Array.isArray(metadata.type)) {
const tagNameToClassType = new Map();
metadata.type.forEach((classType) => {
const classTypeMetadata = registry.get(classType);
if (!classTypeMetadata) {
throw errUnknownClass(classType);
}
const tagName = classTypeMetadata.entity.name;
if (!tagName) {
throw new Error(`No name is specified for ${classType}. Specify it with @XmlEntity({ name: '...' }) decorator.`);
}
tagNameToClassType.set(tagName, classType);
});
const possibleTagNames = [...tagNameToClassType.keys()];
const resolvedValues = [];
(_b = element.elements) === null || _b === void 0 ? void 0 : _b.forEach((el) => {
if (el.name && possibleTagNames.includes(el.name)) {
const classType = tagNameToClassType.get(el.name);
const entity = xmlToClassInternal(el, classType);
resolvedValues.push(entity);
}
});
inst[key] = resolvedValues;
}
else {
const elems = ((_c = element.elements) === null || _c === void 0 ? void 0 : _c.filter((e) => e.name === metadata.name)) || [];
const resolvedValues = [];
elems.forEach((el) => {
const entity = xmlToClassInternal(el, metadata.type);
resolvedValues.push(entity);
});
inst[key] = resolvedValues;
}
}
else {
const el = (_d = element.elements) === null || _d === void 0 ? void 0 : _d.find((el) => el.name === metadata.name);
if (el) {
const value = xmlToClassInternal(el, metadata.type);
inst[key] = value;
}
else {
inst[key] = undefined;
}
}
});
return inst;
}
function getTextForElem(el) {
var _a, _b;
return (_b = (_a = el.elements) === null || _a === void 0 ? void 0 : _a.find((e) => e.type === 'text')) === null || _b === void 0 ? void 0 : _b.text;
}
function classToXml(entity, options) {
const tree = classToXmlInternal(entity, '', entity.constructor);
const rootElem = { elements: [tree] };
if ((options === null || options === void 0 ? void 0 : options.declaration) !== false) {
if (typeof (options === null || options === void 0 ? void 0 : options.declaration) === 'object' &&
(options === null || options === void 0 ? void 0 : options.declaration) !== null) {
rootElem.declaration = options.declaration;
}
else {
rootElem.declaration = {
attributes: { version: '1.0', encoding: 'UTF-8' },
};
}
}
return xmljs__default["default"].js2xml(rootElem, options);
}
function classToXmlInternal(entity, name, entityConstructor) {
if ([String, Number, Boolean].includes(entityConstructor)) {
const text = entity === null ? '' : entity === null || entity === void 0 ? void 0 : entity.toString();
return {
type: 'element',
name: name,
elements: [
{
type: 'text',
text,
},
],
};
}
const meta = registry.get(entityConstructor);
if (!meta) {
throw errUnknownClass(entityConstructor);
}
const elemName = name || meta.entity.name;
if (!elemName) {
throw new Error(`No name is specified for ${entityConstructor}. Specify it with @XmlEntity({ name: '...' }) decorator.`);
}
const children = [];
const attributes = {};
meta.properties.forEach((opts, classKey) => {
var _a;
if (entity[classKey] === undefined) {
return;
}
if (opts.attr) {
if (!opts.name) {
throw new Error(`No name is specified for property ${classKey}. Specify it with @XmlProperty({ name: '...' }) decorator.`);
}
attributes[opts.name] =
entity[classKey] === null ? '' : entity[classKey].toString();
}
else if (opts.chardata) {
children.push({
type: 'text',
text: entity[classKey] === null ? '' : entity[classKey].toString(),
});
}
else if (opts.array) {
(_a = entity[classKey]) === null || _a === void 0 ? void 0 : _a.forEach((e) => {
// If opts.type is an array then we can't guess required class out of it.
// In those cases users should use class constructors (aka new MyEntity({...}))
// so the library can guess the class type just by looking at myEntity.constructor
const classConstructor = Array.isArray(opts.type)
? e.constructor
: opts.type;
children.push(classToXmlInternal(e, opts.name, classConstructor));
});
}
else if ([String, Number, Boolean].includes(opts.type)) {
if (!opts.name) {
throw new Error(`No name is specified for property ${classKey}. Specify it with @XmlProperty({ name: '...' }) decorator.`);
}
children.push(classToXmlInternal(entity[opts.name], opts.name, opts.type));
}
else {
children.push(classToXmlInternal(entity[classKey], opts.name, opts.type));
}
});
if (meta.entity.xmlns) {
attributes['xmlns'] = meta.entity.xmlns;
}
return {
type: 'element',
name: elemName,
attributes,
elements: children,
};
}
function errUnknownClass(classConstructor) {
return new Error(`Class ${classConstructor} not found. Make sure there is @XmlEntity({...}) decorator on it, or @XmlProperty({...}) decorator on its properties.`);
}
/**
* Class decorator
*/
function XmlEntity(opts) {
return (target) => {
opts = opts || {};
opts.name = opts.name || target.name;
if (!opts.name) {
throw new Error(`Failed to get the element name for class ${target}. Specify it with @XmlEntity({ name: '...' }) decorator.`);
}
registry.setEntityOptions(target, opts);
return target;
};
}
/**
* Class property decorator
*/
function XmlProperty(opts) {
return (target, propertyKey) => {
opts.name === opts.name || propertyKey;
if (typeof propertyKey !== 'string') {
throw new TypeError(`Can't use @XmlProperty({...}) decorator on symbol property at ${target.constructor.name}#${propertyKey.toString()}`);
}
registry.setPropertyOptions(target.constructor, propertyKey, opts);
};
}
exports.XmlEntity = XmlEntity;
exports.XmlProperty = XmlProperty;
exports.classToXml = classToXml;
exports.xmlToClass = xmlToClass;
//# sourceMappingURL=index.js.map