@formspec/decorators 0.1.0-alpha.3
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 +179 -0
- package/dist/index.cjs +380 -0
- package/dist/index.d.ts +464 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +694 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @formspec/decorators
|
|
3
|
+
*
|
|
4
|
+
* Decorators for FormSpec form definitions.
|
|
5
|
+
*
|
|
6
|
+
* These decorators work in two modes:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Build-time (CLI generate)**: The FormSpec CLI reads decorators through
|
|
9
|
+
* static analysis and generates JSON Schema + UI Schema files directly.
|
|
10
|
+
*
|
|
11
|
+
* 2. **Runtime (with CLI codegen)**: Run `formspec codegen` to generate a type
|
|
12
|
+
* metadata file, then use `toFormSpec()` to generate specs at runtime.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { Label, Min, Max, EnumOptions, toFormSpec } from '@formspec/decorators';
|
|
17
|
+
*
|
|
18
|
+
* class UserForm {
|
|
19
|
+
* @Label("Full Name")
|
|
20
|
+
* name!: string;
|
|
21
|
+
*
|
|
22
|
+
* @Label("Age")
|
|
23
|
+
* @Min(18)
|
|
24
|
+
* @Max(120)
|
|
25
|
+
* age?: number;
|
|
26
|
+
*
|
|
27
|
+
* @Label("Country")
|
|
28
|
+
* @EnumOptions([
|
|
29
|
+
* { id: "us", label: "United States" },
|
|
30
|
+
* { id: "ca", label: "Canada" }
|
|
31
|
+
* ])
|
|
32
|
+
* country!: "us" | "ca";
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* // After running: formspec codegen ./forms.ts -o ./__formspec_types__.ts
|
|
36
|
+
* // Import the generated file and use toFormSpec:
|
|
37
|
+
* import './__formspec_types__';
|
|
38
|
+
* const spec = toFormSpec(UserForm);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Runtime Metadata Storage
|
|
43
|
+
// =============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Storage for decorator metadata, keyed by constructor.
|
|
46
|
+
* This is a regular Map (not WeakMap) since class constructors
|
|
47
|
+
* live for the lifetime of the application.
|
|
48
|
+
*/
|
|
49
|
+
const decoratorMetadata = new Map();
|
|
50
|
+
/**
|
|
51
|
+
* Gets or creates the metadata map for a class constructor.
|
|
52
|
+
*/
|
|
53
|
+
function getFieldMetadata(
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
ctor, propertyKey) {
|
|
56
|
+
if (!decoratorMetadata.has(ctor)) {
|
|
57
|
+
decoratorMetadata.set(ctor, new Map());
|
|
58
|
+
}
|
|
59
|
+
const classMetadata = decoratorMetadata.get(ctor);
|
|
60
|
+
if (!classMetadata.has(propertyKey)) {
|
|
61
|
+
classMetadata.set(propertyKey, {});
|
|
62
|
+
}
|
|
63
|
+
return classMetadata.get(propertyKey);
|
|
64
|
+
}
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Field Metadata Decorators
|
|
67
|
+
// =============================================================================
|
|
68
|
+
/**
|
|
69
|
+
* Sets the display label for a field.
|
|
70
|
+
*
|
|
71
|
+
* @param text - The label text to display
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* @Label("Email Address")
|
|
75
|
+
* email!: string;
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function Label(text) {
|
|
79
|
+
return function (target, propertyKey) {
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
const ctor = target.constructor;
|
|
82
|
+
getFieldMetadata(ctor, propertyKey).label = text;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sets placeholder text for input fields.
|
|
87
|
+
*
|
|
88
|
+
* @param text - The placeholder text
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* @Placeholder("Enter your email...")
|
|
92
|
+
* email!: string;
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function Placeholder(text) {
|
|
96
|
+
return function (target, propertyKey) {
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
|
+
const ctor = target.constructor;
|
|
99
|
+
getFieldMetadata(ctor, propertyKey).placeholder = text;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Sets a description or help text for a field.
|
|
104
|
+
*
|
|
105
|
+
* @param text - The description text
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* @Description("We'll never share your email with anyone")
|
|
109
|
+
* email!: string;
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function Description(text) {
|
|
113
|
+
return function (target, propertyKey) {
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
+
const ctor = target.constructor;
|
|
116
|
+
getFieldMetadata(ctor, propertyKey).description = text;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// Numeric Constraint Decorators
|
|
121
|
+
// =============================================================================
|
|
122
|
+
/**
|
|
123
|
+
* Sets the minimum allowed value for a numeric field.
|
|
124
|
+
*
|
|
125
|
+
* @param value - The minimum value
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* @Min(0)
|
|
129
|
+
* quantity!: number;
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function Min(value) {
|
|
133
|
+
return function (target, propertyKey) {
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
135
|
+
const ctor = target.constructor;
|
|
136
|
+
getFieldMetadata(ctor, propertyKey).min = value;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Sets the maximum allowed value for a numeric field.
|
|
141
|
+
*
|
|
142
|
+
* @param value - The maximum value
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* @Max(100)
|
|
146
|
+
* percentage!: number;
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export function Max(value) {
|
|
150
|
+
return function (target, propertyKey) {
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
const ctor = target.constructor;
|
|
153
|
+
getFieldMetadata(ctor, propertyKey).max = value;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sets the step increment for a numeric field.
|
|
158
|
+
*
|
|
159
|
+
* @param value - The step value
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* @Step(0.01)
|
|
163
|
+
* price!: number;
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export function Step(value) {
|
|
167
|
+
return function (target, propertyKey) {
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
169
|
+
const ctor = target.constructor;
|
|
170
|
+
getFieldMetadata(ctor, propertyKey).step = value;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// String Constraint Decorators
|
|
175
|
+
// =============================================================================
|
|
176
|
+
/**
|
|
177
|
+
* Sets the minimum length for a string field.
|
|
178
|
+
*
|
|
179
|
+
* @param value - The minimum character count
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* @MinLength(1)
|
|
183
|
+
* name!: string;
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function MinLength(value) {
|
|
187
|
+
return function (target, propertyKey) {
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
+
const ctor = target.constructor;
|
|
190
|
+
getFieldMetadata(ctor, propertyKey).minLength = value;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Sets the maximum length for a string field.
|
|
195
|
+
*
|
|
196
|
+
* @param value - The maximum character count
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* @MaxLength(255)
|
|
200
|
+
* bio!: string;
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function MaxLength(value) {
|
|
204
|
+
return function (target, propertyKey) {
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
206
|
+
const ctor = target.constructor;
|
|
207
|
+
getFieldMetadata(ctor, propertyKey).maxLength = value;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Sets a regex pattern for string validation.
|
|
212
|
+
*
|
|
213
|
+
* @param regex - The regex pattern as a string
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* @Pattern("^[a-z]+$")
|
|
217
|
+
* username!: string;
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export function Pattern(regex) {
|
|
221
|
+
return function (target, propertyKey) {
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
223
|
+
const ctor = target.constructor;
|
|
224
|
+
getFieldMetadata(ctor, propertyKey).pattern = regex;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// =============================================================================
|
|
228
|
+
// Array Constraint Decorators
|
|
229
|
+
// =============================================================================
|
|
230
|
+
/**
|
|
231
|
+
* Sets the minimum number of items for an array field.
|
|
232
|
+
*
|
|
233
|
+
* @param value - The minimum item count
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* @MinItems(1)
|
|
237
|
+
* tags!: string[];
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export function MinItems(value) {
|
|
241
|
+
return function (target, propertyKey) {
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
243
|
+
const ctor = target.constructor;
|
|
244
|
+
getFieldMetadata(ctor, propertyKey).minItems = value;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Sets the maximum number of items for an array field.
|
|
249
|
+
*
|
|
250
|
+
* @param value - The maximum item count
|
|
251
|
+
* @example
|
|
252
|
+
* ```typescript
|
|
253
|
+
* @MaxItems(10)
|
|
254
|
+
* tags!: string[];
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export function MaxItems(value) {
|
|
258
|
+
return function (target, propertyKey) {
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
const ctor = target.constructor;
|
|
261
|
+
getFieldMetadata(ctor, propertyKey).maxItems = value;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// =============================================================================
|
|
265
|
+
// Enum and Options Decorators
|
|
266
|
+
// =============================================================================
|
|
267
|
+
/**
|
|
268
|
+
* Provides custom options for enum fields with labels.
|
|
269
|
+
*
|
|
270
|
+
* Use this to provide human-readable labels for enum values,
|
|
271
|
+
* or to customize the order and display of options.
|
|
272
|
+
*
|
|
273
|
+
* @param options - Array of option values or {id, label} objects
|
|
274
|
+
* @example
|
|
275
|
+
* ```typescript
|
|
276
|
+
* @EnumOptions([
|
|
277
|
+
* { id: "us", label: "United States" },
|
|
278
|
+
* { id: "ca", label: "Canada" },
|
|
279
|
+
* { id: "uk", label: "United Kingdom" }
|
|
280
|
+
* ])
|
|
281
|
+
* country!: "us" | "ca" | "uk";
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
export function EnumOptions(options) {
|
|
285
|
+
return function (target, propertyKey) {
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
+
const ctor = target.constructor;
|
|
288
|
+
getFieldMetadata(ctor, propertyKey).options = options;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// Conditional and Layout Decorators
|
|
293
|
+
// =============================================================================
|
|
294
|
+
/**
|
|
295
|
+
* Makes a field conditionally visible based on another field's value.
|
|
296
|
+
*
|
|
297
|
+
* @param condition - Object specifying the field and value to match
|
|
298
|
+
* @example
|
|
299
|
+
* ```typescript
|
|
300
|
+
* @ShowWhen({ field: "contactMethod", value: "email" })
|
|
301
|
+
* emailAddress!: string;
|
|
302
|
+
*
|
|
303
|
+
* @ShowWhen({ field: "contactMethod", value: "phone" })
|
|
304
|
+
* phoneNumber!: string;
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
export function ShowWhen(condition) {
|
|
308
|
+
return function (target, propertyKey) {
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
310
|
+
const ctor = target.constructor;
|
|
311
|
+
getFieldMetadata(ctor, propertyKey).showWhen = condition;
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Groups fields together under a named section.
|
|
316
|
+
*
|
|
317
|
+
* @param name - The group name
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* @Group("Personal Information")
|
|
321
|
+
* @Label("First Name")
|
|
322
|
+
* firstName!: string;
|
|
323
|
+
*
|
|
324
|
+
* @Group("Personal Information")
|
|
325
|
+
* @Label("Last Name")
|
|
326
|
+
* lastName!: string;
|
|
327
|
+
*
|
|
328
|
+
* @Group("Contact Details")
|
|
329
|
+
* @Label("Email")
|
|
330
|
+
* email!: string;
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export function Group(name) {
|
|
334
|
+
return function (target, propertyKey) {
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
336
|
+
const ctor = target.constructor;
|
|
337
|
+
getFieldMetadata(ctor, propertyKey).group = name;
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
// =============================================================================
|
|
341
|
+
// Runtime API
|
|
342
|
+
// =============================================================================
|
|
343
|
+
/**
|
|
344
|
+
* The name of the static property containing type metadata.
|
|
345
|
+
* This is set by the `formspec codegen` command at build time.
|
|
346
|
+
*/
|
|
347
|
+
const FORMSPEC_TYPES_KEY = "__formspec_types__";
|
|
348
|
+
/**
|
|
349
|
+
* Maps type metadata type to FormSpec field type.
|
|
350
|
+
*/
|
|
351
|
+
function mapTypeToFieldType(type) {
|
|
352
|
+
switch (type) {
|
|
353
|
+
case "string":
|
|
354
|
+
return "text";
|
|
355
|
+
case "number":
|
|
356
|
+
return "number";
|
|
357
|
+
case "boolean":
|
|
358
|
+
return "boolean";
|
|
359
|
+
case "enum":
|
|
360
|
+
return "enum";
|
|
361
|
+
case "array":
|
|
362
|
+
return "array";
|
|
363
|
+
case "object":
|
|
364
|
+
return "object";
|
|
365
|
+
default:
|
|
366
|
+
return "text";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Creates a FormSpec field from type and decorator metadata.
|
|
371
|
+
*/
|
|
372
|
+
function createField(fieldName, typeInfo, decoratorInfo) {
|
|
373
|
+
const field = {
|
|
374
|
+
_field: mapTypeToFieldType(typeInfo.type),
|
|
375
|
+
id: fieldName,
|
|
376
|
+
};
|
|
377
|
+
// Required if not optional and not nullable
|
|
378
|
+
if (!typeInfo.optional && !typeInfo.nullable) {
|
|
379
|
+
field.required = true;
|
|
380
|
+
}
|
|
381
|
+
// Apply decorator metadata
|
|
382
|
+
if (decoratorInfo.label)
|
|
383
|
+
field.label = decoratorInfo.label;
|
|
384
|
+
if (decoratorInfo.placeholder)
|
|
385
|
+
field.placeholder = decoratorInfo.placeholder;
|
|
386
|
+
if (decoratorInfo.description)
|
|
387
|
+
field.description = decoratorInfo.description;
|
|
388
|
+
if (decoratorInfo.min !== undefined)
|
|
389
|
+
field.min = decoratorInfo.min;
|
|
390
|
+
if (decoratorInfo.max !== undefined)
|
|
391
|
+
field.max = decoratorInfo.max;
|
|
392
|
+
if (decoratorInfo.step !== undefined)
|
|
393
|
+
field.step = decoratorInfo.step;
|
|
394
|
+
if (decoratorInfo.minLength !== undefined)
|
|
395
|
+
field.minLength = decoratorInfo.minLength;
|
|
396
|
+
if (decoratorInfo.maxLength !== undefined)
|
|
397
|
+
field.maxLength = decoratorInfo.maxLength;
|
|
398
|
+
if (decoratorInfo.minItems !== undefined)
|
|
399
|
+
field.minItems = decoratorInfo.minItems;
|
|
400
|
+
if (decoratorInfo.maxItems !== undefined)
|
|
401
|
+
field.maxItems = decoratorInfo.maxItems;
|
|
402
|
+
if (decoratorInfo.pattern)
|
|
403
|
+
field.pattern = decoratorInfo.pattern;
|
|
404
|
+
if (decoratorInfo.showWhen)
|
|
405
|
+
field.showWhen = decoratorInfo.showWhen;
|
|
406
|
+
if (decoratorInfo.group)
|
|
407
|
+
field.group = decoratorInfo.group;
|
|
408
|
+
// Options: prefer decorator options, fall back to type values
|
|
409
|
+
if (decoratorInfo.options) {
|
|
410
|
+
field.options = decoratorInfo.options;
|
|
411
|
+
}
|
|
412
|
+
else if (typeInfo.values) {
|
|
413
|
+
// Convert simple values to strings
|
|
414
|
+
field.options = typeInfo.values.map((v) => String(v));
|
|
415
|
+
}
|
|
416
|
+
// Handle nested object properties
|
|
417
|
+
if (typeInfo.type === "object" && typeInfo.properties) {
|
|
418
|
+
field.fields = Object.entries(typeInfo.properties).map(([propName, propType]) => createField(propName, propType, {}));
|
|
419
|
+
}
|
|
420
|
+
return field;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Generates a UI Schema from a decorated class at runtime.
|
|
424
|
+
*
|
|
425
|
+
* **Requirements:**
|
|
426
|
+
* - Run `formspec codegen` to generate type metadata for the class
|
|
427
|
+
* - Decorators must be applied to store field metadata
|
|
428
|
+
*
|
|
429
|
+
* @param ctor - The class constructor
|
|
430
|
+
* @returns FormSpec output with elements array
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```typescript
|
|
434
|
+
* import { Label, toFormSpec } from '@formspec/decorators';
|
|
435
|
+
*
|
|
436
|
+
* class MyForm {
|
|
437
|
+
* @Label("Name")
|
|
438
|
+
* name!: string;
|
|
439
|
+
*
|
|
440
|
+
* @Label("Country")
|
|
441
|
+
* country!: "us" | "ca";
|
|
442
|
+
* }
|
|
443
|
+
*
|
|
444
|
+
* const spec = toFormSpec(MyForm);
|
|
445
|
+
* // { elements: [{ _field: "text", id: "name", label: "Name", required: true }, ...] }
|
|
446
|
+
* ```
|
|
447
|
+
*/
|
|
448
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
449
|
+
export function toFormSpec(ctor) {
|
|
450
|
+
// Get type metadata from codegen (if available)
|
|
451
|
+
const typeMetadata =
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
|
+
ctor[FORMSPEC_TYPES_KEY] ?? {};
|
|
454
|
+
// Get decorator metadata
|
|
455
|
+
const classDecoratorMeta = decoratorMetadata.get(ctor) ?? new Map();
|
|
456
|
+
// Combine to create elements
|
|
457
|
+
const elements = [];
|
|
458
|
+
// Process all fields from type metadata
|
|
459
|
+
for (const [fieldName, typeInfo] of Object.entries(typeMetadata)) {
|
|
460
|
+
const decoratorInfo = classDecoratorMeta.get(fieldName) ?? {};
|
|
461
|
+
elements.push(createField(fieldName, typeInfo, decoratorInfo));
|
|
462
|
+
}
|
|
463
|
+
// If no type metadata, fall back to decorator metadata only
|
|
464
|
+
// (limited functionality - no type information)
|
|
465
|
+
if (Object.keys(typeMetadata).length === 0) {
|
|
466
|
+
for (const [fieldName, decoratorInfo] of classDecoratorMeta.entries()) {
|
|
467
|
+
elements.push(createField(fieldName, { type: "unknown" }, decoratorInfo));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return { elements };
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Gets the raw decorator metadata for a class.
|
|
474
|
+
*
|
|
475
|
+
* Useful for debugging or custom processing.
|
|
476
|
+
*
|
|
477
|
+
* @param ctor - The class constructor
|
|
478
|
+
* @returns Map of field names to decorator metadata
|
|
479
|
+
*/
|
|
480
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
481
|
+
export function getDecoratorMetadata(ctor) {
|
|
482
|
+
return decoratorMetadata.get(ctor) ?? new Map();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Gets the raw type metadata for a class.
|
|
486
|
+
*
|
|
487
|
+
* Useful for debugging or custom processing.
|
|
488
|
+
*
|
|
489
|
+
* @param ctor - The class constructor
|
|
490
|
+
* @returns Record of field names to type metadata, or empty object if not transformed
|
|
491
|
+
*/
|
|
492
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
493
|
+
export function getTypeMetadata(ctor) {
|
|
494
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
495
|
+
return ctor[FORMSPEC_TYPES_KEY] ?? {};
|
|
496
|
+
}
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// Schema Generation
|
|
499
|
+
// =============================================================================
|
|
500
|
+
/**
|
|
501
|
+
* Maps FormSpecField type to JSON Schema type.
|
|
502
|
+
*/
|
|
503
|
+
function fieldTypeToJsonSchemaType(fieldType) {
|
|
504
|
+
switch (fieldType) {
|
|
505
|
+
case "text":
|
|
506
|
+
return "string";
|
|
507
|
+
case "number":
|
|
508
|
+
return "number";
|
|
509
|
+
case "boolean":
|
|
510
|
+
return "boolean";
|
|
511
|
+
case "enum":
|
|
512
|
+
return "string";
|
|
513
|
+
case "array":
|
|
514
|
+
return "array";
|
|
515
|
+
case "object":
|
|
516
|
+
return "object";
|
|
517
|
+
default:
|
|
518
|
+
return "string";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Converts a FormSpecField to JSON Schema.
|
|
523
|
+
*/
|
|
524
|
+
function fieldToJsonSchema(field) {
|
|
525
|
+
const schema = {};
|
|
526
|
+
if (field.label) {
|
|
527
|
+
schema.title = field.label;
|
|
528
|
+
}
|
|
529
|
+
const jsonType = fieldTypeToJsonSchemaType(field._field);
|
|
530
|
+
schema.type = jsonType;
|
|
531
|
+
// Number constraints
|
|
532
|
+
if (field.min !== undefined)
|
|
533
|
+
schema.minimum = field.min;
|
|
534
|
+
if (field.max !== undefined)
|
|
535
|
+
schema.maximum = field.max;
|
|
536
|
+
// String constraints
|
|
537
|
+
if (field.minLength !== undefined)
|
|
538
|
+
schema.minLength = field.minLength;
|
|
539
|
+
if (field.maxLength !== undefined)
|
|
540
|
+
schema.maxLength = field.maxLength;
|
|
541
|
+
if (field.pattern)
|
|
542
|
+
schema.pattern = field.pattern;
|
|
543
|
+
// Enum options
|
|
544
|
+
if (field._field === "enum" && field.options) {
|
|
545
|
+
const hasLabels = field.options.some((opt) => typeof opt === "object" && opt !== null && "id" in opt);
|
|
546
|
+
if (hasLabels) {
|
|
547
|
+
schema.oneOf = field.options.map((opt) => {
|
|
548
|
+
if (typeof opt === "object" && "id" in opt) {
|
|
549
|
+
return { const: opt.id, title: opt.label };
|
|
550
|
+
}
|
|
551
|
+
return { const: opt, title: String(opt) };
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
schema.enum = field.options.map((opt) => typeof opt === "string" ? opt : opt.id);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Array items
|
|
559
|
+
if (field._field === "array" && field.fields) {
|
|
560
|
+
const itemProperties = {};
|
|
561
|
+
const itemRequired = [];
|
|
562
|
+
for (const nested of field.fields) {
|
|
563
|
+
itemProperties[nested.id] = fieldToJsonSchema(nested);
|
|
564
|
+
if (nested.required)
|
|
565
|
+
itemRequired.push(nested.id);
|
|
566
|
+
}
|
|
567
|
+
schema.items = {
|
|
568
|
+
type: "object",
|
|
569
|
+
properties: itemProperties,
|
|
570
|
+
...(itemRequired.length > 0 && { required: itemRequired }),
|
|
571
|
+
};
|
|
572
|
+
if (field.minItems !== undefined)
|
|
573
|
+
schema.minItems = field.minItems;
|
|
574
|
+
if (field.maxItems !== undefined)
|
|
575
|
+
schema.maxItems = field.maxItems;
|
|
576
|
+
}
|
|
577
|
+
// Object properties
|
|
578
|
+
if (field._field === "object" && field.fields) {
|
|
579
|
+
const objProperties = {};
|
|
580
|
+
const objRequired = [];
|
|
581
|
+
for (const nested of field.fields) {
|
|
582
|
+
objProperties[nested.id] = fieldToJsonSchema(nested);
|
|
583
|
+
if (nested.required)
|
|
584
|
+
objRequired.push(nested.id);
|
|
585
|
+
}
|
|
586
|
+
schema.properties = objProperties;
|
|
587
|
+
if (objRequired.length > 0)
|
|
588
|
+
schema.required = objRequired;
|
|
589
|
+
}
|
|
590
|
+
return schema;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Generates JSON Schema from FormSpecField elements.
|
|
594
|
+
*/
|
|
595
|
+
function generateJsonSchemaFromElements(elements) {
|
|
596
|
+
const properties = {};
|
|
597
|
+
const required = [];
|
|
598
|
+
for (const element of elements) {
|
|
599
|
+
properties[element.id] = fieldToJsonSchema(element);
|
|
600
|
+
if (element.required) {
|
|
601
|
+
required.push(element.id);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
$schema: "https://json-schema.org/draft-07/schema#",
|
|
606
|
+
type: "object",
|
|
607
|
+
properties,
|
|
608
|
+
...(required.length > 0 && { required }),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Converts a field name to a JSON Pointer scope.
|
|
613
|
+
*/
|
|
614
|
+
function fieldToScope(fieldName) {
|
|
615
|
+
return `#/properties/${fieldName}`;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Converts FormSpecField elements to UI Schema elements.
|
|
619
|
+
*/
|
|
620
|
+
function elementsToUiSchema(elements) {
|
|
621
|
+
const result = [];
|
|
622
|
+
for (const element of elements) {
|
|
623
|
+
const control = {
|
|
624
|
+
type: "Control",
|
|
625
|
+
scope: fieldToScope(element.id),
|
|
626
|
+
};
|
|
627
|
+
if (element.label) {
|
|
628
|
+
control.label = element.label;
|
|
629
|
+
}
|
|
630
|
+
// Add conditional visibility rule
|
|
631
|
+
if (element.showWhen) {
|
|
632
|
+
control.rule = {
|
|
633
|
+
effect: "SHOW",
|
|
634
|
+
condition: {
|
|
635
|
+
scope: fieldToScope(element.showWhen.field),
|
|
636
|
+
schema: { const: element.showWhen.value },
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
result.push(control);
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Generates UI Schema from FormSpecField elements.
|
|
646
|
+
*/
|
|
647
|
+
function generateUiSchemaFromElements(elements) {
|
|
648
|
+
return {
|
|
649
|
+
type: "VerticalLayout",
|
|
650
|
+
elements: elementsToUiSchema(elements),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Builds both JSON Schema and UI Schema from a decorated class at runtime.
|
|
655
|
+
*
|
|
656
|
+
* This function provides the same API as `buildFormSchemas` from `@formspec/build`,
|
|
657
|
+
* allowing consistent usage across both Chain DSL and Decorator DSL.
|
|
658
|
+
*
|
|
659
|
+
* **Requirements:**
|
|
660
|
+
* - Run `formspec codegen` to generate type metadata for the class
|
|
661
|
+
* - Decorators must be applied to store field metadata
|
|
662
|
+
*
|
|
663
|
+
* @param ctor - The class constructor
|
|
664
|
+
* @returns Object containing both jsonSchema and uiSchema
|
|
665
|
+
*
|
|
666
|
+
* @example
|
|
667
|
+
* ```typescript
|
|
668
|
+
* import { Label, buildFormSchemas } from '@formspec/decorators';
|
|
669
|
+
*
|
|
670
|
+
* class MyForm {
|
|
671
|
+
* @Label("Name")
|
|
672
|
+
* name!: string;
|
|
673
|
+
*
|
|
674
|
+
* @Label("Country")
|
|
675
|
+
* country!: "us" | "ca";
|
|
676
|
+
* }
|
|
677
|
+
*
|
|
678
|
+
* // After running: formspec codegen ./forms.ts -o ./__formspec_types__.ts
|
|
679
|
+
* // And importing: import './__formspec_types__';
|
|
680
|
+
*
|
|
681
|
+
* const { jsonSchema, uiSchema } = buildFormSchemas(MyForm);
|
|
682
|
+
* // jsonSchema: { $schema: "...", type: "object", properties: {...}, required: [...] }
|
|
683
|
+
* // uiSchema: { type: "VerticalLayout", elements: [...] }
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
687
|
+
export function buildFormSchemas(ctor) {
|
|
688
|
+
const { elements } = toFormSpec(ctor);
|
|
689
|
+
return {
|
|
690
|
+
jsonSchema: generateJsonSchemaFromElements(elements),
|
|
691
|
+
uiSchema: generateUiSchemaFromElements(elements),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=index.js.map
|