jgexml
Version:
The Just-Good-Enough XML Toolkit
780 lines (695 loc) • 26.8 kB
JavaScript
;
var util = require('util');
var debuglog = util.debuglog('jgexml');
var target; // for new properties
var attributePrefix = '@';
var laxURIs = false;
var defaultNameSpace = '';
var xsPrefix = 'xs:';
function reset(attrPrefix, laxURIprocessing, newXsPrefix) {
target = null;
attributePrefix = attrPrefix;
laxURIs = laxURIprocessing;
defaultNameSpace = '';
xsPrefix = newXsPrefix || 'xs:';
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function hoik(obj, target, key, newKey) {
if (target && obj && (typeof obj[key] != 'undefined')) {
if (!newKey) {
newKey = key;
}
target[newKey] = clone(obj[key]);
delete obj[key];
}
}
function rename(obj, key, newName) {
obj[newName] = obj[key];
delete obj[key];
}
function isEmpty(obj) {
if (typeof obj !== 'object') return false;
for (var prop in obj) {
if ((obj.hasOwnProperty(prop) && (typeof obj[prop] !== 'undefined'))) {
return false;
}
}
return true;
}
function toArray(item) {
if (!(item instanceof Array)) {
var newitem = [];
if (item) {
newitem.push(item);
}
return newitem;
}
else {
return item;
}
}
function mandate(target, inAnyOf, inAllOf, name) {
if ((name != '#text') && (name != '#')) {
var tempTarget = target;
if (inAnyOf >= 0) {
tempTarget = target.anyOf[inAnyOf];
}
if (inAllOf >= 0) {
tempTarget = target.allOf[inAllOf];
}
if (!tempTarget.required) tempTarget.required = [];
if (tempTarget.required.indexOf(name) < 0) {
tempTarget.required.push(name);
}
}
}
function finaliseType(typeData) {
if ((typeData.type == 'string') || (typeData.type == 'boolean') || (typeData.type == 'array') || (typeData.type == 'object')
|| (typeData.type == 'integer') || (typeData.type == 'number') || (typeData.type == 'null')) {
//typeData.type = typeData.type;
}
else {
if (typeData.type.startsWith('xml:')) { // id, lang, space, base, Father
typeData.type = 'string';
}
else {
var tempType = typeData.type;
if (defaultNameSpace) {
tempType = tempType.replace(defaultNameSpace + ':', '');
}
if (tempType.indexOf(':') >= 0) {
var tempComp = tempType.split(':');
typeData["$ref"] = tempComp[0] + '.json#/definitions/' + tempComp[1]; //'/'+typeData.type.replace(':','/');
}
else {
typeData["$ref"] = '#/definitions/' + tempType;
}
delete typeData.type;
}
}
return typeData;
}
function mapType(type) {
var result = {};
result.type = type;
if (Array.isArray(type)) {
result.type = 'object';
result.oneOf = [];
for (var t in type) {
result.oneOf.push(finaliseType(mapType(type[t])));
}
}
else if (type == xsPrefix + 'integer') {
result.type = 'integer';
}
else if (type == xsPrefix + 'positiveInteger') {
result.type = 'integer';
result.minimum = 1;
}
else if (type == xsPrefix + 'nonPositiveInteger') {
result.type = 'integer';
result.maximum = 0;
}
else if (type == xsPrefix + 'negativeInteger') {
result.type = 'integer';
result.maximum = -1;
}
else if (type == xsPrefix + 'nonNegativeInteger') {
result.type = 'integer';
result.minimum = 0;
}
else if (type == xsPrefix + 'unsignedInt') {
result.type = 'integer';
result.minimum = 0;
result.maximum = 4294967295;
}
else if (type == xsPrefix + 'unsignedShort') {
result.type = 'integer';
result.minimum = 0;
result.maximum = 65535;
}
else if (type == xsPrefix + 'unsignedByte') {
result.type = 'integer';
result.minimum = 0;
result.maximum = 255;
}
else if (type == xsPrefix + 'int') {
result.type = 'integer';
result.maximum = 2147483647;
result.minimum = -2147483648;
}
else if (type == xsPrefix + 'short') {
result.type = 'integer';
result.maximum = 32767;
result.minimum = -32768;
}
else if (type == xsPrefix + 'byte') {
result.type = 'integer';
result.maximum = 127;
result.minimum = -128;
}
else if (type == xsPrefix + 'long') {
result.type = 'integer';
}
else if (type == xsPrefix + 'unsignedLong') {
result.type = 'integer';
result.minimum = 0;
}
if (type == xsPrefix + 'string') result.type = 'string';
if (type == xsPrefix + 'NMTOKEN') result.type = 'string';
if (type == xsPrefix + 'NMTOKENS') result.type = 'string';
if (type == xsPrefix + 'ENTITY') result.type = 'string';
if (type == xsPrefix + 'ENTITIES') result.type = 'string';
if (type == xsPrefix + 'ID') result.type = 'string';
if (type == xsPrefix + 'IDREF') result.type = 'string';
if (type == xsPrefix + 'IDREFS') result.type = 'string';
if (type == xsPrefix + 'NOTATION') result.type = 'string';
if (type == xsPrefix + 'token') result.type = 'string';
if (type == xsPrefix + 'Name') result.type = 'string';
if (type == xsPrefix + 'NCName') result.type = 'string';
if (type == xsPrefix + 'QName') result.type = 'string';
if (type == xsPrefix + 'normalizedString') result.type = 'string';
if (type == xsPrefix + 'base64Binary') {
result.type = 'string';
result.format = 'byte';
}
if (type == xsPrefix + 'hexBinary') {
result.type = 'string';
result.format = '^[0-9,a-f,A-F]*';
}
if (type == xsPrefix + 'boolean') result.type = 'boolean';
if (type == xsPrefix + 'date') {
result.type = 'string';
result.pattern = '^[0-9]{4}\-[0-9]{2}\-[0-9]{2}.*$'; //timezones
}
else if (type == xsPrefix + 'dateTime') {
result.type = 'string';
result.format = 'date-time';
}
else if (type == xsPrefix + 'time') {
result.type = 'string';
result.pattern = '^[0-9]{2}\:[0-9]{2}:[0-9]{2}.*$'; // timezones
}
else if (type == xsPrefix + 'duration') {
result.type = 'string';
result.pattern = '^(-)?P(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)W)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?$';
}
else if (type == xsPrefix + 'gDay') {
result.type = 'string';
result.pattern = '[0-9]{2}';
}
else if (type == xsPrefix + 'gMonth') {
result.type = 'string';
result.pattern = '[0-9]{2}';
}
else if (type == xsPrefix + 'gMonthDay') {
result.type = 'string';
result.pattern = '[0-9]{2}\-[0-9]{2}';
}
else if (type == xsPrefix + 'gYear') {
result.type = 'string';
result.pattern = '[0-9]{4}';
}
else if (type == xsPrefix + 'gYearMonth') {
result.type = 'string';
result.pattern = '[0-9]{4}\-[0-9]{2}';
}
if (type == xsPrefix + 'language') {
result.type = 'string';
result.pattern = '[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*';
}
if (type == xsPrefix + 'decimal') {
result.type = 'number';
}
else if (type == xsPrefix + 'double') {
result.type = 'number';
result.format = 'double';
}
else if (type == xsPrefix + 'float') {
result.type = 'number';
result.format = 'float';
}
if (type == xsPrefix + 'anyURI') {
result.type = 'string';
if (!laxURIs) {
result.format = 'uri'; //XSD allows relative URIs, it seems JSON schema uri format may not?
// this regex breaks swagger validators
//result.pattern = '^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?';
}
}
return result;
}
function initTarget(parent) {
if (!target) target = parent;
if (!target.properties) {
target.properties = {};
target.required = [];
target.additionalProperties = false;
}
if (!target.allOf) target.allOf = [];
}
function doElement(src, parent, key) {
var type = 'object';
var name;
var simpleType;
var doc;
var inAnyOf = -1; // used for attributeGroups - properties can get merged in here later, see mergeAnyOf
var inAllOf = (target && target.allOf) ? target.allOf.length - 1 : -1; // used for extension based composition
var element = src[key];
if ((typeof element == 'undefined') || (null === element)) {
return false;
}
if ((key == xsPrefix + "any") || (key == xsPrefix + "anyAttribute")) {
if (target) target.additionalProperties = true; // target should always be defined at this point
}
if (element[xsPrefix + "annotation"]) {
doc = element[xsPrefix + "annotation"][xsPrefix + "documentation"];
}
if (element["@name"]) {
name = element["@name"];
}
if (element["@type"]) {
type = element["@type"];
}
else if ((element["@name"]) && (element[xsPrefix + "simpleType"])) {
type = element[xsPrefix + "simpleType"][xsPrefix + "restriction"]["@base"];
simpleType = element[xsPrefix + "simpleType"][xsPrefix + "restriction"];
if (element[xsPrefix + "simpleType"][xsPrefix + "annotation"]) {
simpleType[xsPrefix + "annotation"] = element[xsPrefix + "simpleType"][xsPrefix + "annotation"];
}
}
else if ((element["@name"]) && (element[xsPrefix + "restriction"])) {
type = element[xsPrefix + "restriction"]["@base"];
simpleType = element[xsPrefix + "restriction"];
if (element[xsPrefix + "annotation"]) {
simpleType[xsPrefix + "annotation"] = element[xsPrefix + "annotation"];
}
}
else if ((element[xsPrefix + "extension"]) && (element[xsPrefix + "extension"]["@base"])) {
type = element[xsPrefix + "extension"]["@base"];
var tempType = finaliseType(mapType(type));
if (!tempType["$ref"]) {
name = "#text"; // see anonymous types
}
else {
var oldP = clone(target);
oldP.additionalProperties = true;
for (var v in target) {
delete target[v];
}
if (!target.allOf) target.allOf = [];
var newt = {};
target.allOf.push(newt);
target.allOf.push(oldP);
name = '#';
inAllOf = 0; //target.allOf.length-1;
}
}
else if (element[xsPrefix + "union"]) {
var types = element[xsPrefix + "union"]["@memberTypes"].split(' ');
type = [];
for (var t in types) {
type.push(types[t]);
}
}
else if (element[xsPrefix + "list"]) {
type = 'string';
}
else if (element["@ref"]) {
name = element["@ref"];
type = element["@ref"];
}
if (name && type) {
var isAttribute = (element["@isAttr"] == true);
initTarget(parent);
var newTarget = target;
var minOccurs = 1;
var maxOccurs = 1;
var enumList = [];
if (element["@minOccurs"]) minOccurs = parseInt(element["@minOccurs"], 10);
if (element["@maxOccurs"]) maxOccurs = element["@maxOccurs"];
if (maxOccurs == 'unbounded') maxOccurs = Number.MAX_SAFE_INTEGER;
if (isAttribute) {
if ((!element["@use"]) || (element["@use"] != 'required')) minOccurs = 0;
if (element["@fixed"]) enumList.push(element["@fixed"]);
}
if (element["@isChoice"]) minOccurs = 0;
var typeData = mapType(type);
if (isAttribute && (typeData.type == 'object')) {
typeData.type = 'string'; // handle case where attribute has no defined type
}
if (doc) {
typeData.description = doc;
}
if (enumList.length) {
typeData.enum = enumList;
}
if (typeData.type == 'object') {
typeData.properties = {};
typeData.required = [];
typeData.additionalProperties = false;
newTarget = typeData;
}
// handle @ref / attributeGroups
if ((key == xsPrefix + "attributeGroup") && (element["@ref"])) { // || (name == '$ref')) {
if (!target.anyOf) target.anyOf = [];
var newt = {};
newt.properties = {};
newt.required = clone(target.required);
target.anyOf.push(newt);
inAnyOf = target.anyOf.length - 1;
target.required = [];
delete src[key];
minOccurs = 0;
}
if ((parent[xsPrefix + "annotation"]) && ((parent[xsPrefix + "annotation"][xsPrefix + "documentation"]))) {
target.description = parent[xsPrefix + "annotation"][xsPrefix + "documentation"];
}
if ((element[xsPrefix + "annotation"]) && ((element[xsPrefix + "annotation"][xsPrefix + "documentation"]))) {
target.description = (target.description ? target.decription + '\n' : '') + element[xsPrefix + "annotation"][xsPrefix + "documentation"];
}
var enumSource;
if (element[xsPrefix + "simpleType"] && element[xsPrefix + "simpleType"][xsPrefix + "restriction"] && element[xsPrefix + "simpleType"][xsPrefix + "restriction"][xsPrefix + "enumeration"]) {
var enumSource = element[xsPrefix + "simpleType"][xsPrefix + "restriction"][xsPrefix + "enumeration"];
}
else if (element[xsPrefix + "restriction"] && element[xsPrefix + "restriction"][xsPrefix + "enumeration"]) {
var enumSource = element[xsPrefix + "restriction"][xsPrefix + "enumeration"];
}
if (enumSource) {
typeData.description = '';
typeData["enum"] = [];
enumSource = toArray(enumSource); // handle 'const' case
for (var i = 0; i < enumSource.length; i++) {
typeData["enum"].push(enumSource[i]["@value"]);
if ((enumSource[i][xsPrefix + "annotation"]) && (enumSource[i][xsPrefix + "annotation"][xsPrefix + "documentation"])) {
if (typeData.description) {
typeData.description += '';
}
typeData.description += enumSource[i]["@value"] + ': ' + enumSource[i][xsPrefix + "annotation"][xsPrefix + "documentation"];
}
}
if (!typeData.description) delete typeData.description;
}
else {
typeData = finaliseType(typeData);
}
if (maxOccurs > 1) {
var newTD = {};
newTD.type = 'array';
if (minOccurs > 0) newTD.minItems = parseInt(minOccurs, 10);
if (maxOccurs < Number.MAX_SAFE_INTEGER) newTD.maxItems = parseInt(maxOccurs, 10);
newTD.items = typeData;
typeData = newTD;
// TODO add mode where if array minOccurs is 1, add oneOf allowing single object or array with object as item
}
if (minOccurs > 0) {
mandate(target, inAnyOf, inAllOf, name);
}
if (simpleType) {
if (simpleType[xsPrefix + "minLength"]) typeData.minLength = parseInt(simpleType[xsPrefix + "minLength"]["@value"], 10);
if (simpleType[xsPrefix + "maxLength"]) typeData.maxLength = parseInt(simpleType[xsPrefix + "maxLength"]["@value"], 10);
if (simpleType[xsPrefix + "pattern"]) typeData.pattern = simpleType[xsPrefix + "pattern"]["@value"];
if ((simpleType[xsPrefix + "annotation"]) && (simpleType[xsPrefix + "annotation"][xsPrefix + "documentation"])) {
typeData.description = simpleType[xsPrefix + "annotation"][xsPrefix + "documentation"];
}
}
if (inAllOf >= 0) {
if (typeData.$ref) target.allOf[inAllOf].$ref = typeData["$ref"]
else delete target.allOf[inAllOf].$ref;
}
else if (inAnyOf >= 0) {
if (typeData.$ref) target.anyOf[inAnyOf].$ref = typeData["$ref"]
else delete target.anyOf[inAnyOf].$ref;
}
else {
if (!target.type) target.type = 'object';
target.properties[name] = typeData; // Object.assign 'corrupts' property ordering
}
target = newTarget;
}
}
function moveAttributes(obj, parent, key) {
if (key == xsPrefix + 'attribute') {
obj[key] = toArray(obj[key]);
var target;
if (obj[xsPrefix + "sequence"] && obj[xsPrefix + "sequence"][xsPrefix + "element"]) {
obj[xsPrefix + "sequence"][xsPrefix + "element"] = toArray(obj[xsPrefix + "sequence"][xsPrefix + "element"]);
target = obj[xsPrefix + "sequence"][xsPrefix + "element"];
}
if (obj[xsPrefix + "choice"] && obj[xsPrefix + "choice"][xsPrefix + "element"]) {
obj[xsPrefix + "choice"][xsPrefix + "element"] = toArray(obj[xsPrefix + "choice"][xsPrefix + "element"]);
target = obj[xsPrefix + "choice"][xsPrefix + "element"];
}
for (var i = 0; i < obj[key].length; i++) {
var attr = clone(obj[key][i]);
if (attributePrefix) {
attr["@name"] = attributePrefix + attr["@name"];
}
if (typeof attr == 'object') {
attr["@isAttr"] = true;
}
if (target) target.push(attr)
else obj[key][i] = attr;
}
if (target) delete obj[key];
}
}
function processChoice(obj, parent, key) {
if (key == xsPrefix + 'choice') {
var e = obj[key][xsPrefix + "element"] = toArray(obj[key][xsPrefix + "element"]);
for (var i = 0; i < e.length; i++) {
if (!e[i]["@isAttr"]) {
e[i]["@isChoice"] = true;
}
}
if (obj[key][xsPrefix + "group"]) {
var g = obj[key][xsPrefix + "group"] = toArray(obj[key][xsPrefix + "group"]);
for (var i = 0; i < g.length; i++) {
if (!g[i]["@isAttr"]) {
g[i]["@isChoice"] = true;
}
}
}
}
}
function renameObjects(obj, parent, key) {
if (key == xsPrefix + 'complexType') {
var name = obj["@name"];
if (name) {
rename(obj, key, name);
}
else debuglog('complexType with no name');
}
}
function moveProperties(obj, parent, key) {
if (key == xsPrefix + 'sequence') {
if (obj[key].properties) {
obj.properties = obj[key].properties;
obj.required = obj[key].required;
obj.additionalProperties = false;
delete obj[key];
}
}
}
function clean(obj, parent, key) {
if (key == '@name') delete obj[key];
if (key == '@type') delete obj[key];
if (key == xsPrefix + "attribute") delete obj[key];
if (key == xsPrefix + "restriction") delete obj[key];
if (obj.properties && (Object.keys(obj.properties).length == 1) && obj.properties["#text"] && obj.properties["#text"]["$ref"]) {
obj.properties["$ref"] = obj.properties["#text"]["$ref"];
delete obj.properties["#text"]; // anonymous types
}
if (obj.properties && obj.anyOf) { // mergeAnyOf
var newI = {};
if (obj.properties["$ref"]) {
newI["$ref"] = obj.properties["$ref"];
}
else if (Object.keys(obj.properties).length > 0) {
newI.properties = obj.properties;
newI.required = obj.required;
}
if (Object.keys(newI).length > 0) {
obj.anyOf.push(newI);
}
obj.properties = {}; // gets removed later
obj.required = []; // ditto
if (obj.anyOf.length == 1) {
if (obj.anyOf[0]["$ref"]) {
obj["$ref"] = clone(obj.anyOf[0]["$ref"]);
delete obj.type;
delete obj.additionalProperties;
}
// possible missing else here for properties !== {}
obj.anyOf = []; // also gets removed later
}
}
}
function removeEmpties(obj, parent, key) {
var count = 0;
if (isEmpty(obj[key])) {
delete obj[key];
if (key == 'properties') {
if ((!obj.oneOf) && (!obj.anyOf)) {
if (obj.type == 'object') obj.type = 'string';
delete obj.additionalProperties;
}
}
count++;
}
else {
if (Array.isArray(obj[key])) {
var newArray = [];
for (var i = 0; i < obj[key].length; i++) {
if (typeof obj[key][i] !== 'undefined') {
newArray.push(obj[key][i]);
}
else {
count++;
}
}
if (newArray.length == 0) {
delete obj[key];
count++;
}
else {
obj[key] = newArray;
}
}
}
return count;
}
function recurse(obj, parent, callback, depthFirst) {
var oTarget = target;
if (typeof obj != 'string') {
for (var key in obj) {
target = oTarget;
// skip loop if the property is from prototype
if (!obj.hasOwnProperty(key)) continue;
if (!depthFirst) callback(obj, parent, key);
var array = Array.isArray(obj);
if (typeof obj[key] === 'object') {
if (array) {
for (var i in obj[key]) {
recurse(obj[key][i], obj[key], callback);
}
}
recurse(obj[key], obj, callback);
}
if (depthFirst) callback(obj, parent, key);
}
}
return obj;
}
module.exports = {
getJsonSchema: function getJsonSchema(src, title, outputAttrPrefix, laxURIs, newXsPrefix) { // TODO convert to options parameter
reset(outputAttrPrefix, laxURIs, newXsPrefix);
for (let p in src) {
if (p.indexOf(':') >= 0) {
let pp = p.split(':')[0];
if (src[p]["@xmlns:" + pp] === 'http://www.w3.org/2001/XMLSchema') {
xsPrefix = pp + ':';
}
}
}
recurse(src, {}, function (src, parent, key) {
moveAttributes(src, parent, key);
});
recurse(src, {}, function (src, parent, key) {
processChoice(src, parent, key);
});
var obj = {};
var id = '';
if (src[xsPrefix + "schema"]) {
id = src[xsPrefix + "schema"]["@targetNamespace"];
if (!id) {
id = src[xsPrefix + "schema"]["@xmlns"];
}
}
else throw new Error('Could find schema with given prefix: ' + xsPrefix);
for (var a in src[xsPrefix + "schema"]) {
if (a.startsWith('@xmlns:')) {
if (src[xsPrefix + "schema"][a] == id) {
defaultNameSpace = a.replace('@xmlns:', '');
}
}
}
//initial root object transformations
obj.title = title;
obj.$schema = 'http://json-schema.org/schema#'; //for latest, or 'http://json-schema.org/draft-04/schema#' for v4
if (id) {
obj.id = id;
}
if (src[xsPrefix + "schema"] && src[xsPrefix + "schema"][xsPrefix + "annotation"]) {
obj.description = '';
src[xsPrefix + "schema"][xsPrefix + "annotation"] = toArray(src[xsPrefix + "schema"][xsPrefix + "annotation"]);
for (var a in src[xsPrefix + "schema"][xsPrefix + "annotation"]) {
var annotation = src[xsPrefix + "schema"][xsPrefix + "annotation"][a];
if ((annotation[xsPrefix + "documentation"]) && (annotation[xsPrefix + "documentation"]["#text"])) {
obj.description += (obj.description ? '\n' : '') + annotation[xsPrefix + "documentation"]["#text"];
}
else {
if (annotation[xsPrefix + "documentation"]) obj.description += (obj.description ? '\n' : '') + annotation[xsPrefix + "documentation"];
}
}
}
var rootElement = src[xsPrefix + "schema"][xsPrefix + "element"];
if (Array.isArray(rootElement)) {
rootElement = rootElement[0];
}
var rootElementName = rootElement["@name"];
obj.type = 'object';
obj.properties = clone(rootElement);
obj.required = [];
obj.required.push(rootElementName);
obj.additionalProperties = false;
recurse(obj, {}, function (obj, parent, key) {
renameObjects(obj, parent, key);
});
// support for schemas with just a top-level name and type (no complexType/sequence etc)
if (obj.properties["@type"]) {
target = obj; // tell it where to put the properties
}
else {
delete obj.properties["@name"]; // to prevent root-element being picked up twice
}
// main processing of the root element
recurse(obj, {}, function (src, parent, key) { // was obj.properties
doElement(src, parent, key);
});
recurse(obj, {}, function (obj, parent, key) {
moveProperties(obj, parent, key);
});
// remove rootElement to leave ref'd definitions
if (Array.isArray(src[xsPrefix + "schema"][xsPrefix + "element"])) {
//src[xsPrefix+"schema"][xsPrefix+"element"] = src[xsPrefix+"schema"][xsPrefix+"element"].splice(0,1);
delete src[xsPrefix + "schema"][xsPrefix + "element"][0];
}
else {
delete src[xsPrefix + "schema"][xsPrefix + "element"];
}
obj.definitions = clone(src);
obj.definitions.properties = {};
target = obj.definitions;
// main processing of the ref'd elements
recurse(obj.definitions, {}, function (src, parent, key) {
doElement(src, parent, key);
});
// correct for /definitions/properties
obj.definitions = obj.definitions.properties;
recurse(obj, {}, function (obj, parent, key) {
clean(obj, parent, key);
});
delete (obj.definitions[xsPrefix + "schema"]);
var count = 1;
while (count > 0) { // loop until we haven't removed any empties
count = 0;
recurse(obj, {}, function (obj, parent, key) {
count += removeEmpties(obj, parent, key);
});
}
return obj;
}
};