feat: enhance parameter validation (#6878)

* feat: support min/max items validation

* feat: validate array deep and unique items

* feat: validate object deep
This commit is contained in:
Mahtis Michel
2021-02-03 21:29:59 +01:00
committed by GitHub
parent 8ed6c34958
commit 5c4dfc2da8
2 changed files with 167 additions and 71 deletions

View File

@@ -171,7 +171,10 @@ export class JsonSchema_array extends PureComponent {
render() { render() {
let { getComponent, required, schema, errors, fn, disabled } = this.props let { getComponent, required, schema, errors, fn, disabled } = this.props
errors = errors.toJS ? errors.toJS() : [] errors = errors.toJS ? errors.toJS() : Array.isArray(errors) ? errors : []
const arrayErrors = errors.filter(e => typeof e === "string")
const needsRemoveError = errors.filter(e => e.needRemove !== undefined)
.map(e => e.error)
const value = this.state.value // expect Im List const value = this.state.value // expect Im List
const shouldRenderValue = const shouldRenderValue =
value && value.count && value.count() > 0 ? true : false value && value.count && value.count() > 0 ? true : false
@@ -210,10 +213,10 @@ export class JsonSchema_array extends PureComponent {
<div className="json-schema-array"> <div className="json-schema-array">
{shouldRenderValue ? {shouldRenderValue ?
(value.map((item, i) => { (value.map((item, i) => {
if (errors.length) { const itemErrors = fromJS([
let err = errors.filter((err) => err.index === i) ...errors.filter((err) => err.index === i)
if (err.length) errors = [err[0].error + i] .map(e => e.error)
} ])
return ( return (
<div key={i} className="json-schema-form-item"> <div key={i} className="json-schema-form-item">
{ {
@@ -222,7 +225,7 @@ export class JsonSchema_array extends PureComponent {
value={item} value={item}
onChange={(val)=> this.onItemChange(val, i)} onChange={(val)=> this.onItemChange(val, i)}
disabled={disabled} disabled={disabled}
errors={errors} errors={itemErrors}
getComponent={getComponent} getComponent={getComponent}
/> />
: isArrayItemText ? : isArrayItemText ?
@@ -230,13 +233,13 @@ export class JsonSchema_array extends PureComponent {
value={item} value={item}
onChange={(val) => this.onItemChange(val, i)} onChange={(val) => this.onItemChange(val, i)}
disabled={disabled} disabled={disabled}
errors={errors} errors={itemErrors}
/> />
: <ArrayItemsComponent {...this.props} : <ArrayItemsComponent {...this.props}
value={item} value={item}
onChange={(val) => this.onItemChange(val, i)} onChange={(val) => this.onItemChange(val, i)}
disabled={disabled} disabled={disabled}
errors={errors} errors={itemErrors}
schema={schemaItemsSchema} schema={schemaItemsSchema}
getComponent={getComponent} getComponent={getComponent}
fn={fn} fn={fn}
@@ -244,7 +247,9 @@ export class JsonSchema_array extends PureComponent {
} }
{!disabled ? ( {!disabled ? (
<Button <Button
className="btn btn-sm json-schema-form-item-remove" className={`btn btn-sm json-schema-form-item-remove ${needsRemoveError.length ? "invalid" : null}`}
title={needsRemoveError.length ? needsRemoveError : ""}
onClick={() => this.removeItem(i)} onClick={() => this.removeItem(i)}
> - </Button> > - </Button>
) : null} ) : null}
@@ -255,7 +260,8 @@ export class JsonSchema_array extends PureComponent {
} }
{!disabled ? ( {!disabled ? (
<Button <Button
className={`btn btn-sm json-schema-form-item-add ${errors.length ? "invalid" : null}`} className={`btn btn-sm json-schema-form-item-add ${arrayErrors.length ? "invalid" : null}`}
title={arrayErrors.length ? arrayErrors : ""}
onClick={this.addItem} onClick={this.addItem}
> >
Add item Add item
@@ -340,6 +346,31 @@ export class JsonSchema_boolean extends Component {
} }
} }
const stringifyObjectErrors = (errors) => {
return errors.map(err => {
const meta = err.propKey !== undefined ? err.propKey : err.index
let stringError = typeof err === "string" ? err : typeof err.error === "string" ? err.error : null
if(!meta && stringError) {
return stringError
}
let currentError = err.error
let path = `/${err.propKey}`
while(typeof currentError === "object") {
const part = currentError.propKey !== undefined ? currentError.propKey : currentError.index
if(part === undefined) {
break
}
path += `/${part}`
if (!currentError.error) {
break
}
currentError = currentError.error
}
return `${path}: ${currentError}`
})
}
export class JsonSchema_object extends PureComponent { export class JsonSchema_object extends PureComponent {
constructor() { constructor() {
super() super()
@@ -367,18 +398,18 @@ export class JsonSchema_object extends PureComponent {
} = this.props } = this.props
const TextArea = getComponent("TextArea") const TextArea = getComponent("TextArea")
errors = errors.toJS ? errors.toJS() : Array.isArray(errors) ? errors : []
return ( return (
<div> <div>
<TextArea <TextArea
className={cx({ invalid: errors.size })} className={cx({ invalid: errors.length })}
title={ errors.size ? errors.join(", ") : ""} title={ errors.length ? stringifyObjectErrors(errors).join(", ") : ""}
value={stringify(value)} value={stringify(value)}
disabled={disabled} disabled={disabled}
onChange={ this.handleOnChange }/> onChange={ this.handleOnChange }/>
</div> </div>
) )
} }
} }

View File

@@ -10,7 +10,7 @@
in `./helpers` if you have the time. in `./helpers` if you have the time.
*/ */
import Im from "immutable" import Im, { fromJS, Set } from "immutable"
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url" import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"
import camelCase from "lodash/camelCase" import camelCase from "lodash/camelCase"
import upperFirst from "lodash/upperFirst" import upperFirst from "lodash/upperFirst"
@@ -385,6 +385,40 @@ export const validateMaxLength = (val, max) => {
} }
} }
export const validateUniqueItems = (val, uniqueItems) => {
if (!val) {
return
}
if (uniqueItems === "true" || uniqueItems === true) {
const list = fromJS(val)
const set = list.toSet()
const hasDuplicates = val.length > set.size
if(hasDuplicates) {
let errorsPerIndex = Set()
list.forEach((item, i) => {
if(list.filter(v => isFunc(v.equals) ? v.equals(item) : v === item).size > 1) {
errorsPerIndex = errorsPerIndex.add(i)
}
})
if(errorsPerIndex.size !== 0) {
return errorsPerIndex.map(i => ({index: i, error: "No duplicates allowed."})).toArray()
}
}
}
}
export const validateMinItems = (val, min) => {
if (!val && min >= 1 || val && val.length < min) {
return `Array must contain at least ${min} item${min === 1 ? "" : "s"}`
}
}
export const validateMaxItems = (val, max) => {
if (val && val.length > max) {
return `Array must not contain more then ${max} item${max === 1 ? "" : "s"}`
}
}
export const validateMinLength = (val, min) => { export const validateMinLength = (val, min) => {
if (val.length < min) { if (val.length < min) {
return `Value must be at least ${min} character${min !== 1 ? "s" : ""}` return `Value must be at least ${min} character${min !== 1 ? "s" : ""}`
@@ -398,32 +432,28 @@ export const validatePattern = (val, rxPattern) => {
} }
} }
// validation of parameters before execute function validateValueBySchema(value, schema, isParamRequired, bypassRequiredCheck, parameterContentMediaType) {
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => { if(!schema) return []
let errors = [] let errors = []
let required = schema.get("required")
let paramRequired = param.get("required") let maximum = schema.get("maximum")
let minimum = schema.get("minimum")
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 }) let type = schema.get("type")
let format = schema.get("format")
if(!paramDetails) return errors let maxLength = schema.get("maxLength")
let minLength = schema.get("minLength")
let required = paramDetails.get("required") let uniqueItems = schema.get("uniqueItems")
let maximum = paramDetails.get("maximum") let maxItems = schema.get("maxItems")
let minimum = paramDetails.get("minimum") let minItems = schema.get("minItems")
let type = paramDetails.get("type") let pattern = schema.get("pattern")
let format = paramDetails.get("format")
let maxLength = paramDetails.get("maxLength")
let minLength = paramDetails.get("minLength")
let pattern = paramDetails.get("pattern")
/* /*
If the parameter is required OR the parameter has a value (meaning optional, but filled in) If the parameter is required OR the parameter has a value (meaning optional, but filled in)
then we should do our validation routine. then we should do our validation routine.
Only bother validating the parameter if the type was specified. Only bother validating the parameter if the type was specified.
in case of array an empty value needs validation too because constrains can be set to require minItems
*/ */
if ( type && (paramRequired || required || value) ) { if (type && (isParamRequired || required || value !== undefined || type === "array")) {
// These checks should evaluate to true if there is a parameter // These checks should evaluate to true if there is a parameter
let stringCheck = type === "string" && value let stringCheck = type === "string" && value
let arrayCheck = type === "array" && Array.isArray(value) && value.length let arrayCheck = type === "array" && Array.isArray(value) && value.length
@@ -443,30 +473,66 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
const passedAnyCheck = allChecks.some(v => !!v) const passedAnyCheck = allChecks.some(v => !!v)
if ((paramRequired || required) && !passedAnyCheck && !bypassRequiredCheck ) { if ((isParamRequired || required) && !passedAnyCheck && !bypassRequiredCheck) {
errors.push("Required field is not provided") errors.push("Required field is not provided")
return errors return errors
} }
if ( if (
type === "object" && type === "object" &&
typeof value === "string" &&
(parameterContentMediaType === null || (parameterContentMediaType === null ||
parameterContentMediaType === "application/json") parameterContentMediaType === "application/json")
) { ) {
let objectVal = value
if(typeof value === "string") {
try { try {
JSON.parse(value) objectVal = JSON.parse(value)
} catch (e) { } catch (e) {
errors.push("Parameter string value must be valid JSON") errors.push("Parameter string value must be valid JSON")
return errors return errors
} }
} }
if(schema && schema.has("required") && isFunc(required.isList) && required.isList()) {
required.forEach(key => {
if(objectVal[key] === undefined) {
errors.push({ propKey: key, error: "Required property not found" })
}
})
}
if(schema && schema.has("properties")) {
schema.get("properties").forEach((val, key) => {
const errs = validateValueBySchema(objectVal[key], val, false, bypassRequiredCheck, parameterContentMediaType)
errors.push(...errs
.map((error) => ({ propKey: key, error })))
})
}
}
if (pattern) { if (pattern) {
let err = validatePattern(value, pattern) let err = validatePattern(value, pattern)
if (err) errors.push(err) if (err) errors.push(err)
} }
if (minItems) {
if (type === "array") {
let err = validateMinItems(value, minItems)
if (err) errors.push(err)
}
}
if (maxItems) {
if (type === "array") {
let err = validateMaxItems(value, maxItems)
if (err) errors.push({ needRemove: true, error: err })
}
}
if (uniqueItems) {
if (type === "array") {
let errorPerItem = validateUniqueItems(value, uniqueItems)
if (errorPerItem) errors.push(...errorPerItem)
}
}
if (maxLength || maxLength === 0) { if (maxLength || maxLength === 0) {
let err = validateMaxLength(value, maxLength) let err = validateMaxLength(value, maxLength)
if (err) errors.push(err) if (err) errors.push(err)
@@ -487,7 +553,7 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
if (err) errors.push(err) if (err) errors.push(err)
} }
if ( type === "string" ) { if (type === "string") {
let err let err
if (format === "date-time") { if (format === "date-time") {
err = validateDateTime(value) err = validateDateTime(value)
@@ -498,41 +564,30 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
} }
if (!err) return errors if (!err) return errors
errors.push(err) errors.push(err)
} else if ( type === "boolean" ) { } else if (type === "boolean") {
let err = validateBoolean(value) let err = validateBoolean(value)
if (!err) return errors if (!err) return errors
errors.push(err) errors.push(err)
} else if ( type === "number" ) { } else if (type === "number") {
let err = validateNumber(value) let err = validateNumber(value)
if (!err) return errors if (!err) return errors
errors.push(err) errors.push(err)
} else if ( type === "integer" ) { } else if (type === "integer") {
let err = validateInteger(value) let err = validateInteger(value)
if (!err) return errors if (!err) return errors
errors.push(err) errors.push(err)
} else if ( type === "array" ) { } else if (type === "array") {
let itemType if (!(arrayCheck || arrayListCheck)) {
return errors
if ( !arrayListCheck || !value.count() ) { return errors }
itemType = paramDetails.getIn(["items", "type"])
value.forEach((item, index) => {
let err
if (itemType === "number") {
err = validateNumber(item)
} else if (itemType === "integer") {
err = validateInteger(item)
} else if (itemType === "string") {
err = validateString(item)
}
if ( err ) {
errors.push({ index: index, error: err})
} }
if(value) {
value.forEach((item, i) => {
const errs = validateValueBySchema(item, schema.get("items"), false, bypassRequiredCheck, parameterContentMediaType)
errors.push(...errs
.map((err) => ({ index: i, error: err })))
}) })
} else if ( type === "file" ) { }
} else if (type === "file") {
let err = validateFile(value) let err = validateFile(value)
if (!err) return errors if (!err) return errors
errors.push(err) errors.push(err)
@@ -542,6 +597,16 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
return errors return errors
} }
// validation of parameters before execute
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
let paramRequired = param.get("required")
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 })
return validateValueBySchema(value, paramDetails, paramRequired, bypassRequiredCheck, parameterContentMediaType)
}
const getXmlSampleSchema = (schema, config, exampleOverride) => { const getXmlSampleSchema = (schema, config, exampleOverride) => {
if (schema && (!schema.xml || !schema.xml.name)) { if (schema && (!schema.xml || !schema.xml.name)) {
schema.xml = schema.xml || {} schema.xml = schema.xml || {}