From 741a6e64276f4268c4b14f58182d45ded60af765 Mon Sep 17 00:00:00 2001 From: smiling-watermelon <146741211+smiling-watermelon@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:14:36 +0000 Subject: [PATCH] fix: Show first example value on Parameter inputs > [!NOTE] > These changes were mainly made with help of an LLM model, nudged, currated and verified by a human. I've noticed that `example` parameter is now deprecacted (and probably was for some time) for Headers, however `examples` parameter doesn't show one of the examples in the schea. I believe that this behaviour may be unnoticed when fixing the deprecation warning in some codebases, and thus I'd like to fix it in future versions of Swagger-UI. --- src/core/components/headers.jsx | 41 +++---- src/core/components/parameter-row.jsx | 129 +++++++++++++---------- src/core/utils/get-parameter-examples.js | 117 ++++++++++++++++++++ src/style/main.scss | 2 +- test/unit/components/headers.jsx | 92 ++++++++++++++++ test/unit/components/parameter-row.jsx | 124 ++++++++++++++++++++++ 6 files changed, 431 insertions(+), 74 deletions(-) create mode 100644 src/core/utils/get-parameter-examples.js create mode 100644 test/unit/components/headers.jsx diff --git a/src/core/components/headers.jsx b/src/core/components/headers.jsx index c717b61e571..fa712fb5f04 100644 --- a/src/core/components/headers.jsx +++ b/src/core/components/headers.jsx @@ -1,6 +1,7 @@ import React from "react" import PropTypes from "prop-types" import Im from "immutable" +import { getParameterExampleValue } from "core/utils/get-parameter-examples" const propClass = "header-example" @@ -19,7 +20,7 @@ export default class Headers extends React.Component { if ( !headers || !headers.size ) return null - return ( + return (

Headers:

