Added support for OpenAPI v3 schema rendering. (#5)

This commit is contained in:
Alexander Zaslonov 2021-03-16 18:30:22 -07:00 коммит произвёл GitHub
Родитель cc94d6dc0c
Коммит 3caa073b8f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
32 изменённых файлов: 981 добавлений и 359 удалений

Просмотреть файл

@ -8,7 +8,7 @@
},
"servers": [
{
"url": "https://alzasloneuap05.azure-api.net/book-store-api"
"url": "https://contoso.com"
}
],
"paths": {

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

11
package-lock.json сгенерированный
Просмотреть файл

@ -45,6 +45,7 @@
"remark": "^13.0.0",
"remark-html": "^13.0.1",
"routing-controllers": "^0.9.0-alpha.6",
"saxen": "^8.1.2",
"slick": "^1.12.2",
"topojson-client": "^3.1.0",
"truncate-html": "^1.0.3",
@ -12804,6 +12805,11 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/saxen": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/saxen/-/saxen-8.1.2.tgz",
"integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw=="
},
"node_modules/scheduler": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
@ -27221,6 +27227,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=="
},
"scheduler": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",

Просмотреть файл

@ -96,6 +96,7 @@
"remark": "^13.0.0",
"remark-html": "^13.0.1",
"routing-controllers": "^0.9.0-alpha.6",
"saxen": "^8.1.2",
"slick": "^1.12.2",
"topojson-client": "^3.1.0",
"truncate-html": "^1.0.3",

Просмотреть файл

