* 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 fixesbubble
@@ -63,6 +63,7 @@ | |||
"react-addons-shallow-compare": "0.14.8", | |||
"react-addons-test-utils": "^15.6.2", | |||
"react-collapse": "^4.0.3", | |||
"react-debounce-input": "^3.2.0", | |||
"react-dom": "^15.6.2", | |||
"react-height": "^2.0.0", | |||
"react-hot-loader": "1.3.1", | |||
@@ -8,14 +8,17 @@ export default class ModelCollapse extends Component { | |||
children: PropTypes.any, | |||
title: PropTypes.element, | |||
modelName: PropTypes.string, | |||
onToggle: PropTypes.func | |||
classes: PropTypes.string, | |||
onToggle: PropTypes.func, | |||
hideSelfOnExpand: PropTypes.bool, | |||
} | |||
static defaultProps = { | |||
collapsedContent: "{...}", | |||
expanded: false, | |||
title: null, | |||
onToggle: () => {} | |||
onToggle: () => {}, | |||
hideSelfOnExpand: false | |||
} | |||
constructor(props, context) { | |||
@@ -29,17 +32,23 @@ export default class ModelCollapse extends Component { | |||
} | |||
} | |||
componentWillReceiveProps(nextProps){ | |||
componentDidMount() { | |||
const { hideSelfOnExpand, expanded, modelName } = this.props | |||
if(hideSelfOnExpand && expanded) { | |||
// We just mounted pre-expanded, and we won't be going back.. | |||
// So let's give our parent an `onToggle` call.. | |||
// Since otherwise it will never be called. | |||
this.props.onToggle(modelName, expanded) | |||
} | |||
} | |||
if(this.props.expanded!= nextProps.expanded){ | |||
componentWillReceiveProps(nextProps){ | |||
if(this.props.expanded !== nextProps.expanded){ | |||
this.setState({expanded: nextProps.expanded}) | |||
} | |||
} | |||
toggleCollapsed=()=>{ | |||
if(this.props.onToggle){ | |||
this.props.onToggle(this.props.modelName,!this.state.expanded) | |||
} | |||
@@ -50,9 +59,18 @@ export default class ModelCollapse extends Component { | |||
} | |||
render () { | |||
const {title} = this.props | |||
const { title, classes } = this.props | |||
if(this.state.expanded ) { | |||
if(this.props.hideSelfOnExpand) { | |||
return <span className={classes || ""}> | |||
{this.props.children} | |||
</span> | |||
} | |||
} | |||
return ( | |||
<span> | |||
<span className={classes || ""}> | |||
{ title && <span onClick={this.toggleCollapsed} style={{ "cursor": "pointer" }}>{title}</span> } | |||
<span onClick={ this.toggleCollapsed } style={{ "cursor": "pointer" }}> | |||
<span className={ "model-toggle" + ( this.state.expanded ? "" : " collapsed" ) }></span> | |||
@@ -6,11 +6,29 @@ export default class Models extends Component { | |||
static propTypes = { | |||
getComponent: PropTypes.func, | |||
specSelectors: PropTypes.object, | |||
specActions: PropTypes.object.isRequired, | |||
layoutSelectors: PropTypes.object, | |||
layoutActions: PropTypes.object, | |||
getConfigs: PropTypes.func.isRequired | |||
} | |||
getSchemaBasePath = () => { | |||
const isOAS3 = this.props.specSelectors.isOAS3() | |||
return isOAS3 ? ["components", "schemas"] : ["definitions"] | |||
} | |||
getCollapsedContent = () => { | |||
return " " | |||
} | |||
handleToggle = (name, isExpanded) => { | |||
const { layoutActions } = this.props | |||
layoutActions.show(["models", name], isExpanded) | |||
if(isExpanded) { | |||
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name]) | |||
} | |||
} | |||
render(){ | |||
let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props | |||
let definitions = specSelectors.definitions() | |||
@@ -18,10 +36,11 @@ export default class Models extends Component { | |||
if (!definitions.size || defaultModelsExpandDepth < 0) return null | |||
let showModels = layoutSelectors.isShown("models", defaultModelsExpandDepth > 0 && docExpansion !== "none") | |||
const specPathBase = specSelectors.isOAS3() ? ["components", "schemas"] : ["definitions"] | |||
const specPathBase = this.getSchemaBasePath() | |||
const ModelWrapper = getComponent("ModelWrapper") | |||
const Collapse = getComponent("Collapse") | |||
const ModelCollapse = getComponent("ModelCollapse") | |||
return <section className={ showModels ? "models is-open" : "models"}> | |||
<h4 onClick={() => layoutActions.show("models", !showModels)}> | |||
@@ -32,18 +51,40 @@ export default class Models extends Component { | |||
</h4> | |||
<Collapse isOpened={showModels}> | |||
{ | |||
definitions.entrySeq().map( ( [ name, model ])=>{ | |||
definitions.entrySeq().map( ( [ name ])=>{ | |||
const schema = specSelectors.specResolvedSubtree([...specPathBase, name]) | |||
if(layoutSelectors.isShown(["models", name], false) && schema === undefined) { | |||
// Firing an action in a container render is not great, | |||
// but it works for now. | |||
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name]) | |||
} | |||
const content = <ModelWrapper name={ name } | |||
expandDepth={ defaultModelsExpandDepth } | |||
schema={ schema } | |||
specPath={Im.List([...specPathBase, name])} | |||
getComponent={ getComponent } | |||
specSelectors={ specSelectors } | |||
getConfigs = {getConfigs} | |||
layoutSelectors = {layoutSelectors} | |||
layoutActions = {layoutActions}/> | |||
const title = <span className="model-box"> | |||
<span className="model model-title">{name}</span> | |||
</span> | |||
return <div id={ `model-${name}` } className="model-container" key={ `models-section-${name}` }> | |||
<ModelWrapper name={ name } | |||
expandDepth={ defaultModelsExpandDepth } | |||
schema={ model } | |||
specPath={Im.List([...specPathBase, name])} | |||
getComponent={ getComponent } | |||
specSelectors={ specSelectors } | |||
getConfigs = {getConfigs} | |||
layoutSelectors = {layoutSelectors} | |||
layoutActions = {layoutActions}/> | |||
<ModelCollapse | |||
classes="model-box" | |||
collapsedContent={this.getCollapsedContent(name)} | |||
onToggle={this.handleToggle} | |||
title={title} | |||
modelName={name} | |||
hideSelfOnExpand={true} | |||
expanded={defaultModelsExpandDepth > 1} | |||
>{content}</ModelCollapse> | |||
</div> | |||
}).toArray() | |||
} | |||
@@ -9,6 +9,7 @@ export default class Operation extends PureComponent { | |||
static propTypes = { | |||
specPath: ImPropTypes.list.isRequired, | |||
operation: PropTypes.instanceOf(Iterable).isRequired, | |||
summary: PropTypes.string, | |||
response: PropTypes.instanceOf(Iterable), | |||
request: PropTypes.instanceOf(Iterable), | |||
@@ -34,7 +35,8 @@ export default class Operation extends PureComponent { | |||
operation: null, | |||
response: null, | |||
request: null, | |||
specPath: List() | |||
specPath: List(), | |||
summary: "" | |||
} | |||
render() { | |||
@@ -59,6 +61,8 @@ export default class Operation extends PureComponent { | |||
let operationProps = this.props.operation | |||
let { | |||
summary, | |||
deprecated, | |||
isShown, | |||
isAuthorized, | |||
path, | |||
@@ -76,14 +80,13 @@ export default class Operation extends PureComponent { | |||
} = operationProps.toJS() | |||
let { | |||
summary, | |||
summary: resolvedSummary, | |||
description, | |||
deprecated, | |||
externalDocs, | |||
schemes | |||
} = op.operation | |||
} = op | |||
let operation = operationProps.getIn(["op", "operation"]) | |||
let operation = operationProps.getIn(["op"]) | |||
let security = operationProps.get("security") | |||
let responses = operation.get("responses") | |||
let produces = operation.get("produces") | |||
@@ -132,7 +135,7 @@ export default class Operation extends PureComponent { | |||
{ !showSummary ? null : | |||
<div className="opblock-summary-description"> | |||
{ summary } | |||
{ resolvedSummary || summary } | |||
</div> | |||
} | |||
@@ -47,7 +47,7 @@ export default class ParamBody extends PureComponent { | |||
updateValues = (props) => { | |||
let { specSelectors, pathMethod, param, isExecute, consumesValue="" } = props | |||
let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) : fromJS({}) | |||
let parameter = specSelectors ? specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) : fromJS({}) | |||
let isXml = /xml/i.test(consumesValue) | |||
let isJson = /json/i.test(consumesValue) | |||
let paramValue = isXml ? parameter.get("value_xml") : parameter.get("value") | |||
@@ -107,7 +107,7 @@ export default class ParamBody extends PureComponent { | |||
const HighlightCode = getComponent("highlightCode") | |||
const ContentType = getComponent("contentType") | |||
// for domains where specSelectors not passed | |||
let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) : param | |||
let parameter = specSelectors ? specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) : param | |||
let errors = parameter.get("errors", List()) | |||
let consumesValue = specSelectors.contentTypeValues(pathMethod).get("requestContentType") | |||
let consumes = this.props.consumes && this.props.consumes.size ? this.props.consumes : ParamBody.defaultProp.consumes | |||
@@ -24,7 +24,7 @@ export default class ParameterRow extends Component { | |||
let { specSelectors, pathMethod, param } = props | |||
let defaultValue = param.get("default") | |||
let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) | |||
let parameter = specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) | |||
let value = parameter ? parameter.get("value") : "" | |||
if ( defaultValue !== undefined && value === undefined ) { | |||
this.onChangeWrapper(defaultValue) | |||
@@ -37,7 +37,7 @@ export default class ParameterRow extends Component { | |||
let example = param.get("example") | |||
let defaultValue = param.get("default") | |||
let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) | |||
let parameter = specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) | |||
let enumValue | |||
if(isOAS3()) { | |||
@@ -104,8 +104,7 @@ export default class ParameterRow extends Component { | |||
let isFormDataSupported = "FormData" in win | |||
let required = param.get("required") | |||
let itemType = param.getIn(isOAS3 && isOAS3() ? ["schema", "items", "type"] : ["items", "type"]) | |||
let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) | |||
let value = parameter ? parameter.get("value") : "" | |||
let value = param ? param.get("value") : "" | |||
let extensions = getExtensions(param) | |||
@@ -101,7 +101,7 @@ export default class Parameters extends Component { | |||
specPath={specPath.push(i.toString())} | |||
getComponent={ getComponent } | |||
getConfigs={ getConfigs } | |||
param={ parameter } | |||
param={ specSelectors.parameterWithMeta(pathMethod, parameter.get("name"), parameter.get("in")) } | |||
key={ `${parameter.get( "in" )}.${parameter.get("name")}` } | |||
onChange={ this.onChange } | |||
onChangeConsumes={this.onChangeConsumesWrapper} | |||
@@ -3,7 +3,17 @@ import PropTypes from "prop-types" | |||
import Remarkable from "remarkable" | |||
import sanitize from "sanitize-html" | |||
// eslint-disable-next-line no-useless-escape | |||
const isPlainText = (str) => /^[A-Z\s0-9!?\.]+$/gi.test(str) | |||
function Markdown({ source }) { | |||
if(isPlainText(source)) { | |||
// If the source text is not Markdown, | |||
// let's save some time and just render it. | |||
return <div className="markdown"> | |||
{source} | |||
</div> | |||
} | |||
const html = new Remarkable({ | |||
html: true, | |||
typographer: true, | |||
@@ -2,7 +2,7 @@ import React, { PureComponent } from "react" | |||
import PropTypes from "prop-types" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
import { helpers } from "swagger-client" | |||
import { Iterable, fromJS } from "immutable" | |||
import { Iterable, fromJS, Map } from "immutable" | |||
const { opId } = helpers | |||
@@ -82,13 +82,24 @@ export default class OperationContainer extends PureComponent { | |||
} | |||
componentWillReceiveProps(nextProps) { | |||
if(nextProps.response !== this.props.response) { | |||
const { path, method, specActions, specSelectors, response, isShown } = nextProps | |||
const resolvedSubtree = specSelectors.specResolvedSubtree(["paths", path, method]) | |||
if(response !== this.props.response) { | |||
this.setState({ executeInProgress: false }) | |||
} | |||
if(isShown && resolvedSubtree === undefined) { | |||
specActions.requestResolvedSubtree(["paths", path, method]) | |||
} | |||
} | |||
toggleShown =() => { | |||
let { layoutActions, tag, operationId, isShown } = this.props | |||
let { layoutActions, specActions, tag, operationId, path, method, isShown } = this.props | |||
if(!isShown) { | |||
// transitioning from collapsed to expanded | |||
specActions.requestResolvedSubtree(["paths", path, method]) | |||
} | |||
layoutActions.show(["operations", tag, operationId], !isShown) | |||
} | |||
@@ -108,7 +119,7 @@ export default class OperationContainer extends PureComponent { | |||
render() { | |||
let { | |||
op, | |||
op: unresolvedOp, | |||
tag, | |||
path, | |||
method, | |||
@@ -140,10 +151,14 @@ export default class OperationContainer extends PureComponent { | |||
const Operation = getComponent( "operation" ) | |||
const resolvedSubtree = specSelectors.specResolvedSubtree(["paths", path, method]) || Map() | |||
const operationProps = fromJS({ | |||
op, | |||
op: resolvedSubtree || Map(), | |||
tag, | |||
path, | |||
summary: unresolvedOp.getIn(["operation", "summary"]) || "", | |||
deprecated: resolvedSubtree.get("deprecated") || unresolvedOp.getIn(["operation", "deprecated"]) || false, | |||
method, | |||
security, | |||
isAuthorized, | |||
@@ -2,6 +2,7 @@ import React, { PureComponent, Component } from "react" | |||
import PropTypes from "prop-types" | |||
import { List, fromJS } from "immutable" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
import DebounceInput from "react-debounce-input" | |||
//import "less/json-schema-form" | |||
const noop = ()=> {} | |||
@@ -79,10 +80,13 @@ export class JsonSchema_string extends Component { | |||
disabled={isDisabled}/>) | |||
} | |||
else { | |||
return (<Input type={ schema.format === "password" ? "password" : "text" } | |||
return (<DebounceInput | |||
type={ schema.format === "password" ? "password" : "text" } | |||
className={ errors.length ? "invalid" : ""} | |||
title={ errors.length ? errors : ""} | |||
value={value} | |||
minLength={0} | |||
debounceTimeout={350} | |||
placeholder={description} | |||
onChange={ this.onChange } | |||
disabled={isDisabled}/>) | |||
@@ -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 | |||
} | |||
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 | |||
} | |||
// 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 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_RESOLVED_SUBTREE]: (state, action) => { | |||
const { value, path } = action.payload | |||
return state.setIn(["resolvedSubtrees", ...path], fromJSOrdered(value)) | |||
}, | |||
[UPDATE_PARAM]: ( state, {payload} ) => { | |||
let { path, paramName, paramIn, value, isXml } = payload | |||
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) | |||
}) | |||
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 | |||
} | |||
@@ -198,6 +198,7 @@ section.models | |||
.model-box | |||
{ | |||
padding: 10px; | |||
display: inline-block; | |||
border-radius: 4px; | |||
background: rgba($section-models-model-box-background-color,.1); | |||
@@ -24,7 +24,8 @@ describe("<Models/>", function(){ | |||
def1: {}, | |||
def2: {} | |||
}) | |||
} | |||
}, | |||
specResolvedSubtree: () => {} | |||
}, | |||
layoutSelectors: { | |||
isShown: createSpy() | |||
@@ -177,6 +177,10 @@ describe("spec plugin - actions", function(){ | |||
}) | |||
}) | |||
describe("requestResolvedSubtree", () => { | |||
it("should return a promise ") | |||
}) | |||
it.skip("should call errActions.newErr, if the fn.execute rejects", function(){ | |||
}) | |||
@@ -24,7 +24,7 @@ describe("spec plugin - selectors", function(){ | |||
// Given | |||
const spec = fromJS({ | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: { | |||
@@ -55,7 +55,7 @@ describe("spec plugin - selectors", function(){ | |||
it("should return { requestContentType, responseContentType } from an operation", function(){ | |||
// Given | |||
let state = fromJS({ | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: {} | |||
@@ -86,7 +86,7 @@ describe("spec plugin - selectors", function(){ | |||
it("should default to the first `produces` array value if current is not set", function(){ | |||
// Given | |||
let state = fromJS({ | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: { | |||
@@ -121,7 +121,7 @@ describe("spec plugin - selectors", function(){ | |||
it("should default to `application/json` if a default produces value is not available", function(){ | |||
// Given | |||
let state = fromJS({ | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: {} | |||
@@ -151,7 +151,7 @@ describe("spec plugin - selectors", function(){ | |||
it("should prioritize consumes value first from an operation", function(){ | |||
// Given | |||
let state = fromJS({ | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: { | |||
@@ -182,7 +182,7 @@ describe("spec plugin - selectors", function(){ | |||
it("should fallback to multipart/form-data if there is no consumes value but there is a file parameter", function(){ | |||
// Given | |||
let state = fromJS({ | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: { | |||
@@ -204,7 +204,7 @@ describe("spec plugin - selectors", function(){ | |||
it("should fallback to application/x-www-form-urlencoded if there is no consumes value, no file parameter, but there is a formData parameter", function(){ | |||
// Given | |||
let state = fromJS({ | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: { | |||
@@ -244,7 +244,7 @@ describe("spec plugin - selectors", function(){ | |||
// Given | |||
let state = fromJS({ | |||
url: "https://generator.swagger.io/api/swagger.json", | |||
resolved: { | |||
json: { | |||
paths: { | |||
"/one": { | |||
get: { | |||