Skip to content

Commit

Permalink
feat: enhance parameter validation (#6878)
Browse files Browse the repository at this point in the history
* feat: support min/max items validation

* feat: validate array deep and unique items

* feat: validate object deep
  • Loading branch information
mathis-m authored Feb 3, 2021
1 parent 8ed6c34 commit 5c4dfc2
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 71 deletions.
57 changes: 44 additions & 13 deletions src/core/json-schema-components.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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">
{
Expand All @@ -222,29 +225,31 @@ export class JsonSchema_array extends PureComponent {
value={item}
onChange={(val)=> this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
errors={itemErrors}
getComponent={getComponent}
/>
: isArrayItemText ?
<JsonSchemaArrayItemText
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}
/>
}
{!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}
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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>
)

}
}

Expand Down
181 changes: 123 additions & 58 deletions src/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" : ""}`
Expand All @@ -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
Expand All @@ -443,22 +473,37 @@ 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")
) {
try {
JSON.parse(value)
} catch (e) {
errors.push("Parameter string value must be valid JSON")
return errors
let objectVal = value
if(typeof value === "string") {
try {
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 })))
})
}
}

Expand All @@ -467,6 +512,27 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
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)
Expand All @@ -487,52 +553,41 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
if (err) errors.push(err)
}

if ( type === "string" ) {
if (type === "string") {
let err
if (format === "date-time") {
err = validateDateTime(value)
err = validateDateTime(value)
} else if (format === "uuid") {
err = validateGuid(value)
err = validateGuid(value)
} else {
err = validateString(value)
err = validateString(value)
}
if (!err) return errors
errors.push(err)
} else if ( type === "boolean" ) {
} else if (type === "boolean") {
let err = validateBoolean(value)
if (!err) return errors
errors.push(err)
} else if ( type === "number" ) {
} else if (type === "number") {
let err = validateNumber(value)
if (!err) return errors
errors.push(err)
} else if ( type === "integer" ) {
} else if (type === "integer") {
let err = validateInteger(value)
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})
}
})
} else if ( type === "file" ) {
} else if (type === "array") {
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
errors.push(err)
Expand All @@ -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 || {}
Expand Down

0 comments on commit 5c4dfc2

Please sign in to comment.