diff --git a/next/json-schema-test-suite b/next/json-schema-test-suite new file mode 160000 index 00000000..e524505b --- /dev/null +++ b/next/json-schema-test-suite @@ -0,0 +1 @@ +Subproject commit e524505b8ac4a61c5dc162b51d68c2385a134706 diff --git a/src/form.ts b/src/form.ts index 11334419..7af93a90 100644 --- a/src/form.ts +++ b/src/form.ts @@ -5,6 +5,7 @@ import type { LegacyOptions } from './validation/schema' import { getErrorMessage } from './errors/messages' import { buildFieldSchema } from './field/schema' import { calculateFinalSchema, updateFieldProperties } from './mutations' +import { addCustomJsonLogicOperations, removeCustomJsonLogicOperations } from './validation/json-logic' import { validateSchema } from './validation/schema' export { LegacyOptions } from './validation/schema' @@ -228,6 +229,11 @@ export interface CreateHeadlessFormOptions { * @default false */ strictInputType?: boolean + + /** + * Custom user defined functions. A dictionary of name and function + */ + customJsonLogicOps?: Record any> } function buildFields(params: { schema: JsfObjectSchema, originalSchema: JsfObjectSchema, strictInputType?: boolean }): Field[] { @@ -251,6 +257,20 @@ function validateOptions(options: CreateHeadlessFormOptions) { if (Object.prototype.hasOwnProperty.call(options, 'customProperties')) { console.error('[json-schema-form] `customProperties` is a deprecated option and it\'s not supported on json-schema-form v1') } + + if (options.customJsonLogicOps) { + if (typeof options.customJsonLogicOps !== 'object' || options.customJsonLogicOps === null) { + throw new TypeError('validationOptions.customJsonLogicOps must be an object.') + } + + for (const [name, func] of Object.entries(options.customJsonLogicOps)) { + if (typeof func !== 'function') { + throw new TypeError( + `Custom JSON Logic operator '${name}' must be a function, but received type '${typeof func}'.`, + ) + } + } + } } export function createHeadlessForm( @@ -273,18 +293,26 @@ export function createHeadlessForm( const isError = false const handleValidation = (value: SchemaValue) => { - const updatedSchema = calculateFinalSchema({ - schema, - values: value, - options: options.legacyOptions, - }) + const customJsonLogicOps = options?.customJsonLogicOps + + try { + addCustomJsonLogicOperations(customJsonLogicOps) - const result = validate(value, updatedSchema, options.legacyOptions) + const updatedSchema = calculateFinalSchema({ + schema, + values: value, + options: options.legacyOptions, + }) - // Fields properties might have changed, so we need to reset the fields by updating them in place - updateFieldProperties(fields, updatedSchema, schema) + const result = validate(value, updatedSchema, options.legacyOptions) - return result + updateFieldProperties(fields, updatedSchema, schema) + + return result + } + finally { + removeCustomJsonLogicOperations(customJsonLogicOps) + } } return { diff --git a/src/validation/json-logic.ts b/src/validation/json-logic.ts index 1530f1f8..02a28179 100644 --- a/src/validation/json-logic.ts +++ b/src/validation/json-logic.ts @@ -279,3 +279,19 @@ function cycleThroughAttrsAndApplyValues(propertySchema: JsfSchema, computedValu } } } + +export function addCustomJsonLogicOperations(ops?: Record any>) { + if (ops) { + for (const [name, func] of Object.entries(ops)) { + jsonLogic.add_operation(name, func) + } + } +} + +export function removeCustomJsonLogicOperations(ops?: Record any>) { + if (ops) { + for (const name of Object.keys(ops)) { + jsonLogic.rm_operation(name) + } + } +} diff --git a/test/validation/json-logic-v0.test.js b/test/validation/json-logic-v0.test.js index 8fdf2bbe..727e0916 100644 --- a/test/validation/json-logic-v0.test.js +++ b/test/validation/json-logic-v0.test.js @@ -15,6 +15,7 @@ import { schemaWithComputedAttributeThatDoesntExist, schemaWithComputedAttributeThatDoesntExistDescription, schemaWithComputedAttributeThatDoesntExistTitle, + schemaWithCustomValidationFunction, schemaWithDeepVarThatDoesNotExist, schemaWithDeepVarThatDoesNotExistOnFieldset, schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, @@ -446,4 +447,38 @@ describe('jsonLogic: cross-values validations', () => { expect(handleValidation({ field_a: 20, field_b: 41 }).formErrors).toEqual() }) }) + + describe('custom operators', () => { + it('custom function', () => { + const { handleValidation } = createHeadlessForm(schemaWithCustomValidationFunction, { strictInputType: false, customJsonLogicOps: { is_hello: a => a === 'hello world' } }) + expect(handleValidation({ field_a: 'hello world' }).formErrors).toEqual(undefined) + const { formErrors } = handleValidation({ field_a: 'wrong text' }) + expect(formErrors?.field_a).toEqual('Invalid hello world') + }) + + it('custom function are form specific', () => { + const { handleValidation } = createHeadlessForm(schemaWithCustomValidationFunction, { strictInputType: false, customJsonLogicOps: { is_hello: a => a === 'hello world' } }) + expect(handleValidation({ field_a: 'hello world' }).formErrors).toEqual(undefined) + const { formErrors } = handleValidation({ field_a: 'wrong text' }) + expect(formErrors?.field_a).toEqual('Invalid hello world') + + const { handleValidation: handleValidation2 } = createHeadlessForm(schemaWithCustomValidationFunction, { strictInputType: false, customJsonLogicOps: { is_hello: a => a === 'hello world!' } }) + expect(handleValidation2({ field_a: 'hello world!' }).formErrors).toEqual(undefined) + + const { handleValidation: handleValidation3 } = createHeadlessForm(schemaWithCustomValidationFunction, { strictInputType: false }) + const actionThatWillThrow = () => { + handleValidation3({ field_a: 'hello world' }) + } + + expect(actionThatWillThrow).toThrow('Unrecognized operation is_hello') + }) + + it('validation on custom functions', () => { + const actionThatWillThrow = () => { + createHeadlessForm(schemaWithCustomValidationFunction, { strictInputType: false, customJsonLogicOps: { is_hello: 'not a funcion' } }) + } + + expect(actionThatWillThrow).toThrow('Custom JSON Logic operator \'is_hello\' must be a function, but received type \'string\'.') + }) + }) }) diff --git a/test/validation/json-logic.fixtures.js b/test/validation/json-logic.fixtures.js index 4dc7d45b..15a70616 100644 --- a/test/validation/json-logic.fixtures.js +++ b/test/validation/json-logic.fixtures.js @@ -744,3 +744,22 @@ export const schemaWithReduceAccumulator = { }, }, } + +export const schemaWithCustomValidationFunction = { + 'properties': { + field_a: { + 'type': 'string', + 'x-jsf-logic-validations': ['hello_world'], + }, + }, + 'x-jsf-logic': { + validations: { + hello_world: { + errorMessage: 'Invalid hello world', + rule: { + is_hello: { var: 'field_a' }, + }, + }, + }, + }, +}