@krlwlfrt/xsdco
Version:
XSD converter
179 lines (158 loc) • 5.7 kB
text/typescript
/*
* Copyright (C) 2019, 2020 Karl-Philipp Wulfert
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {isEntity, ThingWithNameAndNamespace, Type} from '@krlwlfrt/tsg';
import {exit} from 'process';
import {asyncParseString} from './async';
import {logger} from './common';
/**
* List of primitive types
* @see https://www.w3.org/TR/xmlschema/#built-in-primitive-datatypes
*/
const primitiveTypes = [
'anyURI',
'base64Binary',
'boolean',
'date',
'dateTime',
'decimal',
'double',
'duration',
'float',
'gDay',
'gMonth',
'gMonthDay',
'gYear',
'gYearMonth',
'hexBinary',
'integer',
'NOTATION',
'QName',
'string',
'time',
];
/**
* Flatten part of an XML
* @param xmlPart Part of XML to flatten
* @param types Types to flatten with, extracted from XSD
* @param rootNamespaces List of namespaces to consider as root(s)
* @param root Root for flattening
* @param root.element Root element
* @param root.type Root type
* @returns Flattened part of an XML
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function flattenXMLPart(xmlPart: any,
types: Type[],
rootNamespaces: string[],
root: {
element: string | number;
type: ThingWithNameAndNamespace;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}): any {
const type = types.find((typeInTypes) => {
return typeInTypes.name === root.type.name && typeInTypes.namespace === root.type.namespace;
});
if (typeof type === 'undefined') {
logger.error(`Type '${root.type.name}' could not be found.`);
exit(1);
}
if (isEntity(type)) {
for (const key in xmlPart[root.element]) {
if (!{}.hasOwnProperty.call(xmlPart[root.element], key)) {
continue;
}
const property = type.properties.find((propertyInProperties) => {
return propertyInProperties.name === key;
});
if (typeof property === 'undefined') {
logger.error(`Property '${key}' for '${root.type.name}' could not be found.
Available properties are ${type.properties.map((propertyInProperties) => {
return propertyInProperties.name;
})
.join(', ')}`);
exit(1);
}
for (let i = 0; i < xmlPart[root.element][key].length; i++) {
if (primitiveTypes.indexOf(property.type.name) === -1
&& typeof property.type.namespace === 'string'
&& rootNamespaces.indexOf(property.type.namespace) === -1) {
xmlPart[root.element][key][i] = flattenXMLPart(xmlPart[root.element][key], types, rootNamespaces, {
element: i,
type: property.type,
});
}
// parse booleans
if (property.type.name === 'boolean' && typeof property.type.namespace === 'string' && rootNamespaces.indexOf(property.type.namespace) >= 0) {
xmlPart[root.element][key][i] = !!xmlPart[root.element][key][i];
}
// parse float numbers
if (['decimal', 'double', 'float'].indexOf(property.type.name) >= 0
&& typeof property.type.namespace === 'string'
&& rootNamespaces.indexOf(property.type.namespace) >= 0) {
xmlPart[root.element][key][i] = parseFloat(xmlPart[root.element][key][i]);
}
// parse integer numbers
if (property.type.name === 'integer' && typeof property.type.namespace === 'string' && rootNamespaces.indexOf(property.type.namespace) >= 0) {
xmlPart[root.element][key][i] = parseInt(xmlPart[root.element][key][i], 10);
}
}
// replace multiple entries with
if (property.multiple === false && Array.isArray(xmlPart[root.element][key])) {
xmlPart[root.element][key] = xmlPart[root.element][key][0];
}
}
}
return xmlPart[root.element];
}
/**
* Parse an XML
* @param xml Content of XML to parse
* @param types Types to guide the parsing with, extracted from XSD
* @param rootNamespaces List of namespaces to consider as root(s)
* @param intendedRoot Root for parsing
* @param intendedRoot.element Root element
* @param intendedRoot.type Root type
* @returns Parsed XML
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function parseXML(xml: any,
types: Type[],
rootNamespaces: string[],
intendedRoot?: {
element: string | number;
type: ThingWithNameAndNamespace;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}): Promise<any> {
const parsedXML = await asyncParseString(xml, {
emptyTag: undefined,
ignoreAttrs: true,
});
let rootElement: number | string = Object.keys(parsedXML)[0];
let rootType: ThingWithNameAndNamespace = {
name: rootElement,
namespace: '',
};
if (typeof intendedRoot !== 'undefined') {
rootElement = intendedRoot.element;
rootType = intendedRoot.type;
} else {
logger.info(`Assuming root element and type to be '${rootElement}' with the same type.`);
}
logger.info(`Using root element '${rootElement}' with type '${rootType.name}' from namespace '${rootType.namespace}'.`);
return flattenXMLPart(parsedXML, types, rootNamespaces, {
element: rootElement,
type: rootType,
});
}