Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance parameter validation #6878

Merged
merged 4 commits into from Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 44 additions & 13 deletions src/core/json-schema-components.jsx
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
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