@fogpipe/forma-core 0.6.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 +169 -0
- package/dist/chunk-IRLYWN3R.js +790 -0
- package/dist/chunk-IRLYWN3R.js.map +1 -0
- package/dist/chunk-QTLXVG6P.js +135 -0
- package/dist/chunk-QTLXVG6P.js.map +1 -0
- package/dist/engine/index.cjs +885 -0
- package/dist/engine/index.cjs.map +1 -0
- package/dist/engine/index.d.cts +258 -0
- package/dist/engine/index.d.ts +258 -0
- package/dist/engine/index.js +32 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/feel/index.cjs +165 -0
- package/dist/feel/index.cjs.map +1 -0
- package/dist/feel/index.d.cts +105 -0
- package/dist/feel/index.d.ts +105 -0
- package/dist/feel/index.js +19 -0
- package/dist/feel/index.js.map +1 -0
- package/dist/index.cjs +962 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/types-Bs3CG9JZ.d.cts +283 -0
- package/dist/types-Bs3CG9JZ.d.ts +283 -0
- package/package.json +82 -0
- package/src/engine/calculate.ts +363 -0
- package/src/engine/enabled.ts +147 -0
- package/src/engine/index.ts +54 -0
- package/src/engine/required.ts +151 -0
- package/src/engine/validate.ts +647 -0
- package/src/engine/visibility.ts +228 -0
- package/src/feel/index.ts +299 -0
- package/src/index.ts +15 -0
- package/src/types.ts +364 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility Engine
|
|
3
|
+
*
|
|
4
|
+
* Determines which fields should be visible based on form data
|
|
5
|
+
* and Forma visibility rules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { evaluateBoolean } from "../feel/index.js";
|
|
9
|
+
import type {
|
|
10
|
+
Forma,
|
|
11
|
+
FieldDefinition,
|
|
12
|
+
EvaluationContext,
|
|
13
|
+
VisibilityResult,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
import { calculate } from "./calculate.js";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export interface VisibilityOptions {
|
|
22
|
+
/** Pre-calculated computed values (avoids recalculation) */
|
|
23
|
+
computed?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Main Function
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Determine visibility for all fields in a form
|
|
32
|
+
*
|
|
33
|
+
* Returns a map of field paths to boolean visibility states.
|
|
34
|
+
* Fields without visibleWhen expressions are always visible.
|
|
35
|
+
*
|
|
36
|
+
* @param data - Current form data
|
|
37
|
+
* @param spec - Form specification
|
|
38
|
+
* @param options - Optional pre-calculated computed values
|
|
39
|
+
* @returns Map of field paths to visibility states
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const visibility = getVisibility(
|
|
43
|
+
* { age: 21, hasLicense: true },
|
|
44
|
+
* forma
|
|
45
|
+
* );
|
|
46
|
+
* // => { age: true, hasLicense: true, vehicleType: true, ... }
|
|
47
|
+
*/
|
|
48
|
+
export function getVisibility(
|
|
49
|
+
data: Record<string, unknown>,
|
|
50
|
+
spec: Forma,
|
|
51
|
+
options: VisibilityOptions = {}
|
|
52
|
+
): VisibilityResult {
|
|
53
|
+
// Calculate computed values if not provided
|
|
54
|
+
const computed = options.computed ?? calculate(data, spec);
|
|
55
|
+
|
|
56
|
+
// Build base evaluation context
|
|
57
|
+
const baseContext: EvaluationContext = {
|
|
58
|
+
data,
|
|
59
|
+
computed,
|
|
60
|
+
referenceData: spec.referenceData,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result: VisibilityResult = {};
|
|
64
|
+
|
|
65
|
+
// Process all fields in field order
|
|
66
|
+
for (const fieldPath of spec.fieldOrder) {
|
|
67
|
+
const fieldDef = spec.fields[fieldPath];
|
|
68
|
+
if (fieldDef) {
|
|
69
|
+
evaluateFieldVisibility(fieldPath, fieldDef, data, baseContext, result);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Field Evaluation
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Evaluate visibility for a single field and its nested fields
|
|
82
|
+
*/
|
|
83
|
+
function evaluateFieldVisibility(
|
|
84
|
+
path: string,
|
|
85
|
+
fieldDef: FieldDefinition,
|
|
86
|
+
data: Record<string, unknown>,
|
|
87
|
+
context: EvaluationContext,
|
|
88
|
+
result: VisibilityResult
|
|
89
|
+
): void {
|
|
90
|
+
// Evaluate the field's own visibility
|
|
91
|
+
if (fieldDef.visibleWhen) {
|
|
92
|
+
result[path] = evaluateBoolean(fieldDef.visibleWhen, context);
|
|
93
|
+
} else {
|
|
94
|
+
result[path] = true; // No condition = always visible
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If not visible, children are implicitly not visible
|
|
98
|
+
if (!result[path]) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle array fields with item visibility
|
|
103
|
+
if (fieldDef.itemFields) {
|
|
104
|
+
const arrayData = data[path];
|
|
105
|
+
if (Array.isArray(arrayData)) {
|
|
106
|
+
evaluateArrayItemVisibility(path, fieldDef, arrayData, context, result);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Evaluate visibility for array item fields
|
|
113
|
+
*
|
|
114
|
+
* For each item in the array, evaluates the visibility of each
|
|
115
|
+
* item field using the $item context.
|
|
116
|
+
*/
|
|
117
|
+
function evaluateArrayItemVisibility(
|
|
118
|
+
arrayPath: string,
|
|
119
|
+
fieldDef: FieldDefinition,
|
|
120
|
+
arrayData: unknown[],
|
|
121
|
+
baseContext: EvaluationContext,
|
|
122
|
+
result: VisibilityResult
|
|
123
|
+
): void {
|
|
124
|
+
if (!fieldDef.itemFields) return;
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < arrayData.length; i++) {
|
|
127
|
+
const item = arrayData[i] as Record<string, unknown>;
|
|
128
|
+
|
|
129
|
+
// Create item-specific context
|
|
130
|
+
const itemContext: EvaluationContext = {
|
|
131
|
+
...baseContext,
|
|
132
|
+
item,
|
|
133
|
+
itemIndex: i,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Evaluate each item field's visibility
|
|
137
|
+
for (const [fieldName, itemFieldDef] of Object.entries(fieldDef.itemFields)) {
|
|
138
|
+
const itemFieldPath = `${arrayPath}[${i}].${fieldName}`;
|
|
139
|
+
|
|
140
|
+
if (itemFieldDef.visibleWhen) {
|
|
141
|
+
result[itemFieldPath] = evaluateBoolean(itemFieldDef.visibleWhen, itemContext);
|
|
142
|
+
} else {
|
|
143
|
+
result[itemFieldPath] = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Individual Field Visibility
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if a single field is visible
|
|
155
|
+
*
|
|
156
|
+
* Useful for checking visibility of one field without computing all.
|
|
157
|
+
*
|
|
158
|
+
* @param fieldPath - Field path to check
|
|
159
|
+
* @param data - Current form data
|
|
160
|
+
* @param spec - Form specification
|
|
161
|
+
* @param options - Optional pre-calculated computed values
|
|
162
|
+
* @returns True if the field is visible
|
|
163
|
+
*/
|
|
164
|
+
export function isFieldVisible(
|
|
165
|
+
fieldPath: string,
|
|
166
|
+
data: Record<string, unknown>,
|
|
167
|
+
spec: Forma,
|
|
168
|
+
options: VisibilityOptions = {}
|
|
169
|
+
): boolean {
|
|
170
|
+
const fieldDef = spec.fields[fieldPath];
|
|
171
|
+
if (!fieldDef) {
|
|
172
|
+
return true; // Unknown fields are visible by default
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!fieldDef.visibleWhen) {
|
|
176
|
+
return true; // No condition = always visible
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const computed = options.computed ?? calculate(data, spec);
|
|
180
|
+
const context: EvaluationContext = {
|
|
181
|
+
data,
|
|
182
|
+
computed,
|
|
183
|
+
referenceData: spec.referenceData,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return evaluateBoolean(fieldDef.visibleWhen, context);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Page Visibility
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Determine which pages are visible in a wizard form
|
|
195
|
+
*
|
|
196
|
+
* @param data - Current form data
|
|
197
|
+
* @param spec - Form specification with pages
|
|
198
|
+
* @param options - Optional pre-calculated computed values
|
|
199
|
+
* @returns Map of page IDs to visibility states
|
|
200
|
+
*/
|
|
201
|
+
export function getPageVisibility(
|
|
202
|
+
data: Record<string, unknown>,
|
|
203
|
+
spec: Forma,
|
|
204
|
+
options: VisibilityOptions = {}
|
|
205
|
+
): Record<string, boolean> {
|
|
206
|
+
if (!spec.pages) {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const computed = options.computed ?? calculate(data, spec);
|
|
211
|
+
const context: EvaluationContext = {
|
|
212
|
+
data,
|
|
213
|
+
computed,
|
|
214
|
+
referenceData: spec.referenceData,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const result: Record<string, boolean> = {};
|
|
218
|
+
|
|
219
|
+
for (const page of spec.pages) {
|
|
220
|
+
if (page.visibleWhen) {
|
|
221
|
+
result[page.id] = evaluateBoolean(page.visibleWhen, context);
|
|
222
|
+
} else {
|
|
223
|
+
result[page.id] = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FEEL Expression Evaluator
|
|
3
|
+
*
|
|
4
|
+
* Wraps the feelin library to provide FEEL expression evaluation
|
|
5
|
+
* with Forma context conventions.
|
|
6
|
+
*
|
|
7
|
+
* Context variable conventions:
|
|
8
|
+
* - `fieldName` - Direct field value access
|
|
9
|
+
* - `computed.name` - Computed value access
|
|
10
|
+
* - `ref.path` - Reference data lookup (external lookup tables)
|
|
11
|
+
* - `item.fieldName` - Array item field access (within array context)
|
|
12
|
+
* - `itemIndex` - Current array item index (0-based)
|
|
13
|
+
* - `value` - Current field value (in validation expressions)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { evaluate as feelinEvaluate } from "feelin";
|
|
17
|
+
import type { EvaluationContext, FEELExpression } from "../types.js";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export interface EvaluateResult<T = unknown> {
|
|
24
|
+
success: true;
|
|
25
|
+
value: T;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EvaluateError {
|
|
29
|
+
success: false;
|
|
30
|
+
error: string;
|
|
31
|
+
expression: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type EvaluationOutcome<T = unknown> = EvaluateResult<T> | EvaluateError;
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Context Building
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the FEEL evaluation context from our EvaluationContext
|
|
42
|
+
*
|
|
43
|
+
* Maps our context conventions to feelin context format:
|
|
44
|
+
* - Form data fields are spread directly
|
|
45
|
+
* - computed becomes an object (accessed as computed.fieldName)
|
|
46
|
+
* - ref becomes an object for reference data (accessed as ref.path.to.data)
|
|
47
|
+
* - item becomes an object for array context (accessed as item.fieldName)
|
|
48
|
+
* - itemIndex is a number for array context
|
|
49
|
+
* - value is the current field value
|
|
50
|
+
*/
|
|
51
|
+
function buildFeelContext(ctx: EvaluationContext): Record<string, unknown> {
|
|
52
|
+
const feelContext: Record<string, unknown> = {
|
|
53
|
+
// Spread form data directly so fields are accessible by name
|
|
54
|
+
...ctx.data,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Add computed values under 'computed' (accessed as computed.fieldName)
|
|
58
|
+
if (ctx.computed) {
|
|
59
|
+
feelContext["computed"] = ctx.computed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add reference data under 'ref' (accessed as ref.path.to.data)
|
|
63
|
+
if (ctx.referenceData) {
|
|
64
|
+
feelContext["ref"] = ctx.referenceData;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add array item context (accessed as item.fieldName)
|
|
68
|
+
if (ctx.item !== undefined) {
|
|
69
|
+
feelContext["item"] = ctx.item;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add array index
|
|
73
|
+
if (ctx.itemIndex !== undefined) {
|
|
74
|
+
feelContext["itemIndex"] = ctx.itemIndex;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add current field value for validation expressions
|
|
78
|
+
if (ctx.value !== undefined) {
|
|
79
|
+
feelContext["value"] = ctx.value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return feelContext;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Expression Evaluation
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Evaluate a FEEL expression and return the result
|
|
91
|
+
*
|
|
92
|
+
* @param expression - FEEL expression string
|
|
93
|
+
* @param context - Evaluation context with form data, computed values, etc.
|
|
94
|
+
* @returns Evaluation outcome with success/value or error
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // Simple field comparison
|
|
98
|
+
* evaluate("age >= 18", { data: { age: 21 } })
|
|
99
|
+
* // => { success: true, value: true }
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* // Computed value reference
|
|
103
|
+
* evaluate("computed.bmi > 30", { data: {}, computed: { bmi: 32.5 } })
|
|
104
|
+
* // => { success: true, value: true }
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // Array item context
|
|
108
|
+
* evaluate("item.frequency = \"daily\"", { data: {}, item: { frequency: "daily" } })
|
|
109
|
+
* // => { success: true, value: true }
|
|
110
|
+
*/
|
|
111
|
+
export function evaluate<T = unknown>(
|
|
112
|
+
expression: FEELExpression,
|
|
113
|
+
context: EvaluationContext
|
|
114
|
+
): EvaluationOutcome<T> {
|
|
115
|
+
try {
|
|
116
|
+
const feelContext = buildFeelContext(context);
|
|
117
|
+
const result = feelinEvaluate(expression, feelContext);
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
value: result as T,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: error instanceof Error ? error.message : String(error),
|
|
126
|
+
expression,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Evaluate a FEEL expression expecting a boolean result
|
|
133
|
+
*
|
|
134
|
+
* Used for visibility, required, and enabled conditions.
|
|
135
|
+
* Returns false on error or non-boolean result for safety.
|
|
136
|
+
*
|
|
137
|
+
* @param expression - FEEL expression that should return boolean
|
|
138
|
+
* @param context - Evaluation context
|
|
139
|
+
* @returns Boolean result (false on error)
|
|
140
|
+
*/
|
|
141
|
+
export function evaluateBoolean(
|
|
142
|
+
expression: FEELExpression,
|
|
143
|
+
context: EvaluationContext
|
|
144
|
+
): boolean {
|
|
145
|
+
const result = evaluate<boolean>(expression, context);
|
|
146
|
+
|
|
147
|
+
if (!result.success) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`FEEL expression error: ${result.error}\nExpression: ${result.expression}`
|
|
150
|
+
);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// FEEL uses three-valued logic where comparisons with null return null.
|
|
155
|
+
// For form visibility/required/enabled conditions, we treat null as false.
|
|
156
|
+
if (result.value === null || result.value === undefined) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof result.value !== "boolean") {
|
|
161
|
+
console.warn(
|
|
162
|
+
`FEEL expression did not return boolean: ${expression}\nGot: ${typeof result.value}`
|
|
163
|
+
);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result.value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Evaluate a FEEL expression expecting a numeric result
|
|
172
|
+
*
|
|
173
|
+
* Used for computed values that return numbers.
|
|
174
|
+
*
|
|
175
|
+
* @param expression - FEEL expression that should return number
|
|
176
|
+
* @param context - Evaluation context
|
|
177
|
+
* @returns Numeric result or null on error
|
|
178
|
+
*/
|
|
179
|
+
export function evaluateNumber(
|
|
180
|
+
expression: FEELExpression,
|
|
181
|
+
context: EvaluationContext
|
|
182
|
+
): number | null {
|
|
183
|
+
const result = evaluate<number>(expression, context);
|
|
184
|
+
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
console.warn(
|
|
187
|
+
`FEEL expression error: ${result.error}\nExpression: ${result.expression}`
|
|
188
|
+
);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof result.value !== "number") {
|
|
193
|
+
console.warn(
|
|
194
|
+
`FEEL expression did not return number: ${expression}\nGot: ${typeof result.value}`
|
|
195
|
+
);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result.value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Evaluate a FEEL expression expecting a string result
|
|
204
|
+
*
|
|
205
|
+
* @param expression - FEEL expression that should return string
|
|
206
|
+
* @param context - Evaluation context
|
|
207
|
+
* @returns String result or null on error
|
|
208
|
+
*/
|
|
209
|
+
export function evaluateString(
|
|
210
|
+
expression: FEELExpression,
|
|
211
|
+
context: EvaluationContext
|
|
212
|
+
): string | null {
|
|
213
|
+
const result = evaluate<string>(expression, context);
|
|
214
|
+
|
|
215
|
+
if (!result.success) {
|
|
216
|
+
console.warn(
|
|
217
|
+
`FEEL expression error: ${result.error}\nExpression: ${result.expression}`
|
|
218
|
+
);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof result.value !== "string") {
|
|
223
|
+
console.warn(
|
|
224
|
+
`FEEL expression did not return string: ${expression}\nGot: ${typeof result.value}`
|
|
225
|
+
);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return result.value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Batch Evaluation
|
|
234
|
+
// ============================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Evaluate multiple FEEL expressions at once
|
|
238
|
+
*
|
|
239
|
+
* Useful for evaluating all visibility conditions in a form.
|
|
240
|
+
*
|
|
241
|
+
* @param expressions - Map of field names to FEEL expressions
|
|
242
|
+
* @param context - Evaluation context
|
|
243
|
+
* @returns Map of field names to boolean results
|
|
244
|
+
*/
|
|
245
|
+
export function evaluateBooleanBatch(
|
|
246
|
+
expressions: Record<string, FEELExpression>,
|
|
247
|
+
context: EvaluationContext
|
|
248
|
+
): Record<string, boolean> {
|
|
249
|
+
const results: Record<string, boolean> = {};
|
|
250
|
+
|
|
251
|
+
for (const [key, expression] of Object.entries(expressions)) {
|
|
252
|
+
results[key] = evaluateBoolean(expression, context);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Expression Validation
|
|
260
|
+
// ============================================================================
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if a FEEL expression is syntactically valid
|
|
264
|
+
*
|
|
265
|
+
* @param expression - FEEL expression to validate
|
|
266
|
+
* @returns True if the expression can be parsed
|
|
267
|
+
*/
|
|
268
|
+
export function isValidExpression(expression: FEELExpression): boolean {
|
|
269
|
+
try {
|
|
270
|
+
// Try to evaluate with empty context - we just want to check parsing
|
|
271
|
+
feelinEvaluate(expression, {});
|
|
272
|
+
return true;
|
|
273
|
+
} catch {
|
|
274
|
+
// Expression failed to parse or evaluate
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Validate a FEEL expression and return any parsing errors
|
|
281
|
+
*
|
|
282
|
+
* @param expression - FEEL expression to validate
|
|
283
|
+
* @returns Null if valid, error message if invalid
|
|
284
|
+
*/
|
|
285
|
+
export function validateExpression(expression: FEELExpression): string | null {
|
|
286
|
+
try {
|
|
287
|
+
// Evaluate with minimal context to catch parse errors
|
|
288
|
+
feelinEvaluate(expression, {});
|
|
289
|
+
return null;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// Only return actual parsing errors, not runtime errors
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
if (message.includes("parse") || message.includes("syntax")) {
|
|
294
|
+
return message;
|
|
295
|
+
}
|
|
296
|
+
// Runtime errors (missing variables, etc.) are OK for validation
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fogpipe/forma-core
|
|
3
|
+
*
|
|
4
|
+
* Core runtime for Forma dynamic forms.
|
|
5
|
+
* Provides types, FEEL expression evaluation, and form state engines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export * from "./types.js";
|
|
10
|
+
|
|
11
|
+
// FEEL expression evaluation
|
|
12
|
+
export * from "./feel/index.js";
|
|
13
|
+
|
|
14
|
+
// Form state engines
|
|
15
|
+
export * from "./engine/index.js";
|