diff --git a/README.md b/README.md index 58cf29f6..6d8cb7e9 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,7 @@ You can use all of the following options with the standalone version of the ` theme object * `spacing` @@ -324,6 +325,17 @@ You can use all of the following options with the standalone version of the , document.getElementById('example')); diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index 5f865af5..2dbf7172 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -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, diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 4a219eef..17204f6e 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -6,6 +6,8 @@ import { setRedocLabels } from './Labels'; import { SideNavStyleEnum } from './types'; import type { LabelsConfigRaw, MDXComponentMeta } from './types'; +export type CodeSamplesLanguage = 'json' | 'xml'; + export interface RedocRawOptions { theme?: ThemeInterface; scrollYOffset?: number | string | (() => number); @@ -56,6 +58,7 @@ export interface RedocRawOptions { hideFab?: boolean; minCharacterLengthToInitSearch?: number; showWebhookVerb?: boolean; + codeSamplesLanguages?: CodeSamplesLanguage[]; } export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { @@ -211,6 +214,16 @@ export class RedocNormalizedOptions { return 10; } + private static normalizeCodeSamplesLanguages( + value?: CodeSamplesLanguage[], + ): CodeSamplesLanguage[] { + if (isArray(value)) { + return value.map(lang => lang.toLowerCase()) as CodeSamplesLanguage[]; + } + + return ['json']; + } + theme: ResolvedThemeInterface; scrollYOffset: () => number; hideHostname: boolean; @@ -258,6 +271,7 @@ export class RedocNormalizedOptions { showWebhookVerb: boolean; nonce?: string; + codeSamplesLanguages: CodeSamplesLanguage[]; constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { raw = { ...defaults, ...raw }; @@ -335,5 +349,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, + ); } } diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts index c9fb1a15..a3556d66 100644 --- a/src/services/models/MediaType.ts +++ b/src/services/models/MediaType.ts @@ -4,7 +4,7 @@ import type { OpenAPIMediaType } from '../../types'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { SchemaModel } from './Schema'; -import { isJsonLike, isXml, mapValues } from '../../utils'; +import { isXml, mapValues } from '../../utils'; import type { OpenAPIParser } from '../OpenAPIParser'; import { ExampleModel } from './Example'; import { ConfigAccessOptions, FinalExamples, generateXmlExample } from '../../utils/xml'; @@ -34,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, @@ -48,7 +51,7 @@ export class MediaTypeModel { info.encoding, ), }; - } else if (isJsonLike(name) || isXml(name)) { + } else if (isCodeGenerationSupported) { this.generateExample(parser, info); } } @@ -78,8 +81,9 @@ export class MediaTypeModel { this.name, info.encoding, ); - if (isXml(this.name)) { - const xmlExamples = this.resolveXmlExample(parser, sample as OpenAPIExample); + + const xmlExamples = this.resolveXmlExample(parser, sample as OpenAPIExample); + if (xmlExamples[0]) { this.examples[subSchema.title].value = xmlExamples[0]?.exampleValue; } } diff --git a/src/utils/jsonToXml.ts b/src/utils/jsonToXml.ts new file mode 100644 index 00000000..f245a4aa --- /dev/null +++ b/src/utils/jsonToXml.ts @@ -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: + * + * simple + * + * <0>a + * <1>b + * <2>c + * + * + * val-1 + * val-2 + * + * + **/ +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}`; + } else if (typeof obj[prop] === 'object') { + xmlText = `${xmlText}\n${indent}<${tagName}>${json2xml( + obj[prop], + level + 1, + )}\n${indent}`; + } else { + xmlText = `${xmlText}\n${indent}<${tagName}>${obj[prop].toString()}`; + } + } + return xmlText; +}; diff --git a/src/utils/xml.ts b/src/utils/xml.ts index 5e9416a0..5d762298 100644 --- a/src/utils/xml.ts +++ b/src/utils/xml.ts @@ -1,5 +1,6 @@ import { MergedOpenAPISchema } from '../services'; import { OpenAPISchema } from '../types'; +import { json2xml } from './jsonToXml'; export interface ConfigAccessOptions { includeReadOnly?: boolean; @@ -22,66 +23,6 @@ export interface FinalExamples { exampleValue: string; } -/** - * json2xml - * @example - * Schema: { - * 'prop1' : 'one', - * 'prop2' : 'two', - * 'prop3' : [ 'a', 'b', 'c' ], - * 'prop4' : { - * 'ob1' : 'val-1', - * 'ob2' : 'val-2' - * } - * } - * XML: - * - * simple - * - * <0>a - * <1>b - * <2>c - * - * - * val-1 - * val-2 - * - * - **/ -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}`; - } else if (typeof obj[prop] === 'object') { - xmlText = `${xmlText}\n${indent}<${tagName}>${json2xml( - obj[prop], - level + 1, - )}\n${indent}`; - } else { - xmlText = `${xmlText}\n${indent}<${tagName}>${obj[prop].toString()}`; - } - } - return xmlText; -}; - const mergePropertyExamples = ( obj: { [x: string]: any }, propertyName: string,