xsd2jsonschema
Version:
A pure JavaScript library for converting complex XML Schemas into equivalent JSON Schemas.
321 lines (290 loc) • 12.7 kB
JavaScript
;
const debug = require('debug')('xsd2jsonschema:BaseSpecialCaseIdentifier');
const XsdFile = require('./xmlschema/xsdFileXmlDom');
const XsdAttributes = require('./xmlschema/xsdAttributes');
const XsdAttributeValues = require('./xmlschema/xsdAttributeValues');
const XsdNodeTypes = require('./xmlschema/xsdNodeTypes');
const XsdElements = require('./xmlschema/xsdElements');
const specialCases_NAME = Symbol();
/**
* Class representing a collection of logic to identify special cases in XML Schema that cannot be immediately
* converted to JSON Schmea without inspecting the contents of the tag or the tag's siblings. Examples:
*
* 1. A choice where the goal is really anyOf. For example:
* <xs:choice>
* <xs:sequence>
* <xs:element name='DemandingPartyInfo' type='DemandingPartyInfoType'/>
* <xs:element name='ResponsiblePartyInfo' type='DemandingPartyInfoType' minOccurs='0'/>
* <xs:element name='ArbitrationDecisionInfo' type='DemandingPartyInfoType' minOccurs='0'/>
* </xs:sequence>
* <xs:sequence>
* <xs:element name='ResponsiblePartyInfo' type='DemandingPartyInfoType'/>
* <xs:element name='ArbitrationDecisionInfo' type='DemandingPartyInfoType' minOccurs='0'/>
* </xs:sequence>
* <xs:element name='ArbitrationDecisionInfo' type='DemandingPartyInfoType'/>
* </xs:choice>
*
* 2. Sibling choice tag. For example:
* <xs:complexType name='SiblingChoince'>
* <xs:sequence>
* <xs:choice>
* <xs:element name='OptionB' type='xs:string'/>
* <xs:element name='OptionA' type='xs:string' minOccurs='0'/>
* </xs:choice>
* <xs:choice>
* <xs:element name='Option3' type='xs:string' />
* <xs:element name='Option2' type='xs:string' minOccurs='0' />
* <xs:element name='Option1' type='xs:string' minOccurs='0' />
* </xs:choice>
* </xs:sequence>
* </xs:complexType>
*
* 3. Optional sequence and/or choice tags. For example:
* <xs:choice minOccurs="0">
* <xs:element name="Option2" type="xs:string" minOccurs="0"/>
* <xs:element name="Option1" type="xs:string" minOccurs="0"/>
* </xs:choice>
*
*/
class BaseSpecialCaseIdentifier {
constructor() {
this.specialCases = [];
}
// Getters/Setters
get specialCases() {
return this[specialCases_NAME];
}
set specialCases(newSpecialCase) {
this[specialCases_NAME] = newSpecialCase;
}
addSpecialCase(specialCase, jsonschema, node) {
this.specialCases.push({
specialCase: specialCase,
jsonSchema: jsonschema,
node: node
});
}
isOptional(node, xsd, minOccursAttr) {
var minOccurs;
if (minOccursAttr == undefined) {
minOccurs = XsdFile.getAttrValue(node, XsdAttributes.MIN_OCCURS);
} else {
minOccurs = minOccursAttr;
}
return minOccurs !== undefined && minOccurs == 0;
}
isSiblingChoice(node, xsd) {
var retval = XsdFile.countChildren(node.parentNode, XsdElements.CHOICE) > 1;
return retval;
}
countNonTextNodes(nodelist) {
var count = 0;
for (let i = 0; i < nodelist.length; i++) {
if (nodelist.item(i).nodeType != XsdNodeTypes.TEXT_NODE) {
debug(`NodeType=${XsdNodeTypes.getTypeName(nodelist.item(i).nodeType)} NodeName = ${nodelist.item(i).localName}`);
count++;
}
}
return count;
}
locateNewNameType(nameTypes, childrenOfOneOfTheChoiceOptions) {
for (let nt = 0; nt < nameTypes.length; nt++) {
for (let c = 0; c < childrenOfOneOfTheChoiceOptions.length; c++) {
const node = childrenOfOneOfTheChoiceOptions[c];
const name = XsdFile.getAttrValue(node, XsdAttributes.NAME);
const type = XsdFile.getAttrValue(node, XsdAttributes.TYPE);
const minOccurs = XsdFile.getAttrValue(node, XsdAttributes.MIN_OCCURS);
if (nameTypes[nt].name != name && minOccurs == undefined) {
return {
name: name,
type: type
};
}
}
}
return undefined;
}
verifyPriorChoices(nameTypes, childrenOfOneOfTheChoiceOptions) {
for (let nt = 0; nt < nameTypes.length; nt++) {
let optionalPriorNameTypeFound = false;
for (let c = 0; c < childrenOfOneOfTheChoiceOptions.length; c++) {
const node = childrenOfOneOfTheChoiceOptions[c];
const name = XsdFile.getAttrValue(node, XsdAttributes.NAME);
const type = XsdFile.getAttrValue(node, XsdAttributes.TYPE);
const minOccurs = XsdFile.getAttrValue(node, XsdAttributes.MIN_OCCURS);
if (nameTypes[nt].name === name && nameTypes[nt].type === type && minOccurs === XsdAttributeValues.ZERO) {
optionalPriorNameTypeFound = true;
}
}
if (!optionalPriorNameTypeFound) {
return false;
}
}
return true;
}
checkNode(nameTypes, nextChoiceOption, expectedChildCount) {
var retval;
const children = this.nodeListToArray(nextChoiceOption.childNodes);
const actualChildCount = children.length;
if (actualChildCount == expectedChildCount && this.verifyPriorChoices(nameTypes, children)) {
retval = this.locateNewNameType(nameTypes, children);
}
return retval;
}
isOptionallyIncremental(nameTypes, sortedChoiceChildren, index) {
if (index == sortedChoiceChildren.length) {
return true;
}
const node = sortedChoiceChildren[index];
const checkNameType = this.checkNode(nameTypes, node, index + 1);
if (checkNameType != undefined) {
nameTypes.push(checkNameType);
return this.isOptionallyIncremental(nameTypes, sortedChoiceChildren, index + 1);
} else {
return false;
}
}
nodeListToArray(nodelist) {
var array = [];
for (let i = 0; i < nodelist.length; i++) {
if (nodelist.item(i).nodeType != XsdNodeTypes.TEXT_NODE) {
array.push(nodelist.item(i));
}
}
return array;
}
isAnyOfChoice(node, xsd) {
var retval = false;
if (node.hasChildNodes() && node.childNodes.length > 1) {
var sortedChildren = this.nodeListToArray(node.childNodes).sort((a, b) => {
const aHasNodes = a.hasChildNodes();
const bHasNodes = b.hasChildNodes();
var aNodeCount = 0;
var bNodeCount = 0;
if (aHasNodes) {
aNodeCount = a.childNodes.length;
}
if (bHasNodes) {
bNodeCount = b.childNodes.length;
}
if (aNodeCount < bNodeCount) {
return -1;
} else if (aNodeCount > bNodeCount) {
return 1;
} else {
return 0;
}
});
const firstChild = sortedChildren[0];
if (XsdFile.hasAttribute(firstChild, XsdAttributes.NAME) && XsdFile.hasAttribute(firstChild, XsdAttributes.TYPE)) {
var nameTypes = [{}];
nameTypes[0].name = XsdFile.getAttrValue(firstChild, XsdAttributes.NAME);
nameTypes[0].type = XsdFile.getAttrValue(firstChild, XsdAttributes.TYPE);
retval = this.isOptionallyIncremental(nameTypes, sortedChildren, 1);
}
}
return retval;
}
generateAnyOfChoice(jsonSchema) {
if (jsonSchema.oneOf.length == 0) {
return;
}
debug('BEFORE Generating anyOfChoice\n' + jsonSchema);
var anyOf = jsonSchema.oneOf[0];
anyOf.required.length = 0;
Object.keys(anyOf.properties).forEach(function(prop, index, array) {
debug(prop + '=' + anyOf.properties[prop]);
const newAnyOf = jsonSchema.newJsonSchemaFile();
newAnyOf.setProperty(prop, anyOf.properties[prop]);
newAnyOf.addRequired(prop);
jsonSchema.anyOf.push(newAnyOf);
});
jsonSchema.oneOf = [];
debug('AFTER Generating anyOfChoice\n' + jsonSchema);
}
fixAnyOfChoice(jsonSchema, node) {
if (jsonSchema.allOf.length != 0) {
// A sibling choice will have the siblings in the allOf array.
jsonSchema.allOf.forEach(function(choiceSchema, index, array) {
if (choiceSchema.isAnyOfChoice === true) {
this.generateAnyOfChoice(choiceSchema);
}
}, this);
} else {
this.generateAnyOfChoice(jsonSchema);
}
}
// The "Everything else is valid" SOLUTION
// 1) Push an empty schema onto anyOf. Always passes validation, as if the empty schema {}
fixOptionalChoiceTruthy(jsonSchema, node) {
debug('Fixing optional ' + XsdFile.nodeQuickDumpStr(node) + ' using a Truthy schema.');
debug('Optional choice: ' + jsonSchema.toString());
// Add an the optional part (empty schema)
var emptySchema = jsonSchema.newJsonSchemaFile();
emptySchema.description = "This truthy schema is what makes an optional <choice> optional."
jsonSchema.parent.anyOf.push(emptySchema);
debug('Parent: ' + jsonSchema.parent.toString());
}
// The "Not" SOLUTION - elimiated because allows ANYTHING not listed in the dependent properties.
// 1) push oneOf onto anyOf and create new empty oneOf array
// 2) create new 'optional' schema and also push it onto anyOf
// 3) populate optional schema allOf with a 'not' for each member of the original oneOf
fixOptionalChoiceNot(jsonSchema, node) {
debug('Fixing optional choice ' + XsdFile.nodeQuickDumpStr(node) + ' using the Not solution.');
debug('Optional choice: ' + jsonSchema.toString());
const originalOneOf = jsonSchema.newJsonSchemaFile();
originalOneOf.oneOf = jsonSchema.oneOf.slice(0);
//originalOneOf.description = 'originalOneOf';
jsonSchema.anyOf.push(originalOneOf);
const theOptionalPart = jsonSchema.newJsonSchemaFile();
//theOptionalPart.description = 'theOptionalPart';
jsonSchema.oneOf.forEach(function(option, index, array) {
const notSchema = theOptionalPart.newJsonSchemaFile();
notSchema.not = option;
debug('Pushing not schema');
theOptionalPart.allOf.push(notSchema);
});
jsonSchema.anyOf.push(theOptionalPart);
jsonSchema.oneOf = [];
//jsonSchema.description = 'This is the NOT solution';
debug('Parent: ' + jsonSchema.parent.toString());
}
// The "Property Dependency" SOLUTION
// 1) push oneOf onto anyOf and create new empty oneOf array
// 2) create new 'optional' schema and also push it onto anyOf
// 3) populate optional schema allOf with a 'not' for each member of the original oneOf
fixOptionalChoicePropertyDependency(jsonSchema, node) {
debug('Fixing optional choice ' + XsdFile.nodeQuickDumpStr(node) + ' using the Property Dependency solution.');
debug('Optional choice: ' + jsonSchema.toString());
const originalOneOf = jsonSchema.newJsonSchemaFile();
originalOneOf.oneOf = Array.from(jsonSchema.oneOf);
jsonSchema.anyOf.push(originalOneOf);
const theOptionalPart = jsonSchema.newJsonSchemaFile();
jsonSchema.oneOf.forEach(function(option, index, array) {
const dependencySchema = theOptionalPart.newJsonSchemaFile();
dependencySchema.not = option;
theOptionalPart.addPropertyDependency(option.name, option); // This needs to be checked/finished
//theOptionalPart.allOf.push(notSchema);
});
jsonSchema.anyOf.push(theOptionalPart);
jsonSchema.oneOf = [];
debug('Parent: ' + jsonSchema.parent.toString());
}
fixOptionalChoice(jsonSchema, node) {
// switch (options)
//this.fixOptionalChoiceTruthy(jsonSchema, node)
this.fixOptionalChoiceNot(jsonSchema, node)
return;
}
fixOptionalSequence(jsonSchema, node) {
debug("NOT IMPLEMENTED")
return;
}
processSpecialCases() {
while (this.specialCases.length > 0) {
const sc = this.specialCases.pop()
this[sc.specialCase](sc.jsonSchema, sc.node);
}
}
}
module.exports = BaseSpecialCaseIdentifier;