mirror of
https://github.com/Redocly/redoc.git
synced 2024-12-01 12:53:44 +03:00
Merge pull request #3 from Adthena/feature-FBI-451-implement-xml-response-example-generation-for-redoc
FBI-451: Implement XML example code generation for Redoc
This commit is contained in:
commit
55c66f6d78
12
README.md
12
README.md
|
@ -251,6 +251,7 @@ You can use all of the following options with the standalone version of the <red
|
|||
* **path-only**: displays a path in the sidebar navigation item.
|
||||
* **id-only**: displays the operation id with a fallback to the path in the sidebar navigation item.
|
||||
* `showWebhookVerb` - when set to `true`, shows the HTTP request method for webhooks in operations and in the sidebar.
|
||||
* `codeSamplesLanguages` - enables code sample generation for the provided list of languages.
|
||||
|
||||
### `<redoc>` theme object
|
||||
* `spacing`
|
||||
|
@ -324,6 +325,17 @@ You can use all of the following options with the standalone version of the <red
|
|||
* `backgroundColor`: '#263238'
|
||||
* `color`: '#ffffff'
|
||||
|
||||
## Auto generated code samples
|
||||
A new parameter called `codeSamplesLanguages` was added to `options` Object to provide code sample generation. You can pass an array like this to enable all languages supported by the code generation:
|
||||
|
||||
```javascript
|
||||
['json','xml']
|
||||
```
|
||||
|
||||
Where `['json']` is provided by default.
|
||||
|
||||
When the `x-codeSamples` and `x-code-samples` are not set, it will
|
||||
automatically generate the code samples based on the language(s) you set.
|
||||
-----------
|
||||
## Development
|
||||
see [CONTRIBUTING.md](.github/CONTRIBUTING.md)
|
||||
|
|
|
@ -11,6 +11,10 @@ const userUrl = window.location.search.match(/url=(.*)$/);
|
|||
const specUrl =
|
||||
(userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml');
|
||||
|
||||
const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 };
|
||||
const options: RedocRawOptions = {
|
||||
nativeScrollbars: false,
|
||||
maxDisplayedEnumValues: 3,
|
||||
codeSamplesLanguages: ['json', 'xml'],
|
||||
};
|
||||
|
||||
render(<RedocStandalone specUrl={specUrl} options={options} />, document.getElementById('example'));
|
||||
|
|
|
@ -76,6 +76,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -347,6 +350,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -605,6 +611,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -925,6 +934,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -1208,6 +1220,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -1462,6 +1477,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -1741,6 +1759,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
],
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -2050,6 +2071,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -2321,6 +2345,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
@ -2579,6 +2606,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
"minItems": undefined,
|
||||
"options": RedocNormalizedOptions {
|
||||
"allowedMdComponents": Object {},
|
||||
"codeSamplesLanguages": Array [
|
||||
"json",
|
||||
],
|
||||
"disableSearch": false,
|
||||
"downloadDefinitionUrl": undefined,
|
||||
"downloadFileName": undefined,
|
||||
|
|
4
src/constants/languages.ts
Normal file
4
src/constants/languages.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const CODE_SAMPLE_LANGUAGES = {
|
||||
JSON: 'json',
|
||||
XML: 'xml',
|
||||
} as const;
|
|
@ -1,9 +1,10 @@
|
|||
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 { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
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
|
||||
|
||||
|
@ -335,6 +336,98 @@ export class OpenAPIParser {
|
|||
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
|
||||
* returns map of definition pointer to definition name
|
||||
|
|
|
@ -5,6 +5,9 @@ import { isArray, isNumeric, mergeObjects } from '../utils/helpers';
|
|||
import { setRedocLabels } from './Labels';
|
||||
import { SideNavStyleEnum } from './types';
|
||||
import type { LabelsConfigRaw, MDXComponentMeta } from './types';
|
||||
import { CODE_SAMPLE_LANGUAGES } from '../constants/languages';
|
||||
|
||||
export type CodeSamplesLanguage = typeof CODE_SAMPLE_LANGUAGES[keyof typeof CODE_SAMPLE_LANGUAGES];
|
||||
|
||||
export interface RedocRawOptions {
|
||||
theme?: ThemeInterface;
|
||||
|
@ -56,6 +59,7 @@ export interface RedocRawOptions {
|
|||
hideFab?: boolean;
|
||||
minCharacterLengthToInitSearch?: number;
|
||||
showWebhookVerb?: boolean;
|
||||
codeSamplesLanguages?: CodeSamplesLanguage[];
|
||||
}
|
||||
|
||||
export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
|
||||
|
@ -211,6 +215,16 @@ export class RedocNormalizedOptions {
|
|||
return 10;
|
||||
}
|
||||
|
||||
private static normalizeCodeSamplesLanguages(
|
||||
value?: CodeSamplesLanguage[],
|
||||
): CodeSamplesLanguage[] {
|
||||
if (isArray(value)) {
|
||||
return value.map(lang => lang.toLowerCase()) as CodeSamplesLanguage[];
|
||||
}
|
||||
|
||||
return [CODE_SAMPLE_LANGUAGES.JSON];
|
||||
}
|
||||
|
||||
theme: ResolvedThemeInterface;
|
||||
scrollYOffset: () => number;
|
||||
hideHostname: boolean;
|
||||
|
@ -258,6 +272,7 @@ export class RedocNormalizedOptions {
|
|||
showWebhookVerb: boolean;
|
||||
|
||||
nonce?: string;
|
||||
codeSamplesLanguages: CodeSamplesLanguage[];
|
||||
|
||||
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
|
||||
raw = { ...defaults, ...raw };
|
||||
|
@ -335,5 +350,8 @@ export class RedocNormalizedOptions {
|
|||
this.hideFab = argValueToBoolean(raw.hideFab);
|
||||
this.minCharacterLengthToInitSearch = argValueToNumber(raw.minCharacterLengthToInitSearch) || 3;
|
||||
this.showWebhookVerb = argValueToBoolean(raw.showWebhookVerb);
|
||||
this.codeSamplesLanguages = RedocNormalizedOptions.normalizeCodeSamplesLanguages(
|
||||
raw.codeSamplesLanguages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,5 +72,36 @@ describe('Models', () => {
|
|||
|
||||
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 string = 'string';
|
||||
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`] = `
|
||||
Object {
|
||||
"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 { SchemaModel } from './Schema';
|
||||
|
||||
import { isJsonLike, mapValues } from '../../utils';
|
||||
import { isXml, mapValues } from '../../utils';
|
||||
import type { OpenAPIParser } from '../OpenAPIParser';
|
||||
import { ExampleModel } from './Example';
|
||||
import { ConfigAccessOptions, FinalExamples, generateXmlExample } from '../../utils/xml';
|
||||
import { MergedOpenAPISchema } from '../types';
|
||||
import { OpenAPIExample, Referenced } from '../../types';
|
||||
|
||||
export class MediaTypeModel {
|
||||
examples?: { [name: string]: ExampleModel };
|
||||
|
@ -31,6 +34,9 @@ export class MediaTypeModel {
|
|||
this.schema = info.schema && new SchemaModel(parser, info.schema, '', options);
|
||||
this.onlyRequiredInSamples = options.onlyRequiredInSamples;
|
||||
this.generatedPayloadSamplesMaxDepth = options.generatedPayloadSamplesMaxDepth;
|
||||
const isCodeGenerationSupported = options.codeSamplesLanguages.some(lang =>
|
||||
name.toLowerCase().includes(lang),
|
||||
);
|
||||
if (info.examples !== undefined) {
|
||||
this.examples = mapValues(
|
||||
info.examples,
|
||||
|
@ -45,7 +51,7 @@ export class MediaTypeModel {
|
|||
info.encoding,
|
||||
),
|
||||
};
|
||||
} else if (isJsonLike(name)) {
|
||||
} else if (isCodeGenerationSupported) {
|
||||
this.generateExample(parser, info);
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +63,8 @@ export class MediaTypeModel {
|
|||
skipNonRequired: this.isRequestType && this.onlyRequiredInSamples,
|
||||
maxSampleDepth: this.generatedPayloadSamplesMaxDepth,
|
||||
};
|
||||
if (this.schema && this.schema.oneOf) {
|
||||
if (this.schema) {
|
||||
if (this.schema.oneOf) {
|
||||
this.examples = {};
|
||||
for (const subSchema of this.schema.oneOf) {
|
||||
const sample = Sampler.sample(subSchema.rawSchema as any, samplerOptions, parser.spec);
|
||||
|
@ -74,13 +81,38 @@ export class MediaTypeModel {
|
|||
this.name,
|
||||
info.encoding,
|
||||
);
|
||||
|
||||
const xmlExamples = this.resolveXmlExample(parser, sample as OpenAPIExample);
|
||||
if (xmlExamples[0]) {
|
||||
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 = {
|
||||
default: new ExampleModel(
|
||||
parser,
|
||||
{
|
||||
value: Sampler.sample(info.schema as any, samplerOptions, parser.spec),
|
||||
value: xmlExamples[0]?.exampleValue || infoOrRef.value,
|
||||
},
|
||||
this.name,
|
||||
info.encoding,
|
||||
|
@ -88,4 +120,36 @@ 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
$ref?: string;
|
||||
type?: string | string[];
|
||||
|
@ -161,6 +174,7 @@ export interface OpenAPISchema {
|
|||
contentMediaType?: string;
|
||||
prefixItems?: OpenAPISchema[];
|
||||
additionalItems?: OpenAPISchema | boolean;
|
||||
xml?: XMLObject;
|
||||
}
|
||||
|
||||
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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 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>
|
||||
<city>string</city>
|
||||
<country>string</country>
|
||||
<street>string</street>
|
||||
</addresses>
|
||||
</User>"
|
||||
`;
|
||||
|
||||
exports[`generateXmlExample should generate xml for schemas with an array of items: Example 2 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', () => {
|
||||
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 './memoize';
|
||||
export * from './sort';
|
||||
export * from './object';
|
||||
|
|
85
src/utils/jsonToXml.ts
Normal file
85
src/utils/jsonToXml.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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>
|
||||
**/
|
||||
export 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;
|
||||
};
|
|
@ -26,3 +26,22 @@ export function objectSet(object: object, path: string | Array<string>, value: a
|
|||
const key = _path[limit];
|
||||
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';
|
||||
}
|
||||
|
||||
export const isXml = (contentType: string): boolean => {
|
||||
return contentType === 'application/xml';
|
||||
};
|
||||
|
||||
function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string {
|
||||
if (isArray(fieldVal)) {
|
||||
return fieldVal.map(v => v.toString()).join(delimiter);
|
||||
|
|
521
src/utils/xml.ts
Normal file
521
src/utils/xml.ts
Normal file
|
@ -0,0 +1,521 @@
|
|||
import { MergedOpenAPISchema } from '../services';
|
||||
import { OpenAPISchema } from '../types';
|
||||
import { json2xml } from './jsonToXml';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const exampleId = getExampleId(i + 1);
|
||||
mergedObj[exampleId] = { ...obj[exampleKey] };
|
||||
mergedObj[exampleId][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 '?';
|
||||
};
|
||||
|
||||
const getExampleId = (id: number = 1): string => `Example ${id}`;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
const defaultExampleId = getExampleId();
|
||||
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) {
|
||||
const exampleId = getExampleId(i + 1);
|
||||
// 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[exampleId] = finalExample;
|
||||
addSchemaInfoToExample(schema.oneOf[key], obj[exampleId]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (schema.anyOf) {
|
||||
// First generate values for regular properties
|
||||
let commonObj;
|
||||
if (schema.type === 'object' || schema.properties) {
|
||||
commonObj = { [defaultExampleId]: {} };
|
||||
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) {
|
||||
const exampleId = getExampleId(i + 1);
|
||||
if (typeof commonObj !== 'undefined') {
|
||||
for (const commonKey in commonObj) {
|
||||
obj[exampleId] = { ...commonObj[commonKey], ...anyOfSamples[sampleKey] };
|
||||
}
|
||||
} else {
|
||||
obj[exampleId] = anyOfSamples[sampleKey];
|
||||
}
|
||||
addSchemaInfoToExample(schema.anyOf[key], obj[exampleId]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else if (schema.type === 'object' || schema.properties) {
|
||||
obj[defaultExampleId] = {};
|
||||
addSchemaInfoToExample(schema, obj[defaultExampleId]);
|
||||
if (schema.example) {
|
||||
obj[defaultExampleId] = 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[defaultExampleId],
|
||||
)} } }`,
|
||||
);
|
||||
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[defaultExampleId] = schema.example;
|
||||
} else if (schemaItems?.example) {
|
||||
// schemas and properties support single example but not multiple examples.
|
||||
obj[defaultExampleId] = [schemaItems.example];
|
||||
} else {
|
||||
const samples = schemaToSampleObj(schemaItems, config);
|
||||
let i = 0;
|
||||
for (const key in samples) {
|
||||
const exampleId = getExampleId(i + 1);
|
||||
obj[exampleId] = [samples[key]];
|
||||
addSchemaInfoToExample(schemaItems, obj[exampleId]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obj[defaultExampleId] = [];
|
||||
}
|
||||
} else {
|
||||
return { [defaultExampleId]: 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": {
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"noImplicitAny": false,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"noEmitHelpers": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"jsx": "react",
|
||||
"lib": ["esnext", "dom", "WebWorker.ImportScripts"],
|
||||
"moduleResolution": "node",
|
||||
"noEmitHelpers": true,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "lib",
|
||||
"pretty": true,
|
||||
"lib": ["es2015", "es2016", "es2017", "dom", "WebWorker.ImportScripts"],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "es5",
|
||||
"types": ["webpack", "webpack-env", "jest"]
|
||||
},
|
||||
"compileOnSave": false,
|
||||
|
|
Loading…
Reference in New Issue
Block a user