fix(config): cast configuration values into proper types (#9829)

Refs #9808
This commit is contained in:
Oliwia Rogala
2024-04-18 11:04:05 +02:00
committed by GitHub
parent c2b63ab4d7
commit 7378821669
28 changed files with 359 additions and 83 deletions

View File

@@ -142,7 +142,7 @@ SwaggerUI.defaultProps = {
deepLinking: false,
showExtensions: false,
showCommonExtensions: false,
filter: null,
filter: false,
requestSnippetsEnabled: false,
requestSnippets: {
generators: {
@@ -164,7 +164,7 @@ SwaggerUI.defaultProps = {
},
tryItOutEnabled: false,
displayRequestDuration: false,
withCredentials: undefined,
withCredentials: false,
persistAuthorization: false,
oauth2RedirectUrl: undefined,
}

View File

@@ -73,10 +73,10 @@ export default class LiveResponse extends React.Component {
return (
<div>
{ curlRequest && (requestSnippetsEnabled === true || requestSnippetsEnabled === "true"
{ curlRequest && requestSnippetsEnabled
? <RequestSnippets request={ curlRequest }/>
: <Curl request={ curlRequest } />
)}
}
{ url && <div>
<div className="request-url">
<h4>Request URL</h4>

View File

@@ -47,8 +47,6 @@ export default class OperationTag extends React.Component {
deepLinking,
} = getConfigs()
const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"
const Collapse = getComponent("Collapse")
const Markdown = getComponent("Markdown", true)
const DeepLink = getComponent("DeepLink")
@@ -80,7 +78,7 @@ export default class OperationTag extends React.Component {
data-is-open={showTag}
>
<DeepLink
enabled={isDeepLinkingEnabled}
enabled={deepLinking}
isShown={showTag}
path={createDeepLinkPath(tag)}
text={tag} />

View File

@@ -11,8 +11,8 @@ const defaultOptions = Object.freeze({
urls: null,
layout: "BaseLayout",
docExpansion: "list",
maxDisplayedTags: null,
filter: null,
maxDisplayedTags: -1,
filter: false,
validatorUrl: "https://validator.swagger.io/validator",
oauth2RedirectUrl: `${window.location.protocol}//${window.location.host}${window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/"))}/oauth2-redirect.html`,
persistAuthorization: false,
@@ -30,7 +30,7 @@ const defaultOptions = Object.freeze({
defaultModelsExpandDepth: 1,
showExtensions: false,
showCommonExtensions: false,
withCredentials: undefined,
withCredentials: false,
requestSnippetsEnabled: false,
requestSnippets: {
generators: {

View File

@@ -1,10 +1,10 @@
/**
* @prettier
*/
import merge from "../merge"
import deepExtend from "deep-extend"
const storeFactorization = (options) => {
const state = merge(
const state = deepExtend(
{
layout: {
layout: options.layout,

View File

@@ -7,12 +7,13 @@
*
* NOTE1: lodash.merge & lodash.mergeWith prefers to ignore undefined values
* NOTE2: special handling of `domNode` option is now required as `deep-extend` will corrupt it (lodash.merge handles it correctly)
* NOTE3: oauth2RedirectUrl and withCredentials options can be set to undefined. By expecting null instead of undefined, we can't use lodash.merge.
* NOTE3: oauth2RedirectUrl option can be set to undefined. By expecting null instead of undefined, we can't use lodash.merge.
* NOTE4: urls.primaryName needs to handled in special way, because it's an arbitrary property on Array instance
*
* TODO(vladimir.gorej@gmail.com): remove deep-extend in favor of lodash.merge
*/
import deepExtend from "deep-extend"
import typeCast from "./type-cast"
const merge = (target, ...sources) => {
let domNode = Symbol.for("domNode")
@@ -51,7 +52,7 @@ const merge = (target, ...sources) => {
merged.urls.primaryName = primaryName
}
return merged
return typeCast(merged)
}
export default merge

View File

@@ -0,0 +1,24 @@
/**
* @prettier
*/
import has from "lodash/has"
import get from "lodash/get"
import set from "lodash/fp/set"
import typeCasters from "./mappings"
const typeCast = (options) => {
return Object.entries(typeCasters).reduce(
(acc, [optionPath, { typeCaster, defaultValue }]) => {
if (has(acc, optionPath)) {
const uncasted = get(acc, optionPath)
const casted = typeCaster(uncasted, defaultValue)
acc = set(optionPath, casted, acc)
}
return acc
},
{ ...options }
)
}
export default typeCast

View File

@@ -0,0 +1,115 @@
/**
* @prettier
*/
import arrayTypeCaster from "./type-casters/array"
import booleanTypeCaster from "./type-casters/boolean"
import domNodeTypeCaster from "./type-casters/dom-node"
import filterTypeCaster from "./type-casters/filter"
import nullableArrayTypeCaster from "./type-casters/nullable-array"
import nullableStringTypeCaster from "./type-casters/nullable-string"
import numberTypeCaster from "./type-casters/number"
import objectTypeCaster from "./type-casters/object"
import stringTypeCaster from "./type-casters/string"
import syntaxHighlightTypeCaster from "./type-casters/syntax-highlight"
import undefinedStringTypeCaster from "./type-casters/undefined-string"
import defaultOptions from "../defaults"
const typeCasters = {
configUrl: { typeCaster: stringTypeCaster },
deepLinking: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.deepLinking,
},
defaultModelExpandDepth: {
typeCaster: numberTypeCaster,
defaultValue: defaultOptions.defaultModelExpandDepth,
},
defaultModelRendering: { typeCaster: stringTypeCaster },
defaultModelsExpandDepth: {
typeCaster: numberTypeCaster,
defaultValue: defaultOptions.defaultModelsExpandDepth,
},
displayOperationId: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.displayOperationId,
},
displayRequestDuration: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.displayRequestDuration,
},
docExpansion: { typeCaster: stringTypeCaster },
dom_id: { typeCaster: nullableStringTypeCaster },
domNode: { typeCaster: domNodeTypeCaster },
filter: { typeCaster: filterTypeCaster },
layout: { typeCaster: stringTypeCaster },
maxDisplayedTags: {
typeCaster: numberTypeCaster,
defaultValue: defaultOptions.maxDisplayedTags,
},
oauth2RedirectUrl: { typeCaster: undefinedStringTypeCaster },
persistAuthorization: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.persistAuthorization,
},
plugins: {
typeCaster: arrayTypeCaster,
defaultValue: defaultOptions.plugins,
},
pluginsOptions: {
typeCaster: objectTypeCaster,
pluginsOptions: defaultOptions.pluginsOptions,
},
"pluginsOptions.pluginsLoadType": { typeCaster: stringTypeCaster },
presets: {
typeCaster: arrayTypeCaster,
defaultValue: defaultOptions.presets,
},
requestSnippets: {
typeCaster: objectTypeCaster,
defaultValue: defaultOptions.requestSnippets,
},
requestSnippetsEnabled: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.requestSnippetsEnabled,
},
showCommonExtensions: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.showCommonExtensions,
},
showExtensions: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.showExtensions,
},
showMutatedRequest: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.showMutatedRequest,
},
spec: { typeCaster: objectTypeCaster, defaultValue: defaultOptions.spec },
supportedSubmitMethods: {
typeCaster: arrayTypeCaster,
defaultValue: defaultOptions.supportedSubmitMethods,
},
syntaxHighlight: {
typeCaster: syntaxHighlightTypeCaster,
defaultValue: defaultOptions.syntaxHighlight,
},
"syntaxHighlight.activated": {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.syntaxHighlight.activated,
},
"syntaxHighlight.theme": { typeCaster: stringTypeCaster },
tryItOutEnabled: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.tryItOutEnabled,
},
url: { typeCaster: stringTypeCaster },
urls: { typeCaster: nullableArrayTypeCaster },
"urls.primaryName": { typeCaster: stringTypeCaster },
validatorUrl: { typeCaster: nullableStringTypeCaster },
withCredentials: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.withCredentials,
},
}
export default typeCasters

View File

@@ -0,0 +1,7 @@
/**
* @prettier
*/
const arrayTypeCaster = (value, defaultValue = []) =>
Array.isArray(value) ? value : defaultValue
export default arrayTypeCaster

View File

@@ -0,0 +1,11 @@
/**
* @prettier
*/
const booleanTypeCaster = (value, defaultValue = false) =>
value === true || value === "true" || value === 1 || value === "1"
? true
: value === false || value === "false" || value === 0 || value === "0"
? false
: defaultValue
export default booleanTypeCaster

View File

@@ -0,0 +1,7 @@
/**
* @prettier
*/
const domNodeTypeCaster = (value) =>
value === null || value === "null" ? null : value
export default domNodeTypeCaster

View File

@@ -0,0 +1,11 @@
/**
* @prettier
*/
import booleanTypeCaster from "./boolean"
const filterTypeCaster = (value) => {
const defaultValue = String(value)
return booleanTypeCaster(value, defaultValue)
}
export default filterTypeCaster

View File

@@ -0,0 +1,6 @@
/**
* @prettier
*/
const nullableArrayTypeCaster = (value) => (Array.isArray(value) ? value : null)
export default nullableArrayTypeCaster

View File

@@ -0,0 +1,7 @@
/**
* @prettier
*/
const nullableStringTypeCaster = (value) =>
value === null || value === "null" ? null : String(value)
export default nullableStringTypeCaster

View File

@@ -0,0 +1,9 @@
/**
* @prettier
*/
const numberTypeCaster = (value, defaultValue = -1) => {
const parsedValue = parseInt(value, 10)
return Number.isNaN(parsedValue) ? defaultValue : parsedValue
}
export default numberTypeCaster

View File

@@ -0,0 +1,9 @@
/**
* @prettier
*/
import isPlainObject from "lodash/isPlainObject"
const objectTypeCaster = (value, defaultValue = {}) =>
isPlainObject(value) ? value : defaultValue
export default objectTypeCaster

View File

@@ -0,0 +1,6 @@
/**
* @prettier
*/
const stringTypeCaster = (value) => String(value)
export default stringTypeCaster

View File

@@ -0,0 +1,14 @@
/**
* @prettier
*/
import isPlainObject from "lodash/isPlainObject"
const syntaxHighlightTypeCaster = (value, defaultValue) => {
return isPlainObject(value)
? value
: value === false || value === "false" || value === 0 || value === "0"
? { activated: false }
: defaultValue
}
export default syntaxHighlightTypeCaster

View File

@@ -0,0 +1,7 @@
/**
* @prettier
*/
const undefinedStringTypeCaster = (value) =>
value === undefined || value === "undefined" ? undefined : String(value)
export default undefinedStringTypeCaster

View File

@@ -11,7 +11,7 @@ export default class OperationContainer extends PureComponent {
const { tryItOutEnabled } = props.getConfigs()
this.state = {
tryItOutEnabled: tryItOutEnabled === true || tryItOutEnabled === "true",
tryItOutEnabled,
executeInProgress: false
}
}
@@ -61,14 +61,13 @@ export default class OperationContainer extends PureComponent {
const showSummary = layoutSelectors.showSummary()
const operationId = op.getIn(["operation", "__originalOperationId"]) || op.getIn(["operation", "operationId"]) || opId(op.get("operation"), props.path, props.method) || op.get("id")
const isShownKey = ["operations", props.tag, operationId]
const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"
const allowTryItOut = supportedSubmitMethods.indexOf(props.method) >= 0 && (typeof props.allowTryItOut === "undefined" ?
props.specSelectors.allowTryItOutFor(props.path, props.method) : props.allowTryItOut)
const security = op.getIn(["operation", "security"]) || props.specSelectors.security()
return {
operationId,
isDeepLinkingEnabled,
isDeepLinkingEnabled: deepLinking,
showSummary,
displayOperationId,
displayRequestDuration,

View File

@@ -29,11 +29,11 @@ export default class FilterContainer extends React.Component {
return (
<div>
{filter === null || filter === false || filter === "false" ? null :
{filter === false ? null :
<div className="filter-container">
<Col className="filter wrapper" mobile={12}>
<input className={classNames.join(" ")} placeholder="Filter by tag" type="text"
onChange={this.onFilterChange} value={filter === true || filter === "true" ? "" : filter}
onChange={this.onFilterChange} value={typeof filter === "string" ? filter : ""}
disabled={isLoading}/>
</Col>
</div>

View File

@@ -9,12 +9,12 @@ export const taggedOperations = (oriSelector, system) => (state, ...args) => {
// Filter, if requested
let filter = layoutSelectors.currentFilter()
if (filter) {
if (filter !== true && filter !== "true" && filter !== "false") {
if (filter !== true) {
taggedOps = fn.opsFilter(taggedOps, filter)
}
}
// Limit to [max] items, if specified
if (maxDisplayedTags && !isNaN(maxDisplayedTags) && maxDisplayedTags >= 0) {
if (maxDisplayedTags >= 0) {
taggedOps = taggedOps.slice(0, maxDisplayedTags)
}

View File

@@ -2,7 +2,5 @@ export const loaded = (ori, system) => (...args) => {
ori(...args)
const value = system.getConfigs().withCredentials
if(value !== undefined) {
system.fn.fetch.withCredentials = typeof value === "string" ? (value === "true") : !!value
}
system.fn.fetch.withCredentials = value
}

View File

@@ -4,7 +4,6 @@
import React from "react"
import PropTypes from "prop-types"
import ReactSyntaxHighlighter from "react-syntax-highlighter/dist/esm/light"
import get from "lodash/get"
const SyntaxHighlighter = ({
language,
@@ -13,8 +12,7 @@ const SyntaxHighlighter = ({
syntaxHighlighting = {},
children = "",
}) => {
const configs = getConfigs()
const theme = get(configs, "syntaxHighlight.theme")
const theme = getConfigs().syntaxHighlight.theme
const { styles, defaultStyle } = syntaxHighlighting
const style = styles?.[theme] ?? defaultStyle

View File

@@ -3,12 +3,10 @@
*/
import React from "react"
import PropTypes from "prop-types"
import get from "lodash/get"
const SyntaxHighlighterWrapper = (Original, system) => {
const SyntaxHighlighter = ({ renderPlainText, children, ...rest }) => {
const configs = system.getConfigs()
const canSyntaxHighlight = !!get(configs, "syntaxHighlight.activated")
const canSyntaxHighlight = system.getConfigs().syntaxHighlight.activated
const PlainTextViewer = system.getComponent("PlainTextViewer")
if (!canSyntaxHighlight && typeof renderPlainText === "function") {

View File

@@ -30,21 +30,6 @@ describe("<FilterContainer/>", function(){
expect(renderedColInsideFilter.length).toEqual(1)
})
it("does not render FilterContainer if filter is null", function(){
// Given
let props = {...mockedProps}
props.layoutSelectors = {...mockedProps.specSelectors}
props.layoutSelectors.currentFilter = function() {return null}
// When
let wrapper = mount(<FilterContainer {...props}/>)
// Then
const renderedColInsideFilter = wrapper.find(Col)
expect(renderedColInsideFilter.length).toEqual(0)
})
it("does not render FilterContainer if filter is false", function(){
// Given

View File

@@ -0,0 +1,104 @@
/**
* @prettier
*/
import typeCast from "core/config/type-cast"
jest.mock("core/presets/apis", () => {})
describe("typeCast", () => {
it("should cast stringified `true` and `false` values to `boolean`", () => {
const config = {
deepLinking: "true",
tryItOutEnabled: "false",
withCredentials: "true",
filter: "false",
}
const expectedConfig = {
deepLinking: true,
tryItOutEnabled: false,
withCredentials: true,
filter: false,
}
expect(typeCast(config)).toStrictEqual(expectedConfig)
})
it("should cast stringified `number` values to `number`", () => {
const config = {
defaultModelExpandDepth: "5",
defaultModelsExpandDepth: "-1",
maxDisplayedTags: "1",
}
const expectedConfig = {
defaultModelExpandDepth: 5,
defaultModelsExpandDepth: -1,
maxDisplayedTags: 1,
}
expect(typeCast(config)).toStrictEqual(expectedConfig)
})
it("should cast stringified `null` values to `null`", () => {
const config = {
validatorUrl: "null",
}
const expectedConfig = {
validatorUrl: null,
}
expect(typeCast(config)).toStrictEqual(expectedConfig)
})
it("should cast `string` values to `string`", () => {
const config = { defaultModelRendering: "model", filter: "pet" }
const expectedConfig = { defaultModelRendering: "model", filter: "pet" }
expect(typeCast(config)).toStrictEqual(expectedConfig)
})
it("should cast stringified values to correct type", () => {
const config = {
dom_id: "null",
oauth2RedirectUrl: "undefined",
syntaxHighlight: "false",
urls: "null",
}
const expectedConfig = {
dom_id: null,
oauth2RedirectUrl: undefined,
syntaxHighlight: { activated: false },
urls: null,
}
expect(typeCast(config)).toStrictEqual(expectedConfig)
})
it("should cast incorrect value types to default value", () => {
const config = {
deepLinking: "deepLinking",
urls: "urls",
syntaxHighlight: "syntaxHighlight",
spec: "spec",
maxDisplayedTags: "null",
defaultModelExpandDepth: {},
defaultModelsExpandDepth: false,
}
const expectedConfig = {
deepLinking: false,
urls: null,
syntaxHighlight: { activated: true, theme: "agate" },
spec: {},
maxDisplayedTags: -1,
defaultModelExpandDepth: 1,
defaultModelsExpandDepth: 1,
}
expect(typeCast(config)).toStrictEqual(expectedConfig)
})
})

View File

@@ -49,44 +49,6 @@ describe("swagger-client plugin - withCredentials", () => {
const loadedFn = loaded(oriExecute, system)
loadedFn()
expect(oriExecute.mock.calls.length).toBe(1)
expect(system.fn.fetch.withCredentials).toBe(false)
})
it("should allow setting flag to true via config as string", () => {
// for query string config
const system = {
fn: {
fetch: jest.fn().mockImplementation(() => Promise.resolve())
},
getConfigs: () => ({
withCredentials: "true"
})
}
const oriExecute = jest.fn()
const loadedFn = loaded(oriExecute, system)
loadedFn()
expect(oriExecute.mock.calls.length).toBe(1)
expect(system.fn.fetch.withCredentials).toBe(true)
})
it("should allow setting flag to false via config as string", () => {
// for query string config
const system = {
fn: {
fetch: jest.fn().mockImplementation(() => Promise.resolve())
},
getConfigs: () => ({
withCredentials: "false"
})
}
const oriExecute = jest.fn()
const loadedFn = loaded(oriExecute, system)
loadedFn()
expect(oriExecute.mock.calls.length).toBe(1)
expect(system.fn.fetch.withCredentials).toBe(false)
})