Skip to content

Commit abf3496

Browse files
authored
bug: parameter allowEmptyValue + required interactions (via #5142)
* add failing tests * standardize parameter keying * validateParam test migrations * migrate test cases to new pattern * disambiguate name/in ordering in `body.body` test cases * `name+in`=> `{in}.{name}` * consider allowEmptyValue parameter inclusion in runtime validation * use config object for all validateParam options * drop isXml flag from validateParams
1 parent be3500c commit abf3496

File tree

9 files changed

+719
-383
lines changed

9 files changed

+719
-383
lines changed

src/core/plugins/spec/actions.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import serializeError from "serialize-error"
55
import isString from "lodash/isString"
66
import debounce from "lodash/debounce"
77
import set from "lodash/set"
8-
import { isJSONObject } from "core/utils"
8+
import { isJSONObject, paramToValue } from "core/utils"
99

1010
// Actions conform to FSA (flux-standard-actions)
1111
// {type: string,payload: Any|Error, meta: obj, error: bool}
@@ -345,19 +345,19 @@ export const executeRequest = (req) =>
345345

346346
// ensure that explicitly-included params are in the request
347347

348-
if(op && op.parameters && op.parameters.length) {
349-
op.parameters
350-
.filter(param => param && param.allowEmptyValue === true)
348+
if (operation && operation.get("parameters")) {
349+
operation.get("parameters")
350+
.filter(param => param && param.get("allowEmptyValue") === true)
351351
.forEach(param => {
352-
if (specSelectors.parameterInclusionSettingFor([pathName, method], param.name, param.in)) {
352+
if (specSelectors.parameterInclusionSettingFor([pathName, method], param.get("name"), param.get("in"))) {
353353
req.parameters = req.parameters || {}
354-
const paramValue = req.parameters[param.name]
354+
const paramValue = paramToValue(param, req.parameters)
355355

356356
// if the value is falsy or an empty Immutable iterable...
357357
if(!paramValue || (paramValue && paramValue.size === 0)) {
358358
// set it to empty string, so Swagger Client will treat it as
359359
// present but empty.
360-
req.parameters[param.name] = ""
360+
req.parameters[param.get("name")] = ""
361361
}
362362
}
363363
})

src/core/plugins/spec/reducers.js

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { fromJS, List } from "immutable"
2-
import { fromJSOrdered, validateParam } from "core/utils"
2+
import { fromJSOrdered, validateParam, paramToValue } from "core/utils"
33
import win from "../../window"
44

55
// selector-in-reducer is suboptimal, but `operationWithMeta` is more of a helper
66
import {
7-
operationWithMeta
7+
specJsonWithResolvedSubtrees,
8+
parameterValues,
9+
parameterInclusionSettingFor,
810
} from "./selectors"
911

1012
import {
@@ -25,6 +27,7 @@ import {
2527
CLEAR_VALIDATE_PARAMS,
2628
SET_SCHEME
2729
} from "./actions"
30+
import { paramToIdentifier } from "../../utils"
2831

2932
export default {
3033

@@ -54,14 +57,7 @@ export default {
5457
[UPDATE_PARAM]: ( state, {payload} ) => {
5558
let { path: pathMethod, paramName, paramIn, param, value, isXml } = payload
5659

57-
let paramKey
58-
59-
// `hashCode` is an Immutable.js Map method
60-
if(param && param.hashCode && !paramIn && !paramName) {
61-
paramKey = `${param.get("name")}.${param.get("in")}.hash-${param.hashCode()}`
62-
} else {
63-
paramKey = `${paramName}.${paramIn}`
64-
}
60+
let paramKey = param ? paramToIdentifier(param) : `${paramIn}.${paramName}`
6561

6662
const valueKey = isXml ? "value_xml" : "value"
6763

@@ -79,7 +75,7 @@ export default {
7975
return state
8076
}
8177

82-
const paramKey = `${paramName}.${paramIn}`
78+
const paramKey = `${paramIn}.${paramName}`
8379

8480
return state.setIn(
8581
["meta", "paths", ...pathMethod, "parameter_inclusions", paramKey],
@@ -88,15 +84,18 @@ export default {
8884
},
8985

9086
[VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => {
91-
let meta = state.getIn( [ "meta", "paths", ...pathMethod ], fromJS({}) )
92-
let isXml = /xml/i.test(meta.get("consumes_value"))
93-
94-
const op = operationWithMeta(state, ...pathMethod)
87+
const op = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod])
88+
const paramValues = parameterValues(state, pathMethod).toJS()
9589

9690
return state.updateIn(["meta", "paths", ...pathMethod, "parameters"], fromJS({}), paramMeta => {
9791
return op.get("parameters", List()).reduce((res, param) => {
98-
const errors = validateParam(param, isXml, isOAS3)
99-
return res.setIn([`${param.get("name")}.${param.get("in")}`, "errors"], fromJS(errors))
92+
const value = paramToValue(param, paramValues)
93+
const isEmptyValueIncluded = parameterInclusionSettingFor(state, pathMethod, param.get("name"), param.get("in"))
94+
const errors = validateParam(param, value, {
95+
bypassRequiredCheck: isEmptyValueIncluded,
96+
isOAS3,
97+
})
98+
return res.setIn([paramToIdentifier(param), "errors"], fromJS(errors))
10099
}, paramMeta)
101100
})
102101
},

src/core/plugins/spec/selectors.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createSelector } from "reselect"
22
import { sorters } from "core/utils"
33
import { fromJS, Set, Map, OrderedMap, List } from "immutable"
4+
import { paramToIdentifier } from "../../utils"
45

56
const DEFAULT_TAG = "default"
67

@@ -302,11 +303,11 @@ export const parameterWithMetaByIdentity = (state, pathMethod, param) => {
302303
const metaParams = state.getIn(["meta", "paths", ...pathMethod, "parameters"], OrderedMap())
303304

304305
const mergedParams = opParams.map((currentParam) => {
305-
const nameInKeyedMeta = metaParams.get(`${param.get("name")}.${param.get("in")}`)
306-
const hashKeyedMeta = metaParams.get(`${param.get("name")}.${param.get("in")}.hash-${param.hashCode()}`)
306+
const inNameKeyedMeta = metaParams.get(`${param.get("in")}.${param.get("name")}`)
307+
const hashKeyedMeta = metaParams.get(`${param.get("in")}.${param.get("name")}.hash-${param.hashCode()}`)
307308
return OrderedMap().merge(
308309
currentParam,
309-
nameInKeyedMeta,
310+
inNameKeyedMeta,
310311
hashKeyedMeta
311312
)
312313
})
@@ -315,7 +316,7 @@ export const parameterWithMetaByIdentity = (state, pathMethod, param) => {
315316
}
316317

317318
export const parameterInclusionSettingFor = (state, pathMethod, paramName, paramIn) => {
318-
const paramKey = `${paramName}.${paramIn}`
319+
const paramKey = `${paramIn}.${paramName}`
319320
return state.getIn(["meta", "paths", ...pathMethod, "parameter_inclusions", paramKey], false)
320321
}
321322

@@ -364,7 +365,7 @@ export function parameterValues(state, pathMethod, isXml) {
364365
let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List())
365366
return paramValues.reduce( (hash, p) => {
366367
let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value")
367-
return hash.set(`${p.get("in")}.${p.get("name")}`, value)
368+
return hash.set(paramToIdentifier(p, { allowHashes: false }), value)
368369
}, fromJS({}))
369370
}
370371

src/core/utils.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,8 @@ export const validatePattern = (val, rxPattern) => {
484484
}
485485

486486
// validation of parameters before execute
487-
export const validateParam = (param, isXml, isOAS3 = false) => {
487+
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
488488
let errors = []
489-
let value = isXml && param.get("in") === "body" ? param.get("value_xml") : param.get("value")
490489
let required = param.get("required")
491490

492491
let paramDetails = isOAS3 ? param.get("schema") : param
@@ -501,7 +500,6 @@ export const validateParam = (param, isXml, isOAS3 = false) => {
501500
let minLength = paramDetails.get("minLength")
502501
let pattern = paramDetails.get("pattern")
503502

504-
505503
/*
506504
If the parameter is required OR the parameter has a value (meaning optional, but filled in)
507505
then we should do our validation routine.
@@ -540,7 +538,7 @@ export const validateParam = (param, isXml, isOAS3 = false) => {
540538

541539
const passedAnyCheck = allChecks.some(v => !!v)
542540

543-
if ( required && !passedAnyCheck ) {
541+
if (required && !passedAnyCheck && !bypassRequiredCheck ) {
544542
errors.push("Required field is not provided")
545543
return errors
546544
}
@@ -805,3 +803,43 @@ export function numberToString(thing) {
805803

806804
return thing
807805
}
806+
807+
export function paramToIdentifier(param, { returnAll = false, allowHashes = true } = {}) {
808+
if(!Im.Map.isMap(param)) {
809+
throw new Error("paramToIdentifier: received a non-Im.Map parameter as input")
810+
}
811+
const paramName = param.get("name")
812+
const paramIn = param.get("in")
813+
814+
let generatedIdentifiers = []
815+
816+
// Generate identifiers in order of most to least specificity
817+
818+
if (param && param.hashCode && paramIn && paramName && allowHashes) {
819+
generatedIdentifiers.push(`${paramIn}.${paramName}.hash-${param.hashCode()}`)
820+
}
821+
822+
if(paramIn && paramName) {
823+
generatedIdentifiers.push(`${paramIn}.${paramName}`)
824+
}
825+
826+
generatedIdentifiers.push(paramName)
827+
828+
// Return the most preferred identifier, or all if requested
829+
830+
return returnAll ? generatedIdentifiers : (generatedIdentifiers[0] || "")
831+
}
832+
833+
export function paramToValue(param, paramValues) {
834+
const allIdentifiers = paramToIdentifier(param, { returnAll: true })
835+
836+
// Map identifiers to values in the provided value hash, filter undefined values,
837+
// and return the first value found
838+
const values = allIdentifiers
839+
.map(id => {
840+
return paramValues[id]
841+
})
842+
.filter(value => value !== undefined)
843+
844+
return values[0]
845+
}

test/core/plugins/spec/reducer.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ describe("spec plugin - reducer", function(){
130130
})
131131
})
132132
describe("SPEC_UPDATE_PARAM", function() {
133-
it("should store parameter values by name+in", () => {
133+
it("should store parameter values by {in}.{name}", () => {
134134
const updateParam = reducer["spec_update_param"]
135135

136136
const path = "/pet/post"
@@ -140,14 +140,14 @@ describe("spec plugin - reducer", function(){
140140
const result = updateParam(state, {
141141
payload: {
142142
path: [path, method],
143-
paramName: "body",
143+
paramName: "myBody",
144144
paramIn: "body",
145145
value: `{ "a": 123 }`,
146146
isXml: false
147147
}
148148
})
149149

150-
const response = result.getIn(["meta", "paths", path, method, "parameters", "body.body", "value"])
150+
const response = result.getIn(["meta", "paths", path, method, "parameters", "body.myBody", "value"])
151151
expect(response).toEqual(`{ "a": 123 }`)
152152
})
153153
it("should store parameter values by identity", () => {
@@ -157,7 +157,7 @@ describe("spec plugin - reducer", function(){
157157
const method = "POST"
158158

159159
const param = fromJS({
160-
name: "body",
160+
name: "myBody",
161161
in: "body",
162162
schema: {
163163
type: "string"
@@ -174,12 +174,12 @@ describe("spec plugin - reducer", function(){
174174
}
175175
})
176176

177-
const value = result.getIn(["meta", "paths", path, method, "parameters", `body.body.hash-${param.hashCode()}`, "value"])
177+
const value = result.getIn(["meta", "paths", path, method, "parameters", `body.myBody.hash-${param.hashCode()}`, "value"])
178178
expect(value).toEqual(`{ "a": 123 }`)
179179
})
180180
})
181181
describe("SPEC_UPDATE_EMPTY_PARAM_INCLUSION", function() {
182-
it("should store parameter values by name+in", () => {
182+
it("should store parameter values by {in}.{name}", () => {
183183
const updateParam = reducer["spec_update_empty_param_inclusion"]
184184

185185
const path = "/pet/post"
@@ -196,7 +196,7 @@ describe("spec plugin - reducer", function(){
196196
}
197197
})
198198

199-
const response = result.getIn(["meta", "paths", path, method, "parameter_inclusions", "param.query"])
199+
const response = result.getIn(["meta", "paths", path, method, "parameter_inclusions", "query.param"])
200200
expect(response).toEqual(true)
201201
})
202202
})

0 commit comments

Comments
 (0)