feat: lazy resolver (#4249)
* default to empty `ImmutableMap` when grabbing op metadata
* pass `errors` into JsonSchema components
* Account for Immutable data structure in JavaScriptonSchema...
...and create empty Lists instead of Maps by default.
* Pass ImmutableList through to JsonSchema child components
* Add lazy resolving spec state extensions
* TEMPORARY: disable conventional resolved spec
* WIP
* Use resolveSubtree in Operation display
* Freebie: short-circuit Markdown component if it is given plaintext
* NEW DEFAULT BEHAVIOR: `defaultModelsExpandDepth: 1` does not expand individual models
* Render faked Model expander to trigger resolution
* Baseline support for Editor lifecycles
* Display operation summaries before the operation is resolved
* Test migrations
* WIP
* Swagger2 TIO Body params
* a bit of cleanup
* Debounce string param inputs
* Reach into unresolved operation for deprecated flag, if available
* Fire subtree request outside of render
* Remove debugging flags
* Fix logical errors in spec statePlugins
* TODOs become TODONEs!
* Migrate deeplinking feature to non-resolved spec action
* ESLint fixes
This commit is contained in:
@@ -4,7 +4,7 @@ import { escapeDeepLinkPath } from "core/utils"
|
||||
let hasHashBeenParsed = false //TODO this forces code to only run once which may prevent scrolling if page not refreshed
|
||||
|
||||
|
||||
export const updateResolved = (ori, { layoutActions, getConfigs }) => (...args) => {
|
||||
export const updateJsonSpec = (ori, { layoutActions, getConfigs }) => (...args) => {
|
||||
ori(...args)
|
||||
|
||||
const isDeepLinkingEnabled = getConfigs().deepLinking
|
||||
|
||||
@@ -6,6 +6,7 @@ export const NEW_SPEC_ERR = "err_new_spec_err"
|
||||
export const NEW_SPEC_ERR_BATCH = "err_new_spec_err_batch"
|
||||
export const NEW_AUTH_ERR = "err_new_auth_err"
|
||||
export const CLEAR = "err_clear"
|
||||
export const CLEAR_BY = "err_clear_by"
|
||||
|
||||
export function newThrownErr(err) {
|
||||
return {
|
||||
@@ -49,3 +50,11 @@ export function clear(filter = {}) {
|
||||
payload: filter
|
||||
}
|
||||
}
|
||||
|
||||
export function clearBy(filter = () => true) {
|
||||
// filter is a function
|
||||
return {
|
||||
type: CLEAR_BY,
|
||||
payload: filter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@ import {
|
||||
NEW_SPEC_ERR,
|
||||
NEW_SPEC_ERR_BATCH,
|
||||
NEW_AUTH_ERR,
|
||||
CLEAR
|
||||
CLEAR,
|
||||
CLEAR_BY,
|
||||
} from "./actions"
|
||||
|
||||
import reject from "lodash/reject"
|
||||
|
||||
import Im, { fromJS, List } from "immutable"
|
||||
import { fromJS, List } from "immutable"
|
||||
|
||||
import transformErrors from "./error-transformers/hook"
|
||||
|
||||
@@ -65,11 +64,34 @@ export default function(system) {
|
||||
},
|
||||
|
||||
[CLEAR]: (state, { payload }) => {
|
||||
if(!payload) {
|
||||
return
|
||||
if(!payload || !state.get("errors")) {
|
||||
return state
|
||||
}
|
||||
// TODO: Rework, to use immutable only, no need for lodash
|
||||
let newErrors = Im.fromJS(reject((state.get("errors") || List()).toJS(), payload))
|
||||
|
||||
let newErrors = state.get("errors")
|
||||
.filter(err => {
|
||||
return err.keySeq().every(k => {
|
||||
const errValue = err.get(k)
|
||||
const filterValue = payload[k]
|
||||
|
||||
if(!filterValue) return true
|
||||
|
||||
return errValue !== filterValue
|
||||
})
|
||||
})
|
||||
return state.merge({
|
||||
errors: newErrors
|
||||
})
|
||||
},
|
||||
|
||||
[CLEAR_BY]: (state, { payload }) => {
|
||||
if(!payload || typeof payload !== "function") {
|
||||
return state
|
||||
}
|
||||
let newErrors = state.get("errors")
|
||||
.filter(err => {
|
||||
return payload(err)
|
||||
})
|
||||
return state.merge({
|
||||
errors: newErrors
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ const state = state => {
|
||||
return state || Map()
|
||||
}
|
||||
|
||||
const nullSelector = createSelector(() => null)
|
||||
const nullSelector = createSelector(() => null)
|
||||
|
||||
const OAS3NullSelector = onlyOAS3(nullSelector)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import YAML from "js-yaml"
|
||||
import parseUrl from "url-parse"
|
||||
import serializeError from "serialize-error"
|
||||
import { Map } from "immutable"
|
||||
import isString from "lodash/isString"
|
||||
import { isJSONObject } from "core/utils"
|
||||
|
||||
@@ -21,6 +22,7 @@ export const CLEAR_REQUEST = "spec_clear_request"
|
||||
export const CLEAR_VALIDATE_PARAMS = "spec_clear_validate_param"
|
||||
export const UPDATE_OPERATION_META_VALUE = "spec_update_operation_meta_value"
|
||||
export const UPDATE_RESOLVED = "spec_update_resolved"
|
||||
export const UPDATE_RESOLVED_SUBTREE = "spec_update_resolved_subtree"
|
||||
export const SET_SCHEME = "set_scheme"
|
||||
|
||||
const toStr = (str) => isString(str) ? str : ""
|
||||
@@ -74,7 +76,14 @@ export const parseToJson = (str) => ({specActions, specSelectors, errActions}) =
|
||||
return {}
|
||||
}
|
||||
|
||||
let hasWarnedAboutResolveSpecDeprecation = false
|
||||
|
||||
export const resolveSpec = (json, url) => ({specActions, specSelectors, errActions, fn: { fetch, resolve, AST }, getConfigs}) => {
|
||||
if(!hasWarnedAboutResolveSpecDeprecation) {
|
||||
console.warn(`specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use resolveIn instead!`)
|
||||
hasWarnedAboutResolveSpecDeprecation = true
|
||||
}
|
||||
|
||||
const {
|
||||
modelPropertyMacro,
|
||||
parameterMacro,
|
||||
@@ -124,6 +133,55 @@ export const resolveSpec = (json, url) => ({specActions, specSelectors, errActio
|
||||
})
|
||||
}
|
||||
|
||||
export const requestResolvedSubtree = path => system => {
|
||||
const {
|
||||
errActions,
|
||||
fn: {
|
||||
resolveSubtree,
|
||||
AST: { getLineNumberForPath }
|
||||
},
|
||||
specSelectors,
|
||||
specActions,
|
||||
} = system
|
||||
|
||||
const specStr = specSelectors.specStr()
|
||||
|
||||
if(!resolveSubtree) {
|
||||
console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.")
|
||||
return
|
||||
}
|
||||
|
||||
const currentValue = specSelectors.specResolvedSubtree(path)
|
||||
|
||||
if(currentValue) {
|
||||
return
|
||||
}
|
||||
|
||||
return resolveSubtree(specSelectors.specJson().toJS(), path)
|
||||
.then(({ spec, errors }) => {
|
||||
errActions.clear({
|
||||
type: "thrown"
|
||||
})
|
||||
if(Array.isArray(errors) && errors.length > 0) {
|
||||
let preparedErrors = errors
|
||||
.map(err => {
|
||||
console.error(err)
|
||||
err.line = err.fullPath ? getLineNumberForPath(specStr, err.fullPath) : null
|
||||
err.path = err.fullPath ? err.fullPath.join(".") : null
|
||||
err.level = "error"
|
||||
err.type = "thrown"
|
||||
err.source = "resolver"
|
||||
Object.defineProperty(err, "message", { enumerable: true, value: err.message })
|
||||
return err
|
||||
})
|
||||
errActions.newThrownErrBatch(preparedErrors)
|
||||
}
|
||||
|
||||
return specActions.updateResolvedSubtree(path, spec)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
|
||||
export function changeParam( path, paramName, paramIn, value, isXml ){
|
||||
return {
|
||||
type: UPDATE_PARAM,
|
||||
@@ -131,6 +189,23 @@ export function changeParam( path, paramName, paramIn, value, isXml ){
|
||||
}
|
||||
}
|
||||
|
||||
export const updateResolvedSubtree = (path, value) => {
|
||||
return {
|
||||
type: UPDATE_RESOLVED_SUBTREE,
|
||||
payload: { path, value }
|
||||
}
|
||||
}
|
||||
|
||||
export const invalidateResolvedSubtreeCache = () => {
|
||||
return {
|
||||
type: UPDATE_RESOLVED_SUBTREE,
|
||||
payload: {
|
||||
path: [],
|
||||
value: Map()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const validateParams = ( payload, isOAS3 ) =>{
|
||||
return {
|
||||
type: VALIDATE_PARAMS,
|
||||
@@ -251,6 +326,7 @@ export const executeRequest = (req) =>
|
||||
// track duration of request
|
||||
const startTime = Date.now()
|
||||
|
||||
|
||||
return fn.execute(req)
|
||||
.then( res => {
|
||||
res.duration = Date.now() - startTime
|
||||
@@ -267,13 +343,22 @@ export const executeRequest = (req) =>
|
||||
// I'm using extras as a way to inject properties into the final, `execute` method - It's not great. Anyone have a better idea? @ponelat
|
||||
export const execute = ( { path, method, ...extras }={} ) => (system) => {
|
||||
let { fn:{fetch}, specSelectors, specActions } = system
|
||||
let spec = specSelectors.spec().toJS()
|
||||
let spec = specSelectors.specJsonWithResolvedSubtrees().toJS()
|
||||
let scheme = specSelectors.operationScheme(path, method)
|
||||
let { requestContentType, responseContentType } = specSelectors.contentTypeValues([path, method]).toJS()
|
||||
let isXml = /xml/i.test(requestContentType)
|
||||
let parameters = specSelectors.parameterValues([path, method], isXml).toJS()
|
||||
|
||||
return specActions.executeRequest({fetch, spec, pathName: path, method, parameters, requestContentType, scheme, responseContentType, ...extras })
|
||||
return specActions.executeRequest({
|
||||
...extras,
|
||||
fetch,
|
||||
spec,
|
||||
pathName: path,
|
||||
method, parameters,
|
||||
requestContentType,
|
||||
scheme,
|
||||
responseContentType
|
||||
})
|
||||
}
|
||||
|
||||
export function clearResponse (path, method) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { fromJS, List } from "immutable"
|
||||
import { fromJSOrdered, validateParam } from "core/utils"
|
||||
import win from "../../window"
|
||||
|
||||
// selector-in-reducer is suboptimal, but `operationWithMeta` is more of a helper
|
||||
import {
|
||||
operationWithMeta
|
||||
} from "./selectors"
|
||||
|
||||
import {
|
||||
UPDATE_SPEC,
|
||||
UPDATE_URL,
|
||||
@@ -12,6 +17,7 @@ import {
|
||||
SET_REQUEST,
|
||||
SET_MUTATED_REQUEST,
|
||||
UPDATE_RESOLVED,
|
||||
UPDATE_RESOLVED_SUBTREE,
|
||||
UPDATE_OPERATION_META_VALUE,
|
||||
CLEAR_RESPONSE,
|
||||
CLEAR_REQUEST,
|
||||
@@ -39,38 +45,38 @@ export default {
|
||||
return state.setIn(["resolved"], fromJSOrdered(action.payload))
|
||||
},
|
||||
|
||||
[UPDATE_PARAM]: ( state, {payload} ) => {
|
||||
let { path, paramName, paramIn, value, isXml } = payload
|
||||
[UPDATE_RESOLVED_SUBTREE]: (state, action) => {
|
||||
const { value, path } = action.payload
|
||||
return state.setIn(["resolvedSubtrees", ...path], fromJSOrdered(value))
|
||||
},
|
||||
|
||||
return state.updateIn( [ "resolved", "paths", ...path, "parameters" ], fromJS([]), parameters => {
|
||||
const index = parameters.findIndex(p => p.get( "name" ) === paramName && p.get("in") === paramIn )
|
||||
if (!(value instanceof win.File)) {
|
||||
value = fromJSOrdered( value )
|
||||
}
|
||||
return parameters.setIn( [ index, isXml ? "value_xml" : "value" ], value)
|
||||
})
|
||||
[UPDATE_PARAM]: ( state, {payload} ) => {
|
||||
let { path: pathMethod, paramName, paramIn, value, isXml } = payload
|
||||
|
||||
const valueKey = isXml ? "value_xml" : "value"
|
||||
|
||||
return state.setIn(
|
||||
["meta", "paths", ...pathMethod, "parameters", `${paramName}.${paramIn}`, valueKey],
|
||||
value
|
||||
)
|
||||
},
|
||||
|
||||
[VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => {
|
||||
let meta = state.getIn( [ "meta", "paths", ...pathMethod ], fromJS({}) )
|
||||
let isXml = /xml/i.test(meta.get("consumes_value"))
|
||||
|
||||
return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => {
|
||||
return parameters.withMutations( parameters => {
|
||||
for ( let i = 0, len = parameters.count(); i < len; i++ ) {
|
||||
let errors = validateParam(parameters.get(i), isXml, isOAS3)
|
||||
parameters.setIn([i, "errors"], fromJS(errors))
|
||||
}
|
||||
})
|
||||
const op = operationWithMeta(state, ...pathMethod)
|
||||
|
||||
return state.updateIn(["meta", "paths", ...pathMethod, "parameters"], fromJS({}), paramMeta => {
|
||||
return op.get("parameters", List()).reduce((res, param) => {
|
||||
const errors = validateParam(param, isXml, isOAS3)
|
||||
return res.setIn([`${param.get("name")}.${param.get("in")}`, "errors"], fromJS(errors))
|
||||
}, paramMeta)
|
||||
})
|
||||
},
|
||||
[CLEAR_VALIDATE_PARAMS]: ( state, { payload: { pathMethod } } ) => {
|
||||
return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => {
|
||||
return parameters.withMutations( parameters => {
|
||||
for ( let i = 0, len = parameters.count(); i < len; i++ ) {
|
||||
parameters.setIn([i, "errors"], fromJS([]))
|
||||
}
|
||||
})
|
||||
return state.updateIn( [ "meta", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => {
|
||||
return parameters.map(param => param.set("errors", fromJS([])))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -109,10 +115,10 @@ export default {
|
||||
|
||||
[UPDATE_OPERATION_META_VALUE]: (state, { payload: { path, value, key } }) => {
|
||||
// path is a pathMethod tuple... can't change the name now.
|
||||
let operationPath = ["resolved", "paths", ...path]
|
||||
let operationPath = ["paths", ...path]
|
||||
let metaPath = ["meta", "paths", ...path]
|
||||
|
||||
if(!state.getIn(operationPath)) {
|
||||
if(!state.getIn(["json", ...operationPath]) && !state.getIn(["resolved", ...operationPath])) {
|
||||
// do nothing if the operation does not exist
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -42,9 +42,18 @@ export const specResolved = createSelector(
|
||||
spec => spec.get("resolved", Map())
|
||||
)
|
||||
|
||||
export const specResolvedSubtree = (state, path) => {
|
||||
return state.getIn(["resolvedSubtrees", ...path], undefined)
|
||||
}
|
||||
|
||||
export const specJsonWithResolvedSubtrees = createSelector(
|
||||
state,
|
||||
spec => Map().merge(spec.get("json"), spec.get("resolvedSubtrees"))
|
||||
)
|
||||
|
||||
// Default Spec ( as an object )
|
||||
export const spec = state => {
|
||||
let res = specResolved(state)
|
||||
let res = specJson(state)
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -137,7 +146,9 @@ export const securityDefinitions = createSelector(
|
||||
|
||||
|
||||
export const findDefinition = ( state, name ) => {
|
||||
return specResolved(state).getIn(["definitions", name], null)
|
||||
const resolvedRes = state.getIn(["resolvedSubtrees", "definitions", name], null)
|
||||
const unresolvedRes = state.getIn(["json", "definitions", name], null)
|
||||
return resolvedRes || unresolvedRes || null
|
||||
}
|
||||
|
||||
export const definitions = createSelector(
|
||||
@@ -261,10 +272,40 @@ export const allowTryItOutFor = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
export const operationWithMeta = (state, path, method) => {
|
||||
const op = specJsonWithResolvedSubtrees(state).getIn(["paths", path, method], Map())
|
||||
const meta = state.getIn(["meta", "paths", path, method], Map())
|
||||
|
||||
const mergedParams = op.get("parameters", List()).map((param) => {
|
||||
return Map().merge(
|
||||
param,
|
||||
meta.getIn(["parameters", `${param.get("name")}.${param.get("in")}`])
|
||||
)
|
||||
})
|
||||
|
||||
return Map()
|
||||
.merge(op, meta)
|
||||
.set("parameters", mergedParams)
|
||||
}
|
||||
|
||||
export const parameterWithMeta = (state, pathMethod, paramName, paramIn) => {
|
||||
const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], Map())
|
||||
const metaParams = state.getIn(["meta", "paths", ...pathMethod, "parameters"], Map())
|
||||
|
||||
const mergedParams = opParams.map((param) => {
|
||||
return Map().merge(
|
||||
param,
|
||||
metaParams.get(`${param.get("name")}.${param.get("in")}`)
|
||||
)
|
||||
})
|
||||
|
||||
return mergedParams.find(param => param.get("in") === paramIn && param.get("name") === paramName, Map())
|
||||
}
|
||||
|
||||
// Get the parameter value by parameter name
|
||||
export function getParameter(state, pathMethod, name, inType) {
|
||||
pathMethod = pathMethod || []
|
||||
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([]))
|
||||
let params = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
|
||||
return params.find( (p) => {
|
||||
return Map.isMap(p) && p.get("name") === name && p.get("in") === inType
|
||||
}) || Map() // Always return a map
|
||||
@@ -281,8 +322,9 @@ export const hasHost = createSelector(
|
||||
// Get the parameter values, that the user filled out
|
||||
export function parameterValues(state, pathMethod, isXml) {
|
||||
pathMethod = pathMethod || []
|
||||
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([]))
|
||||
return params.reduce( (hash, p) => {
|
||||
// let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
|
||||
let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List())
|
||||
return paramValues.reduce( (hash, p) => {
|
||||
let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value")
|
||||
return hash.set(`${p.get("in")}.${p.get("name")}`, value)
|
||||
}, fromJS({}))
|
||||
@@ -305,7 +347,7 @@ export function parametersIncludeType(parameters, typeValue="") {
|
||||
// Get the consumes/produces value that the user selected
|
||||
export function contentTypeValues(state, pathMethod) {
|
||||
pathMethod = pathMethod || []
|
||||
let op = spec(state).getIn(["paths", ...pathMethod], fromJS({}))
|
||||
let op = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod], fromJS({}))
|
||||
let meta = state.getIn(["meta", "paths", ...pathMethod], fromJS({}))
|
||||
let producesValue = currentProducesFor(state, pathMethod)
|
||||
|
||||
@@ -327,14 +369,14 @@ export function contentTypeValues(state, pathMethod) {
|
||||
// Get the consumes/produces by path
|
||||
export function operationConsumes(state, pathMethod) {
|
||||
pathMethod = pathMethod || []
|
||||
return spec(state).getIn(["paths", ...pathMethod, "consumes"], fromJS({}))
|
||||
return state.getIn(["meta", ...pathMethod, "consumes"], fromJS({}))
|
||||
}
|
||||
|
||||
// Get the currently selected produces value for an operation
|
||||
export function currentProducesFor(state, pathMethod) {
|
||||
pathMethod = pathMethod || []
|
||||
|
||||
const operation = spec(state).getIn(["paths", ...pathMethod], null)
|
||||
const operation = specJsonWithResolvedSubtrees(state).getIn([ "paths", ...pathMethod], null)
|
||||
|
||||
if(operation === null) {
|
||||
// return nothing if the operation does not exist
|
||||
@@ -362,10 +404,10 @@ export const canExecuteScheme = ( state, path, method ) => {
|
||||
|
||||
export const validateBeforeExecute = ( state, pathMethod ) => {
|
||||
pathMethod = pathMethod || []
|
||||
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([]))
|
||||
let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
|
||||
let isValid = true
|
||||
|
||||
params.forEach( (p) => {
|
||||
paramValues.forEach( (p) => {
|
||||
let errors = p.get("errors")
|
||||
if ( errors && errors.count() ) {
|
||||
isValid = false
|
||||
|
||||
@@ -5,7 +5,7 @@ export const updateSpec = (ori, {specActions}) => (...args) => {
|
||||
|
||||
export const updateJsonSpec = (ori, {specActions}) => (...args) => {
|
||||
ori(...args)
|
||||
specActions.resolveSpec(...args)
|
||||
specActions.invalidateResolvedSubtreeCache()
|
||||
}
|
||||
|
||||
// Log the request ( just for debugging, shouldn't affect prod )
|
||||
@@ -16,4 +16,4 @@ export const executeRequest = (ori, { specActions }) => (req) => {
|
||||
|
||||
export const validateParams = (ori, { specSelectors }) => (req) => {
|
||||
return ori(req, specSelectors.isOAS3())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = function({ configs }) {
|
||||
buildRequest: Swagger.buildRequest,
|
||||
execute: Swagger.execute,
|
||||
resolve: Swagger.resolve,
|
||||
resolveSubtree: Swagger.resolveSubtree,
|
||||
serializeRes: Swagger.serializeRes,
|
||||
opId: Swagger.helpers.opId
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user