@@ -31,25 +32,25 @@ export default class Headers extends React.Component { - { - headers.entrySeq().map( ([ key, header ]) => { - if(!Im.Map.isMap(header)) { - return null - } - - const description = header.get("description") - const type = header.getIn(["schema"]) ? header.getIn(["schema", "type"]) : header.getIn(["type"]) - const schemaExample = header.getIn(["schema", "example"]) - - return ( - - - - ) - }).toArray() - } + { + headers.entrySeq().map( ([ key, header ]) => { + if(!Im.Map.isMap(header)) { + return null + } + + const description = header.get("description") + const type = header.getIn(["schema"]) ? header.getIn(["schema", "type"]) : header.getIn(["type"]) + const headerExample = getParameterExampleValue(header) + + return ( + + + + ) + }).toArray() + }
{ key }{ - !description ? null : - }{ type } { schemaExample ? : null }
{ key }{ + !description ? null : + }{ type } { headerExample !== undefined ? : null }
diff --git a/src/core/components/parameter-row.jsx b/src/core/components/parameter-row.jsx index 8b7ba16d165..3474dea2a97 100644 --- a/src/core/components/parameter-row.jsx +++ b/src/core/components/parameter-row.jsx @@ -4,6 +4,11 @@ import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import win from "core/window" import { getExtensions, getCommonExtensions, numberToString, stringify, isEmptyValue } from "core/utils" +import { + getParameterExample, + getParameterExampleValue, + getParameterExamples, +} from "core/utils/get-parameter-examples" import getParameterSchema from "core/utils/get-parameter-schema.js" export default class ParameterRow extends Component { @@ -102,6 +107,14 @@ export default class ParameterRow extends Component { .get("content", Map()) .keySeq() .first() + const currentExampleKey = oas3Selectors.activeExamplesMember( + ...pathMethod, + "parameters", + this.getParamKey() + ) + const parameterExamples = getParameterExamples(paramWithMeta, { + parameterMediaType, + }) // getSampleSchema could return null const generatedSampleValue = schema ? fn.getSampleSchema(schema.toJS(), parameterMediaType, { @@ -121,26 +134,25 @@ export default class ParameterRow extends Component { if (specSelectors.isSwagger2()) { initialValue = paramWithMeta.get("x-example") !== undefined - ? paramWithMeta.get("x-example") - : paramWithMeta.getIn(["schema", "example"]) !== undefined - ? paramWithMeta.getIn(["schema", "example"]) - : (schema && schema.getIn(["default"])) + ? paramWithMeta.get("x-example") + : paramWithMeta.getIn(["schema", "example"]) !== undefined + ? paramWithMeta.getIn(["schema", "example"]) + : (schema && schema.getIn(["default"])) } else if (specSelectors.isOAS3()) { schema = this.composeJsonSchema(schema) - const currentExampleKey = oas3Selectors.activeExamplesMember(...pathMethod, "parameters", this.getParamKey()) initialValue = - paramWithMeta.getIn(["examples", currentExampleKey, "value"]) !== undefined - ? paramWithMeta.getIn(["examples", currentExampleKey, "value"]) - : paramWithMeta.getIn(["content", parameterMediaType, "example"]) !== undefined - ? paramWithMeta.getIn(["content", parameterMediaType, "example"]) - : paramWithMeta.get("example") !== undefined - ? paramWithMeta.get("example") - : (schema && schema.get("example")) !== undefined - ? (schema && schema.get("example")) - : (schema && schema.get("default")) !== undefined - ? (schema && schema.get("default")) - : paramWithMeta.get("default") // ensures support for `parameterMacro` + getParameterExampleValue(paramWithMeta, { + parameterMediaType, + activeExampleKey: currentExampleKey, + }) !== undefined + ? getParameterExampleValue(paramWithMeta, { + parameterMediaType, + activeExampleKey: currentExampleKey, + }) + : (schema && schema.get("default")) !== undefined + ? (schema && schema.get("default")) + : paramWithMeta.get("default") // ensures support for `parameterMacro` } //// Process the initial value @@ -160,7 +172,7 @@ export default class ParameterRow extends Component { } else if( schemaObjectType === "object" && generatedSampleValue - && !paramWithMeta.get("examples") + && !parameterExamples ) { // Object parameters get special treatment.. if the user doesn't set any // default or example values, we'll provide initial values generated from @@ -179,7 +191,7 @@ export default class ParameterRow extends Component { schemaObjectType === "array" && schemaItemsType === "object" && generatedSampleValue - && !paramWithMeta.get("examples") + && !parameterExamples ) { this.onChangeWrapper( List.isList(generatedSampleValue) ? ( @@ -226,16 +238,16 @@ export default class ParameterRow extends Component { let inType = param.get("in") let bodyParam = inType !== "body" ? null : const ModelExample = getComponent("modelExample") @@ -251,6 +263,12 @@ export default class ParameterRow extends Component { .get("content", Map()) .keySeq() .first() + const currentExampleKey = isOAS3 + ? oas3Selectors.activeExamplesMember(...pathMethod, "parameters", this.getParamKey()) + : undefined + const parameterExamples = isOAS3 + ? getParameterExamples(param, { parameterMediaType }) + : undefined if (isOAS3) { schema = this.composeJsonSchema(schema) @@ -300,7 +318,12 @@ export default class ParameterRow extends Component { if (paramDefaultValue === undefined) { paramDefaultValue = param.get("default") } - paramExample = param.get("example") + paramExample = isOAS3 + ? getParameterExampleValue(param, { + parameterMediaType, + activeExampleKey: currentExampleKey, + }) + : param.get("example") if (paramExample === undefined) { paramExample = param.get("x-example") } @@ -340,9 +363,9 @@ export default class ParameterRow extends Component { { (bodyParam || !isExecute) && isDisplayParamEnum ? Available values : " + paramEnum.map(function(item) { - return item - }).toArray().map(String).join(", ")}/> + "Available values : " + paramEnum.map(function(item) { + return item + }).toArray().map(String).join(", ")}/> : null } @@ -359,15 +382,15 @@ export default class ParameterRow extends Component { {(isFormData && !isFormDataSupported) &&
Error: your browser does not support FormData
} { - isOAS3 && param.get("examples") ? ( + isOAS3 && parameterExamples ? (
@@ -388,37 +411,37 @@ export default class ParameterRow extends Component { schema={schema} example={jsonSchemaForm} /> - ) : jsonSchemaForm + ) : jsonSchemaForm } { bodyParam && schema ? + specPath={specPath.push("schema")} + getConfigs={ getConfigs } + isExecute={ isExecute } + specSelectors={ specSelectors } + schema={ schema } + example={ bodyParam } + includeWriteOnly={ true }/> : null } { !bodyParam && isExecute && param.get("allowEmptyValue") ? - - : null + + : null } { - isOAS3 && param.get("examples") ? ( + isOAS3 && parameterExamples ? ( diff --git a/src/core/utils/get-parameter-examples.js b/src/core/utils/get-parameter-examples.js new file mode 100644 index 00000000000..fc49ab8ce08 --- /dev/null +++ b/src/core/utils/get-parameter-examples.js @@ -0,0 +1,117 @@ +/** + * @prettier + */ + +import Im from "immutable" + +const getCurrentExample = (examples, activeExampleKey) => { + if (!Im.Map.isMap(examples) || !examples.size) { + return undefined + } + + if ( + activeExampleKey !== undefined && + activeExampleKey !== null && + examples.has(activeExampleKey) + ) { + return examples.get(activeExampleKey) + } + + return examples.first() +} + +const getExampleValue = (example) => { + if (Im.Map.isMap(example)) { + return example.get("value") + } + + return example +} + +const getParameterMediaType = (parameter, parameterMediaType) => { + if (parameterMediaType) { + return parameterMediaType + } + + return parameter.get("content", Im.Map()).keySeq().first() +} + +export const getParameterExamples = ( + parameter, + { parameterMediaType } = {} +) => { + if (!Im.Map.isMap(parameter)) { + return undefined + } + + const examples = parameter.get("examples") + + if (Im.Map.isMap(examples) && examples.size) { + return examples + } + + const mediaType = getParameterMediaType(parameter, parameterMediaType) + const contentExamples = mediaType + ? parameter.getIn(["content", mediaType, "examples"]) + : undefined + + if (Im.Map.isMap(contentExamples) && contentExamples.size) { + return contentExamples + } + + return undefined +} + +export const getParameterExample = ( + parameter, + { parameterMediaType, activeExampleKey } = {} +) => { + const examples = getParameterExamples(parameter, { parameterMediaType }) + + if (!examples) { + return undefined + } + + return getCurrentExample(examples, activeExampleKey) +} + +export const getParameterExampleValue = ( + parameter, + { parameterMediaType, activeExampleKey } = {} +) => { + if (!Im.Map.isMap(parameter)) { + return undefined + } + + const exampleValue = getExampleValue( + getParameterExample(parameter, { parameterMediaType, activeExampleKey }) + ) + + if (exampleValue !== undefined) { + return exampleValue + } + + const mediaType = getParameterMediaType(parameter, parameterMediaType) + const contentExample = mediaType + ? parameter.getIn(["content", mediaType, "example"]) + : undefined + + if (contentExample !== undefined) { + return contentExample + } + + if (parameter.get("example") !== undefined) { + return parameter.get("example") + } + + const schemaExamples = parameter.getIn(["schema", "examples"]) + const firstSchemaExample = Im.Iterable.isIndexed(schemaExamples) + ? schemaExamples.first() + : undefined + + if (firstSchemaExample !== undefined) { + return firstSchemaExample + } + + return parameter.getIn(["schema", "example"]) +} diff --git a/src/style/main.scss b/src/style/main.scss index f5da6aceed8..0ed24f55f09 100644 --- a/src/style/main.scss +++ b/src/style/main.scss @@ -4,7 +4,7 @@ .swagger-ui { container-name: swagger-ui; container-type: inline-size; - + @include type.text_body(); @include meta.load-css("~tachyons-sass/tachyons.scss"); @include meta.load-css("mixins"); diff --git a/test/unit/components/headers.jsx b/test/unit/components/headers.jsx new file mode 100644 index 00000000000..3ca68fe5f12 --- /dev/null +++ b/test/unit/components/headers.jsx @@ -0,0 +1,92 @@ +import React from "react" +import { fromJS } from "immutable" +import { render } from "enzyme" + +import Headers from "core/components/headers" +import Property from "core/components/property" + +describe("", function () { + const components = { + Property, + Markdown: ({ source }) => {source}, + } + + const getComponent = componentName => components[componentName] + + it("renders the first OpenAPI 3 `examples` value for headers", function () { + const headers = fromJS({ + "X-Rate-Limit": { + description: "Rate limit header", + schema: { + type: "integer", + example: 999, + }, + example: 777, + examples: { + primary: { + summary: "Preferred", + value: 123, + }, + secondary: { + value: 456, + }, + }, + }, + }) + + const wrapper = render() + expect(wrapper.text()).toContain("Example: 123") + expect(wrapper.text()).not.toContain("Example: 777") + expect(wrapper.text()).not.toContain("Example: 999") + }) + + it("falls back to schema.examples when header.examples is absent", function () { + const headers = fromJS({ + "X-Request-ID": { + description: "Request id", + schema: { + type: "string", + examples: ["abc123", "def456"], + }, + }, + }) + + const wrapper = render() + expect(wrapper.text()).toContain("Example: abc123") + }) + + it("falls back to content.examples when a header uses content", function () { + const headers = fromJS({ + "X-Trace": { + description: "Trace header", + content: { + "text/plain": { + examples: { + primary: { + value: "trace-123", + }, + }, + }, + }, + }, + }) + + const wrapper = render() + expect(wrapper.text()).toContain("Example: trace-123") + }) + + it("falls back to deprecated singular example and supports falsy values", function () { + const headers = fromJS({ + "X-Deprecated": { + description: "Deprecated example usage", + schema: { + type: "integer", + }, + example: 0, + }, + }) + + const wrapper = render() + expect(wrapper.text()).toContain("Example: 0") + }) +}) diff --git a/test/unit/components/parameter-row.jsx b/test/unit/components/parameter-row.jsx index 862a055c4de..86ab6f9d275 100644 --- a/test/unit/components/parameter-row.jsx +++ b/test/unit/components/parameter-row.jsx @@ -363,4 +363,128 @@ describe("bug #5573: zero default and example values", function () { expect(props.onChange).toHaveBeenCalled() expect(props.onChange).toHaveBeenCalledWith(paramValue, "0", false) }) + + it("should apply the first OpenAPI 3.1 schema.examples value", function () { + const paramValue = fromJS({ + name: "traceId", + in: "header", + schema: { + type: "string", + examples: ["abc123", "def456"], + }, + }) + const getSystem = () => ({ + getComponent: () => "div", + specSelectors: { + security() {}, + parameterWithMetaByIdentity() { + return paramValue + }, + isOAS3() { + return true + }, + isSwagger2() { + return false + }, + }, + oas3Selectors: { + activeExamplesMember: () => null, + }, + getConfigs: () => { + return {} + }, + fn: { + memoizedSampleFromSchema, + memoizedCreateXMLExample, + getSchemaObjectTypeLabel, + getSchemaObjectType, + getJsonSampleSchema: makeGetJsonSampleSchema(getSystem), + getYamlSampleSchema: makeGetYamlSampleSchema(getSystem), + getXmlSampleSchema: makeGetXmlSampleSchema(getSystem), + getSampleSchema: makeGetSampleSchema(getSystem), + mergeJsonSchema, + }, + }) + const props = { + ...getSystem(), + operation: { get: () => {} }, + onChange: jest.fn(), + param: paramValue, + rawParam: paramValue, + onChangeConsumes: () => {}, + pathMethod: [], + specPath: List([]), + } + + render() + + expect(props.onChange).toHaveBeenCalled() + expect(props.onChange).toHaveBeenCalledWith(paramValue, "abc123", false) + }) + + it("should apply the first OpenAPI 3 content.examples value", function () { + const paramValue = fromJS({ + name: "traceId", + in: "header", + content: { + "text/plain": { + schema: { + type: "string", + }, + examples: { + primary: { + value: "trace-123", + }, + }, + }, + }, + }) + const getSystem = () => ({ + getComponent: () => "div", + specSelectors: { + security() {}, + parameterWithMetaByIdentity() { + return paramValue + }, + isOAS3() { + return true + }, + isSwagger2() { + return false + }, + }, + oas3Selectors: { + activeExamplesMember: () => null, + }, + getConfigs: () => { + return {} + }, + fn: { + memoizedSampleFromSchema, + memoizedCreateXMLExample, + getSchemaObjectTypeLabel, + getSchemaObjectType, + getJsonSampleSchema: makeGetJsonSampleSchema(getSystem), + getYamlSampleSchema: makeGetYamlSampleSchema(getSystem), + getXmlSampleSchema: makeGetXmlSampleSchema(getSystem), + getSampleSchema: makeGetSampleSchema(getSystem), + mergeJsonSchema, + }, + }) + const props = { + ...getSystem(), + operation: { get: () => {} }, + onChange: jest.fn(), + param: paramValue, + rawParam: paramValue, + onChangeConsumes: () => {}, + pathMethod: [], + specPath: List([]), + } + + render() + + expect(props.onChange).toHaveBeenCalled() + expect(props.onChange).toHaveBeenCalledWith(paramValue, "trace-123", false) + }) })