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() {
|
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>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -511,27 +577,16 @@ 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 === "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
|
||||||
@@ -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 || {}
|
||||||
|
|||||||
Reference in New Issue
Block a user