@a2ui-sdk/utils 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/dist/0.8/dataBinding.d.ts +55 -0
- package/dist/0.8/dataBinding.js +118 -0
- package/dist/0.8/dataBinding.test.d.ts +6 -0
- package/dist/0.8/dataBinding.test.js +271 -0
- package/dist/0.8/index.d.ts +2 -0
- package/dist/0.8/index.js +2 -0
- package/dist/0.8/pathUtils.d.ts +42 -0
- package/dist/0.8/pathUtils.js +111 -0
- package/dist/0.8/pathUtils.test.d.ts +6 -0
- package/dist/0.8/pathUtils.test.js +211 -0
- package/dist/0.9/dataBinding.d.ts +103 -0
- package/dist/0.9/dataBinding.js +165 -0
- package/dist/0.9/dataBinding.test.d.ts +6 -0
- package/dist/0.9/dataBinding.test.js +180 -0
- package/dist/0.9/index.d.ts +4 -1
- package/dist/0.9/index.js +4 -1
- package/dist/0.9/interpolation/index.d.ts +0 -4
- package/dist/0.9/interpolation/index.js +0 -3
- package/dist/0.9/interpolation.test.js +1 -1
- package/dist/0.9/pathUtils.d.ts +0 -12
- package/dist/0.9/pathUtils.js +0 -19
- package/dist/0.9/pathUtils.test.js +1 -21
- package/dist/0.9/validation.d.ts +50 -0
- package/dist/0.9/validation.js +162 -0
- package/dist/0.9/validation.test.d.ts +4 -0
- package/dist/0.9/validation.test.js +307 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +10 -3
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dataBinding Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for data binding utility functions used in A2UI 0.9.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { isPathBinding, isFunctionCall, resolveValue, resolveString, resolveContext, } from './dataBinding.js';
|
|
8
|
+
describe('dataBinding', () => {
|
|
9
|
+
const testModel = {
|
|
10
|
+
user: {
|
|
11
|
+
name: 'John',
|
|
12
|
+
age: 30,
|
|
13
|
+
active: true,
|
|
14
|
+
},
|
|
15
|
+
items: ['a', 'b', 'c'],
|
|
16
|
+
count: 42,
|
|
17
|
+
};
|
|
18
|
+
describe('isPathBinding', () => {
|
|
19
|
+
it('should return true for path binding object', () => {
|
|
20
|
+
expect(isPathBinding({ path: '/user/name' })).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('should return true for relative path binding', () => {
|
|
23
|
+
expect(isPathBinding({ path: 'name' })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('should return false for string literal', () => {
|
|
26
|
+
expect(isPathBinding('Hello')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it('should return false for number literal', () => {
|
|
29
|
+
expect(isPathBinding(42)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it('should return false for boolean literal', () => {
|
|
32
|
+
expect(isPathBinding(true)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it('should return false for function call', () => {
|
|
35
|
+
expect(isPathBinding({ call: 'required' })).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it('should return false for undefined', () => {
|
|
38
|
+
expect(isPathBinding(undefined)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it('should return false for null', () => {
|
|
41
|
+
expect(isPathBinding(null)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('should return false for object without path property', () => {
|
|
44
|
+
expect(isPathBinding({ other: 'value' })).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('isFunctionCall', () => {
|
|
48
|
+
it('should return true for function call object', () => {
|
|
49
|
+
expect(isFunctionCall({ call: 'required' })).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('should return true for function call with args', () => {
|
|
52
|
+
expect(isFunctionCall({ call: 'regex', args: { pattern: '.*' } })).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('should return false for path binding', () => {
|
|
55
|
+
expect(isFunctionCall({ path: '/user/name' })).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it('should return false for string literal', () => {
|
|
58
|
+
expect(isFunctionCall('Hello')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it('should return false for undefined', () => {
|
|
61
|
+
expect(isFunctionCall(undefined)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
it('should return false for null', () => {
|
|
64
|
+
expect(isFunctionCall(null)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// Note: hasInterpolation is now an internal function (not exported)
|
|
68
|
+
// Tests for interpolation behavior are in interpolation.test.ts
|
|
69
|
+
describe('resolveValue', () => {
|
|
70
|
+
it('should resolve string literal', () => {
|
|
71
|
+
expect(resolveValue('Hello', testModel)).toBe('Hello');
|
|
72
|
+
});
|
|
73
|
+
it('should resolve number literal', () => {
|
|
74
|
+
expect(resolveValue(42, testModel)).toBe(42);
|
|
75
|
+
});
|
|
76
|
+
it('should resolve boolean literal', () => {
|
|
77
|
+
expect(resolveValue(true, testModel)).toBe(true);
|
|
78
|
+
expect(resolveValue(false, testModel)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it('should resolve absolute path binding', () => {
|
|
81
|
+
expect(resolveValue({ path: '/user/name' }, testModel)).toBe('John');
|
|
82
|
+
});
|
|
83
|
+
it('should resolve nested path binding', () => {
|
|
84
|
+
expect(resolveValue({ path: '/user/age' }, testModel)).toBe(30);
|
|
85
|
+
});
|
|
86
|
+
it('should resolve relative path binding with base path', () => {
|
|
87
|
+
expect(resolveValue({ path: 'name' }, testModel, '/user')).toBe('John');
|
|
88
|
+
});
|
|
89
|
+
it('should resolve absolute path even with base path', () => {
|
|
90
|
+
expect(resolveValue({ path: '/count' }, testModel, '/user')).toBe(42);
|
|
91
|
+
});
|
|
92
|
+
it('should return default for undefined value', () => {
|
|
93
|
+
expect(resolveValue(undefined, testModel, null, 'default')).toBe('default');
|
|
94
|
+
});
|
|
95
|
+
it('should return default for null value', () => {
|
|
96
|
+
expect(resolveValue(null, testModel, null, 'default')).toBe('default');
|
|
97
|
+
});
|
|
98
|
+
it('should return default for non-existent path', () => {
|
|
99
|
+
expect(resolveValue({ path: '/nonexistent' }, testModel, null, 'default')).toBe('default');
|
|
100
|
+
});
|
|
101
|
+
it('should return default for function call', () => {
|
|
102
|
+
expect(resolveValue({ call: 'required' }, testModel, null, 'default')).toBe('default');
|
|
103
|
+
});
|
|
104
|
+
it('should resolve array element by index', () => {
|
|
105
|
+
expect(resolveValue({ path: '/items/0' }, testModel)).toBe('a');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('resolveString', () => {
|
|
109
|
+
it('should resolve string literal', () => {
|
|
110
|
+
expect(resolveString('Hello', testModel)).toBe('Hello');
|
|
111
|
+
});
|
|
112
|
+
it('should resolve path binding to string', () => {
|
|
113
|
+
expect(resolveString({ path: '/user/name' }, testModel)).toBe('John');
|
|
114
|
+
});
|
|
115
|
+
it('should convert number to string', () => {
|
|
116
|
+
expect(resolveString({ path: '/count' }, testModel)).toBe('42');
|
|
117
|
+
});
|
|
118
|
+
it('should return default for undefined', () => {
|
|
119
|
+
expect(resolveString(undefined, testModel, null, 'default')).toBe('default');
|
|
120
|
+
});
|
|
121
|
+
it('should return empty string as default', () => {
|
|
122
|
+
expect(resolveString(undefined, testModel)).toBe('');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('resolveContext', () => {
|
|
126
|
+
it('should resolve context with literal values', () => {
|
|
127
|
+
const context = {
|
|
128
|
+
name: 'John',
|
|
129
|
+
age: 30,
|
|
130
|
+
active: true,
|
|
131
|
+
};
|
|
132
|
+
expect(resolveContext(context, testModel)).toEqual({
|
|
133
|
+
name: 'John',
|
|
134
|
+
age: 30,
|
|
135
|
+
active: true,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
it('should resolve context with path bindings', () => {
|
|
139
|
+
const context = {
|
|
140
|
+
name: { path: '/user/name' },
|
|
141
|
+
age: { path: '/user/age' },
|
|
142
|
+
};
|
|
143
|
+
expect(resolveContext(context, testModel)).toEqual({
|
|
144
|
+
name: 'John',
|
|
145
|
+
age: 30,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
it('should resolve context with mixed values', () => {
|
|
149
|
+
const context = {
|
|
150
|
+
userName: { path: '/user/name' },
|
|
151
|
+
action: 'submit',
|
|
152
|
+
count: 42,
|
|
153
|
+
};
|
|
154
|
+
expect(resolveContext(context, testModel)).toEqual({
|
|
155
|
+
userName: 'John',
|
|
156
|
+
action: 'submit',
|
|
157
|
+
count: 42,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
it('should resolve relative paths with base path', () => {
|
|
161
|
+
const context = {
|
|
162
|
+
name: { path: 'name' },
|
|
163
|
+
};
|
|
164
|
+
expect(resolveContext(context, testModel, '/user')).toEqual({
|
|
165
|
+
name: 'John',
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
it('should return empty object for undefined context', () => {
|
|
169
|
+
expect(resolveContext(undefined, testModel)).toEqual({});
|
|
170
|
+
});
|
|
171
|
+
it('should handle non-existent paths', () => {
|
|
172
|
+
const context = {
|
|
173
|
+
name: { path: '/nonexistent' },
|
|
174
|
+
};
|
|
175
|
+
expect(resolveContext(context, testModel)).toEqual({
|
|
176
|
+
name: undefined,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
package/dist/0.9/index.d.ts
CHANGED
package/dist/0.9/index.js
CHANGED
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
* const result = interpolate('Hello, ${/user/name}!', { user: { name: 'John' } })
|
|
12
12
|
* // result: "Hello, John!"
|
|
13
13
|
*/
|
|
14
|
-
export type { TokenType, Token, ASTNode, LiteralNode, PathNode, FunctionCallNode, InterpolatedStringNode, ParseError, DataModel, FunctionRegistry, InterpolationFunction, EvaluationContext, } from './types.js';
|
|
15
|
-
export { tokenize } from './lexer.js';
|
|
16
|
-
export { parse } from './parser.js';
|
|
17
|
-
export { evaluate } from './evaluator.js';
|
|
18
14
|
import type { InterpolatedStringNode, DataModel, FunctionRegistry } from './types.js';
|
|
19
15
|
export declare function hasInterpolation(value: string): boolean;
|
|
20
16
|
/**
|
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
* const result = interpolate('Hello, ${/user/name}!', { user: { name: 'John' } })
|
|
12
12
|
* // result: "Hello, John!"
|
|
13
13
|
*/
|
|
14
|
-
export { tokenize } from './lexer.js';
|
|
15
|
-
export { parse } from './parser.js';
|
|
16
|
-
export { evaluate } from './evaluator.js';
|
|
17
14
|
import { tokenize } from './lexer.js';
|
|
18
15
|
import { parse } from './parser.js';
|
|
19
16
|
import { evaluate } from './evaluator.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tests the new refactored parser module.
|
|
4
4
|
*/
|
|
5
5
|
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { parseInterpolation, interpolate } from './interpolation';
|
|
6
|
+
import { parseInterpolation, interpolate } from './interpolation/index.js';
|
|
7
7
|
describe('parseInterpolation', () => {
|
|
8
8
|
it('should parse simple path expression', () => {
|
|
9
9
|
const ast = parseInterpolation('${/user/name}');
|
package/dist/0.9/pathUtils.d.ts
CHANGED
|
@@ -19,18 +19,6 @@ import type { DataModel } from '@a2ui-sdk/types/0.9';
|
|
|
19
19
|
* parseJsonPointer("/m~0n"); // ["m~n"] (escaped tilde)
|
|
20
20
|
*/
|
|
21
21
|
export declare function parseJsonPointer(path: string): string[];
|
|
22
|
-
/**
|
|
23
|
-
* Creates a JSON Pointer path from segments.
|
|
24
|
-
*
|
|
25
|
-
* @param segments - Array of path segments
|
|
26
|
-
* @returns JSON Pointer path string
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* createJsonPointer(["user", "name"]); // "/user/name"
|
|
30
|
-
* createJsonPointer(["a/b"]); // "/a~1b"
|
|
31
|
-
* createJsonPointer([]); // "/"
|
|
32
|
-
*/
|
|
33
|
-
export declare function createJsonPointer(segments: string[]): string;
|
|
34
22
|
/**
|
|
35
23
|
* Gets a value from the data model by JSON Pointer path.
|
|
36
24
|
*
|
package/dist/0.9/pathUtils.js
CHANGED
|
@@ -30,25 +30,6 @@ export function parseJsonPointer(path) {
|
|
|
30
30
|
.filter((s) => s !== '')
|
|
31
31
|
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Creates a JSON Pointer path from segments.
|
|
35
|
-
*
|
|
36
|
-
* @param segments - Array of path segments
|
|
37
|
-
* @returns JSON Pointer path string
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* createJsonPointer(["user", "name"]); // "/user/name"
|
|
41
|
-
* createJsonPointer(["a/b"]); // "/a~1b"
|
|
42
|
-
* createJsonPointer([]); // "/"
|
|
43
|
-
*/
|
|
44
|
-
export function createJsonPointer(segments) {
|
|
45
|
-
if (segments.length === 0) {
|
|
46
|
-
return '/';
|
|
47
|
-
}
|
|
48
|
-
// Escape special characters (~ -> ~0, / -> ~1)
|
|
49
|
-
const escaped = segments.map((segment) => segment.replace(/~/g, '~0').replace(/\//g, '~1'));
|
|
50
|
-
return '/' + escaped.join('/');
|
|
51
|
-
}
|
|
52
33
|
/**
|
|
53
34
|
* Gets a value from the data model by JSON Pointer path.
|
|
54
35
|
*
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Tests for path utility functions used in A2UI 0.9 data model operations.
|
|
5
5
|
*/
|
|
6
6
|
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import { parseJsonPointer,
|
|
7
|
+
import { parseJsonPointer, getValueByPath, setValueByPath, normalizePath, isAbsolutePath, resolvePath, joinPaths, } from './pathUtils.js';
|
|
8
8
|
describe('pathUtils', () => {
|
|
9
9
|
describe('parseJsonPointer', () => {
|
|
10
10
|
it('should return empty array for empty path', () => {
|
|
@@ -38,26 +38,6 @@ describe('pathUtils', () => {
|
|
|
38
38
|
expect(parseJsonPointer('user/name')).toEqual(['user', 'name']);
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
-
describe('createJsonPointer', () => {
|
|
42
|
-
it('should return "/" for empty array', () => {
|
|
43
|
-
expect(createJsonPointer([])).toBe('/');
|
|
44
|
-
});
|
|
45
|
-
it('should create simple path', () => {
|
|
46
|
-
expect(createJsonPointer(['user'])).toBe('/user');
|
|
47
|
-
});
|
|
48
|
-
it('should create nested path', () => {
|
|
49
|
-
expect(createJsonPointer(['user', 'name'])).toBe('/user/name');
|
|
50
|
-
});
|
|
51
|
-
it('should escape / to ~1', () => {
|
|
52
|
-
expect(createJsonPointer(['a/b'])).toBe('/a~1b');
|
|
53
|
-
});
|
|
54
|
-
it('should escape ~ to ~0', () => {
|
|
55
|
-
expect(createJsonPointer(['m~n'])).toBe('/m~0n');
|
|
56
|
-
});
|
|
57
|
-
it('should handle combined escapes', () => {
|
|
58
|
-
expect(createJsonPointer(['~/'])).toBe('/~0~1');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
41
|
describe('getValueByPath', () => {
|
|
62
42
|
const testModel = {
|
|
63
43
|
user: {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for A2UI 0.9 Renderer.
|
|
3
|
+
*
|
|
4
|
+
* Implements validation functions and LogicExpression evaluation
|
|
5
|
+
* for the `checks` property on input components and Buttons.
|
|
6
|
+
*/
|
|
7
|
+
import type { CheckRule, DynamicValue, DataModel, ValidationResult } from '@a2ui-sdk/types/0.9';
|
|
8
|
+
/**
|
|
9
|
+
* Type for a validation function.
|
|
10
|
+
* Takes resolved arguments and returns a boolean.
|
|
11
|
+
*/
|
|
12
|
+
export type ValidationFunction = (args: Record<string, unknown>) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Built-in validation functions.
|
|
15
|
+
*/
|
|
16
|
+
export declare const validationFunctions: Record<string, ValidationFunction>;
|
|
17
|
+
/**
|
|
18
|
+
* Context for evaluating expressions.
|
|
19
|
+
*/
|
|
20
|
+
export interface EvaluationContext {
|
|
21
|
+
dataModel: DataModel;
|
|
22
|
+
basePath: string | null;
|
|
23
|
+
functions?: Record<string, ValidationFunction>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolves function arguments from DynamicValue to actual values.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveArgs(args: Record<string, DynamicValue> | undefined, dataModel: DataModel, basePath: string | null): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Evaluates a function call.
|
|
31
|
+
*/
|
|
32
|
+
export declare function evaluateFunctionCall(call: string, args: Record<string, DynamicValue> | undefined, context: EvaluationContext): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Evaluates a CheckRule (which is also a LogicExpression).
|
|
35
|
+
*
|
|
36
|
+
* @param rule - The check rule to evaluate
|
|
37
|
+
* @param context - Evaluation context with data model and scope
|
|
38
|
+
* @returns true if the check passes, false if it fails
|
|
39
|
+
*/
|
|
40
|
+
export declare function evaluateCheckRule(rule: CheckRule, context: EvaluationContext): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Evaluates all checks for a component and returns validation result.
|
|
43
|
+
*
|
|
44
|
+
* @param checks - Array of check rules
|
|
45
|
+
* @param dataModel - The data model for value resolution
|
|
46
|
+
* @param basePath - The current scope base path (for relative paths)
|
|
47
|
+
* @param functions - Optional custom validation functions
|
|
48
|
+
* @returns ValidationResult with valid flag and error messages
|
|
49
|
+
*/
|
|
50
|
+
export declare function evaluateChecks(checks: CheckRule[] | undefined, dataModel: DataModel, basePath: string | null, functions?: Record<string, ValidationFunction>): ValidationResult;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for A2UI 0.9 Renderer.
|
|
3
|
+
*
|
|
4
|
+
* Implements validation functions and LogicExpression evaluation
|
|
5
|
+
* for the `checks` property on input components and Buttons.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveValue } from './dataBinding.js';
|
|
8
|
+
/**
|
|
9
|
+
* Built-in validation functions.
|
|
10
|
+
*/
|
|
11
|
+
export const validationFunctions = {
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a value is present (not null, undefined, or empty string).
|
|
14
|
+
*/
|
|
15
|
+
required: ({ value }) => {
|
|
16
|
+
if (value === null || value === undefined)
|
|
17
|
+
return false;
|
|
18
|
+
if (typeof value === 'string')
|
|
19
|
+
return value.trim().length > 0;
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.length > 0;
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
/**
|
|
25
|
+
* Validates email format.
|
|
26
|
+
*/
|
|
27
|
+
email: ({ value }) => {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return false;
|
|
30
|
+
// Simple email validation - matches most valid emails
|
|
31
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
32
|
+
return emailRegex.test(value);
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Tests value against a regular expression pattern.
|
|
36
|
+
*/
|
|
37
|
+
regex: ({ value, pattern }) => {
|
|
38
|
+
if (typeof value !== 'string')
|
|
39
|
+
return false;
|
|
40
|
+
if (typeof pattern !== 'string')
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
return new RegExp(pattern).test(value);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Invalid regex pattern
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Validates string length within min/max bounds.
|
|
52
|
+
*/
|
|
53
|
+
length: ({ value, min, max }) => {
|
|
54
|
+
const str = String(value ?? '');
|
|
55
|
+
const len = str.length;
|
|
56
|
+
if (min !== undefined && min !== null && len < Number(min))
|
|
57
|
+
return false;
|
|
58
|
+
if (max !== undefined && max !== null && len > Number(max))
|
|
59
|
+
return false;
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Validates numeric value within min/max bounds.
|
|
64
|
+
*/
|
|
65
|
+
numeric: ({ value, min, max }) => {
|
|
66
|
+
const num = Number(value);
|
|
67
|
+
if (isNaN(num))
|
|
68
|
+
return false;
|
|
69
|
+
if (min !== undefined && min !== null && num < Number(min))
|
|
70
|
+
return false;
|
|
71
|
+
if (max !== undefined && max !== null && num > Number(max))
|
|
72
|
+
return false;
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Resolves function arguments from DynamicValue to actual values.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveArgs(args, dataModel, basePath) {
|
|
80
|
+
if (!args)
|
|
81
|
+
return {};
|
|
82
|
+
const resolved = {};
|
|
83
|
+
for (const [key, value] of Object.entries(args)) {
|
|
84
|
+
resolved[key] = resolveValue(value, dataModel, basePath, undefined);
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Evaluates a function call.
|
|
90
|
+
*/
|
|
91
|
+
export function evaluateFunctionCall(call, args, context) {
|
|
92
|
+
const { dataModel, basePath, functions = validationFunctions } = context;
|
|
93
|
+
const fn = functions[call];
|
|
94
|
+
if (!fn) {
|
|
95
|
+
console.warn(`[A2UI] Unknown validation function: ${call}`);
|
|
96
|
+
return true; // Unknown functions pass by default
|
|
97
|
+
}
|
|
98
|
+
const resolvedArgs = resolveArgs(args, dataModel, basePath);
|
|
99
|
+
return fn(resolvedArgs);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Evaluates a CheckRule (which is also a LogicExpression).
|
|
103
|
+
*
|
|
104
|
+
* @param rule - The check rule to evaluate
|
|
105
|
+
* @param context - Evaluation context with data model and scope
|
|
106
|
+
* @returns true if the check passes, false if it fails
|
|
107
|
+
*/
|
|
108
|
+
export function evaluateCheckRule(rule, context) {
|
|
109
|
+
// Handle logical operators
|
|
110
|
+
if ('and' in rule && rule.and) {
|
|
111
|
+
return rule.and.every((r) => evaluateCheckRule(r, context));
|
|
112
|
+
}
|
|
113
|
+
if ('or' in rule && rule.or) {
|
|
114
|
+
return rule.or.some((r) => evaluateCheckRule(r, context));
|
|
115
|
+
}
|
|
116
|
+
if ('not' in rule && rule.not) {
|
|
117
|
+
return !evaluateCheckRule(rule.not, context);
|
|
118
|
+
}
|
|
119
|
+
// Handle boolean constants
|
|
120
|
+
if ('true' in rule && rule.true === true) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if ('false' in rule && rule.false === false) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// Handle function call
|
|
127
|
+
if ('call' in rule && rule.call) {
|
|
128
|
+
return evaluateFunctionCall(rule.call, rule.args, context);
|
|
129
|
+
}
|
|
130
|
+
// No valid expression found - default to pass
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Evaluates all checks for a component and returns validation result.
|
|
135
|
+
*
|
|
136
|
+
* @param checks - Array of check rules
|
|
137
|
+
* @param dataModel - The data model for value resolution
|
|
138
|
+
* @param basePath - The current scope base path (for relative paths)
|
|
139
|
+
* @param functions - Optional custom validation functions
|
|
140
|
+
* @returns ValidationResult with valid flag and error messages
|
|
141
|
+
*/
|
|
142
|
+
export function evaluateChecks(checks, dataModel, basePath, functions) {
|
|
143
|
+
if (!checks || checks.length === 0) {
|
|
144
|
+
return { valid: true, errors: [] };
|
|
145
|
+
}
|
|
146
|
+
const context = {
|
|
147
|
+
dataModel,
|
|
148
|
+
basePath,
|
|
149
|
+
functions: functions ?? validationFunctions,
|
|
150
|
+
};
|
|
151
|
+
const errors = [];
|
|
152
|
+
for (const check of checks) {
|
|
153
|
+
const passes = evaluateCheckRule(check, context);
|
|
154
|
+
if (!passes && check.message) {
|
|
155
|
+
errors.push(check.message);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
valid: errors.length === 0,
|
|
160
|
+
errors,
|
|
161
|
+
};
|
|
162
|
+
}
|