From ac414f8356d9323a237ca11105bb0530004a404c Mon Sep 17 00:00:00 2001 From: Simone Erba Date: Sat, 2 Aug 2025 11:55:40 +0200 Subject: [PATCH 1/3] v1 --- next/json-schema-test-suite | 1 + src/form.ts | 48 ++++++++++++++++++++------ src/validation/json-logic.ts | 16 +++++++++ test/validation/json-logic-v0.test.js | 10 ++++++ test/validation/json-logic.fixtures.js | 11 ++++++ 5 files changed, 76 insertions(+), 10 deletions(-) create mode 160000 next/json-schema-test-suite diff --git a/next/json-schema-test-suite b/next/json-schema-test-suite new file mode 160000 index 000000000..e524505b8 --- /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 113344199..ce108c131 100644 --- a/src/form.ts +++ b/src/form.ts @@ -6,8 +6,8 @@ import { getErrorMessage } from './errors/messages' import { buildFieldSchema } from './field/schema' import { calculateFinalSchema, updateFieldProperties } from './mutations' import { validateSchema } from './validation/schema' - export { LegacyOptions } from './validation/schema' +import { addCustomJsonLogicOperations, removeCustomJsonLogicOperations } from './validation/json-logic' interface FormResult { fields: Field[] @@ -228,6 +228,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 +256,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 +292,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 { @@ -293,4 +320,5 @@ export function createHeadlessForm( error: null, handleValidation, } + } diff --git a/src/validation/json-logic.ts b/src/validation/json-logic.ts index 1530f1f8d..02a281793 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 8fdf2bbe1..1d954af27 100644 --- a/test/validation/json-logic-v0.test.js +++ b/test/validation/json-logic-v0.test.js @@ -29,6 +29,7 @@ import { schemaWithUnknownVariableInComputedValues, schemaWithUnknownVariableInValidations, schemaWithValidationThatDoesNotExistOnProperty, + schemaForCustomValidationFunctions } from './json-logic.fixtures' beforeEach(mockConsole) @@ -445,5 +446,14 @@ describe('jsonLogic: cross-values validations', () => { }) expect(handleValidation({ field_a: 20, field_b: 41 }).formErrors).toEqual() }) + + it('custom validation functions', () => { + const { handleValidation } = createHeadlessForm( + schemaForCustomValidationFunctions, + { strictInputType: false, customJsonLogicOps: {"is_hello": (text) => text === "Hello!"} }, + ) + + expect(handleValidation({ field_a: "Hello!" }).formErrors).toEqual() + }) }) }) diff --git a/test/validation/json-logic.fixtures.js b/test/validation/json-logic.fixtures.js index 4dc7d45b6..42f487a75 100644 --- a/test/validation/json-logic.fixtures.js +++ b/test/validation/json-logic.fixtures.js @@ -744,3 +744,14 @@ export const schemaWithReduceAccumulator = { }, }, } + +export const schemaForCustomValidationFunctions = { + 'properties': { + field_a: { + 'type': 'string', + 'x-jsf-logic-validations': [ + 'is_hello', + ], + }, + }, +} \ No newline at end of file From 068f6eea13d7b971a55a8c825f4d2b282fcac94a Mon Sep 17 00:00:00 2001 From: Simone Erba Date: Sat, 2 Aug 2025 12:06:15 +0200 Subject: [PATCH 2/3] tests --- test/validation/json-logic-v0.test.js | 39 +++++++++++++++++++++----- test/validation/json-logic.fixtures.js | 16 ++++++++--- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/test/validation/json-logic-v0.test.js b/test/validation/json-logic-v0.test.js index 1d954af27..484a0fcbb 100644 --- a/test/validation/json-logic-v0.test.js +++ b/test/validation/json-logic-v0.test.js @@ -29,7 +29,7 @@ import { schemaWithUnknownVariableInComputedValues, schemaWithUnknownVariableInValidations, schemaWithValidationThatDoesNotExistOnProperty, - schemaForCustomValidationFunctions + schemaWithCustomValidationFunction } from './json-logic.fixtures' beforeEach(mockConsole) @@ -446,14 +446,39 @@ describe('jsonLogic: cross-values validations', () => { }) expect(handleValidation({ field_a: 20, field_b: 41 }).formErrors).toEqual() }) + }) - it('custom validation functions', () => { - const { handleValidation } = createHeadlessForm( - schemaForCustomValidationFunctions, - { strictInputType: false, customJsonLogicOps: {"is_hello": (text) => text === "Hello!"} }, - ) + 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(handleValidation({ field_a: "Hello!" }).formErrors).toEqual() + 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 42f487a75..34eda898c 100644 --- a/test/validation/json-logic.fixtures.js +++ b/test/validation/json-logic.fixtures.js @@ -745,13 +745,21 @@ export const schemaWithReduceAccumulator = { }, } -export const schemaForCustomValidationFunctions = { +export const schemaWithCustomValidationFunction = { 'properties': { field_a: { 'type': 'string', - 'x-jsf-logic-validations': [ - 'is_hello', - ], + 'x-jsf-logic-validations': ['hello_world'], + }, + }, + 'x-jsf-logic': { + validations: { + hello_world: { + errorMessage: 'Invalid hello world', + rule: { + is_hello: { var: 'field_a' }, + }, + }, }, }, } \ No newline at end of file From 44724d72cc2d7d8a17f2bf0d5df0fb8fd939e694 Mon Sep 17 00:00:00 2001 From: Simone Erba Date: Mon, 4 Aug 2025 19:26:27 +0200 Subject: [PATCH 3/3] fix tests --- src/form.ts | 6 +++--- test/validation/json-logic-v0.test.js | 2 +- test/validation/json-logic.fixtures.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/form.ts b/src/form.ts index ce108c131..7af93a906 100644 --- a/src/form.ts +++ b/src/form.ts @@ -5,9 +5,10 @@ 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' -import { addCustomJsonLogicOperations, removeCustomJsonLogicOperations } from './validation/json-logic' interface FormResult { fields: Field[] @@ -232,7 +233,7 @@ export interface CreateHeadlessFormOptions { /** * Custom user defined functions. A dictionary of name and function */ - customJsonLogicOps?: Record any>; + customJsonLogicOps?: Record any> } function buildFields(params: { schema: JsfObjectSchema, originalSchema: JsfObjectSchema, strictInputType?: boolean }): Field[] { @@ -320,5 +321,4 @@ export function createHeadlessForm( error: null, handleValidation, } - } diff --git a/test/validation/json-logic-v0.test.js b/test/validation/json-logic-v0.test.js index 484a0fcbb..727e0916a 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, @@ -29,7 +30,6 @@ import { schemaWithUnknownVariableInComputedValues, schemaWithUnknownVariableInValidations, schemaWithValidationThatDoesNotExistOnProperty, - schemaWithCustomValidationFunction } from './json-logic.fixtures' beforeEach(mockConsole) diff --git a/test/validation/json-logic.fixtures.js b/test/validation/json-logic.fixtures.js index 34eda898c..15a706165 100644 --- a/test/validation/json-logic.fixtures.js +++ b/test/validation/json-logic.fixtures.js @@ -762,4 +762,4 @@ export const schemaWithCustomValidationFunction = { }, }, }, -} \ No newline at end of file +}