diff --git a/package-lock.json b/package-lock.json index c0e99e7af..0b7eebeb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@remoteoss/json-schema-form", - "version": "0.4.3-beta.0", + "version": "0.4.4-dev.20230829101351", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@remoteoss/json-schema-form", - "version": "0.4.3-beta.0", + "version": "0.4.4-dev.20230829101351", "license": "MIT", "dependencies": { "json-logic-js": "^2.0.2", diff --git a/package.json b/package.json index b1151cd36..5b595e6a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@remoteoss/json-schema-form", - "version": "0.4.3-beta.0", + "version": "0.4.4-dev.20230829101351", "description": "Headless UI form powered by JSON Schemas", "author": "Remote.com (https://remote.com/)", "license": "MIT", diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 48b5728a2..f979c3def 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -100,7 +100,7 @@ function removeInvalidAttributes(fields) { * * @returns {FieldParameters} */ -function buildFieldParameters(name, fieldProperties, required = [], config = {}) { +function buildFieldParameters(name, fieldProperties, required = [], config = {}, validations) { const { position } = pickXKey(fieldProperties, 'presentation') ?? {}; let fields; @@ -108,9 +108,14 @@ function buildFieldParameters(name, fieldProperties, required = [], config = {}) if (inputType === supportedTypes.FIELDSET) { // eslint-disable-next-line no-use-before-define - fields = getFieldsFromJSONSchema(fieldProperties, { - customProperties: get(config, `customProperties.${name}`, {}), - }); + fields = getFieldsFromJSONSchema( + fieldProperties, + { + customProperties: get(config, `customProperties.${name}`, {}), + parentID: name, + }, + validations + ); } const result = { @@ -285,7 +290,11 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config, validations) { return []; } - const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters(scopedJsonSchema, config); + const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters( + scopedJsonSchema, + config, + validations + ); applyFieldsDependencies(fieldParamsList, scopedJsonSchema); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index a57ca4dd1..5715bce33 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -2,8 +2,17 @@ import { createHeadlessForm } from '../createHeadlessForm'; import { createSchemaWithRulesOnFieldA, - createSchemaWithThreePropertiesWithRuleOnFieldA, multiRuleSchema, + schemaWithComputedAttributesAndErrorMessages, + schemaWithNonRequiredField, + aConditionallyAppliedComputedAttributeMinimumAndMaximum, + aConditionallyAppliedComputedAttributeValue, + createSchemaWithThreePropertiesWithRuleOnFieldA, + fieldsetWithAConditionalToApplyExtraValidations, + fieldsetWithComputedAttributes, + ifConditionWithMissingComputedValue, + ifConditionWithMissingValidation, + nestedFieldsetWithValidationSchema, schemaSelfContainedValueForMaximumMinimumValues, schemaSelfContainedValueForTitleWithNoTemplate, schemaWhereValidationAndComputedValueIsAppliedOnNormalThenStatement, @@ -12,13 +21,13 @@ import { schemaWithComputedAttributeThatDoesntExistDescription, schemaWithComputedAttributeThatDoesntExistTitle, schemaWithComputedAttributes, - schemaWithComputedAttributesAndErrorMessages, schemaWithComputedValueChecksInIf, schemaWithDeepVarThatDoesNotExist, schemaWithDeepVarThatDoesNotExistOnFieldset, schemaWithGreaterThanChecksForThreeFields, schemaWithIfStatementWithComputedValuesAndValidationChecks, schemaWithInlineMultipleRulesForComputedAttributes, + schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema, schemaWithInlineRuleForComputedAttributeWithCopy, schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, schemaWithJSFLogicAndInlineRule, @@ -27,13 +36,16 @@ import { schemaWithMissingValueInlineRule, schemaWithMultipleComputedValueChecks, schemaWithNativeAndJSONLogicChecks, - schemaWithNonRequiredField, schemaWithPropertiesCheckAndValidationsInAIf, schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, schemaWithTwoRules, schemaWithTwoValidationsWhereOneOfThemIsAppliedConditionally, schemaWithValidationThatDoesNotExistOnProperty, schemaWithVarThatDoesNotExist, + simpleArrayValidationSchema, + twoLevelsOfJSONLogicSchema, + validatingASingleItemInTheArray, + validatingTwoNestedFieldsSchema, } from './jsonLogicFixtures'; describe('cross-value validations', () => { @@ -116,6 +128,22 @@ describe('cross-value validations', () => { console.error.mockRestore(); }); + it('Should throw when theres a missing rule', () => { + createHeadlessForm(schemaWithMissingRule, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('Missing rule for validation with id of: "a_greater_than_ten".') + ); + }); + + it('Should throw when theres a missing computed value', () => { + createHeadlessForm(schemaWithMissingComputedValue, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('Missing rule for computedValue with id of: "a_plus_ten".') + ); + }); + it('Should throw when a var does not exist in a rule.', () => { createHeadlessForm(schemaWithVarThatDoesNotExist, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( @@ -124,93 +152,97 @@ describe('cross-value validations', () => { ); }); - it('Should throw when a var does not exist in a deeply nested rule', () => { - createHeadlessForm(schemaWithDeepVarThatDoesNotExist, { strictInputType: false }); + it('Should throw when theres an inline computed ruleset with no value.', () => { + createHeadlessForm(schemaWithMissingValueInlineRule, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') + Error('Cannot define multiple rules without a template string with key `value`.') ); }); - it('Should throw when a var does not exist in a fieldset.', () => { - createHeadlessForm(schemaWithDeepVarThatDoesNotExistOnFieldset, { strictInputType: false }); + it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist.', () => { + createHeadlessForm(schemaWithComputedAttributeThatDoesntExist, { + strictInputType: false, + }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('"field_a" in rule "a_greater_than_ten" does not exist as a JSON schema property.') + Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) ); }); - it('On a property, it should throw an error for a requiredValidation that does not exist', () => { - createHeadlessForm(schemaWithValidationThatDoesNotExistOnProperty, { + it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a title.', () => { + createHeadlessForm(schemaWithComputedAttributeThatDoesntExistTitle, { strictInputType: false, }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error(`Validation "iDontExist" required for "field_a" doesn't exist.`) + Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) ); }); - it('A top level logic keyword will not be able to reference fieldset properties', () => { - createHeadlessForm(schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, { + it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a description.', () => { + createHeadlessForm(schemaWithComputedAttributeThatDoesntExistDescription, { strictInputType: false, }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('"child" in rule "validation_parent" does not exist as a JSON schema property.') + Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) ); }); - it('Should throw when theres a missing rule', () => { - createHeadlessForm(schemaWithMissingRule, { strictInputType: false }); + it('Should throw when a var does not exist in a deeply nested rule', () => { + createHeadlessForm(schemaWithDeepVarThatDoesNotExist, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('Missing rule for validation with id of: "a_greater_than_ten".') + Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') ); }); - it('Should throw when theres a missing computed value', () => { - createHeadlessForm(schemaWithMissingComputedValue, { strictInputType: false }); + it('Should throw when a var does not exist in a fieldset.', () => { + createHeadlessForm(schemaWithDeepVarThatDoesNotExistOnFieldset, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('Missing rule for computedValue with id of: "a_plus_ten".') + Error('"field_a" in rule "a_greater_than_ten" does not exist as a JSON schema property.') ); }); - it('Should throw when theres an inline computed ruleset with no value.', () => { - createHeadlessForm(schemaWithMissingValueInlineRule, { strictInputType: false }); + it('On a property, it should throw an error for a requiredValidation that does not exist', () => { + createHeadlessForm(schemaWithValidationThatDoesNotExistOnProperty, { + strictInputType: false, + }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('Cannot define multiple rules without a template string with key `value`.') + Error(`Validation "iDontExist" required for "field_a" doesn't exist.`) ); }); - it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist.', () => { - createHeadlessForm(schemaWithComputedAttributeThatDoesntExist, { + it('A top level logic keyword will not be able to reference fieldset properties', () => { + createHeadlessForm(schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, { strictInputType: false, }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) + Error('"child" in rule "validation_parent" does not exist as a JSON schema property.') ); }); - it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a title.', () => { - createHeadlessForm(schemaWithComputedAttributeThatDoesntExistTitle, { + it('Error for a missing computed value in an if', () => { + createHeadlessForm(ifConditionWithMissingComputedValue, { strictInputType: false, }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) + Error(`"iDontExist" computedValue in if condition doesn't exist.`) ); }); - it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a description.', () => { - createHeadlessForm(schemaWithComputedAttributeThatDoesntExistDescription, { + it('Error for a missing validation in an if', () => { + createHeadlessForm(ifConditionWithMissingValidation, { strictInputType: false, }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) + Error(`"iDontExist" validation in if condition doesn't exist.`) ); }); @@ -524,6 +556,40 @@ describe('cross-value validations', () => { expect(fieldB.statement).toEqual({ description: 'Must be bigger than 4 and smaller than 8' }); }); + it('computedAttribute test that minimum, maximum, errorMessages.minimum, errorMessage.maximum is working', () => { + const { handleValidation } = createHeadlessForm( + aConditionallyAppliedComputedAttributeMinimumAndMaximum, + { + strictInputType: false, + } + ); + expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual({ + field_b: 'use 10 or more', + }); + expect(handleValidation({ field_a: 20, field_b: 60 }).formErrors).toEqual({ + field_b: 'use less than 40', + }); + expect(handleValidation({ field_a: 20, field_b: 30 }).formErrors).toEqual(undefined); + }); + + it('Apply a conditional computed attribute value', () => { + const { fields, handleValidation } = createHeadlessForm( + aConditionallyAppliedComputedAttributeValue, + { + strictInputType: false, + } + ); + + expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual({ + field_b: 'The only accepted value is 10.', + }); + + const [, fieldB] = fields; + expect(fieldB.value).toEqual(10); + expect(handleValidation({ field_a: 10, field_b: 1 }).formErrors).toEqual(); + expect(fieldB.value).toEqual(undefined); + }); + it('Use a self contained rule in a schema for a title attribute', () => { const { fields, handleValidation } = createHeadlessForm( schemaWithInlineRuleForComputedAttributeWithCopy, @@ -579,6 +645,19 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 50, field_b: 50 }).formErrors).toEqual(undefined); }); + it('Use a self contained rule for a conditionally applied schema', () => { + const { fields, handleValidation } = createHeadlessForm( + schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema, + { + strictInputType: false, + } + ); + const [, fieldB] = fields; + expect(fieldB.description).toEqual('Hello world'); + handleValidation({ field_a: 20, field_b: 0 }); + expect(fieldB.description).toEqual('Must be between 10 and 40.'); + }); + it('Mix use of multiple inline rules and an external rule', () => { const { fields, handleValidation } = createHeadlessForm(schemaWithJSFLogicAndInlineRule, { strictInputType: false, @@ -588,4 +667,148 @@ describe('cross-value validations', () => { expect(fieldB.label).toEqual('Going to use 20 and 4'); }); }); + + describe('Nested fieldsets', () => { + it('Basic nested validation works', () => { + const { handleValidation } = createHeadlessForm(nestedFieldsetWithValidationSchema, { + strictInputType: false, + }); + expect(handleValidation({}).formErrors).toEqual({ field_a: { child: 'Required field' } }); + expect(handleValidation({ field_a: { child: 0 } }).formErrors).toEqual({ + field_a: { child: 'Must be greater than 10!' }, + }); + expect(handleValidation({ field_a: { child: 11 } }).formErrors).toEqual(undefined); + }); + + it('Validating two nested fields together', () => { + const { handleValidation } = createHeadlessForm(validatingTwoNestedFieldsSchema, { + strictInputType: false, + }); + expect(handleValidation({}).formErrors).toEqual({ + field_a: { child: 'Required field', other_child: 'Required field' }, + }); + expect(handleValidation({ field_a: { child: 0, other_child: 0 } }).formErrors).toEqual({ + field_a: { child: 'Must be greater than 10!', other_child: 'Must be greater than child' }, + }); + expect(handleValidation({ field_a: { child: 11, other_child: 12 } }).formErrors).toEqual( + undefined + ); + }); + + it('Validate a field and a nested field together', () => { + const { handleValidation } = createHeadlessForm(twoLevelsOfJSONLogicSchema, { + strictInputType: false, + }); + expect(handleValidation({}).formErrors).toEqual({ + field_a: { child: 'Required field' }, + field_b: 'Required field', + }); + expect(handleValidation({ field_a: { child: 0 }, field_b: 0 }).formErrors).toEqual({ + field_a: { child: 'Must be greater than 10!' }, + field_b: 'Must be greater than 10!', + }); + expect(handleValidation({ field_a: { child: 11 }, field_b: 11 }).formErrors).toEqual({ + field_b: 'child must be greater than 15!', + }); + expect(handleValidation({ field_a: { child: 16 }, field_b: 11 }).formErrors).toEqual( + undefined + ); + }); + + it('compute a nested field attribute', () => { + const { fields, handleValidation } = createHeadlessForm(fieldsetWithComputedAttributes, { + strictInputType: false, + }); + const [fieldA] = fields; + const [, computedField] = fieldA.fields; + expect(handleValidation({}).formErrors).toEqual({ + field_a: { child: 'Required field' }, + }); + expect(computedField.value).toEqual(NaN); + + expect(handleValidation({ field_a: { child: 10 } }).formErrors).toEqual(undefined); + expect(computedField.value).toEqual(100); + expect(computedField.description).toEqual('this is 100'); + + expect(handleValidation({ field_a: { child: 11 } }).formErrors).toEqual(undefined); + expect(computedField.value).toEqual(110); + expect(computedField.description).toEqual('this is 110'); + }); + + it('Apply a conditional value in a nested field with a conditional extra validation.', () => { + const { fields, handleValidation } = createHeadlessForm( + fieldsetWithAConditionalToApplyExtraValidations, + { + strictInputType: false, + } + ); + const [fieldA] = fields; + const [, , thirdChild] = fieldA.fields; + expect(thirdChild.isVisible).toEqual(false); + expect(thirdChild.required).toEqual(false); + + expect(handleValidation({ field_a: {} }).formErrors).toEqual({ + field_a: { child: 'Required field', other_child: 'Required field' }, + }); + expect(handleValidation({ field_a: { child: 0, other_child: 0 } }).formErrors).toEqual( + undefined + ); + expect(handleValidation({ field_a: { child: 10, other_child: 0 } }).formErrors).toEqual( + undefined + ); + expect(handleValidation({ field_a: { child: 10, other_child: 20 } }).formErrors).toEqual({ + field_a: { third_child: 'Required field' }, + }); + expect(thirdChild.isVisible).toEqual(true); + expect(thirdChild.required).toEqual(true); + + expect( + handleValidation({ field_a: { child: 10, other_child: 20, third_child: 10 } }).formErrors + ).toEqual({ + field_a: { third_child: 'Must be greater than other child.' }, + }); + + expect( + handleValidation({ field_a: { child: 10, other_child: 20, third_child: 30 } }).formErrors + ).toEqual(undefined); + }); + }); + + describe('Array validation', () => { + it('Should apply the json logic on an individual array item', () => { + const { handleValidation } = createHeadlessForm(simpleArrayValidationSchema, { + strictInputType: false, + }); + expect(handleValidation({ field_array: [] }).formErrors).toEqual(undefined); + expect(handleValidation({ field_array: [{}] }).formErrors).toEqual({ + field_array: [{ array_item: 'Required field' }], + }); + expect(handleValidation({ field_array: [{ array_item: 1 }] }).formErrors).toEqual({ + field_array: [{ array_item: 'Must be divisible by two' }], + }); + expect(handleValidation({ field_array: [{ array_item: 2 }] }).formErrors).toEqual(undefined); + expect( + handleValidation({ field_array: [{ array_item: 2 }, { array_item: 1 }] }).formErrors + ).toEqual({ + field_array: [undefined, { array_item: 'Must be divisible by two' }], + }); + expect( + handleValidation({ field_array: [{ array_item: 2 }, { array_item: 2 }] }).formErrors + ).toEqual(undefined); + }); + + it('Validating a single item in an array should work', () => { + const { handleValidation } = createHeadlessForm(validatingASingleItemInTheArray, { + strictInputType: false, + }); + expect(handleValidation({ field_array: [] }).formErrors).toEqual(undefined); + expect(handleValidation({ field_array: [{ item: 0 }] }).formErrors).toEqual(undefined); + expect(handleValidation({ field_array: [{ item: 0 }, { item: 3 }] }).formErrors).toEqual({ + field_array: 'Second item in array must be divisible by 4', + }); + }); + + // FIXME: This doesn't work because conditionals in items are not supported. + it.todo('Should be able to use conditionals in items'); + }); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index b501b67e3..a6428f6a6 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -246,6 +246,48 @@ export const schemaWithComputedAttributeThatDoesntExistDescription = { }, }; +export const ifConditionWithMissingComputedValue = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + allOf: [ + { + if: { + computedValues: { + iDontExist: { + const: 10, + }, + }, + }, + }, + ], + }, +}; + +export const ifConditionWithMissingValidation = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + allOf: [ + { + if: { + validations: { + iDontExist: { + const: true, + }, + }, + }, + }, + ], + }, +}; + export const schemaWithGreaterThanChecksForThreeFields = { properties: { field_a: { @@ -753,6 +795,239 @@ export const schemaWithComputedAttributes = { }, }; +export const nestedFieldsetWithValidationSchema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-logic-validations': ['child_greater_than_10'], + }, + }, + required: ['child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + }, + }, + }, + }, + required: ['field_a'], +}; + +export const validatingTwoNestedFieldsSchema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-logic-validations': ['child_greater_than_10'], + }, + other_child: { + type: 'number', + 'x-jsf-logic-validations': ['greater_than_child'], + }, + }, + required: ['child', 'other_child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + }, + }, + required: ['field_a'], +}; + +export const twoLevelsOfJSONLogicSchema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-logic-validations': ['child_greater_than_10'], + }, + }, + required: ['child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + }, + }, + }, + field_b: { + type: 'number', + 'x-jsf-logic-validations': ['validation_parent', 'peek_to_nested'], + }, + }, + 'x-jsf-logic': { + validations: { + validation_parent: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'field_b' }, 10], + }, + }, + peek_to_nested: { + errorMessage: 'child must be greater than 15!', + rule: { + '>': [{ var: 'field_a.child' }, 15], + }, + }, + }, + }, + required: ['field_a', 'field_b'], +}; + +export const fieldsetWithComputedAttributes = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + }, + other_child: { + type: 'number', + readOnly: true, + 'x-jsf-logic-computedAttrs': { + default: 'child_times_10', + const: 'child_times_10', + description: 'this is {{child_times_10}}', + }, + }, + }, + required: ['child'], + 'x-jsf-logic': { + computedValues: { + child_times_10: { + rule: { + '*': [{ var: 'child' }, 10], + }, + }, + }, + }, + }, + }, + required: ['field_a'], +}; + +export const fieldsetWithAConditionalToApplyExtraValidations = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + }, + other_child: { + type: 'number', + }, + third_child: { + type: 'number', + }, + }, + required: ['child', 'other_child'], + 'x-jsf-logic': { + validations: { + child_is_greater_than_other_child: { + rule: { + '>': [{ var: 'child' }, { var: 'other_child' }], + }, + }, + third_child_is_greater_than_other_child: { + errorMessage: 'Must be greater than other child.', + rule: { + '>': [{ var: 'third_child' }, { var: 'other_child' }], + }, + }, + }, + computedValues: { + child_times_10: { + rule: { + '*': [{ var: 'child' }, 10], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + child_times_10: { + const: 100, + }, + }, + validations: { + child_is_greater_than_other_child: { + const: false, + }, + }, + properties: { + child: { + const: 10, + }, + }, + }, + then: { + required: ['third_child'], + properties: { + third_child: { + 'x-jsf-logic-validations': ['third_child_is_greater_than_other_child'], + }, + }, + }, + else: { + properties: { + third_child: false, + }, + }, + }, + ], + }, + }, + }, + required: ['field_a'], +}; + export const schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset = { properties: { field_a: { @@ -792,6 +1067,192 @@ export const schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset = { required: ['field_a'], }; +export const simpleArrayValidationSchema = { + properties: { + field_array: { + type: 'array', + items: { + properties: { + array_item: { + type: 'number', + 'x-jsf-logic-validations': ['divisible_by_two'], + }, + }, + required: ['array_item'], + 'x-jsf-logic': { + validations: { + divisible_by_two: { + errorMessage: 'Must be divisible by two', + rule: { + '===': [{ '%': [{ var: 'array_item' }, 2] }, 0], + }, + }, + }, + }, + }, + }, + }, +}; + +export const validatingASingleItemInTheArray = { + properties: { + field_array: { + type: 'array', + 'x-jsf-logic-validations': ['second_item_is_divisible_by_four'], + items: { + properties: { + item: { + type: 'number', + }, + }, + required: ['item'], + }, + }, + }, + 'x-jsf-logic': { + validations: { + second_item_is_divisible_by_four: { + errorMessage: 'Second item in array must be divisible by 4', + rule: { + '===': [{ '%': [{ var: 'field_array.1.item' }, 4] }, 0], + }, + }, + }, + }, +}; + +// FIXME: This doesn't work because conditionals in items are not supported. +export const conditionalAppliedInAnItem = { + properties: { + field_array: { + type: 'array', + items: { + properties: { + item: { + type: 'number', + }, + other_item: { + type: 'number', + }, + }, + required: ['item'], + 'x-jsf-logic': { + validations: { + divisible_by_three: { + rule: { + '===': [{ '%': [{ var: 'item' }, 3] }, 0], + }, + }, + other_item_divisible_by_three: { + errorMessage: 'Must be disivisble_by_three', + rule: { + '===': [{ '%': [{ var: 'other_item' }, 3] }, 0], + }, + }, + }, + allOf: [ + { + if: { validations: { divisible_by_three: { cosnt: true } } }, + then: { + required: ['other_item'], + other_item: { 'x-jsf-logic-validations': ['other_item_divisible_by_three'] }, + }, + else: { properties: { other_item: false } }, + }, + ], + }, + }, + }, + }, +}; + +export const aConditionallyAppliedComputedAttributeMinimumAndMaximum = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + }, + allOf: [ + { + if: { properties: { field_a: { const: 20 } } }, + then: { + properties: { + field_b: { + 'x-jsf-logic-computedAttrs': { + minimum: 'a_divided_by_two', + maximum: 'a_multiplied_by_two', + 'x-jsf-errorMessage': { + minimum: 'use {{a_divided_by_two}} or more', + maximum: 'use less than {{a_multiplied_by_two}}', + }, + }, + }, + }, + }, + }, + ], + 'x-jsf-logic': { + computedValues: { + a_divided_by_two: { + rule: { + '/': [{ var: 'field_a' }, 2], + }, + }, + a_multiplied_by_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + }, +}; + +export const aConditionallyAppliedComputedAttributeValue = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + }, + allOf: [ + { + if: { properties: { field_a: { const: 20 } } }, + then: { + properties: { + field_b: { + readOnly: true, + 'x-jsf-logic-computedAttrs': { + const: 'a_divided_by_two', + default: 'a_divided_by_two', + }, + }, + }, + }, + else: { + properties: { + field_b: { + readOnly: false, + }, + }, + }, + }, + ], + 'x-jsf-logic': { + computedValues: { + a_divided_by_two: { + rule: { + '/': [{ var: 'field_a' }, 2], + }, + }, + }, + }, +}; + export const schemaWithInlineRuleForComputedAttributeWithCopy = { properties: { field_a: { @@ -829,6 +1290,73 @@ export const schemaWithInlineRuleForComputedAttributeWithoutCopy = { }, }; +export const schemaWithInlineRuleForComputedAttributeWithOnlyTheRule = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-logic-computedAttrs': { + minimum: { + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + 'x-jsf-errorMessage': { + minimum: { + value: 'This should be greater than {{rule}}.', + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + }, + }, +}; + +export const schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + description: 'Hello world', + type: 'number', + }, + }, + allOf: [ + { + if: { + properties: { + field_a: { + const: 20, + }, + }, + required: ['field_a'], + }, + then: { + properties: { + field_b: { + 'x-jsf-logic-computedAttrs': { + description: { + value: 'Must be between {{half_a}} and {{double_a}}.', + half_a: { + '/': [{ var: 'field_a' }, 2], + }, + double_a: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + }, + }, + }, + }, + ], +}; + export const schemaWithInlineMultipleRulesForComputedAttributes = { properties: { field_a: { diff --git a/src/yupSchema.js b/src/yupSchema.js index 0c8bee152..33bb45025 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -305,6 +305,19 @@ export function buildYupSchema(field, config, validations) { ); } + function withConst(yupSchema) { + return yupSchema.test( + 'isConst', + errorMessage.const ?? + errorMessageFromConfig.const ?? + `The only accepted value is ${propertyFields.const}.`, + (value) => + (propertyFields.required === false && value === undefined) || + value === null || + value === propertyFields.const + ); + } + function withBaseSchema() { const customErrorMsg = errorMessage.type || errorMessageFromConfig.type; if (customErrorMsg) { @@ -326,7 +339,8 @@ export function buildYupSchema(field, config, validations) { ...fieldSetfield, inputType: fieldSetfield.type, }, - config + { ...config, parentID: field.name }, + validations )(); } }); @@ -338,7 +352,11 @@ export function buildYupSchema(field, config, validations) { propertyFields.nthFieldGroup.fields().reduce( (schema, groupArrayField) => ({ ...schema, - [groupArrayField.name]: buildYupSchema(groupArrayField, config)(), + [groupArrayField.name]: buildYupSchema( + groupArrayField, + { ...config, parentID: `${propertyFields.nthFieldGroup.name}[]` }, + validations + )(), }), {} ) @@ -390,6 +408,10 @@ export function buildYupSchema(field, config, validations) { validators.push(withFileFormat); } + if (propertyFields.const) { + validators.push(withConst); + } + if (propertyFields.requiredValidations) { propertyFields.requiredValidations.forEach((id) => validators.push(yupSchemaWithCustomJSONLogic({ field, id, validations, config })) @@ -412,7 +434,7 @@ export function getNoSortEdges(fields = []) { }, []); } -function getSchema(fields = [], config) { +function getSchema(fields = [], config, validations) { const newSchema = {}; fields.forEach((field) => { @@ -421,13 +443,17 @@ function getSchema(fields = [], config) { if (field.inputType === supportedTypes.FIELDSET) { // Fieldset validation schemas depend on the inner schemas of their fields, // so we need to rebuild it to take into account any of those updates. - const fieldsetSchema = buildYupSchema(field, config)(); + const fieldsetSchema = buildYupSchema( + field, + { ...config, parentID: field.name }, + validations + )(); newSchema[field.name] = fieldsetSchema; } else { newSchema[field.name] = field.schema; } } else { - Object.assign(newSchema, getSchema(field.fields, config)); + Object.assign(newSchema, getSchema(field.fields, config, validations)); } } }); @@ -443,6 +469,6 @@ function getSchema(fields = [], config) { * @param {JsfConfig} config - Config * @returns */ -export function buildCompleteYupSchema(fields, config) { - return object().shape(getSchema(fields, config), getNoSortEdges(fields)); +export function buildCompleteYupSchema(fields, config, validations) { + return object().shape(getSchema(fields, config, validations), getNoSortEdges(fields)); }