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:
@@ -171,7 +171,10 @@ export class JsonSchema_array extends PureComponent {
|
||||
render() {
|
||||
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 shouldRenderValue =
|
||||
value && value.count && value.count() > 0 ? true : false
|
||||
@@ -210,10 +213,10 @@ export class JsonSchema_array extends PureComponent {
|
||||
<div className="json-schema-array">
|
||||
{shouldRenderValue ?
|
||||
(value.map((item, i) => {
|
||||
if (errors.length) {
|
||||
let err = errors.filter((err) => err.index === i)
|
||||
if (err.length) errors = [err[0].error + i]
|
||||
}
|
||||
const itemErrors = fromJS([
|
||||
...errors.filter((err) => err.index === i)
|
||||
.map(e => e.error)
|
||||
])
|
||||
return (
|
||||
<div key={i} className="json-schema-form-item">
|
||||
{
|
||||
@@ -222,7 +225,7 @@ export class JsonSchema_array extends PureComponent {
|
||||
value={item}
|
||||
onChange={(val)=> this.onItemChange(val, i)}
|
||||
disabled={disabled}
|
||||
errors={errors}
|
||||
errors={itemErrors}
|
||||
getComponent={getComponent}
|
||||
/>
|
||||
: isArrayItemText ?
|
||||
@@ -230,13 +233,13 @@ export class JsonSchema_array extends PureComponent {
|
||||
value={item}
|
||||
onChange={(val) => this.onItemChange(val, i)}
|
||||
disabled={disabled}
|
||||
errors={errors}
|
||||
errors={itemErrors}
|
||||
/>
|
||||
: <ArrayItemsComponent {...this.props}
|
||||
value={item}
|
||||
onChange={(val) => this.onItemChange(val, i)}
|
||||
disabled={disabled}
|
||||
errors={errors}
|
||||
errors={itemErrors}
|
||||
schema={schemaItemsSchema}
|
||||
getComponent={getComponent}
|
||||
fn={fn}
|
||||
@@ -244,7 +247,9 @@ export class JsonSchema_array extends PureComponent {
|
||||
}
|
||||
{!disabled ? (
|
||||
<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)}
|
||||
> - </Button>
|
||||
) : null}
|
||||
@@ -255,7 +260,8 @@ export class JsonSchema_array extends PureComponent {
|
||||
}
|
||||
{!disabled ? (
|
||||
<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}
|
||||
>
|
||||
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 {
|
||||
constructor() {
|
||||
super()
|
||||
@@ -367,18 +398,18 @@ export class JsonSchema_object extends PureComponent {
|
||||
} = this.props
|
||||
|
||||
const TextArea = getComponent("TextArea")
|
||||
errors = errors.toJS ? errors.toJS() : Array.isArray(errors) ? errors : []
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextArea
|
||||
className={cx({ invalid: errors.size })}
|
||||
title={ errors.size ? errors.join(", ") : ""}
|
||||
className={cx({ invalid: errors.length })}
|
||||
title={ errors.length ? stringifyObjectErrors(errors).join(", ") : ""}
|
||||
value={stringify(value)}
|
||||
disabled={disabled}
|
||||
onChange={ this.handleOnChange }/>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
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 camelCase from "lodash/camelCase"
|
||||
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) => {
|
||||
if (val.length < min) {
|
||||
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
|
||||
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
|
||||
|
||||
function validateValueBySchema(value, schema, isParamRequired, bypassRequiredCheck, parameterContentMediaType) {
|
||||
if(!schema) return []
|
||||
let errors = []
|
||||
|
||||
let paramRequired = param.get("required")
|
||||
|
||||
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 })
|
||||
|
||||
if(!paramDetails) return errors
|
||||
|
||||
let required = paramDetails.get("required")
|
||||
let maximum = paramDetails.get("maximum")
|
||||
let minimum = paramDetails.get("minimum")
|
||||
let type = paramDetails.get("type")
|
||||
let format = paramDetails.get("format")
|
||||
let maxLength = paramDetails.get("maxLength")
|
||||
let minLength = paramDetails.get("minLength")
|
||||
let pattern = paramDetails.get("pattern")
|
||||
let required = schema.get("required")
|
||||
let maximum = schema.get("maximum")
|
||||
let minimum = schema.get("minimum")
|
||||
let type = schema.get("type")
|
||||
let format = schema.get("format")
|
||||
let maxLength = schema.get("maxLength")
|
||||
let minLength = schema.get("minLength")
|
||||
let uniqueItems = schema.get("uniqueItems")
|
||||
let maxItems = schema.get("maxItems")
|
||||
let minItems = schema.get("minItems")
|
||||
let pattern = schema.get("pattern")
|
||||
|
||||
/*
|
||||
If the parameter is required OR the parameter has a value (meaning optional, but filled in)
|
||||
then we should do our validation routine.
|
||||
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
|
||||
let stringCheck = type === "string" && value
|
||||
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)
|
||||
|
||||
if ((paramRequired || required) && !passedAnyCheck && !bypassRequiredCheck ) {
|
||||
if ((isParamRequired || required) && !passedAnyCheck && !bypassRequiredCheck) {
|
||||
errors.push("Required field is not provided")
|
||||
return errors
|
||||
}
|
||||
|
||||
if (
|
||||
type === "object" &&
|
||||
typeof value === "string" &&
|
||||
(parameterContentMediaType === null ||
|
||||
parameterContentMediaType === "application/json")
|
||||
) {
|
||||
let objectVal = value
|
||||
if(typeof value === "string") {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
objectVal = JSON.parse(value)
|
||||
} catch (e) {
|
||||
errors.push("Parameter string value must be valid JSON")
|
||||
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) {
|
||||
let err = validatePattern(value, pattern)
|
||||
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) {
|
||||
let err = validateMaxLength(value, maxLength)
|
||||
if (err) errors.push(err)
|
||||
@@ -511,27 +577,16 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
if (!err) return errors
|
||||
errors.push(err)
|
||||
} else if (type === "array") {
|
||||
let itemType
|
||||
|
||||
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 (!(arrayCheck || arrayListCheck)) {
|
||||
return errors
|
||||
}
|
||||
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") {
|
||||
let err = validateFile(value)
|
||||
if (!err) return errors
|
||||
@@ -542,6 +597,16 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
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) => {
|
||||
if (schema && (!schema.xml || !schema.xml.name)) {
|
||||
schema.xml = schema.xml || {}
|
||||
|
||||
Reference in New Issue
Block a user