-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow deeply nested if property checks (#33)
- Loading branch information
Showing
5 changed files
with
185 additions
and
65 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { compareFormValueWithSchemaValue, getField, validateFieldSchema } from './helpers'; | ||
import { hasProperty } from './utils'; | ||
|
||
/** | ||
* Checks if a "IF" condition matches given the current form state | ||
* @param {Object} node - JSON schema node | ||
* @param {Object} formValues - form state | ||
* @returns {Boolean} | ||
*/ | ||
export function checkIfConditionMatches(node, formValues, formFields) { | ||
return Object.keys(node.if.properties).every((name) => { | ||
const currentProperty = node.if.properties[name]; | ||
const value = formValues[name]; | ||
const hasEmptyValue = | ||
typeof value === 'undefined' || | ||
// NOTE: This is a "Remote API" dependency, as empty fields are sent as "null". | ||
value === null; | ||
const hasIfExplicit = node.if.required?.includes(name); | ||
|
||
if (hasEmptyValue && !hasIfExplicit) { | ||
// A property with empty value in a "if" will always match (lead to "then"), | ||
// even if the actual conditional isn't true. Unless it's explicit in the if.required. | ||
// WRONG:: if: { properties: { foo: {...} } } | ||
// CORRECT:: if: { properties: { foo: {...} }, required: ['foo'] } | ||
// Check MR !14408 for further explanation about the official specs | ||
// https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else | ||
return true; | ||
} | ||
|
||
if (hasProperty(currentProperty, 'const')) { | ||
return compareFormValueWithSchemaValue(value, currentProperty.const); | ||
} | ||
|
||
if (currentProperty.contains?.pattern) { | ||
// TODO: remove this || after D#4098 is merged and transformValue does not run for the parser anymore | ||
const formValue = value || []; | ||
|
||
// Making sure the form value type matches the expected type (array) when theres' a "contains" condition | ||
if (Array.isArray(formValue)) { | ||
const pattern = new RegExp(currentProperty.contains.pattern); | ||
return (value || []).some((item) => pattern.test(item)); | ||
} | ||
} | ||
|
||
if (currentProperty.enum) { | ||
return currentProperty.enum.includes(value); | ||
} | ||
|
||
if (currentProperty.properties) { | ||
return checkIfConditionMatches( | ||
{ if: currentProperty }, | ||
formValues[name], | ||
getField(name, formFields).fields | ||
); | ||
} | ||
|
||
const field = getField(name, formFields); | ||
|
||
return validateFieldSchema( | ||
{ | ||
options: field.options, | ||
// @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field. | ||
...currentProperty, | ||
inputType: field.inputType, | ||
required: true, | ||
}, | ||
value | ||
); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { checkIfConditionMatches } from '../checkIfConditionMatches'; | ||
|
||
it('Empty if is always going to be true', () => { | ||
expect(checkIfConditionMatches({ if: { properties: {} } })).toBe(true); | ||
}); | ||
|
||
it('Basic if check passes with correct value', () => { | ||
expect( | ||
checkIfConditionMatches( | ||
{ if: { properties: { a: { const: 'hello' } } } }, | ||
{ | ||
a: 'hello', | ||
} | ||
) | ||
).toBe(true); | ||
}); | ||
|
||
it('Basic if check fails with incorrect value', () => { | ||
expect( | ||
checkIfConditionMatches( | ||
{ if: { properties: { a: { const: 'hello' } } } }, | ||
{ | ||
a: 'goodbye', | ||
} | ||
) | ||
).toBe(false); | ||
}); | ||
|
||
it('Nested properties check passes with correct value', () => { | ||
expect( | ||
checkIfConditionMatches( | ||
{ if: { properties: { parent: { properties: { child: { const: 'hello from child' } } } } } }, | ||
{ | ||
parent: { child: 'hello from child' }, | ||
}, | ||
[{ name: 'parent', fields: [] }] | ||
) | ||
).toBe(true); | ||
}); | ||
|
||
it('Nested properties check passes with correct value', () => { | ||
expect( | ||
checkIfConditionMatches( | ||
{ if: { properties: { parent: { properties: { child: { const: 'hello from child' } } } } } }, | ||
{ | ||
parent: { child: 'goodbye from child' }, | ||
}, | ||
[{ name: 'parent', fields: [] }] | ||
) | ||
).toBe(false); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { createHeadlessForm } from '../createHeadlessForm'; | ||
|
||
it('Should allow check of a nested property in a conditional', () => { | ||
const { handleValidation } = createHeadlessForm( | ||
{ | ||
additionalProperties: false, | ||
allOf: [ | ||
{ | ||
if: { | ||
properties: { | ||
parent: { | ||
properties: { | ||
child: { | ||
const: 'yes', | ||
}, | ||
}, | ||
required: ['child'], | ||
}, | ||
}, | ||
required: ['parent'], | ||
}, | ||
then: { required: ['parent_sibling'] }, | ||
}, | ||
], | ||
properties: { | ||
parent: { | ||
additionalProperties: false, | ||
properties: { | ||
child: { | ||
oneOf: [ | ||
{ | ||
const: 'yes', | ||
}, | ||
{ const: 'no' }, | ||
], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['child'], | ||
type: 'object', | ||
}, | ||
parent_sibling: { | ||
type: 'integer', | ||
}, | ||
}, | ||
required: ['parent'], | ||
type: 'object', | ||
}, | ||
{ strictInputType: false } | ||
); | ||
expect(handleValidation({ parent: { child: 'no' } }).formErrors).toEqual(undefined); | ||
expect(handleValidation({ parent: { child: 'yes' } }).formErrors).toEqual({ | ||
parent_sibling: 'Required field', | ||
}); | ||
expect(handleValidation({ parent: { child: 'yes' }, parent_sibling: 1 }).formErrors).toEqual( | ||
undefined | ||
); | ||
}); |