diff --git a/next/src/form.ts b/next/src/form.ts index 671d7ce4..fb228227 100644 --- a/next/src/form.ts +++ b/next/src/form.ts @@ -6,6 +6,7 @@ import { getErrorMessage } from './errors/messages' import { buildFieldSchema } from './field/schema' import { calculateFinalSchema, updateFieldProperties } from './mutations' import { validateSchema } from './validation/schema' +import { registerCustomFunctionsToJsonLogic } from './validation/json-logic' export { ValidationOptions } from './validation/schema' @@ -255,6 +256,10 @@ export function createHeadlessForm( options: options.validationOptions, }) + if (options.validationOptions?.customJsonLogicOps) { + registerCustomFunctionsToJsonLogic(options.validationOptions.customJsonLogicOps) + } + const fields = buildFields({ schema: updatedSchema, originalSchema: schema, strictInputType }) // TODO: check if we need this isError variable exposed diff --git a/next/src/validation/json-logic.ts b/next/src/validation/json-logic.ts index 3c5c5d30..96f7f2fc 100644 --- a/next/src/validation/json-logic.ts +++ b/next/src/validation/json-logic.ts @@ -3,6 +3,16 @@ import type { ValidationError, ValidationErrorPath } from '../errors' import type { JsfObjectSchema, JsfSchema, JsonLogicContext, JsonLogicRules, JsonLogicSchema, NonBooleanJsfSchema, ObjectValue, SchemaValue } from '../types' import jsonLogic from 'json-logic-js' +/** + * Register user defined functions to be used in JSON Logic rules. + * @param customJsonLogicOps An object containing custom JSON Logic operations to register + */ +export function registerCustomFunctionsToJsonLogic(customJsonLogicOps: Record any>) { + for (const [name, func] of Object.entries(customJsonLogicOps)) { + jsonLogic.add_operation(name, func) + } +} + /** * Builds a json-logic context based on a schema and the current value * @param schema - The schema to build the context from diff --git a/next/src/validation/schema.ts b/next/src/validation/schema.ts index 63d7ccb0..04f6992d 100644 --- a/next/src/validation/schema.ts +++ b/next/src/validation/schema.ts @@ -27,6 +27,12 @@ export interface ValidationOptions { * @default false */ allowForbiddenValues?: boolean + + /** + * Custom jsonLogic operations to register (only applies once at setup) + * Format: { [operationName]: (...args: any[]) => any } + */ + customJsonLogicOps?: Record any> } /** diff --git a/next/test/validation/json-logic-v0.test.js b/next/test/validation/json-logic-v0.test.js index 8fdf2bbe..2b1ddc1b 100644 --- a/next/test/validation/json-logic-v0.test.js +++ b/next/test/validation/json-logic-v0.test.js @@ -1,6 +1,7 @@ -import { createHeadlessForm } from '@/createHeadlessForm' import { afterEach, beforeEach, describe, expect, it } from '@jest/globals' + import { mockConsole, restoreConsoleAndEnsureItWasNotCalled } from '../test-utils' + import { badSchemaThatWillNotSetAForcedValue, createSchemaWithRulesOnFieldA, @@ -15,6 +16,7 @@ import { schemaWithComputedAttributeThatDoesntExist, schemaWithComputedAttributeThatDoesntExistDescription, schemaWithComputedAttributeThatDoesntExistTitle, + schemaWithCustomValidationFunction, schemaWithDeepVarThatDoesNotExist, schemaWithDeepVarThatDoesNotExistOnFieldset, schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, @@ -30,6 +32,7 @@ import { schemaWithUnknownVariableInValidations, schemaWithValidationThatDoesNotExistOnProperty, } from './json-logic.fixtures' +import { createHeadlessForm } from '@/createHeadlessForm' beforeEach(mockConsole) afterEach(restoreConsoleAndEnsureItWasNotCalled) @@ -446,4 +449,23 @@ 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, validationOptions: { 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, validationOptions: { 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, validationOptions: { customJsonLogicOps: { is_hello: a => a === 'hello world!' } } }) + expect(handleValidation2({ field_a: 'hello world!' }).formErrors).toEqual(undefined) + }) + }) }) diff --git a/next/test/validation/json-logic.fixtures.js b/next/test/validation/json-logic.fixtures.js index 4dc7d45b..15a70616 100644 --- a/next/test/validation/json-logic.fixtures.js +++ b/next/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' }, + }, + }, + }, + }, +}