@ -211,7 +211,7 @@ export class OperationDetails {
.map(p => p.typeName)
.filter((item, pos, self) => self.indexOf(item) === pos);
const schemasPromises = schemaIds.map(schemaId => this.apiService.getApiSchema(`${apiId}/${schemaId}`));
const schemasPromises = schemaIds.map(schemaId => this.apiService.getApiSchema(this.selectedApiName()));
const schemas = await Promise.all(schemasPromises);
const definitions = schemas.map(x => x.definitions).flat();

Просмотреть файл

@ -0,0 +1,16 @@
export * from "./openApiComponents";
export * from "./openApiExample";
export * from "./openApiExternalDoc";
export * from "./openApiMediaType";
export * from "./openApiObjectInfo";
export * from "./openApiOperation";
export * from "./openApiParameter";
export * from "./openApiPath";
export * from "./openApiPaths";
export * from "./openApiReference";
export * from "./openApiRequestBody";
export * from "./openApiResponse";
export * from "./openApiResponses";
export * from "./openApiSchema";
export * from "./openApiServer";
export * from "./openApiTag";

Просмотреть файл

@ -0,0 +1,50 @@
import {
OpenApiComponents,
OpenApiExternalDoc,
OpenApiObjectInfo,
OpenApiPaths,
OpenApiServer,
OpenApiTag
} from "./";
/**
* This is the root document object of the
*/
export interface OpenApiSpec30 {
/**
* This string MUST be the semantic version number of the OpenAPI Specification version that the OpenAPI document uses.
*/
openapi: string;
/**
* The available paths and operations for the API.
*/
paths: OpenApiPaths;
/**
* Provides metadata about the API. The metadata MAY be used by tooling as required.
*/
info: OpenApiObjectInfo;
/**
* Additional external documentation.
*/
externalDocs?: OpenApiExternalDoc;
/**
* An array of Server Objects, which provide connectivity information to a target server.
* If the servers property is not provided, or is an empty array, the default value would
* be a Server Object with a URL value of /.
*/
servers?: OpenApiServer[];
/**
* A list of tags used by the specification with additional metadata.
*/
tags?: OpenApiTag[];
/**
* An element to hold various schemas for the specification.
*/
components: OpenApiComponents;
}

Просмотреть файл

@ -0,0 +1,3 @@
export interface OpenApiComponents {
schemas?: any;
}

Просмотреть файл

@ -0,0 +1,21 @@
export interface OpenApiExample {
/**
* Short description for the example.
*/
summary: string;
/**
* Long description for the example.
*/
description: string;
/**
* Embedded literal example. The value field and externalValue field are mutually exclusive. To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary.
*/
value: any;
/**
* A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents. The value field and externalValue field are mutually exclusive.
*/
externalValue: string;
}

Просмотреть файл

@ -0,0 +1,4 @@
export interface OpenApiExternalDoc {
url: string;
description?: string;
}

Просмотреть файл

@ -0,0 +1,9 @@
import { OpenApiExample } from "./openApiExample";
export interface OpenApiMediaType {
schema: any; // OpenApiSchema | OpenApiReference;
example: OpenApiExample;
examples: any;
encoding: any;
}

Просмотреть файл

@ -0,0 +1,6 @@
export interface OpenApiObjectInfo {
title: string;
description?: string;
version: string;
termsOfService?: string;
}

Просмотреть файл

@ -0,0 +1,15 @@
import { OpenApiParameter } from "./openApiParameter";
import { OpenApiResponses } from "./openApiResponses";
import { OpenApiRequestBody } from "./openApiRequestBody";
export interface OpenApiOperation {
operationId: string;
description: string;
parameters: OpenApiParameter[];
responses: OpenApiResponses;
summary: string;
consumes?: string[];
produces?: string[];
requestBody?: OpenApiRequestBody;
}

Просмотреть файл

@ -0,0 +1,10 @@
export interface OpenApiParameter {
name: string;
in: string;
required: boolean;
description: string;
type?: string;
schema?: any;
default?: string;
enum: string[];
}

Просмотреть файл

@ -0,0 +1,6 @@
import { OpenApiOperation } from "./openApiOperation";
export interface OpenApiPath {
[key: string]: OpenApiOperation;
}

Просмотреть файл

@ -0,0 +1,5 @@
import { OpenApiPath } from "./openApiPath";
export interface OpenApiPaths {
[key: string]: OpenApiPath;
}

Просмотреть файл

@ -0,0 +1,3 @@
export interface OpenApiReference {
$ref: string;
}

Просмотреть файл

@ -0,0 +1,24 @@
import { Bag } from "@paperbits/common";
import { OpenApiMediaType } from "./openApiMediaType";
/**
* Describes a single request body.
*/
export interface OpenApiRequestBody {
/**
* A brief description of the request body. This could contain examples of use.
*/
description: string;
/**
* Determines if the request body is required in the request. Defaults to false.
*/
required: boolean;
/**
* The content of the request body. The key is a media type or media type range and the value describes it.
* For requests that match multiple keys, only the most specific key is applicable,
* e.g. text/plain overrides text/*
*/
content: Bag<OpenApiMediaType>;
}

Просмотреть файл

@ -0,0 +1,10 @@
import { Bag } from "@paperbits/common";
import { OpenApiMediaType } from "./openApiMediaType";
import { OpenApiParameter } from "./openApiParameter";
export interface OpenApiResponse {
description: string;
headers: Bag<OpenApiParameter>;
content: Bag<OpenApiMediaType>;
}

Просмотреть файл

@ -0,0 +1,5 @@
import { OpenApiResponse } from "./openApiResponse";
export interface OpenApiResponses {
[key: string]: OpenApiResponse;
}

Просмотреть файл

@ -0,0 +1 @@
export interface OpenApiSchema { }

Просмотреть файл

@ -0,0 +1,5 @@
export interface OpenApiServer {
url: string;
description?: string;
}

Просмотреть файл

@ -0,0 +1,8 @@
import { OpenApiExternalDoc } from "./openApiExternalDoc";
export interface OpenApiTag {
name: string;
description?: string;
externalDocs?: OpenApiExternalDoc;
}

Просмотреть файл

@ -1,12 +1,12 @@
import { ParameterContract } from "./parameter";
import { RepresentationContract } from "./representation";
/*
Model of API operation request
*/
/**
* Model of API operation request
*/
export interface RequestContract {
description?: string;
queryParameters: ParameterContract[];
headers: ParameterContract[];
representations: RepresentationContract[];
queryParameters?: ParameterContract[];
headers?: ParameterContract[];
representations?: RepresentationContract[];
}

Просмотреть файл

@ -30,6 +30,8 @@ export interface SchemaObjectContract extends ReferenceObjectContract {
*/
required?: string[];
readOnly?: boolean;
properties?: Bag<SchemaObjectContract>;
items?: SchemaObjectContract;
@ -72,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;
}
/**
@ -122,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"
}

Просмотреть файл

@ -1,57 +0,0 @@
export interface SwaggerObjectInfo {
title: string;
description: string;
version: string;
}
export interface SwaggerParameter {
name: string;
in: string;
required: boolean;
description: string;
type?: string;
schema?: Object;
default?: string;
enum: string[];
}
export interface SwaggerOperation {
operationId: string;
description: string;
parameters: SwaggerParameter[];
responses: Object;
security: SecurityType[];
summary: string;
}
export interface PathItem {
[key: string]: SwaggerOperation;
}
export interface SwaggerPath {
[key: string]: PathItem;
}
export interface SwaggerObject {
swagger: string;
info: SwaggerObjectInfo;
host: string;
basePath: string;
schemes: string[];
consumes: string[];
produces: string[];
paths: SwaggerPath;
definitions?: Object;
securityDefinitions?: SecurityDefinitions;
}
export interface SecurityDefinitions {
apikeyQuery: SecurityType;
apikeyHeader: SecurityType;
}
export interface SecurityType {
type: string;
name: string;
in: string;
}

Просмотреть файл

@ -76,6 +76,8 @@ export class StaticContentMiddleware implements ExpressMiddlewareInterface {
return;
}
// if no published website yet, we can serve specs from /data/spec folder directly
// if (!websiteVersion) { // no website yet, non-admins receive error message.
// response.statusCode = 200;
// return "Developer portal has not been published yet.";

314
src/models/jObject.ts Normal file
Просмотреть файл

@ -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}</${tag}>`;
} else if (content) {
result += `>${content}</${tag}>`;
} else {
result += ` />`;
}
break;
case "question":
result += this.value;
break;
case "comment":
result += `${lineBreak}<!--${content}-->`;
break;
case "cdata":
result += `<![CDATA[${content}]]>`;
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[][] = [
["\"", "&quot;"],
["&", "&amp;"],
["'", "&apos;"],
["<", "&lt;"],
[">", "&gt;"],
["\t", "&#x9;"],
["\n", "&#xA;"],
["\r", "&#xD;"],
];
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]);
}
}

Просмотреть файл

@ -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 = <SwaggerSchemaContract>contract.properties?.document;
definitions = swaggerDoc?.definitions || {};
} else {
if (definitionType === SchemaType.openapi) {
const openApiDoc = <OpenApiSchemaContract>contract.properties?.document;
definitions = openApiDoc?.components?.schemas || {};
break;
case SchemaType.openapi:
const openApiDoc = <OpenApiSchemaContract>contract.properties?.document;
definitions = openApiDoc?.components?.schemas || {};
break;
case SchemaType.xsd:
const xsdDoc = <XsdSchemaContract>contract.properties?.document;
try {
definitions = new XsdSchemaConverter().convertXsdSchema(xsdDoc.value);
}
}
this.definitions = Object.keys(definitions)
.map(definitionName => {
return new TypeDefinition(definitionName, definitions[definitionName]);
});
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]);
});
}
}

Просмотреть файл

@ -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<SchemaObjectContract> {
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;
}
}

Просмотреть файл

@ -1,6 +1,5 @@
import * as lunr from "lunr";
import { HttpClient, HttpResponse } from "@paperbits/common/http";
import { AzureBlobStorage } from "@paperbits/azure";
import { SearchQuery } from "../contracts/searchQuery";
import { Api } from "../models/api";
import { VersionSet } from "../models/versionSet";
@ -266,13 +265,13 @@ export class ApiService {
* Returns API schema with sepcified identifier.
* @param schemaId {string} ARM-formatted schema identifier.
*/
public async getApiSchema(schemaId: string): Promise<Schema> {
// const contract = await this.mapiClient.get<SchemaContract>(`${schemaId}`);
// const model = new Schema(contract);
public async getApiSchema(apiName: string): Promise<Schema> {
const specs = await this.fetchSpecs();
const converter = new OpenApiConverter();
const spec = specs.find(spec => spec.info.title === apiName);
const schema = converter.getSchema(spec);
// return model;
return null;
return schema;
}
public async getApiHostnames(apiName: string): Promise<string[]> {

Просмотреть файл

@ -1,104 +1,123 @@
import { OpenApiOperation } from "./../contracts/openapi/openApiOperation";
import { OpenApiMediaType } from "../contracts/openapi/openApiMediaType";
import { OpenApiParameter } from "../contracts/openapi/openApiParameter";
import { RepresentationContract } from "./../contracts/representation";
import { ResponseContract } from "./../contracts/response";
import { ParameterContract } from "../contracts/parameter";
import { RequestContract } from "../contracts/request";
import { OperationContract } from "./../contracts/operation";
import { ApiContract } from "../contracts/api";
import { Schema } from "../models/schema";
import { TypeDefinition } from "../models/typeDefinition";
import { OpenApiSpec30 } from "../contracts/openapi/openApi";
import { OpenApiResponse } from "../contracts/openapi/openApiResponse";
import { Bag } from "@paperbits/common";
export class OpenApiConverter {
public convertParameter(parameterObject: object): ParameterContract {
public convertParameter(openApiParameter: OpenApiParameter): ParameterContract {
const parameter: ParameterContract = {
name: parameterObject["name"],
description: parameterObject["description"],
in: parameterObject["in"],
type: parameterObject["schema"]
? parameterObject["schema"]["type"]
: null,
name: openApiParameter.name,
description: openApiParameter.description,
in: openApiParameter.in,
type: openApiParameter.schema?.type,
values: [],
required: parameterObject["required"]
required: openApiParameter.required
};
return parameter;
}
// public convertRequest(requestObject: object): RequestContract {
// const request: RequestContract = {
// description: "",
// queryParameters: ParameterContract[];
// headers: ParameterContract[];
// representations: RepresentationContract[];
// }
// }
public convertResponse(statusCode: number, responseObject: object): ResponseContract {
const response: ResponseContract = {
statusCode: statusCode,
// representations?: RepresentationContract[];
description: responseObject["description"]
public convertRequest(spec: OpenApiSpec30, openApiOperation: OpenApiOperation): RequestContract {
const request: RequestContract = {
description: openApiOperation.description,
};
const headersObject = responseObject["headers"];
if (openApiOperation.parameters) {
request.queryParameters = openApiOperation.parameters
? openApiOperation.parameters
.filter(parameter => parameter.in === "query")
.map(parameter => this.convertParameter(parameter))
: [];
if (headersObject) {
const headers: ParameterContract[] = [];
for (const headerKey of Object.keys(headersObject)) {
const headerObject = headersObject[headerKey];
const header: ParameterContract = {
name: headerKey,
description: headerObject["description"],
in: headerObject["in"],
type: headerObject["schema"]
? headerObject["schema"]["type"]
: null
};
headers.push(header);
}
response.headers = headers;
request.headers = openApiOperation.parameters
? openApiOperation.parameters
.filter(parameter => parameter.in === "header")
.map(parameter => this.convertParameter(parameter))
: [];
}
const contentObject = responseObject["content"];
if (openApiOperation.requestBody) {
request.representations = this.convertRepresentations(spec, openApiOperation.requestBody.content);
}
return request;
}
public getTypeNameFromRef($ref: string): string {
return $ref && $ref.split("/").pop();
}
public convertRepresentation(spec: OpenApiSpec30, contentType: string, mediaType: OpenApiMediaType): RepresentationContract {
const representation: RepresentationContract = {
contentType: contentType,
typeName: this.getTypeNameFromRef(mediaType.schema?.$ref),
schemaId: `${spec.info.title}`
};
return representation;
}
public convertRepresentations(spec: OpenApiSpec30, representationObjects: Bag<OpenApiMediaType>): RepresentationContract[] {
const mediaTypes = Object.keys(representationObjects);
const representations = mediaTypes.map(mediaType =>
this.convertRepresentation(spec, mediaType, representationObjects[mediaType]));
return representations;
}
private convertHeaders(headersObject: Bag<OpenApiParameter>): ParameterContract[] {
const parameters: ParameterContract[] = [];
for (const headerKey of Object.keys(headersObject)) {
const headerObject = headersObject[headerKey];
const header: ParameterContract = {
name: headerKey,
description: headerObject.description,
in: headerObject.in,
type: headerObject.schema?.type
};
parameters.push(header);
}
return parameters;
}
public convertResponse(spec: OpenApiSpec30, statusCode: number, responseObject: OpenApiResponse): ResponseContract {
const response: ResponseContract = {
statusCode: statusCode,
description: responseObject.description
};
const headersObject = responseObject.headers;
if (headersObject) {
response.headers = this.convertHeaders(headersObject);
}
const contentObject = responseObject.content;
if (contentObject) {
const representations: RepresentationContract[] = [];
for (const representationKey of Object.keys(contentObject)) {
const representationObject = contentObject[representationKey];
const representation: RepresentationContract = {
contentType: representationKey,
sample: representationObject.examples?.["response"]
// generatedSample?: string;
// schemaId?: string;
// typeName?: string;
// formParameters?: ParameterContract[];
};
const representationExamplesObject = representationObject.examples;
if (representationExamplesObject) {
const exampleKeys = Object.keys(representationExamplesObject);
if (exampleKeys.length > 0) {
representation.sample = JSON.stringify(representationExamplesObject[exampleKeys[0]]);
}
}
representations.push(representation);
}
response.representations = representations;
response.representations = this.convertRepresentations(spec, contentObject);
}
return response;
}
public convertPaths(pathsObject: object): OperationContract[] {
public convertPaths(spec: OpenApiSpec30): OperationContract[] {
const pathsObject = spec.paths;
const operations: OperationContract[] = [];
for (const pathKey of Object.keys(pathsObject)) {
@ -117,19 +136,19 @@ export class OpenApiConverter {
urlTemplate: pathKey,
templateParameters: methodObject.parameters
? methodObject.parameters
.filter(x => x["in"] === "template")
.map(x => this.convertParameter(x))
.filter(parameter => parameter.in === "template")
.map(parameter => this.convertParameter(parameter))
: [],
method: methodKey.toUpperCase(),
version: "",
request: null, // RequestContract;
request: this.convertRequest(spec, methodObject)
}
};
const responsesObject = methodObject["responses"];
const responsesObject = methodObject.responses;
if (responsesObject) {
const responses: ResponseContract[] = [];
const responseContracts: ResponseContract[] = [];
for (const responseKey of Object.keys(responsesObject)) {
const statusCode = parseInt(responseKey);
@ -138,10 +157,10 @@ export class OpenApiConverter {
continue;
}
const response = this.convertResponse(statusCode, responsesObject[responseKey]);
responses.push(response);
const responseContract = this.convertResponse(spec, statusCode, responsesObject[responseKey]);
responseContracts.push(responseContract);
}
operation.properties.responses = responses;
operation.properties.responses = responseContracts;
}
operations.push(operation);
@ -151,27 +170,53 @@ export class OpenApiConverter {
return operations;
}
public getApi(spec: any): ApiContract {
const api: ApiContract = {
public getApi(spec: OpenApiSpec30): ApiContract {
const apiContract: ApiContract = {
name: spec.info.title,
properties: {
displayName: spec.info.title,
description: spec.info.description,
subscriptionRequired: false,
protocols: ["http", "https"],
thumbnail: spec.info["x:thumbnail"] || "https://repository-images.githubusercontent.com/168243877/bc582a00-838e-11e9-82cd-708afc2d2a11"
thumbnail: spec.info["x:thumbnail"]
}
};
return api;
return apiContract;
}
public getOperations(spec: any): OperationContract[] {
const operations = this.convertPaths(spec.paths);
public getOperations(spec: OpenApiSpec30): OperationContract[] {
const operations = this.convertPaths(spec);
return operations;
}
public getHostnames(spec: any): string[] {
return spec.servers?.map(x => new URL(x.url).hostname) || [];
public getHostnames(spec: OpenApiSpec30): string[] {
if (!spec.servers) {
return [];
}
return spec.servers?.map(server =>
server.url.startsWith("http://") || server.url.startsWith("https://")
? new URL(server.url).hostname
: "https://contoso.com");
}
public getSchema(spec: OpenApiSpec30): Schema {
const schemasObject = spec.components?.schemas;
if (!schemasObject) {
return null;
}
const definitions = Object
.keys(schemasObject)
.map(definitionName => {
return new TypeDefinition(definitionName, schemasObject[definitionName]);
});
const schema = new Schema();
schema.definitions = definitions;
return schema;
}
}