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:
91
src/core/config/defaults.js
Normal file
91
src/core/config/defaults.js
Normal 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
|
||||
11
src/core/config/factorization/inline-plugin.js
Normal file
11
src/core/config/factorization/inline-plugin.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
const InlinePluginFactorization = (options) => () => ({
|
||||
fn: options.fn,
|
||||
components: options.components,
|
||||
state: options.state,
|
||||
})
|
||||
|
||||
export default InlinePluginFactorization
|
||||
45
src/core/config/factorization/store.js
Normal file
45
src/core/config/factorization/store.js
Normal 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
7
src/core/config/index.js
Normal 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
40
src/core/config/merge.js
Normal 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
|
||||
15
src/core/config/sources/query.js
Normal file
15
src/core/config/sources/query.js
Normal 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
|
||||
16
src/core/config/sources/system.js
Normal file
16
src/core/config/sources/system.js
Normal 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
|
||||
33
src/core/config/sources/url.js
Normal file
33
src/core/config/sources/url.js
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user