Skip to content

Commit

Permalink
feat: validate array deep and unique items
Browse files Browse the repository at this point in the history
Signed-off-by: mathis-m <mathis.michel@outlook.de>
  • Loading branch information
mathis-m committed Jan 31, 2021
1 parent bd4d778 commit d65b21b
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 70 deletions.
26 changes: 16 additions & 10 deletions src/core/json-schema-components.jsx
Expand Up @@ -170,7 +170,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 @@ -209,10 +212,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 @@ -221,29 +224,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 @@ -254,7 +259,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
142 changes: 82 additions & 60 deletions src/core/utils.js
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,14 +385,36 @@ 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 || val.size) < 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 || val.size) > max) {
if (val && val.length > max) {
return `Array must not contain more then ${max} item${max === 1 ? "" : "s"}`
}
}
Expand All @@ -410,34 +432,27 @@ 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) {
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 maxItems = paramDetails.get("maxItems")
let minItems = paramDetails.get("minItems")
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 || type === "array" && !value) ) {
if (type && (isParamRequired || required || value || type === "array" && !value)) {
// 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 @@ -457,7 +472,7 @@ 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
}
Expand All @@ -481,17 +496,24 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
if (err) errors.push(err)
}

if(minItems) {
if(type === "array") {
if (minItems) {
if (type === "array") {
let err = validateMinItems(value, minItems)
if (err) errors.push(err)
}
}

if(maxItems) {
if(type === "array") {
if (maxItems) {
if (type === "array") {
let err = validateMaxItems(value, maxItems)
if (err) errors.push(err)
if (err) errors.push({ needRemove: true, error: err })
}
}

if (uniqueItems) {
if (type === "array") {
let errorPerItem = validateUniqueItems(value, uniqueItems)
if (errorPerItem) errors.push(...errorPerItem)
}
}

Expand All @@ -515,52 +537,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 @@ -570,6 +581,17 @@ 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 })

if(!paramDetails) return []
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 d65b21b

Please sign in to comment.