refactor: consolidate all config related code into config module (#9811)

Refs #9806

Co-authored-by: Vladimír Gorej <vladimir.gorej@gmail.com>
This commit is contained in:
Oliwia Rogala
2024-04-15 11:29:11 +02:00
committed by GitHub
parent fb075df9e6
commit 68eb346a23
9 changed files with 303 additions and 190 deletions

View File

@@ -0,0 +1,91 @@
/**
* @prettier
*/
import ApisPreset from "core/presets/apis"
const defaultOptions = Object.freeze({
dom_id: null,
domNode: null,
spec: {},
url: "",
urls: null,
layout: "BaseLayout",
docExpansion: "list",
maxDisplayedTags: null,
filter: null,
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,
configs: {},
custom: {},
displayOperationId: false,
displayRequestDuration: false,
deepLinking: false,
tryItOutEnabled: false,
requestInterceptor: (a) => a,
responseInterceptor: (a) => a,
showMutatedRequest: true,
defaultModelRendering: "example",
defaultModelExpandDepth: 1,
defaultModelsExpandDepth: 1,
showExtensions: false,
showCommonExtensions: false,
withCredentials: undefined,
requestSnippetsEnabled: false,
requestSnippets: {
generators: {
curl_bash: {
title: "cURL (bash)",
syntax: "bash",
},
curl_powershell: {
title: "cURL (PowerShell)",
syntax: "powershell",
},
curl_cmd: {
title: "cURL (CMD)",
syntax: "bash",
},
},
defaultExpanded: true,
languages: null, // e.g. only show curl bash = ["curl_bash"]
},
supportedSubmitMethods: [
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace",
],
queryConfigEnabled: false,
// Initial set of plugins ( TODO rename this, or refactor - we don't need presets _and_ plugins. Its just there for performance.
// Instead, we can compile the first plugin ( it can be a collection of plugins ), then batch the rest.
presets: [ApisPreset],
// Plugins; ( loaded after presets )
plugins: [],
pluginsOptions: {
// Behavior during plugin registration. Can be :
// - legacy (default) : the current behavior for backward compatibility last plugin takes precedence over the others
// - chain : chain wrapComponents when targeting the same core component
pluginLoadType: "legacy",
},
initialState: {},
// Inline Plugin
fn: {},
components: {},
syntaxHighlight: {
activated: true,
theme: "agate",
},
})
export default defaultOptions

View File

@@ -0,0 +1,11 @@
/**
* @prettier
*/
const InlinePluginFactorization = (options) => () => ({
fn: options.fn,
components: options.components,
state: options.state,
})
export default InlinePluginFactorization

View File

@@ -0,0 +1,45 @@
/**
* @prettier
*/
import merge from "../merge"
const storeFactorization = (options) => {
const state = merge(
{
layout: {
layout: options.layout,
filter: options.filter,
},
spec: {
spec: "",
url: options.url,
},
requestSnippets: options.requestSnippets,
},
options.initialState
)
if (options.initialState) {
/**
* If the user sets a key as `undefined`, that signals to us that we
* should delete the key entirely.
* known usage: Swagger-Editor validate plugin tests
*/
for (const [key, value] of Object.entries(options.initialState)) {
if (value === undefined) {
delete state[key]
}
}
}
return {
system: {
configs: options.configs,
},
plugins: options.presets,
pluginsOptions: options.pluginsOptions,
state,
}
}
export default storeFactorization

7
src/core/config/index.js Normal file
View File

@@ -0,0 +1,7 @@
export { default as inlinePluginOptionsFactorization } from "./factorization/inline-plugin"
export { default as storeOptionsFactorization } from "./factorization/store"
export { default as optionsFromQuery } from "./sources/query"
export { default as optionsFromURL } from "./sources/url"
export { default as optionsFromSystem } from "./sources/system"
export { default as defaultOptions } from "./defaults"
export { default as mergeOptions } from "./merge"

40
src/core/config/merge.js Normal file
View File

@@ -0,0 +1,40 @@
/**
* @prettier
*
* We're currently stuck with using deep-extend as it handles the following case:
*
* deepExtend({ a: 1 }, { a: undefined }) => { a: undefined }
*
* 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.
*
* TODO(vladimir.gorej@gmail.com): remove deep-extend in favor of lodash.merge
*/
import deepExtend from "deep-extend"
const merge = (target, ...sources) => {
let domNode = Symbol.for("domNode")
const sourcesWithoutDomNode = []
for (const source of sources) {
if (Object.hasOwn(source, "domNode")) {
domNode = source.domNode
const sourceWithoutDomNode = { ...source }
delete sourceWithoutDomNode.domNode
sourcesWithoutDomNode.push(sourceWithoutDomNode)
} else {
sourcesWithoutDomNode.push(source)
}
}
const merged = deepExtend(target, ...sourcesWithoutDomNode)
if (domNode !== Symbol.for("domNode")) {
merged.domNode = domNode
}
return merged
}
export default merge

View File

@@ -0,0 +1,15 @@
/**
* @prettier
*/
import { parseSearch } from "core/utils"
/**
* Receives options from the query string of the URL where SwaggerUI
* is being served.
*/
const optionsFromQuery = () => (options) => {
return options.queryConfigEnabled ? parseSearch() : {}
}
export default optionsFromQuery

View File

@@ -0,0 +1,16 @@
/**
* @prettier
*
* Receives options from a System.
* These options are baked-in to the System during the compile time.
*/
const optionsFromSystem =
({ system }) =>
() => {
if (typeof system.specSelectors?.getLocalConfig !== "function") return {}
return system.specSelectors.getLocalConfig()
}
export default optionsFromSystem

View File

@@ -0,0 +1,33 @@
/**
* @prettier
* Receives options from a remote URL.
*/
const optionsFromURL =
({ url, system }) =>
async (options) => {
if (!url) return {}
if (typeof system.specActions?.getConfigByUrl !== "function") return {}
let resolve
const deferred = new Promise((res) => {
resolve = res
})
const callback = (fetchedOptions) => {
// receives null on remote URL fetch failure
resolve(fetchedOptions)
}
system.specActions.getConfigByUrl(
{
url,
loadRemoteConfig: true,
requestInterceptor: options.requestInterceptor,
responseInterceptor: options.responseInterceptor,
},
callback
)
return deferred
}
export default optionsFromURL

View File

@@ -1,5 +1,3 @@
import deepExtend from "deep-extend"
import System from "./system"
// presets
import BasePreset from "./presets/base"
@@ -30,14 +28,21 @@ import DownloadUrlPlugin from "./plugins/download-url"
import SyntaxHighlightingPlugin from "core/plugins/syntax-highlighting"
import SafeRenderPlugin from "./plugins/safe-render"
import { parseSearch } from "./utils"
import win from "./window"
import {
defaultOptions,
optionsFromQuery,
optionsFromURL,
optionsFromSystem,
mergeOptions,
inlinePluginOptionsFactorization,
storeOptionsFactorization
} from "./config"
// eslint-disable-next-line no-undef
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION, BUILD_TIME } = buildInfo
export default function SwaggerUI(opts) {
export default function SwaggerUI(userOptions) {
win.versions = win.versions || {}
win.versions.swaggerUi = {
version: PACKAGE_VERSION,
@@ -46,200 +51,50 @@ export default function SwaggerUI(opts) {
buildTimestamp: BUILD_TIME,
}
const defaults = {
// Some general settings, that we floated to the top
dom_id: null, // eslint-disable-line camelcase
domNode: null,
spec: {},
url: "",
urls: null,
layout: "BaseLayout",
docExpansion: "list",
maxDisplayedTags: null,
filter: null,
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,
configs: {},
custom: {},
displayOperationId: false,
displayRequestDuration: false,
deepLinking: false,
tryItOutEnabled: false,
requestInterceptor: (a => a),
responseInterceptor: (a => a),
showMutatedRequest: true,
defaultModelRendering: "example",
defaultModelExpandDepth: 1,
defaultModelsExpandDepth: 1,
showExtensions: false,
showCommonExtensions: false,
withCredentials: undefined,
requestSnippetsEnabled: false,
requestSnippets: {
generators: {
"curl_bash": {
title: "cURL (bash)",
syntax: "bash"
},
"curl_powershell": {
title: "cURL (PowerShell)",
syntax: "powershell"
},
"curl_cmd": {
title: "cURL (CMD)",
syntax: "bash"
},
},
defaultExpanded: true,
languages: null, // e.g. only show curl bash = ["curl_bash"]
},
supportedSubmitMethods: [
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace"
],
queryConfigEnabled: false,
const queryOptions = optionsFromQuery()(userOptions)
let mergedOptions = mergeOptions({}, defaultOptions, userOptions, queryOptions)
const storeOptions = storeOptionsFactorization(mergedOptions)
const InlinePlugin = inlinePluginOptionsFactorization(mergedOptions)
// Initial set of plugins ( TODO rename this, or refactor - we don't need presets _and_ plugins. Its just there for performance.
// Instead, we can compile the first plugin ( it can be a collection of plugins ), then batch the rest.
presets: [
ApisPreset
],
// Plugins; ( loaded after presets )
plugins: [
],
const store = new System(storeOptions)
store.register([mergedOptions.plugins, InlinePlugin])
const system = store.getSystem()
pluginsOptions: {
// Behavior during plugin registration. Can be :
// - legacy (default) : the current behavior for backward compatibility last plugin takes precedence over the others
// - chain : chain wrapComponents when targeting the same core component
pluginLoadType: "legacy"
},
const configURL = queryOptions.config ?? mergedOptions.configUrl
const systemOptions = optionsFromSystem({ system })(mergedOptions)
// Initial state
initialState: { },
optionsFromURL({ url: configURL, system })(mergedOptions)
.then((urlOptions) => {
const urlOptionsFailedToFetch = urlOptions === null
// Inline Plugin
fn: { },
components: { },
mergedOptions = mergeOptions({}, systemOptions, mergedOptions, urlOptions, queryOptions)
store.setConfigs(mergedOptions)
system.configsActions.loaded()
syntaxHighlight: {
activated: true,
theme: "agate"
}
}
let queryConfig = opts.queryConfigEnabled ? parseSearch() : {}
const domNode = opts.domNode
delete opts.domNode
const constructorConfig = deepExtend({}, defaults, opts, queryConfig)
const storeConfigs = {
system: {
configs: constructorConfig.configs
},
plugins: constructorConfig.presets,
pluginsOptions: constructorConfig.pluginsOptions,
state: deepExtend({
layout: {
layout: constructorConfig.layout,
filter: constructorConfig.filter
},
spec: {
spec: "",
// support Relative References
url: constructorConfig.url,
},
requestSnippets: constructorConfig.requestSnippets
}, constructorConfig.initialState)
}
if(constructorConfig.initialState) {
// if the user sets a key as `undefined`, that signals to us that we
// should delete the key entirely.
// known usage: Swagger-Editor validate plugin tests
for (var key in constructorConfig.initialState) {
if(
Object.prototype.hasOwnProperty.call(constructorConfig.initialState, key)
&& constructorConfig.initialState[key] === undefined
) {
delete storeConfigs.state[key]
if (!urlOptionsFailedToFetch) {
if (!queryOptions.url && typeof mergedOptions.spec === "object" && Object.keys(mergedOptions.spec).length > 0) {
system.specActions.updateUrl("")
system.specActions.updateLoadingStatus("success")
system.specActions.updateSpec(JSON.stringify(mergedOptions.spec))
} else if (typeof system.specActions.download === "function" && mergedOptions.url && !mergedOptions.urls) {
system.specActions.updateUrl(mergedOptions.url)
system.specActions.download(mergedOptions.url)
}
}
}
}
let inlinePlugin = ()=> {
return {
fn: constructorConfig.fn,
components: constructorConfig.components,
state: constructorConfig.state,
}
}
var store = new System(storeConfigs)
store.register([constructorConfig.plugins, inlinePlugin])
var system = store.getSystem()
const downloadSpec = (fetchedConfig) => {
let localConfig = system.specSelectors.getLocalConfig ? system.specSelectors.getLocalConfig() : {}
let mergedConfig = deepExtend({}, localConfig, constructorConfig, fetchedConfig || {}, queryConfig)
// deep extend mangles domNode, we need to set it manually
if(domNode) {
mergedConfig.domNode = domNode
}
store.setConfigs(mergedConfig)
system.configsActions.loaded()
if (fetchedConfig !== null) {
if (!queryConfig.url && typeof mergedConfig.spec === "object" && Object.keys(mergedConfig.spec).length) {
system.specActions.updateUrl("")
system.specActions.updateLoadingStatus("success")
system.specActions.updateSpec(JSON.stringify(mergedConfig.spec))
} else if (system.specActions.download && mergedConfig.url && !mergedConfig.urls) {
system.specActions.updateUrl(mergedConfig.url)
system.specActions.download(mergedConfig.url)
if (mergedOptions.domNode) {
system.render(mergedOptions.domNode, "App")
} else if(mergedOptions.dom_id) {
let domNode = document.querySelector(mergedOptions.dom_id)
system.render(domNode, "App")
} else if(mergedOptions.dom_id === null || mergedOptions.domNode === null) {
// do nothing
// this is useful for testing that does not need to do any rendering
} else {
console.error("Skipped rendering: no `dom_id` or `domNode` was specified")
}
}
if(mergedConfig.domNode) {
system.render(mergedConfig.domNode, "App")
} else if(mergedConfig.dom_id) {
let domNode = document.querySelector(mergedConfig.dom_id)
system.render(domNode, "App")
} else if(mergedConfig.dom_id === null || mergedConfig.domNode === null) {
// do nothing
// this is useful for testing that does not need to do any rendering
} else {
console.error("Skipped rendering: no `dom_id` or `domNode` was specified")
}
return system
}
const configUrl = queryConfig.config || constructorConfig.configUrl
if (configUrl && system.specActions && system.specActions.getConfigByUrl) {
system.specActions.getConfigByUrl({
url: configUrl,
loadRemoteConfig: true,
requestInterceptor: constructorConfig.requestInterceptor,
responseInterceptor: constructorConfig.responseInterceptor,
}, downloadSpec)
} else {
return downloadSpec()
}
})
return system
}