Skip to content

Commit 7754139

Browse files
committed
feat(core): Add validate function based rule condition
Add a new rule condition `ValidateFunctionCondition` using a given `validate` function to evaluate the condition result. This allows using arbitrary custom logic to evaluate condition results. This facilitates not using schema-conditions to be able to only use one pre-compiled AJV for the data schema at a later stage.
1 parent 9a60420 commit 7754139

File tree

4 files changed

+125
-1
lines changed

4 files changed

+125
-1
lines changed

packages/core/src/models/uischema.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ export interface SchemaBasedCondition extends BaseCondition, Scoped {
150150
failWhenUndefined?: boolean;
151151
}
152152

153+
/** A condition using a validation function to determine its fulfillment. */
154+
export interface ValidateFunctionCondition extends BaseCondition, Scoped {
155+
/**
156+
* Validates whether the condition is fulfilled.
157+
*
158+
* @param data The data as resolved via the scope.
159+
* @returns `true` if the condition is fulfilled */
160+
validate: (data: unknown) => boolean;
161+
}
162+
153163
/**
154164
* A composable condition.
155165
*/
@@ -179,7 +189,8 @@ export type Condition =
179189
| LeafCondition
180190
| OrCondition
181191
| AndCondition
182-
| SchemaBasedCondition;
192+
| SchemaBasedCondition
193+
| ValidateFunctionCondition;
183194

184195
/**
185196
* Common base interface for any UI schema element.

packages/core/src/util/runtime.ts

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
SchemaBasedCondition,
3434
Scopable,
3535
UISchemaElement,
36+
ValidateFunctionCondition,
3637
} from '../models';
3738
import { resolveData } from './resolvers';
3839
import type Ajv from 'ajv';
@@ -51,6 +52,12 @@ const isSchemaCondition = (
5152
condition: Condition
5253
): condition is SchemaBasedCondition => has(condition, 'schema');
5354

55+
const isValidateFunctionCondition = (
56+
condition: Condition
57+
): condition is ValidateFunctionCondition =>
58+
has(condition, 'validate') &&
59+
typeof (condition as ValidateFunctionCondition).validate === 'function';
60+
5461
const getConditionScope = (condition: Scopable, path: string): string => {
5562
return composeWithUi(condition, path);
5663
};
@@ -80,6 +87,9 @@ const evaluateCondition = (
8087
return false;
8188
}
8289
return ajv.validate(condition.schema, value) as boolean;
90+
} else if (isValidateFunctionCondition(condition)) {
91+
const value = resolveData(data, getConditionScope(condition, path));
92+
return condition.validate(value);
8393
} else {
8494
// unknown condition
8595
return true;

packages/core/test/util/runtime.test.ts

+85
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
OrCondition,
3434
RuleEffect,
3535
SchemaBasedCondition,
36+
ValidateFunctionCondition,
3637
} from '../../src';
3738
import { evalEnablement, evalVisibility } from '../../src/util/runtime';
3839

@@ -491,6 +492,90 @@ test('evalEnablement disable valid case', (t) => {
491492
t.is(evalEnablement(uischema, data, undefined, createAjv()), false);
492493
});
493494

495+
// Add test case for ValidateFunctionCondition with evalEnablement (valid enable case)
496+
test('evalEnablement enable valid case based on ValidateFunctionCondition', (t) => {
497+
const condition: ValidateFunctionCondition = {
498+
scope: '#/properties/ruleValue',
499+
validate: (data) => data === 'bar',
500+
};
501+
const uischema: ControlElement = {
502+
type: 'Control',
503+
scope: '#/properties/value',
504+
rule: {
505+
effect: RuleEffect.ENABLE,
506+
condition: condition,
507+
},
508+
};
509+
const data = {
510+
value: 'foo',
511+
ruleValue: 'bar',
512+
};
513+
t.is(evalEnablement(uischema, data, undefined, createAjv()), true);
514+
});
515+
516+
// Add test case for ValidateFunctionCondition with evalEnablement (invalid enable case)
517+
test('evalEnablement enable invalid case based on ValidateFunctionCondition', (t) => {
518+
const condition: ValidateFunctionCondition = {
519+
scope: '#/properties/ruleValue',
520+
validate: (data) => data === 'bar',
521+
};
522+
const uischema: ControlElement = {
523+
type: 'Control',
524+
scope: '#/properties/value',
525+
rule: {
526+
effect: RuleEffect.ENABLE,
527+
condition: condition,
528+
},
529+
};
530+
const data = {
531+
value: 'foo',
532+
ruleValue: 'foobar',
533+
};
534+
t.is(evalEnablement(uischema, data, undefined, createAjv()), false);
535+
});
536+
537+
// Add test case for ValidateFunctionCondition with evalEnablement (valid disable case)
538+
test('evalEnablement disable valid case based on ValidateFunctionCondition', (t) => {
539+
const condition: ValidateFunctionCondition = {
540+
scope: '#/properties/ruleValue',
541+
validate: (data) => data === 'bar',
542+
};
543+
const uischema: ControlElement = {
544+
type: 'Control',
545+
scope: '#/properties/value',
546+
rule: {
547+
effect: RuleEffect.DISABLE,
548+
condition: condition,
549+
},
550+
};
551+
const data = {
552+
value: 'foo',
553+
ruleValue: 'bar',
554+
};
555+
t.is(evalEnablement(uischema, data, undefined, createAjv()), false);
556+
});
557+
558+
// Add test case for ValidateFunctionCondition with evalEnablement (invalid disable case)
559+
test('evalEnablement disable invalid case based on ValidateFunctionCondition', (t) => {
560+
const condition: ValidateFunctionCondition = {
561+
scope: '#/properties/ruleValue',
562+
validate: (data) => data === 'bar',
563+
};
564+
const uischema: ControlElement = {
565+
type: 'Control',
566+
scope: '#/properties/value',
567+
rule: {
568+
effect: RuleEffect.DISABLE,
569+
condition: condition,
570+
},
571+
};
572+
const data = {
573+
value: 'foo',
574+
ruleValue: 'foobar',
575+
};
576+
t.is(evalEnablement(uischema, data, undefined, createAjv()), true);
577+
});
578+
494579
test('evalEnablement disable invalid case', (t) => {
495580
const leafCondition: LeafCondition = {
496581
type: 'LEAF',

packages/examples/src/examples/rule.ts

+18
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export const schema = {
4444
type: 'string',
4545
enum: ['All', 'Some', 'Only potatoes'],
4646
},
47+
vitaminDeficiency: {
48+
type: 'string',
49+
enum: ['None', 'Vitamin A', 'Vitamin B', 'Vitamin C'],
50+
},
4751
},
4852
};
4953

@@ -101,6 +105,20 @@ export const uischema = {
101105
},
102106
},
103107
},
108+
{
109+
type: 'Control',
110+
label: 'Vitamin deficiency?',
111+
scope: '#/properties/vitaminDeficiency',
112+
rule: {
113+
effect: 'SHOW',
114+
condition: {
115+
scope: '#',
116+
validate: (data: any) => {
117+
return !data.dead && data.kindOfVegetables !== 'All';
118+
},
119+
},
120+
},
121+
},
104122
],
105123
},
106124
],

0 commit comments

Comments
 (0)