From 8ca00a570ab913729554ff2c930a6eab65a84ac3 Mon Sep 17 00:00:00 2001 From: Alexander Zaslonov Date: Fri, 29 Jan 2021 15:54:12 -0800 Subject: [PATCH] Added basic XSD schema support in API documentation. (#1137) --- package-lock.json | 7 +- package.json | 1 + .../ko/runtime/operation-details.ts | 6 +- .../ko/runtime/type-definition-object.html | 2 +- .../ko/runtime/type-definition.html | 2 +- .../ko/runtime/type-definition.ts | 16 +- src/contracts/schema.ts | 30 +- src/models/authorizationServer.ts | 15 +- src/models/jObject.ts | 314 ++++++++++++++++++ src/models/schema.ts | 52 ++- src/models/service.ts | 15 - src/models/typeDefinition.ts | 39 ++- src/models/xsdSchemaConverter.spec.ts | 87 +++++ src/models/xsdSchemaConverter.ts | 238 +++++++++++++ 14 files changed, 761 insertions(+), 63 deletions(-) create mode 100644 src/models/jObject.ts delete mode 100644 src/models/service.ts create mode 100644 src/models/xsdSchemaConverter.spec.ts create mode 100644 src/models/xsdSchemaConverter.ts diff --git a/package-lock.json b/package-lock.json index e9b2601e2..26d8bbbd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6805,7 +6805,7 @@ "dependencies": { "json5": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true } @@ -10153,6 +10153,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "saxen": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/saxen/-/saxen-8.1.2.tgz", + "integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==" + }, "schema-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", diff --git a/package.json b/package.json index 1214553c8..160547662 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "prismjs": "^1.22.0", "remark": "^13.0.0", "remark-html": "^13.0.1", + "saxen": "^8.1.2", "slick": "^1.12.2", "topojson-client": "^3.1.0", "truncate-html": "^1.0.3", diff --git a/src/components/operations/operation-details/ko/runtime/operation-details.ts b/src/components/operations/operation-details/ko/runtime/operation-details.ts index 4eb8119f9..2859404b8 100644 --- a/src/components/operations/operation-details/ko/runtime/operation-details.ts +++ b/src/components/operations/operation-details/ko/runtime/operation-details.ts @@ -122,8 +122,8 @@ export class OperationDetails { return; } - if (!operationName) { - this.selectedOperationName(null); + if (!operationName) { + this.selectedOperationName(null); this.operation(null); return; } @@ -229,7 +229,7 @@ export class OperationDetails { } } - this.definitions(definitions.filter(d => typeNames.indexOf(d.name) !== -1)); + this.definitions(definitions.filter(definition => typeNames.indexOf(definition.name) !== -1)); } private lookupReferences(definitions: TypeDefinition[], skipNames: string[]): string[] { diff --git a/src/components/operations/operation-details/ko/runtime/type-definition-object.html b/src/components/operations/operation-details/ko/runtime/type-definition-object.html index 3848b099b..b248e3cd9 100644 --- a/src/components/operations/operation-details/ko/runtime/type-definition-object.html +++ b/src/components/operations/operation-details/ko/runtime/type-definition-object.html @@ -67,5 +67,5 @@
Example
- + \ No newline at end of file diff --git a/src/components/operations/operation-details/ko/runtime/type-definition.html b/src/components/operations/operation-details/ko/runtime/type-definition.html index 49cf2cd99..12e705201 100644 --- a/src/components/operations/operation-details/ko/runtime/type-definition.html +++ b/src/components/operations/operation-details/ko/runtime/type-definition.html @@ -25,7 +25,7 @@

- + diff --git a/src/components/operations/operation-details/ko/runtime/type-definition.ts b/src/components/operations/operation-details/ko/runtime/type-definition.ts index 6006a7ebb..1fbe0ee8b 100644 --- a/src/components/operations/operation-details/ko/runtime/type-definition.ts +++ b/src/components/operations/operation-details/ko/runtime/type-definition.ts @@ -21,17 +21,19 @@ export class TypeDefinitionViewModel { public readonly description: ko.Observable; public readonly kind: ko.Observable; public readonly example: ko.Observable; - public readonly exampleLanguage: ko.Observable; - public readonly schemaObject: ko.Observable; + public readonly exampleFormat: ko.Observable; + public readonly rawSchema: ko.Observable; + public readonly rawSchemaFormat: ko.Observable; public readonly schemaView: ko.Observable; constructor(private readonly routeHelper: RouteHelper) { this.name = ko.observable(); - this.schemaObject = ko.observable(); this.description = ko.observable(); this.kind = ko.observable(); this.example = ko.observable(); - this.exampleLanguage = ko.observable(); + this.exampleFormat = ko.observable(); + this.rawSchema = ko.observable(); + this.rawSchemaFormat = ko.observable(); this.schemaView = ko.observable(); this.defaultSchemaView = ko.observable(); } @@ -54,13 +56,15 @@ export class TypeDefinitionViewModel { @OnMounted() public initialize(): void { this.schemaView(this.defaultSchemaView() || "table"); - this.schemaObject(JSON.stringify(this.definition.schemaObject, null, 4)); + this.rawSchema(this.definition.rawSchema); + this.rawSchemaFormat(this.definition.rawSchemaFormat); + this.name(this.definition.name); this.description(this.definition.description); this.kind(this.definition.kind); if (this.definition.example) { - this.exampleLanguage(this.definition.exampleFormat); + this.exampleFormat(this.definition.exampleFormat); this.example(this.definition.example); } } diff --git a/src/contracts/schema.ts b/src/contracts/schema.ts index a28c52dc2..3bc61dd7a 100644 --- a/src/contracts/schema.ts +++ b/src/contracts/schema.ts @@ -74,7 +74,25 @@ export interface SchemaObjectContract extends ReferenceObjectContract { minProperties?: number; + /** + * Example of the payload represented by this schema object. + */ example?: string; + + /** + * Format of payload example represented by this schema object. It is used for syntax highlighting. + */ + exampleFormat?: string; + + /** + * Raw schema representation. + */ + rawSchema?: string; + + /** + * Raw schema format. It is used for syntax highlighting. + */ + rawSchemaFormat?: string; } /** @@ -124,17 +142,23 @@ export interface OpenApiSchemaContract { }; } +export interface XsdSchemaContract { + value: string; +} + + /** * */ -export interface SchemaContract extends ArmResource { +export interface SchemaContract extends ArmResource { properties: { contentType: string; - document?: SwaggerSchemaContract | OpenApiSchemaContract; + document?: SwaggerSchemaContract | OpenApiSchemaContract | XsdSchemaContract; }; } export enum SchemaType { swagger = "application/vnd.ms-azure-apim.swagger.definitions+json", - openapi = "application/vnd.oai.openapi.components+json" + openapi = "application/vnd.oai.openapi.components+json", + xsd = "application/vnd.ms-azure-apim.xsd+xml" } \ No newline at end of file diff --git a/src/models/authorizationServer.ts b/src/models/authorizationServer.ts index 101a7bfb9..310735dd8 100644 --- a/src/models/authorizationServer.ts +++ b/src/models/authorizationServer.ts @@ -26,8 +26,12 @@ export class AuthorizationServer { ? contract.properties.defaultScope.split(" ") : []; - if (contract.properties.grantTypes) { - this.grantTypes = contract.properties.grantTypes.map(grantType => { + if (!contract.properties.grantTypes) { + return; + } + + this.grantTypes = contract.properties.grantTypes + .map(grantType => { switch (grantType) { case "authorizationCode": return "authorization_code"; @@ -37,8 +41,11 @@ export class AuthorizationServer { return "client_credentials"; case "resourceOwnerPassword": return "password"; + default: + console.log(`Unsupported grant type ${grantType}`); + return null; } - }); - } + }) + .filter(grantType => !!grantType); } } \ No newline at end of file diff --git a/src/models/jObject.ts b/src/models/jObject.ts new file mode 100644 index 000000000..8439dc5ea --- /dev/null +++ b/src/models/jObject.ts @@ -0,0 +1,314 @@ +import { Parser } from "saxen"; + +type JElementType = "element" | "comment" | "cdata" | "text" | "document" | "template" | "question"; + +export class JAttribute { + public ns: string; + public name: string; + public value: string; + + constructor(name: string, value?: string, ns?: string) { + this.name = name; + this.value = value; + this.ns = ns; + } +} + +export class JObject { + public ns: string; + public children: JObject[]; + public attributes: JAttribute[]; + public name: string; + public value: string; + public type: JElementType; + + constructor(name?: string, ns?: string) { + this.type = "element"; + this.name = name; + this.children = []; + this.attributes = []; + this.ns = ns; + } + + public toString(): string { + return this.name; + } + + public join(values: string[], separator: string): string { + return values.filter(x => x && x !== "").join(separator); + } + + public static fromXml(xml: string, parseCallbacks?: { + attribute?: (value: string) => string, + text?: (value: string) => string, + cdata?: (value: string) => string, + comment?: (value: string) => string, + }): JObject { + + const root = new JObject("document"); + root.type = "document"; + const elementStack = [root]; + const parser = new Parser({ proxy: true }); + + const pushChild = (element: JObject) => { + const currentElement = elementStack[elementStack.length - 1]; + currentElement.children.push(element); + + elementStack.push(element); + }; + + const popChild = () => { + elementStack.pop(); + }; + + const pushSibling = (element: JObject) => { + const currentElement = elementStack[elementStack.length - 1]; + currentElement.children.push(element); + }; + + parser.on("question", (str, decodeEntities, contextGetter) => { + const element = new JObject("", ""); + element.type = "question"; + element.value = str; + + pushSibling(element); + }); + + parser.on("openTag", (el, decodeEntities, selfClosing, getContext) => { + const elementNameParts = el.name.split(":"); + + let elementNamespace: string; + let elementName: string; + + if (elementNameParts.length > 1) { + elementNamespace = elementNameParts[0]; + elementName = elementNameParts[1]; + } else { + elementName = el.name; + } + + const element = new JObject(elementName, elementNamespace); + + Object.keys(el.attrs).forEach(key => { + const attributeNameParts = key.split(":"); + + let attributeNamespace: string; + let attributeName: string; + + if (attributeNameParts.length > 1) { + attributeNamespace = attributeNameParts[0]; + attributeName = attributeNameParts[1]; + } else { + attributeName = key; + } + + const tempValue = XmlUtil.decode(el.attrs[key]); + const attributeValue = parseCallbacks && parseCallbacks.attribute ? parseCallbacks.attribute(tempValue) : tempValue; + element.attributes.push(new JAttribute(attributeName, attributeValue, attributeNamespace)); + }); + + if (el.attrs["template"] && el.attrs["template"].toUpperCase() === "LIQUID" || el.name === "xsl-transform") { + element.type = "template"; + } + + pushChild(element); + }); + + parser.on("closeTag", (el, decodeEntities, selfClosing, getContext) => { + popChild(); + }); + + parser.on("error", (err, contextGetter) => { + throw new Error("Unable to parse XML."); + }); + + parser.on("text", (text: string, decodeEntities, contextGetter) => { + text = text.trim(); + + if (!text) { + return; + } + + const currentElement = elementStack[elementStack.length - 1]; + + if (!currentElement.value) { + currentElement.value = ""; + } + + currentElement.value += parseCallbacks && parseCallbacks.text ? parseCallbacks.text(text) : text; + }); + + parser.on("cdata", (value: string) => { + const element = new JObject("", ""); + element.value = parseCallbacks && parseCallbacks.cdata ? parseCallbacks.cdata(value) : value; + element.type = "cdata"; + + pushSibling(element); + }); + + parser.on("comment", (value: string,) => { + pushSibling(new JComment(parseCallbacks && parseCallbacks.comment ? parseCallbacks.comment(value) : value)); + }); + + parser.parse(xml); + + return root; + } + + private toFormattedXml(identation: number = 0, escapeCallbacks?: { + attribute?: (value: string) => boolean + }): string { + let result = ""; + const content = this.value; + let lineBreak = "\n"; + + for (let i = 0; i < identation; i++) { + lineBreak += " "; + } + + switch (this.type) { + case "document": + this.children.forEach(child => { + result += child.toFormattedXml(0, escapeCallbacks) + "\n"; + }); + break; + + case "element": + case "template": + const tag = this.join([this.ns, this.name], ":"); + + result += `${lineBreak}<${tag}`; + + this.attributes.forEach(attribute => { + let value = attribute.value.toString(); + value = escapeCallbacks && escapeCallbacks.attribute && !escapeCallbacks.attribute(value) ? value : XmlUtil.encode(value); + result += ` ${this.join([attribute.ns, attribute.name], ":")}="${value}"`; + }); + + if (this.children.length > 0) { + result += `>`; + + this.children.forEach(child => { + result += child.toFormattedXml(identation + 4, escapeCallbacks); + }); + + result += `${lineBreak}`; + } else if (content) { + result += `>${content}`; + } else { + result += ` />`; + } + break; + + case "question": + result += this.value; + break; + + case "comment": + result += `${lineBreak}`; + break; + + case "cdata": + result += ``; + break; + + case "text": + if (content) { + result += content; + } + break; + + default: + throw new Error(`Unknown element type ${this.type}.`); + } + + return result; + } + + public toXml(escapeCallbacks?: { attribute?: (value: string) => boolean }): string { + return this.toFormattedXml(0, escapeCallbacks); + } + + public innerXml(): string { + return this.children.map(x => x.toFormattedXml()).join(); + } + + public getAttribute(attributeName: string): string { + const attribute = this.attributes.find(x => x.name === attributeName); + + if (attribute && attribute.value) { + return attribute.value; + } + + return undefined; + } + + public getAttributeAsNumber(attributeName: string): number { + const value = this.getAttribute(attributeName); + const result = +value; + return isNaN(+value) ? undefined : result; + } + + public setAttribute(attributeName: string, attributeValue: string): void { + if (attributeValue) { + this.attributes.push(new JAttribute(attributeName, attributeValue)); + } + } +} + +export class JComment extends JObject { + constructor(comment: string) { + super("", ""); + + this.value = comment; + this.type = "comment"; + } +} + +export class JText extends JObject { + constructor(text: string) { + super("", ""); + + this.value = text; + this.type = "text"; + } +} + +class XmlUtil { + private static readonly chars: string[][] = [ + ["\"", """], + ["&", "&"], + ["'", "'"], + ["<", "<"], + [">", ">"], + ["\t", " "], + ["\n", " "], + ["\r", " "], + ]; + + private static encodeRegex(): RegExp { + return new RegExp(XmlUtil.chars.map((e) => e[0]).join("|"), "g"); + } + + private static decodeRegex(): RegExp { + return new RegExp(XmlUtil.chars.map((e) => e[1]).join("|"), "g"); + } + + private static encodeMap = XmlUtil.chars.reduce((i, v) => { + i[v[0]] = v[1]; + return i; + }, {}); + + private static decodeMap = XmlUtil.chars.reduce((i, v) => { + i[v[1]] = v[0]; + return i; + }, {}); + + public static encode(str: string): string { + return str.replace(XmlUtil.encodeRegex(), (s) => XmlUtil.encodeMap[s]); + } + + public static decode(str: string): string { + return str.replace(XmlUtil.decodeRegex(), (s) => XmlUtil.decodeMap[s]); + } +} \ No newline at end of file diff --git a/src/models/schema.ts b/src/models/schema.ts index a2ac01b1f..fc035a878 100644 --- a/src/models/schema.ts +++ b/src/models/schema.ts @@ -1,4 +1,5 @@ -import { SchemaContract, SchemaObjectContract, SchemaType, OpenApiSchemaContract, SwaggerSchemaContract } from "../contracts/schema"; +import { XsdSchemaConverter } from "./xsdSchemaConverter"; +import { SchemaContract, SchemaType, OpenApiSchemaContract, SwaggerSchemaContract, XsdSchemaContract } from "../contracts/schema"; import { TypeDefinition } from "./typeDefinition"; export class Schema { @@ -6,26 +7,43 @@ export class Schema { constructor(contract?: SchemaContract) { this.definitions = []; - if (contract) { - const definitionType = contract.properties?.contentType; - let definitions = {}; - - if (definitionType === SchemaType.swagger) { + + if (!contract) { + return; + } + + const definitionType = contract.properties?.contentType; + let definitions = {}; + + switch (definitionType) { + case SchemaType.swagger: const swaggerDoc = contract.properties?.document; definitions = swaggerDoc?.definitions || {}; - } else { - if (definitionType === SchemaType.openapi) { - const openApiDoc = contract.properties?.document; - definitions = openApiDoc?.components?.schemas || {}; - } - } + break; - this.definitions = Object.keys(definitions) - .map(definitionName => { - return new TypeDefinition(definitionName, definitions[definitionName]); - }); + case SchemaType.openapi: + const openApiDoc = contract.properties?.document; + definitions = openApiDoc?.components?.schemas || {}; + break; + case SchemaType.xsd: + const xsdDoc = contract.properties?.document; + + try { + definitions = new XsdSchemaConverter().convertXsdSchema(xsdDoc.value); + } + catch (error) { + console.warn(`Unable to parse XSD schema document. Skipping type definition setup.`); + } + break; + + default: + console.warn(`Unsupported schema type: ${definitionType}`); } - + + this.definitions = Object.keys(definitions) + .map(definitionName => { + return new TypeDefinition(definitionName, definitions[definitionName]); + }); } } \ No newline at end of file diff --git a/src/models/service.ts b/src/models/service.ts deleted file mode 100644 index 656cbf01a..000000000 --- a/src/models/service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ServiceDescriptionContract, HostnameConfiguration, ServiceSku } from "./../contracts/service"; - -export class ServiceDescription { - public name: string; - public hostnameConfigurations: HostnameConfiguration[]; - public sku: ServiceSku; - public gatewayUrl: string; - - constructor(contract: ServiceDescriptionContract) { - this.name = contract.name; - this.sku = contract.sku; - this.gatewayUrl = contract.properties.gatewayUrl; - this.hostnameConfigurations = contract.properties.hostnameConfigurations; - } -} \ No newline at end of file diff --git a/src/models/typeDefinition.ts b/src/models/typeDefinition.ts index fbfd8d5a7..ff641c898 100644 --- a/src/models/typeDefinition.ts +++ b/src/models/typeDefinition.ts @@ -1,6 +1,5 @@ import { SchemaObjectContract } from "../contracts/schema"; - export abstract class TypeDefinitionPropertyType { public displayAs: string; @@ -84,24 +83,41 @@ export abstract class TypeDefinitionProperty { */ public enum: any[]; - public schemaObject: SchemaObjectContract; + /** + * Raw schema representation. + */ + public rawSchema: string; + + /** + * Raw schema format. It is used for syntax highlighting. + */ + public rawSchemaFormat: string; constructor(name: string, contract: SchemaObjectContract, isRequired: boolean) { this.name = contract.title || name; - this.schemaObject = contract; this.description = contract.description; this.type = new TypeDefinitionPropertyTypePrimitive(contract.format || contract.type || "object"); + this.required = isRequired; - if (contract.example) { - if (typeof contract.example === "object") { - this.example = JSON.stringify(contract.example, null, 4); - } - else { - this.example = contract.example; - } + if (contract.rawSchemaFormat) { + this.rawSchema = contract.rawSchema; + this.rawSchemaFormat = contract.rawSchemaFormat; + } + else { // fallback to JSON + this.rawSchema = JSON.stringify(contract, null, 4); + this.rawSchemaFormat = "json"; } - this.required = isRequired; + if (contract.exampleFormat) { + this.example = contract.example; + this.exampleFormat = contract.exampleFormat; + } + else { // fallback to JSON + this.example = typeof contract.example === "object" + ? JSON.stringify(contract.example, null, 4) + : contract.example; + this.exampleFormat = "json"; + } } } @@ -339,7 +355,6 @@ export class TypeDefinitionIndexerProperty extends TypeDefinitionObjectProperty export class TypeDefinition extends TypeDefinitionObjectProperty { constructor(name: string, contract: SchemaObjectContract) { super(name, contract, true); - this.name = name; } diff --git a/src/models/xsdSchemaConverter.spec.ts b/src/models/xsdSchemaConverter.spec.ts new file mode 100644 index 000000000..c424db85c --- /dev/null +++ b/src/models/xsdSchemaConverter.spec.ts @@ -0,0 +1,87 @@ +import { expect, assert } from "chai"; +import { XsdSchemaConverter } from "./xsdSchemaConverter"; + +const xmlSchema = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; + + +describe("Schema converter", async () => { + it("Converts XML schema to JSON scheme", async () => { + const converter = new XsdSchemaConverter(); + const jsonSchema = converter.convertXsdSchema(xmlSchema); + + assert.strictEqual(jsonSchema.stringtype.type, "string"); + assert.strictEqual(jsonSchema.inttype.type, "positiveInteger"); + assert.strictEqual(jsonSchema.dectype.type, "decimal"); + assert.strictEqual(jsonSchema.orderidtype.type, "string"); + + assert.strictEqual(jsonSchema.submitCustomerOrder.type, "object"); + assert.isDefined(jsonSchema.submitCustomerOrder.properties.submitCustomerOrderRequest); + + assert.strictEqual(jsonSchema.shipordertype.type, "object"); + assert.isDefined(jsonSchema.shipordertype.properties.orderperson); + assert.isDefined(jsonSchema.shipordertype.properties.shipto); + assert.isDefined(jsonSchema.shipordertype.properties.item); + + assert.strictEqual(jsonSchema.shiporder.type, "object"); + assert.strictEqual(jsonSchema.shiporder.$ref, "shipordertype"); + }); +}); \ No newline at end of file diff --git a/src/models/xsdSchemaConverter.ts b/src/models/xsdSchemaConverter.ts new file mode 100644 index 000000000..4a564a8c2 --- /dev/null +++ b/src/models/xsdSchemaConverter.ts @@ -0,0 +1,238 @@ +import { JObject } from "./jObject"; +import { SchemaObjectContract } from "../contracts/schema"; +import { Bag } from "@paperbits/common"; + +interface SchemaNode { + name: string; + definition?: any; +} + +/** + * Basic XSD to internal schema representation converter. + */ +export class XsdSchemaConverter { + /** + * Determines if specified type is built-in primitive type. + * @param type {string} Type name. + */ + private isPrimitiveType(type: string): boolean { + return [ + "anySimpleType", + "anyType", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ID", + "IDREF", + "IDREFS", + "ENTITY", + "ENTITIES", + "NMTOKEN", + "NMTOKENS", + "boolean", + "base64Binary", + "hexBinary", + "float", + "decimal", + "integer", + "nonPositiveInteger", + "negativeInteger", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "positiveInteger", + "double", + "anyURI", + "QName", + "duration", + "dateTime", + "date", + "time", + "anySimpleType", + "anyType", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ID", + "IDREF", + "IDREFS", + "ENTITY", + "ENTITIES", + "NMTOKEN", + "NMTOKENS", + "boolean", + "base64Binary", + "hexBinary", + "float", + "decimal", + "integer", + "nonPositiveInteger", + "negativeInteger", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "positiveInteger", + "double", + "anyURI", + "QName", + "duration", + "dateTime", + "date", + "time", + ].includes(type); + } + + /** + * Converts XSD element into schema node. + * @param jObject {JObject} JObject representing XSD element. + */ + private convertElement(jObject: JObject): SchemaNode { + const name = jObject.getAttribute("name"); + const originalType = jObject.getAttribute("type"); + const isPrimitive = this.isPrimitiveType(originalType); + + let type: string; + let $ref: string; + + if (isPrimitive) { + type = originalType; + $ref = undefined; + } + else { + type = "object"; + $ref = originalType?.split(":").pop(); + } + + const definition: SchemaObjectContract = { + type: type, + properties: undefined, + $ref: $ref, + rawSchema: jObject.toXml().trim(), + rawSchemaFormat: "xml" + }; + + jObject.children.forEach(child => { + switch (child.name) { + case "simpleType": + definition.properties = definition.properties || {}; + const simpleTypeNode = this.convertSimpleType(child); + definition.properties[simpleTypeNode.name] = simpleTypeNode.definition; + break; + + case "complexType": + const complexTypeNode = this.convertComplexType(child); + if (complexTypeNode.name) { + definition.properties = definition.properties || {}; + definition.properties[complexTypeNode.name] = complexTypeNode.definition; + } + else { + Object.assign(definition, complexTypeNode.definition); + } + + break; + + case "element": + const elementNode = this.convertElement(child); + definition.properties = definition.properties || {}; + definition.properties[elementNode.name] = elementNode.definition; + break; + + default: + console.warn(`Element "${child.name}" by XSD schema converter.`); + break; + } + }); + + const resultNode: SchemaNode = { + name: name, + definition: definition + }; + + return resultNode; + } + + /** + * Converts XSD simple type into schema node. + * @param jObject {JObject} JObject representing XSD simple type. + */ + private convertSimpleType(jObject: JObject): SchemaNode { + const restriction = jObject.children[0]; + const type = restriction.getAttribute("base").split(":").pop(); + + const definition: SchemaObjectContract = { + type: type, + rawSchema: jObject.toXml().trim(), + rawSchemaFormat: "xml" + }; + + const resultNode: SchemaNode = { + name: jObject.getAttribute("name"), + definition: definition + }; + + return resultNode; + } + + /** + * Converts XSD simple type into schema node + * @param jObject {JObject} JObject representing XSD complex type. + */ + private convertComplexType(jObject: JObject): SchemaNode { + const name = jObject.getAttribute("name"); + + const definition: SchemaObjectContract = { + type: "object" + }; + + const collection = jObject.children.find(x => x.name === "sequence" || x.name === "all"); + + collection?.children.forEach(x => { + const elementNode = this.convertElement(x); + definition.properties = definition.properties || {}; + definition.properties[elementNode.name] = elementNode.definition; + }); + + const resultNode: SchemaNode = { + name: name, + definition: definition + }; + + return resultNode; + } + + /** + * Converts XSD schema into internal schema representation. + * @param xsdDocument {string} String containing XSD document. + */ + public convertXsdSchema(xsdDocument: string): Bag { + const documentJObject = JObject.fromXml(xsdDocument); + + const schemaJObject = documentJObject.children.find(x => x.name === "schema"); + + if (!schemaJObject) { + throw new Error(`Element "schema" not found in the document.`); + } + + const schemaNode = this.convertElement(schemaJObject); + + return schemaNode.definition.properties; + } +}