mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-28 19:43:43 +03:00
FBI-451: Implement XML example code generation for Redoc
This commit is contained in:
parent
7e4639e8cf
commit
00256a8da4
|
@ -1,9 +1,10 @@
|
||||||
import type { OpenAPIRef, OpenAPISchema, OpenAPISpec } from '../types';
|
import type { OpenAPIRef, OpenAPISchema, OpenAPISpec } from '../types';
|
||||||
import { IS_BROWSER, getDefinitionName } from '../utils/';
|
import { IS_BROWSER, getDefinitionName, compact, isObject, isObjectEmpty } from '../utils/';
|
||||||
import { JsonPointer } from '../utils/JsonPointer';
|
import { JsonPointer } from '../utils/JsonPointer';
|
||||||
|
|
||||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||||
import type { MergedOpenAPISchema } from './types';
|
import type { MergedOpenAPISchema } from './types';
|
||||||
|
import type { OpenAPIExample } from '../types';
|
||||||
|
|
||||||
const MAX_DEREF_DEPTH = 999; // prevent circular detection crashes by adding hard limit on deref depth
|
const MAX_DEREF_DEPTH = 999; // prevent circular detection crashes by adding hard limit on deref depth
|
||||||
|
|
||||||
|
@ -335,6 +336,98 @@ export class OpenAPIParser {
|
||||||
return receiver;
|
return receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
derefSchemaWithExample(
|
||||||
|
schema: MergedOpenAPISchema,
|
||||||
|
example: OpenAPIExample & OpenAPISchema,
|
||||||
|
): OpenAPISchema {
|
||||||
|
const { resolved: resolvedSchema } = this.deref(schema);
|
||||||
|
|
||||||
|
const worker = (
|
||||||
|
currentSchema: MergedOpenAPISchema,
|
||||||
|
currentExample: OpenAPIExample & OpenAPISchema,
|
||||||
|
) => {
|
||||||
|
const receiver: OpenAPISchema = {
|
||||||
|
...currentSchema,
|
||||||
|
};
|
||||||
|
if (isObject(currentSchema.properties)) {
|
||||||
|
receiver.properties = Object.fromEntries(
|
||||||
|
Object.entries(currentSchema.properties).map(([key, value]) => {
|
||||||
|
let resolvedValue: OpenAPISchema = {};
|
||||||
|
const exampleForProp = currentExample?.[key];
|
||||||
|
|
||||||
|
if (Array.isArray(value.allOf) && !isObjectEmpty(exampleForProp)) {
|
||||||
|
resolvedValue = this.mergeAllOf(value, undefined, value['x-refsStack'] || []);
|
||||||
|
} else if (Array.isArray(value.oneOf) && !isObjectEmpty(exampleForProp)) {
|
||||||
|
resolvedValue = this.deref(value.oneOf[0]).resolved;
|
||||||
|
} else if (value.$ref) {
|
||||||
|
resolvedValue = this.deref(value).resolved;
|
||||||
|
} else if ((value.items as OpenAPISchema)?.$ref) {
|
||||||
|
resolvedValue = {
|
||||||
|
...value,
|
||||||
|
items: this.deref(value.items as OpenAPISchema, value.items?.['x-refsStack'] || [])
|
||||||
|
.resolved,
|
||||||
|
};
|
||||||
|
} else if (Array.isArray(value.items)) {
|
||||||
|
resolvedValue = {
|
||||||
|
...value,
|
||||||
|
items: value.items.map((item, i) =>
|
||||||
|
item.properties
|
||||||
|
? worker(item, exampleForProp[i])
|
||||||
|
: this.deref(item, item['x-refsStack'] || []).resolved,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
resolvedValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
resolvedValue.properties &&
|
||||||
|
(!isObjectEmpty(exampleForProp) || exampleForProp.length > 0)
|
||||||
|
) {
|
||||||
|
resolvedValue = worker(resolvedValue, exampleForProp?.[0] ?? exampleForProp);
|
||||||
|
}
|
||||||
|
if ((resolvedValue.items as OpenAPISchema)?.properties && isObject(exampleForProp[0])) {
|
||||||
|
resolvedValue.items = worker(resolvedValue.items as OpenAPISchema, exampleForProp[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isObject(exampleForProp)) {
|
||||||
|
resolvedValue = {
|
||||||
|
...resolvedValue,
|
||||||
|
example: exampleForProp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = compact({
|
||||||
|
const: resolvedValue.const,
|
||||||
|
description: resolvedValue.description,
|
||||||
|
deprecated: resolvedValue.deprecated,
|
||||||
|
enum: resolvedValue.enum,
|
||||||
|
example: resolvedValue.example,
|
||||||
|
exclusiveMinimum: resolvedValue.exclusiveMinimum,
|
||||||
|
format: resolvedValue.format,
|
||||||
|
items: resolvedValue.items,
|
||||||
|
maximum: resolvedValue.maximum,
|
||||||
|
maxLength: resolvedValue.maxLength,
|
||||||
|
minimum: resolvedValue.minimum,
|
||||||
|
minLength: resolvedValue.minLength,
|
||||||
|
pattern: resolvedValue.pattern,
|
||||||
|
properties: resolvedValue.properties,
|
||||||
|
readOnly: resolvedValue.readOnly,
|
||||||
|
type: resolvedValue.type,
|
||||||
|
writeOnly: resolvedValue.writeOnly,
|
||||||
|
xml: resolvedValue.xml,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [key, resolved];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return receiver;
|
||||||
|
};
|
||||||
|
|
||||||
|
return worker(resolvedSchema, example);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all derived definitions among #/components/schemas from any of $refs
|
* Find all derived definitions among #/components/schemas from any of $refs
|
||||||
* returns map of definition pointer to definition name
|
* returns map of definition pointer to definition name
|
||||||
|
|
|
@ -72,5 +72,35 @@ describe('Models', () => {
|
||||||
|
|
||||||
expect(parser.deref(schemaOrRef, [], true)).toMatchSnapshot();
|
expect(parser.deref(schemaOrRef, [], true)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should deref the properties of a schema', () => {
|
||||||
|
const spec = require('./fixtures/properties.json');
|
||||||
|
parser = new OpenAPIParser(spec, undefined, opts);
|
||||||
|
const example = {
|
||||||
|
id: 0,
|
||||||
|
category: {
|
||||||
|
id: 0,
|
||||||
|
name: 'string',
|
||||||
|
sub: {
|
||||||
|
prop1: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'Guru',
|
||||||
|
photoUrls: ['string'],
|
||||||
|
friend: {},
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: 'available',
|
||||||
|
petType: 'string',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parser.derefSchemaWithExample(spec.components.schemas.test, example),
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,131 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Models Schema should deref the properties of a schema 1`] = `
|
||||||
|
Object {
|
||||||
|
"discriminator": Object {
|
||||||
|
"mapping": Object {
|
||||||
|
"bee": "#/components/schemas/HoneyBee",
|
||||||
|
"cat": "#/components/schemas/Cat",
|
||||||
|
"dog": "#/components/schemas/Dog",
|
||||||
|
},
|
||||||
|
"propertyName": "petType",
|
||||||
|
},
|
||||||
|
"properties": Object {
|
||||||
|
"category": Object {
|
||||||
|
"description": "Categories this pet belongs to",
|
||||||
|
"properties": Object {
|
||||||
|
"id": Object {
|
||||||
|
"description": "Category ID",
|
||||||
|
"example": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"readOnly": true,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"name": Object {
|
||||||
|
"description": "Category name",
|
||||||
|
"example": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"sub": Object {
|
||||||
|
"description": "Test Sub Category",
|
||||||
|
"properties": Object {
|
||||||
|
"prop1": Object {
|
||||||
|
"description": "Dumb Property",
|
||||||
|
"example": "string",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"xml": Object {
|
||||||
|
"name": "Category",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"friend": Object {},
|
||||||
|
"id": Object {
|
||||||
|
"description": "Pet ID",
|
||||||
|
"example": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"readOnly": true,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"name": Object {
|
||||||
|
"description": "The name given to a pet",
|
||||||
|
"example": "Guru",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"petType": Object {
|
||||||
|
"description": "Type of a pet",
|
||||||
|
"example": "string",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"photoUrls": Object {
|
||||||
|
"description": "The list of URL to a cute photos featuring pet",
|
||||||
|
"items": Object {
|
||||||
|
"format": "url",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"xml": Object {
|
||||||
|
"name": "photoUrl",
|
||||||
|
"wrapped": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": Object {
|
||||||
|
"description": "Pet status in the store",
|
||||||
|
"enum": Array [
|
||||||
|
"available",
|
||||||
|
"pending",
|
||||||
|
"sold",
|
||||||
|
],
|
||||||
|
"example": "available",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"tags": Object {
|
||||||
|
"description": "Tags attached to the pet",
|
||||||
|
"items": Object {
|
||||||
|
"properties": Object {
|
||||||
|
"id": Object {
|
||||||
|
"description": "Tag ID",
|
||||||
|
"example": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"readOnly": true,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"name": Object {
|
||||||
|
"description": "Tag name",
|
||||||
|
"example": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"xml": Object {
|
||||||
|
"name": "Tag",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"xml": Object {
|
||||||
|
"name": "tag",
|
||||||
|
"wrapped": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"name",
|
||||||
|
"photoUrls",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"xml": Object {
|
||||||
|
"name": "Pet",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Models Schema should hoist oneOfs when mergin allOf 1`] = `
|
exports[`Models Schema should hoist oneOfs when mergin allOf 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"oneOf": Array [
|
"oneOf": Array [
|
||||||
|
|
234
src/services/__tests__/fixtures/properties.json
Normal file
234
src/services/__tests__/fixtures/properties.json
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"version": "1.0",
|
||||||
|
"title": "Foo"
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"test": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "photoUrls"],
|
||||||
|
"discriminator": {
|
||||||
|
"propertyName": "petType",
|
||||||
|
"mapping": {
|
||||||
|
"cat": "#/components/schemas/Cat",
|
||||||
|
"dog": "#/components/schemas/Dog",
|
||||||
|
"bee": "#/components/schemas/HoneyBee"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"externalDocs": {
|
||||||
|
"description": "Find more info here",
|
||||||
|
"url": "https://example.com"
|
||||||
|
},
|
||||||
|
"description": "Pet ID",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"description": "Categories this pet belongs to",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Category"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name given to a pet",
|
||||||
|
"type": "string",
|
||||||
|
"example": "Guru"
|
||||||
|
},
|
||||||
|
"photoUrls": {
|
||||||
|
"description": "The list of URL to a cute photos featuring pet",
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 20,
|
||||||
|
"xml": {
|
||||||
|
"name": "photoUrl",
|
||||||
|
"wrapped": true
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"friend": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Pet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"description": "Tags attached to the pet",
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"xml": {
|
||||||
|
"name": "tag",
|
||||||
|
"wrapped": true
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Tag"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Pet status in the store",
|
||||||
|
"enum": ["available", "pending", "sold"]
|
||||||
|
},
|
||||||
|
"petType": {
|
||||||
|
"description": "Type of a pet",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "Pet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Category": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Category ID",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "Category name",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"sub": {
|
||||||
|
"description": "Test Sub Category",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"prop1": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Dumb Property"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "Category"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
|
"Pet": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "photoUrls"],
|
||||||
|
"discriminator": {
|
||||||
|
"propertyName": "petType",
|
||||||
|
"mapping": {
|
||||||
|
"cat": "#/components/schemas/Cat",
|
||||||
|
"dog": "#/components/schemas/Dog",
|
||||||
|
"bee": "#/components/schemas/HoneyBee"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"externalDocs": {
|
||||||
|
"description": "Find more info here",
|
||||||
|
"url": "https://example.com"
|
||||||
|
},
|
||||||
|
"description": "Pet ID",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"description": "Categories this pet belongs to",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Category"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name given to a pet",
|
||||||
|
"type": "string",
|
||||||
|
"example": "Guru"
|
||||||
|
},
|
||||||
|
"photoUrls": {
|
||||||
|
"description": "The list of URL to a cute photos featuring pet",
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 20,
|
||||||
|
"xml": {
|
||||||
|
"name": "photoUrl",
|
||||||
|
"wrapped": true
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"friend": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Pet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"description": "Tags attached to the pet",
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"xml": {
|
||||||
|
"name": "tag",
|
||||||
|
"wrapped": true
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Tag"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Pet status in the store",
|
||||||
|
"enum": ["available", "pending", "sold"]
|
||||||
|
},
|
||||||
|
"petType": {
|
||||||
|
"description": "Type of a pet",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "Pet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Tag": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Tag ID",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "Tag name",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xml": {
|
||||||
|
"name": "Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,12 @@ import type { OpenAPIMediaType } from '../../types';
|
||||||
import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||||
import { SchemaModel } from './Schema';
|
import { SchemaModel } from './Schema';
|
||||||
|
|
||||||
import { isJsonLike, mapValues } from '../../utils';
|
import { isJsonLike, isXml, mapValues } from '../../utils';
|
||||||
import type { OpenAPIParser } from '../OpenAPIParser';
|
import type { OpenAPIParser } from '../OpenAPIParser';
|
||||||
import { ExampleModel } from './Example';
|
import { ExampleModel } from './Example';
|
||||||
|
import { ConfigAccessOptions, FinalExamples, generateXmlExample } from '../../utils/xml';
|
||||||
|
import { MergedOpenAPISchema } from '../types';
|
||||||
|
import { OpenAPIExample, Referenced } from '../../types';
|
||||||
|
|
||||||
export class MediaTypeModel {
|
export class MediaTypeModel {
|
||||||
examples?: { [name: string]: ExampleModel };
|
examples?: { [name: string]: ExampleModel };
|
||||||
|
@ -45,7 +48,7 @@ export class MediaTypeModel {
|
||||||
info.encoding,
|
info.encoding,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isJsonLike(name)) {
|
} else if (isJsonLike(name) || isXml(name)) {
|
||||||
this.generateExample(parser, info);
|
this.generateExample(parser, info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +60,8 @@ export class MediaTypeModel {
|
||||||
skipNonRequired: this.isRequestType && this.onlyRequiredInSamples,
|
skipNonRequired: this.isRequestType && this.onlyRequiredInSamples,
|
||||||
maxSampleDepth: this.generatedPayloadSamplesMaxDepth,
|
maxSampleDepth: this.generatedPayloadSamplesMaxDepth,
|
||||||
};
|
};
|
||||||
if (this.schema && this.schema.oneOf) {
|
if (this.schema) {
|
||||||
|
if (this.schema.oneOf) {
|
||||||
this.examples = {};
|
this.examples = {};
|
||||||
for (const subSchema of this.schema.oneOf) {
|
for (const subSchema of this.schema.oneOf) {
|
||||||
const sample = Sampler.sample(subSchema.rawSchema as any, samplerOptions, parser.spec);
|
const sample = Sampler.sample(subSchema.rawSchema as any, samplerOptions, parser.spec);
|
||||||
|
@ -74,13 +78,37 @@ export class MediaTypeModel {
|
||||||
this.name,
|
this.name,
|
||||||
info.encoding,
|
info.encoding,
|
||||||
);
|
);
|
||||||
|
if (isXml(this.name)) {
|
||||||
|
const xmlExamples = this.resolveXmlExample(parser, sample as OpenAPIExample);
|
||||||
|
this.examples[subSchema.title].value = xmlExamples[0]?.exampleValue;
|
||||||
}
|
}
|
||||||
} else if (this.schema) {
|
}
|
||||||
|
} else {
|
||||||
|
const infoOrRef: Referenced<OpenAPIExample> = {
|
||||||
|
value: Sampler.sample(info.schema as any, samplerOptions, parser.spec),
|
||||||
|
};
|
||||||
|
const xmlExamples = this.resolveXmlExample(parser, infoOrRef.value);
|
||||||
|
|
||||||
|
if (xmlExamples.length > 1) {
|
||||||
|
this.examples = Object.fromEntries(
|
||||||
|
xmlExamples.map(item => [
|
||||||
|
item.exampleId,
|
||||||
|
new ExampleModel(
|
||||||
|
parser,
|
||||||
|
{
|
||||||
|
value: item.exampleValue,
|
||||||
|
},
|
||||||
|
this.name,
|
||||||
|
info.encoding,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
this.examples = {
|
this.examples = {
|
||||||
default: new ExampleModel(
|
default: new ExampleModel(
|
||||||
parser,
|
parser,
|
||||||
{
|
{
|
||||||
value: Sampler.sample(info.schema as any, samplerOptions, parser.spec),
|
value: xmlExamples[0]?.exampleValue || infoOrRef.value,
|
||||||
},
|
},
|
||||||
this.name,
|
this.name,
|
||||||
info.encoding,
|
info.encoding,
|
||||||
|
@ -89,3 +117,35 @@ export class MediaTypeModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveXmlExample(parser: OpenAPIParser, sample: OpenAPIExample) {
|
||||||
|
const configAccessOptions: ConfigAccessOptions = {
|
||||||
|
includeReadOnly: !this.isRequestType,
|
||||||
|
includeWriteOnly: this.isRequestType,
|
||||||
|
};
|
||||||
|
const subSchema = this.schema?.schema;
|
||||||
|
let xmlExamples: FinalExamples[] = [];
|
||||||
|
if (subSchema && isXml(this.name)) {
|
||||||
|
let resolved;
|
||||||
|
if (subSchema.items) {
|
||||||
|
resolved = {
|
||||||
|
...subSchema,
|
||||||
|
items: parser.derefSchemaWithExample(
|
||||||
|
subSchema.items as MergedOpenAPISchema,
|
||||||
|
Array.isArray(sample) ? sample[0] : sample,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
resolved = parser.derefSchemaWithExample(subSchema, sample);
|
||||||
|
}
|
||||||
|
xmlExamples = generateXmlExample({
|
||||||
|
includeReadOnly: configAccessOptions?.includeReadOnly,
|
||||||
|
includeWriteOnly: configAccessOptions?.includeWriteOnly,
|
||||||
|
schema: resolved,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmlExamples;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -112,6 +112,19 @@ export interface OpenAPIExample {
|
||||||
externalValue?: string;
|
externalValue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface XMLObject {
|
||||||
|
/** Replaces the name of the element/attribute used for the described schema property. When defined within `items`, it will affect the name of the individual XML elements within the list. When defined alongside `type` being `array` (outside the `items`), it will affect the wrapping element and only if `wrapped` is `true`. If `wrapped` is `false`, it will be ignored. */
|
||||||
|
name?: string;
|
||||||
|
/** The URI of the namespace definition. This MUST be in the form of an absolute URI. */
|
||||||
|
namespace?: string;
|
||||||
|
/** The prefix to be used for the name. */
|
||||||
|
prefix?: string;
|
||||||
|
/** Declares whether the property definition translates to an attribute instead of an element. Default value is `false`. */
|
||||||
|
attribute?: boolean;
|
||||||
|
/** MAY be used only for an array definition. Signifies whether the array is wrapped (for example, `<books><book/><book/></books>`) or unwrapped (`<book/><book/>`). Default value is `false`. The definition takes effect only when defined alongside `type` being `array` (outside the `items`). */
|
||||||
|
wrapped?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OpenAPISchema {
|
export interface OpenAPISchema {
|
||||||
$ref?: string;
|
$ref?: string;
|
||||||
type?: string | string[];
|
type?: string | string[];
|
||||||
|
@ -161,6 +174,7 @@ export interface OpenAPISchema {
|
||||||
contentMediaType?: string;
|
contentMediaType?: string;
|
||||||
prefixItems?: OpenAPISchema[];
|
prefixItems?: OpenAPISchema[];
|
||||||
additionalItems?: OpenAPISchema | boolean;
|
additionalItems?: OpenAPISchema | boolean;
|
||||||
|
xml?: XMLObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAPIDiscriminator {
|
export interface OpenAPIDiscriminator {
|
||||||
|
|
167
src/utils/__tests__/__snapshots__/xml.test.ts.snap
Normal file
167
src/utils/__tests__/__snapshots__/xml.test.ts.snap
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml example with a complex list and each property has an example: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<root>
|
||||||
|
<foo>
|
||||||
|
<foo>this is foo</foo>
|
||||||
|
<bar>this is bar</bar>
|
||||||
|
</foo>
|
||||||
|
<bar>
|
||||||
|
<foo>this is foo2</foo>
|
||||||
|
<bar>this is bar2</bar>
|
||||||
|
</bar>
|
||||||
|
</root>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml example with a list: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<root>
|
||||||
|
<foo>string</foo>
|
||||||
|
<bar>string</bar>
|
||||||
|
</root>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml example with readOnly and writeOnly: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<root>
|
||||||
|
<foo>string</foo>
|
||||||
|
<bar>string</bar>
|
||||||
|
</root>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml example with readOnly: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<root>
|
||||||
|
<foo>string</foo>
|
||||||
|
<bar>string</bar>
|
||||||
|
</root>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml example with writeOnly: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<root>
|
||||||
|
<foo>string</foo>
|
||||||
|
<bar>string</bar>
|
||||||
|
</root>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml example with xml attributes: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<root>
|
||||||
|
<Pet>
|
||||||
|
<id>0</id>
|
||||||
|
<Category>
|
||||||
|
<id>0</id>
|
||||||
|
<name>string</name>
|
||||||
|
<sub>
|
||||||
|
<prop1>string</prop1>
|
||||||
|
</sub>
|
||||||
|
</Category>
|
||||||
|
<name>Guru</name>
|
||||||
|
<photoUrl>
|
||||||
|
<photoUrl>http://example.com</photoUrl>
|
||||||
|
</photoUrl>
|
||||||
|
<friend>
|
||||||
|
</friend>
|
||||||
|
<tag>
|
||||||
|
<Tag>
|
||||||
|
<id>0</id>
|
||||||
|
<name>string</name>
|
||||||
|
</Tag>
|
||||||
|
</tag>
|
||||||
|
<status>available</status>
|
||||||
|
<petType>string</petType>
|
||||||
|
</Pet>
|
||||||
|
</root>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml example: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<root>
|
||||||
|
<foo>string</foo>
|
||||||
|
<bar>string</bar>
|
||||||
|
</root>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml for schemas with an array of items: example-0 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<User>
|
||||||
|
<id>0</id>
|
||||||
|
<Pet>
|
||||||
|
<id>0</id>
|
||||||
|
<Category>
|
||||||
|
<id>0</id>
|
||||||
|
<name>string</name>
|
||||||
|
<sub>
|
||||||
|
<prop1>string</prop1>
|
||||||
|
</sub>
|
||||||
|
</Category>
|
||||||
|
<name>Guru</name>
|
||||||
|
<photoUrl>
|
||||||
|
<photoUrl>http://example.com</photoUrl>
|
||||||
|
</photoUrl>
|
||||||
|
<friend>
|
||||||
|
</friend>
|
||||||
|
<tag>
|
||||||
|
<Tag>
|
||||||
|
<id>0</id>
|
||||||
|
<name>string</name>
|
||||||
|
</Tag>
|
||||||
|
</tag>
|
||||||
|
<status>available</status>
|
||||||
|
<petType>string</petType>
|
||||||
|
</Pet>
|
||||||
|
<username>John78</username>
|
||||||
|
<firstName>John</firstName>
|
||||||
|
<lastName>Smith</lastName>
|
||||||
|
<email>john.smith@example.com</email>
|
||||||
|
<password>drowssaP123</password>
|
||||||
|
<phone>+1-202-555-0192</phone>
|
||||||
|
<userStatus>0</userStatus>
|
||||||
|
<addresses>
|
||||||
|
<city>string</city>
|
||||||
|
<country>string</country>
|
||||||
|
<street>string</street>
|
||||||
|
</addresses>
|
||||||
|
</User>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateXmlExample should generate xml for schemas with an array of items: example-1 1`] = `
|
||||||
|
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
||||||
|
<User>
|
||||||
|
<id>0</id>
|
||||||
|
<Pet>
|
||||||
|
<id>0</id>
|
||||||
|
<Category>
|
||||||
|
<id>0</id>
|
||||||
|
<name>string</name>
|
||||||
|
<sub>
|
||||||
|
<prop1>string</prop1>
|
||||||
|
</sub>
|
||||||
|
</Category>
|
||||||
|
<name>Guru</name>
|
||||||
|
<photoUrl>
|
||||||
|
<photoUrl>http://example.com</photoUrl>
|
||||||
|
</photoUrl>
|
||||||
|
<friend>
|
||||||
|
</friend>
|
||||||
|
<tag>
|
||||||
|
<Tag>
|
||||||
|
<id>0</id>
|
||||||
|
<name>string</name>
|
||||||
|
</Tag>
|
||||||
|
</tag>
|
||||||
|
<status>available</status>
|
||||||
|
<petType>string</petType>
|
||||||
|
</Pet>
|
||||||
|
<username>John78</username>
|
||||||
|
<firstName>John</firstName>
|
||||||
|
<lastName>Smith</lastName>
|
||||||
|
<email>john.smith@example.com</email>
|
||||||
|
<password>drowssaP123</password>
|
||||||
|
<phone>+1-202-555-0192</phone>
|
||||||
|
<userStatus>0</userStatus>
|
||||||
|
<addresses>0</addresses>
|
||||||
|
</User>"
|
||||||
|
`;
|
|
@ -1,4 +1,4 @@
|
||||||
import { objectHas, objectSet } from '../object';
|
import { compact, objectHas, objectSet } from '../object';
|
||||||
|
|
||||||
describe('object utils', () => {
|
describe('object utils', () => {
|
||||||
let obj;
|
let obj;
|
||||||
|
@ -48,3 +48,23 @@ describe('object utils', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('compact', () => {
|
||||||
|
const obj = {
|
||||||
|
foo: 'bar',
|
||||||
|
bar: null,
|
||||||
|
cool: undefined,
|
||||||
|
test: '',
|
||||||
|
};
|
||||||
|
const obj2 = {
|
||||||
|
foo: 'bar',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should strip away nullish values from the object', () => {
|
||||||
|
expect(compact(obj)).toMatchObject(obj2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same object if there is nothing to compact', () => {
|
||||||
|
expect(compact(obj2)).toMatchObject(obj2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
497
src/utils/__tests__/xml.test.ts
Normal file
497
src/utils/__tests__/xml.test.ts
Normal file
|
@ -0,0 +1,497 @@
|
||||||
|
import { generateXmlExample } from '../xml';
|
||||||
|
|
||||||
|
describe('generateXmlExample', () => {
|
||||||
|
it('should generate xml example', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
includeReadOnly: false,
|
||||||
|
includeWriteOnly: false,
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate xml example with readOnly', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
includeReadOnly: true,
|
||||||
|
includeWriteOnly: false,
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'string',
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate xml example with writeOnly', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
includeReadOnly: false,
|
||||||
|
includeWriteOnly: true,
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'string',
|
||||||
|
writeOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate xml example with readOnly and writeOnly', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
includeReadOnly: true,
|
||||||
|
includeWriteOnly: true,
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'string',
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'string',
|
||||||
|
writeOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate xml example with a list', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
includeReadOnly: false,
|
||||||
|
includeWriteOnly: false,
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate xml example with a complex list and each property has an example', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
includeReadOnly: false,
|
||||||
|
includeWriteOnly: false,
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'this is foo',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'this is bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'this is foo2',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'this is bar2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate xml example with xml attributes', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
includeReadOnly: true,
|
||||||
|
includeWriteOnly: true,
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
maxItems: 999,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'photoUrls'],
|
||||||
|
discriminator: {
|
||||||
|
propertyName: 'petType',
|
||||||
|
mapping: {
|
||||||
|
cat: '#/components/schemas/Cat',
|
||||||
|
dog: '#/components/schemas/Dog',
|
||||||
|
bee: '#/components/schemas/HoneyBee',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
description: 'Pet ID',
|
||||||
|
example: 0,
|
||||||
|
format: 'int64',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
description: 'Categories this pet belongs to',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
description: 'Category ID',
|
||||||
|
example: 0,
|
||||||
|
format: 'int64',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
description: 'Category name',
|
||||||
|
example: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
sub: {
|
||||||
|
description: 'Test Sub Category',
|
||||||
|
properties: {
|
||||||
|
prop1: {
|
||||||
|
description: 'Dumb Property',
|
||||||
|
example: 'string',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
xml: {
|
||||||
|
name: 'Category',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
description: 'The name given to a pet',
|
||||||
|
example: 'Guru',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
photoUrls: {
|
||||||
|
description: 'The list of URL to a cute photos featuring pet',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'url',
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
xml: {
|
||||||
|
name: 'photoUrl',
|
||||||
|
wrapped: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
friend: {},
|
||||||
|
tags: {
|
||||||
|
description: 'Tags attached to the pet',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
description: 'Tag ID',
|
||||||
|
example: 0,
|
||||||
|
format: 'int64',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
description: 'Tag name',
|
||||||
|
example: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xml: {
|
||||||
|
name: 'Tag',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
xml: {
|
||||||
|
name: 'tag',
|
||||||
|
wrapped: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
description: 'Pet status in the store',
|
||||||
|
enum: ['available', 'pending', 'sold'],
|
||||||
|
example: 'available',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
petType: {
|
||||||
|
description: 'Type of a pet',
|
||||||
|
example: 'string',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xml: {
|
||||||
|
name: 'Pet',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate xml for schemas with an array of items', () => {
|
||||||
|
const examples = generateXmlExample({
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
example: 0,
|
||||||
|
format: 'int64',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
pet: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
description: 'Pet ID',
|
||||||
|
example: 0,
|
||||||
|
format: 'int64',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
description: 'Categories this pet belongs to',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
description: 'Category ID',
|
||||||
|
example: 0,
|
||||||
|
format: 'int64',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
description: 'Category name',
|
||||||
|
example: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
sub: {
|
||||||
|
description: 'Test Sub Category',
|
||||||
|
properties: {
|
||||||
|
prop1: {
|
||||||
|
description: 'Dumb Property',
|
||||||
|
example: 'string',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
xml: {
|
||||||
|
name: 'Category',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
description: 'The name given to a pet',
|
||||||
|
example: 'Guru',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
photoUrls: {
|
||||||
|
description: 'The list of URL to a cute photos featuring pet',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'url',
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
xml: {
|
||||||
|
name: 'photoUrl',
|
||||||
|
wrapped: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
friend: {},
|
||||||
|
tags: {
|
||||||
|
description: 'Tags attached to the pet',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
description: 'Tag ID',
|
||||||
|
example: 0,
|
||||||
|
format: 'int64',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
description: 'Tag name',
|
||||||
|
example: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xml: {
|
||||||
|
name: 'Tag',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
xml: {
|
||||||
|
name: 'tag',
|
||||||
|
wrapped: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
description: 'Pet status in the store',
|
||||||
|
enum: ['available', 'pending', 'sold'],
|
||||||
|
example: 'available',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
petType: {
|
||||||
|
description: 'Type of a pet',
|
||||||
|
example: 'string',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
xml: {
|
||||||
|
name: 'Pet',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
description: 'User supplied username',
|
||||||
|
example: 'John78',
|
||||||
|
minLength: 4,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
firstName: {
|
||||||
|
description: 'User first name',
|
||||||
|
example: 'John',
|
||||||
|
minLength: 1,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
description: 'User last name',
|
||||||
|
example: 'Smith',
|
||||||
|
minLength: 1,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
description: 'User email address',
|
||||||
|
example: 'john.smith@example.com',
|
||||||
|
format: 'email',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
description:
|
||||||
|
'User password, MUST contain a mix of upper and lower case letters, as well as digits',
|
||||||
|
example: 'drowssaP123',
|
||||||
|
format: 'password',
|
||||||
|
minLength: 8,
|
||||||
|
pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
description: 'User phone number in international format',
|
||||||
|
example: '+1-202-555-0192',
|
||||||
|
pattern: '/^\\+(?:[0-9]-?){6,14}[0-9]$/',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
userStatus: {
|
||||||
|
description: 'User status',
|
||||||
|
example: 0,
|
||||||
|
format: 'int32',
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
addresses: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
city: {
|
||||||
|
example: 'string',
|
||||||
|
minLength: 0,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
example: 'string',
|
||||||
|
minLength: 0,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
street: {
|
||||||
|
description: 'includes build/apartment number',
|
||||||
|
example: 'string',
|
||||||
|
minLength: 0,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
maxLength: 10,
|
||||||
|
type: 'array',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xml: {
|
||||||
|
name: 'User',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
examples.forEach(example => {
|
||||||
|
expect(example.exampleValue).toMatchSnapshot(example.exampleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,3 +9,4 @@ export * from './decorators';
|
||||||
export * from './debug';
|
export * from './debug';
|
||||||
export * from './memoize';
|
export * from './memoize';
|
||||||
export * from './sort';
|
export * from './sort';
|
||||||
|
export * from './object';
|
||||||
|
|
|
@ -26,3 +26,22 @@ export function objectSet(object: object, path: string | Array<string>, value: a
|
||||||
const key = _path[limit];
|
const key = _path[limit];
|
||||||
object[key] = value;
|
object[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isObjectEmpty = (obj: object): boolean =>
|
||||||
|
!!obj && Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||||
|
|
||||||
|
const emptyValues = new Set([undefined, 'undefined', null, 'null', NaN, 'NaN', '']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out falsy / empty values from an object
|
||||||
|
*/
|
||||||
|
export const compact = (toCompact: object): object => {
|
||||||
|
const removeEmpty = (obj: object) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(obj)
|
||||||
|
.filter(([, v]) => !emptyValues.has(v))
|
||||||
|
.map(([k, v]) => [k, typeof v === 'object' && !Array.isArray(v) ? removeEmpty(v) : v]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return removeEmpty(toCompact);
|
||||||
|
};
|
||||||
|
|
|
@ -168,6 +168,10 @@ export function isFormUrlEncoded(contentType: string): boolean {
|
||||||
return contentType === 'application/x-www-form-urlencoded';
|
return contentType === 'application/x-www-form-urlencoded';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isXml = (contentType: string): boolean => {
|
||||||
|
return contentType === 'application/xml';
|
||||||
|
};
|
||||||
|
|
||||||
function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string {
|
function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string {
|
||||||
if (isArray(fieldVal)) {
|
if (isArray(fieldVal)) {
|
||||||
return fieldVal.map(v => v.toString()).join(delimiter);
|
return fieldVal.map(v => v.toString()).join(delimiter);
|
||||||
|
|
573
src/utils/xml.ts
Normal file
573
src/utils/xml.ts
Normal file
|
@ -0,0 +1,573 @@
|
||||||
|
import { MergedOpenAPISchema } from '../services';
|
||||||
|
import { OpenAPISchema } from '../types';
|
||||||
|
|
||||||
|
export interface ConfigAccessOptions {
|
||||||
|
includeReadOnly?: boolean;
|
||||||
|
includeWriteOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExampleConfig extends ConfigAccessOptions {
|
||||||
|
includeDeprecated?: boolean;
|
||||||
|
useXmlTagForProp?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateExampleProps extends ConfigAccessOptions {
|
||||||
|
schema: MergedOpenAPISchema | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinalExamples {
|
||||||
|
exampleDescription: string;
|
||||||
|
exampleId: string;
|
||||||
|
exampleSummary: string;
|
||||||
|
exampleValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* json2xml
|
||||||
|
* @example
|
||||||
|
* Schema: {
|
||||||
|
* 'prop1' : 'one',
|
||||||
|
* 'prop2' : 'two',
|
||||||
|
* 'prop3' : [ 'a', 'b', 'c' ],
|
||||||
|
* 'prop4' : {
|
||||||
|
* 'ob1' : 'val-1',
|
||||||
|
* 'ob2' : 'val-2'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* XML:
|
||||||
|
* <root>
|
||||||
|
* <prop1>simple</prop1>
|
||||||
|
* <prop2>
|
||||||
|
* <0>a</0>
|
||||||
|
* <1>b</1>
|
||||||
|
* <2>c</2>
|
||||||
|
* </prop2>
|
||||||
|
* <prop3>
|
||||||
|
* <ob1>val-1</ob1>
|
||||||
|
* <ob2>val-2</ob2>
|
||||||
|
* </prop3>
|
||||||
|
* </root>
|
||||||
|
**/
|
||||||
|
const json2xml = (obj: any, level: number = 1): string => {
|
||||||
|
const indent = ' '.repeat(level);
|
||||||
|
let xmlText = '';
|
||||||
|
if (level === 1 && typeof obj !== 'object') {
|
||||||
|
return `\n${indent}${obj.toString()}`;
|
||||||
|
}
|
||||||
|
for (const prop in obj) {
|
||||||
|
const tagNameOrProp = obj[prop]?.['::XML_TAG'] || prop;
|
||||||
|
let tagName = '';
|
||||||
|
if (Array.isArray(obj[prop])) {
|
||||||
|
tagName = tagNameOrProp[0]?.['::XML_TAG'] || `${prop}`;
|
||||||
|
} else {
|
||||||
|
tagName = tagNameOrProp;
|
||||||
|
}
|
||||||
|
if (prop.startsWith('::')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj[prop])) {
|
||||||
|
xmlText = `${xmlText}\n${indent}<${tagName}>${json2xml(
|
||||||
|
obj[prop],
|
||||||
|
level + 1,
|
||||||
|
)}\n${indent}</${tagName}>`;
|
||||||
|
} else if (typeof obj[prop] === 'object') {
|
||||||
|
xmlText = `${xmlText}\n${indent}<${tagName}>${json2xml(
|
||||||
|
obj[prop],
|
||||||
|
level + 1,
|
||||||
|
)}\n${indent}</${tagName}>`;
|
||||||
|
} else {
|
||||||
|
xmlText = `${xmlText}\n${indent}<${tagName}>${obj[prop].toString()}</${tagName}>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return xmlText;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergePropertyExamples = (
|
||||||
|
obj: { [x: string]: any },
|
||||||
|
propertyName: string,
|
||||||
|
propExamples: never[],
|
||||||
|
) => {
|
||||||
|
// Create an example for each variant of the propertyExample, merging them with the current (parent) example
|
||||||
|
let i = 0;
|
||||||
|
const maxCombinations = 10;
|
||||||
|
const mergedObj = {};
|
||||||
|
for (const exampleKey in obj) {
|
||||||
|
for (const propExampleKey in propExamples) {
|
||||||
|
mergedObj[`example-${i}`] = { ...obj[exampleKey] };
|
||||||
|
mergedObj[`example-${i}`][propertyName] = propExamples[propExampleKey];
|
||||||
|
i++;
|
||||||
|
if (i >= maxCombinations) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i >= maxCombinations) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedObj;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSchemaInfoToExample = (schema: OpenAPISchema, obj: any) => {
|
||||||
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (schema.title) {
|
||||||
|
obj['::TITLE'] = schema.title;
|
||||||
|
}
|
||||||
|
if (schema.description) {
|
||||||
|
obj['::DESCRIPTION'] = schema.description;
|
||||||
|
}
|
||||||
|
if (schema.xml?.name) {
|
||||||
|
obj['::XML_TAG'] = schema.xml?.name;
|
||||||
|
}
|
||||||
|
if (schema.xml?.wrapped) {
|
||||||
|
obj['::XML_WRAP'] = schema.xml?.wrapped.toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPropertyExampleToObjectExamples = (example: any, obj: object, propertyKey: string) => {
|
||||||
|
for (const key in obj) {
|
||||||
|
obj[key][propertyKey] = example;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleValueByType = (schemaObj: OpenAPISchema) => {
|
||||||
|
const example = schemaObj.examples
|
||||||
|
? schemaObj.examples[0]
|
||||||
|
: schemaObj.example === null
|
||||||
|
? null
|
||||||
|
: schemaObj.example || undefined;
|
||||||
|
if (example === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (example === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (example === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (example === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (example instanceof Date) {
|
||||||
|
switch (schemaObj.format?.toLowerCase()) {
|
||||||
|
case 'date':
|
||||||
|
return example.toISOString().split('T')[0];
|
||||||
|
case 'time':
|
||||||
|
return example.toISOString().split('T')[1];
|
||||||
|
default:
|
||||||
|
return example.toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (example) {
|
||||||
|
return example;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(schemaObj).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (schemaObj.$ref) {
|
||||||
|
// Indicates a Circular ref
|
||||||
|
return schemaObj.$ref;
|
||||||
|
}
|
||||||
|
if (schemaObj.const === null || schemaObj.const === '') {
|
||||||
|
return schemaObj.const;
|
||||||
|
}
|
||||||
|
if (schemaObj.const) {
|
||||||
|
return schemaObj.const;
|
||||||
|
}
|
||||||
|
const typeValue = Array.isArray(schemaObj.type) ? schemaObj.type[0] : schemaObj.type;
|
||||||
|
if (!typeValue) {
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
if (typeValue.match(/^integer|^number/g)) {
|
||||||
|
const multipleOf = Number.isNaN(Number(schemaObj.multipleOf))
|
||||||
|
? undefined
|
||||||
|
: Number(schemaObj.multipleOf);
|
||||||
|
const maximum = Number.isNaN(Number(schemaObj.maximum)) ? undefined : Number(schemaObj.maximum);
|
||||||
|
const minimumPossibleVal = Number.isNaN(Number(schemaObj.minimum))
|
||||||
|
? Number.isNaN(Number(schemaObj.exclusiveMinimum))
|
||||||
|
? maximum || 0
|
||||||
|
: Number(schemaObj.exclusiveMinimum) + (typeValue.startsWith('integer') ? 1 : 0.001)
|
||||||
|
: Number(schemaObj.minimum);
|
||||||
|
return multipleOf
|
||||||
|
? multipleOf >= minimumPossibleVal
|
||||||
|
? multipleOf
|
||||||
|
: minimumPossibleVal % multipleOf === 0
|
||||||
|
? minimumPossibleVal
|
||||||
|
: Math.ceil(minimumPossibleVal / multipleOf) * multipleOf
|
||||||
|
: minimumPossibleVal;
|
||||||
|
}
|
||||||
|
if (typeValue.match(/^boolean/g)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeValue.match(/^null/g)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeValue.match(/^string/g)) {
|
||||||
|
if (schemaObj.enum) {
|
||||||
|
return schemaObj.enum[0];
|
||||||
|
}
|
||||||
|
if (schemaObj.const) {
|
||||||
|
return schemaObj.const;
|
||||||
|
}
|
||||||
|
if (schemaObj.pattern) {
|
||||||
|
return schemaObj.pattern;
|
||||||
|
}
|
||||||
|
if (schemaObj.format) {
|
||||||
|
const u = `${Date.now().toString(16)}${Math.random().toString(16)}0`.repeat(16);
|
||||||
|
switch (schemaObj.format.toLowerCase()) {
|
||||||
|
case 'url':
|
||||||
|
case 'uri':
|
||||||
|
return 'http://example.com';
|
||||||
|
case 'date':
|
||||||
|
return new Date(0).toISOString().split('T')[0];
|
||||||
|
case 'time':
|
||||||
|
return new Date(0).toISOString().split('T')[1];
|
||||||
|
case 'date-time':
|
||||||
|
return new Date(0).toISOString();
|
||||||
|
case 'duration':
|
||||||
|
return 'P3Y6M4DT12H30M5S'; // P=Period 3-Years 6-Months 4-Days 12-Hours 30-Minutes 5-Seconds
|
||||||
|
case 'email':
|
||||||
|
case 'idn-email':
|
||||||
|
return 'user@example.com';
|
||||||
|
case 'hostname':
|
||||||
|
case 'idn-hostname':
|
||||||
|
return 'www.example.com';
|
||||||
|
case 'ipv4':
|
||||||
|
return '198.51.100.42';
|
||||||
|
case 'ipv6':
|
||||||
|
return '2001:0db8:5b96:0000:0000:426f:8e17:642a';
|
||||||
|
case 'uuid':
|
||||||
|
return [
|
||||||
|
u.substr(0, 8),
|
||||||
|
u.substr(8, 4),
|
||||||
|
`4000-8${u.substr(13, 3)}`,
|
||||||
|
u.substr(16, 12),
|
||||||
|
].join('-');
|
||||||
|
case 'byte':
|
||||||
|
return 'ZXhhbXBsZQ=='; // 'example' base64 encoded. See https://spec.openapis.org/oas/v3.0.0#data-types
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const minLength = Number.isNaN(schemaObj.minLength) ? undefined : Number(schemaObj.minLength);
|
||||||
|
const maxLength = Number.isNaN(schemaObj.maxLength) ? undefined : Number(schemaObj.maxLength);
|
||||||
|
const finalLength = minLength || (maxLength && maxLength > 6 ? 6 : maxLength || undefined);
|
||||||
|
return finalLength ? 'A'.repeat(finalLength) : 'string';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If type cannot be determined
|
||||||
|
return '?';
|
||||||
|
};
|
||||||
|
|
||||||
|
/* For changing JSON-Schema to a Sample Object, as per the schema (to generate examples based on schema) */
|
||||||
|
const schemaToSampleObj = (schema: OpenAPISchema, config: ExampleConfig = {}) => {
|
||||||
|
let obj = {};
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (schema.allOf) {
|
||||||
|
const objWithAllProps = {};
|
||||||
|
|
||||||
|
if (schema.allOf.length === 1 && !schema.allOf[0]?.properties && !schema.allOf[0]?.items) {
|
||||||
|
// If allOf has single item and the type is not an object or array, then its a primitive
|
||||||
|
if (schema.allOf[0].$ref) {
|
||||||
|
return '{ }';
|
||||||
|
}
|
||||||
|
if (schema.allOf[0].readOnly && config.includeReadOnly) {
|
||||||
|
const tempSchema = schema.allOf[0];
|
||||||
|
return getSampleValueByType(tempSchema);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
schema.allOf.forEach(v => {
|
||||||
|
if (v.type === 'object' || v.properties || v.allOf || v.anyOf || v.oneOf) {
|
||||||
|
const partialObj = schemaToSampleObj(v, config);
|
||||||
|
Object.assign(objWithAllProps, partialObj);
|
||||||
|
} else if (v.type === 'array' || v.items) {
|
||||||
|
const partialObj = [schemaToSampleObj(v, config)];
|
||||||
|
Object.assign(objWithAllProps, partialObj);
|
||||||
|
} else if (v.type) {
|
||||||
|
const prop = `prop${Object.keys(objWithAllProps).length}`;
|
||||||
|
objWithAllProps[prop] = getSampleValueByType(v);
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj = objWithAllProps;
|
||||||
|
} else if (schema.oneOf) {
|
||||||
|
// 1. First create example with scheme.properties
|
||||||
|
const objWithSchemaProps = {};
|
||||||
|
if (schema.properties) {
|
||||||
|
for (const propertyName in schema.properties) {
|
||||||
|
if (
|
||||||
|
schema.properties[propertyName].properties ||
|
||||||
|
schema.properties[propertyName].properties?.items
|
||||||
|
) {
|
||||||
|
objWithSchemaProps[propertyName] = schemaToSampleObj(
|
||||||
|
schema.properties[propertyName],
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
objWithSchemaProps[propertyName] = getSampleValueByType(schema.properties[propertyName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.oneOf.length > 0) {
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* oneOf:
|
||||||
|
* - type: object
|
||||||
|
* properties:
|
||||||
|
* option1_PropA:
|
||||||
|
* type: string
|
||||||
|
* option1_PropB:
|
||||||
|
* type: string
|
||||||
|
* - type: object
|
||||||
|
* properties:
|
||||||
|
* option2_PropX:
|
||||||
|
* type: string
|
||||||
|
* properties:
|
||||||
|
* prop1:
|
||||||
|
* type: string
|
||||||
|
* prop2:
|
||||||
|
* type: string
|
||||||
|
* minLength: 10
|
||||||
|
*
|
||||||
|
* The above Schema should generate the following 2 examples
|
||||||
|
*
|
||||||
|
* Example-1
|
||||||
|
* {
|
||||||
|
* prop1: 'string',
|
||||||
|
* prop2: 'AAAAAAAAAA', <-- min-length 10
|
||||||
|
* option1_PropA: 'string',
|
||||||
|
* option1_PropB: 'string'
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Example-2
|
||||||
|
* {
|
||||||
|
* prop1: 'string',
|
||||||
|
* prop2: 'AAAAAAAAAA', <-- min-length 10
|
||||||
|
* option2_PropX: 'string'
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
let i = 0;
|
||||||
|
// Merge all examples of each oneOf-schema
|
||||||
|
for (const key in schema.oneOf) {
|
||||||
|
const oneOfSamples = schemaToSampleObj(schema.oneOf[key], config);
|
||||||
|
for (const sampleKey in oneOfSamples) {
|
||||||
|
// 2. In the final example include a one-of item along with properties
|
||||||
|
let finalExample;
|
||||||
|
if (Object.keys(objWithSchemaProps).length > 0) {
|
||||||
|
if (oneOfSamples[sampleKey] === null || typeof oneOfSamples[sampleKey] !== 'object') {
|
||||||
|
// This doesn't really make sense since every oneOf schema _should_ be an object if there are common properties, so we'll skip this
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
finalExample = Object.assign(oneOfSamples[sampleKey], objWithSchemaProps);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalExample = oneOfSamples[sampleKey];
|
||||||
|
}
|
||||||
|
obj[`example-${i}`] = finalExample;
|
||||||
|
addSchemaInfoToExample(schema.oneOf[key], obj[`example-${i}`]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (schema.anyOf) {
|
||||||
|
// First generate values for regular properties
|
||||||
|
let commonObj;
|
||||||
|
if (schema.type === 'object' || schema.properties) {
|
||||||
|
commonObj = { 'example-0': {} };
|
||||||
|
for (const propertyName in schema.properties) {
|
||||||
|
if (schema.example) {
|
||||||
|
commonObj = schema;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (schema.properties[propertyName].deprecated && !config.includeDeprecated) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (schema.properties[propertyName].readOnly && !config.includeReadOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (schema.properties[propertyName].writeOnly && !config.includeWriteOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
commonObj = mergePropertyExamples(
|
||||||
|
commonObj,
|
||||||
|
propertyName,
|
||||||
|
schemaToSampleObj(schema.properties[propertyName], config),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine every variant of the regular properties with every variant of the anyOf samples
|
||||||
|
let i = 0;
|
||||||
|
for (const key in schema.anyOf) {
|
||||||
|
const anyOfSamples = schemaToSampleObj(schema.anyOf[key], config);
|
||||||
|
for (const sampleKey in anyOfSamples) {
|
||||||
|
if (typeof commonObj !== 'undefined') {
|
||||||
|
for (const commonKey in commonObj) {
|
||||||
|
obj[`example-${i}`] = { ...commonObj[commonKey], ...anyOfSamples[sampleKey] };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj[`example-${i}`] = anyOfSamples[sampleKey];
|
||||||
|
}
|
||||||
|
addSchemaInfoToExample(schema.anyOf[key], obj[`example-${i}`]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (schema.type === 'object' || schema.properties) {
|
||||||
|
obj['example-0'] = {};
|
||||||
|
addSchemaInfoToExample(schema, obj['example-0']);
|
||||||
|
if (schema.example) {
|
||||||
|
obj['example-0'] = schema.example;
|
||||||
|
} else {
|
||||||
|
for (const propertyName in schema.properties) {
|
||||||
|
const prop = schema.properties[propertyName] as OpenAPISchema;
|
||||||
|
if (prop?.deprecated && !config.includeDeprecated) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (prop?.readOnly && !config.includeReadOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (prop?.writeOnly && !config.includeWriteOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const propItems = prop?.items as OpenAPISchema;
|
||||||
|
if (prop?.type === 'array' || propItems) {
|
||||||
|
if (prop.example) {
|
||||||
|
addPropertyExampleToObjectExamples(prop.example, obj, propertyName);
|
||||||
|
} else if (propItems?.example) {
|
||||||
|
// schemas and properties support single example but not multiple examples.
|
||||||
|
addPropertyExampleToObjectExamples([propItems.example], obj, propertyName);
|
||||||
|
} else {
|
||||||
|
const itemSamples = schemaToSampleObj(
|
||||||
|
Array.isArray(propItems) ? { allOf: propItems } : propItems,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
if (config.useXmlTagForProp) {
|
||||||
|
const xmlTagName = prop.xml?.name || propertyName;
|
||||||
|
if (prop.xml?.wrapped) {
|
||||||
|
const wrappedItemSample = JSON.parse(
|
||||||
|
`{ "${xmlTagName}" : { "${xmlTagName}" : ${JSON.stringify(
|
||||||
|
itemSamples['example-0'],
|
||||||
|
)} } }`,
|
||||||
|
);
|
||||||
|
obj = mergePropertyExamples(obj, xmlTagName, wrappedItemSample);
|
||||||
|
} else {
|
||||||
|
obj = mergePropertyExamples(obj, xmlTagName, itemSamples);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const arraySamples = [];
|
||||||
|
for (const key in itemSamples) {
|
||||||
|
arraySamples[key] = [itemSamples[key]];
|
||||||
|
}
|
||||||
|
obj = mergePropertyExamples(obj, propertyName, arraySamples);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
obj = mergePropertyExamples(
|
||||||
|
obj,
|
||||||
|
propertyName,
|
||||||
|
schemaToSampleObj(schema.properties[propertyName], config),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (schema.type === 'array' || schema.items) {
|
||||||
|
const schemaItems = schema.items as OpenAPISchema;
|
||||||
|
if (schemaItems || schema.example) {
|
||||||
|
if (schema.example) {
|
||||||
|
obj['example-0'] = schema.example;
|
||||||
|
} else if (schemaItems?.example) {
|
||||||
|
// schemas and properties support single example but not multiple examples.
|
||||||
|
obj['example-0'] = [schemaItems.example];
|
||||||
|
} else {
|
||||||
|
const samples = schemaToSampleObj(schemaItems, config);
|
||||||
|
let i = 0;
|
||||||
|
for (const key in samples) {
|
||||||
|
obj[`example-${i}`] = [samples[key]];
|
||||||
|
addSchemaInfoToExample(schemaItems, obj[`example-${i}`]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj['example-0'] = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { 'example-0': getSampleValueByType(schema) };
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022 Mrinmoy Majumdar
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
export const generateXmlExample = ({
|
||||||
|
includeReadOnly = true,
|
||||||
|
includeWriteOnly = true,
|
||||||
|
schema,
|
||||||
|
}: GenerateExampleProps): FinalExamples[] => {
|
||||||
|
const finalExamples: FinalExamples[] = [];
|
||||||
|
|
||||||
|
if (!schema) return finalExamples;
|
||||||
|
|
||||||
|
const xmlRootStart = schema.xml?.name
|
||||||
|
? `<${schema.xml.name}${schema.xml.namespace ? ` xmlns="${schema.xml.namespace}"` : ''}>`
|
||||||
|
: '<root>';
|
||||||
|
const xmlRootEnd = schema.xml?.name ? `</${schema.xml.name}>` : '</root>';
|
||||||
|
const samples = schemaToSampleObj(schema, {
|
||||||
|
includeReadOnly,
|
||||||
|
includeWriteOnly,
|
||||||
|
includeDeprecated: true,
|
||||||
|
useXmlTagForProp: true,
|
||||||
|
});
|
||||||
|
let i = 0;
|
||||||
|
for (const samplesKey in samples) {
|
||||||
|
if (!samples[samplesKey]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const summary = samples[samplesKey]['::TITLE'] || `Example ${++i}`;
|
||||||
|
const description = samples[samplesKey]['::DESCRIPTION'] || '';
|
||||||
|
const exampleValue = `<?xml version="1.0" encoding="UTF-8"?>\n${xmlRootStart}${json2xml(
|
||||||
|
samples[samplesKey],
|
||||||
|
1,
|
||||||
|
)}\n${xmlRootEnd}`;
|
||||||
|
|
||||||
|
finalExamples.push({
|
||||||
|
exampleDescription: description,
|
||||||
|
exampleId: samplesKey,
|
||||||
|
exampleSummary: summary,
|
||||||
|
exampleValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalExamples;
|
||||||
|
};
|
|
@ -1,20 +1,21 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"moduleResolution": "node",
|
|
||||||
"target": "es5",
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"noEmitHelpers": true,
|
"experimentalDecorators": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"lib": ["esnext", "dom", "WebWorker.ImportScripts"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noEmitHelpers": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"lib": ["es2015", "es2016", "es2017", "dom", "WebWorker.ImportScripts"],
|
"sourceMap": true,
|
||||||
"jsx": "react",
|
"strictNullChecks": true,
|
||||||
|
"target": "es5",
|
||||||
"types": ["webpack", "webpack-env", "jest"]
|
"types": ["webpack", "webpack-env", "jest"]
|
||||||
},
|
},
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user