@petrarca/sonnet-core 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.
@@ -0,0 +1,533 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Generic JSON Schema type definitions.
5
+ *
6
+ * These types follow Draft 2020-12 and include x-ui-* extension points
7
+ * for form/widget integration. They are domain-agnostic -- no
8
+ * application-specific entity concepts live here.
9
+ */
10
+ /**
11
+ * JSON Schema property definition following Draft 2020-12 specification.
12
+ * Supports basic types, validation constraints, conditionals, and custom extensions.
13
+ */
14
+ type JsonSchemaProperty = {
15
+ type?: "string" | "number" | "integer" | "boolean" | "array" | "object" | "null";
16
+ title?: string;
17
+ description?: string;
18
+ default?: any;
19
+ const?: any;
20
+ enum?: any[];
21
+ pattern?: string;
22
+ format?: string;
23
+ minLength?: number;
24
+ maxLength?: number;
25
+ minimum?: number;
26
+ maximum?: number;
27
+ items?: JsonSchemaProperty;
28
+ minItems?: number;
29
+ maxItems?: number;
30
+ properties?: Record<string, JsonSchemaProperty>;
31
+ required?: string[];
32
+ $ref?: string;
33
+ allOf?: JsonSchemaProperty[];
34
+ anyOf?: JsonSchemaProperty[];
35
+ oneOf?: JsonSchemaProperty[];
36
+ not?: JsonSchemaProperty;
37
+ if?: JsonSchemaProperty;
38
+ then?: JsonSchemaProperty;
39
+ else?: JsonSchemaProperty;
40
+ "x-ui-order"?: string[];
41
+ "x-ui-widget"?: string;
42
+ "x-ui-options"?: Record<string, any>;
43
+ "x-ui-layout"?: UILayoutConfig;
44
+ "x-ui-title"?: string;
45
+ "x-ui-help"?: string;
46
+ "x-ui-placeholder"?: string;
47
+ "x-ui-description"?: string;
48
+ "x-ui-column"?: {
49
+ /** Display order (ascending). Properties without order are appended sorted by key. */
50
+ order?: number;
51
+ /** Exclude this column from the table entirely. */
52
+ hidden?: boolean;
53
+ /** Fixed column width in pixels. */
54
+ width?: number;
55
+ /** Minimum column width in pixels. */
56
+ minWidth?: number;
57
+ };
58
+ /** Cell renderer name. Inferred from type+format when absent. */
59
+ "x-ui-cell"?: string;
60
+ /** Cell renderer configuration options (colors, href template, etc.). */
61
+ "x-ui-cell-options"?: Record<string, unknown>;
62
+ };
63
+ /**
64
+ * JSON Schema following Draft 2020-12 specification.
65
+ *
66
+ * This is the generic base type. Domain-specific consumers extend it
67
+ * with their own x-* extension keys via intersection types in their
68
+ * own type files.
69
+ */
70
+ type JsonSchema = {
71
+ $schema?: string;
72
+ title: string;
73
+ type: "object";
74
+ properties: Record<string, JsonSchemaProperty>;
75
+ required?: string[];
76
+ allOf?: JsonSchemaProperty[];
77
+ anyOf?: JsonSchemaProperty[];
78
+ oneOf?: JsonSchemaProperty[];
79
+ not?: JsonSchemaProperty;
80
+ if?: JsonSchemaProperty;
81
+ then?: JsonSchemaProperty;
82
+ else?: JsonSchemaProperty;
83
+ $defs?: Record<string, JsonSchemaProperty>;
84
+ definitions?: Record<string, JsonSchemaProperty>;
85
+ "x-ui-order"?: string[];
86
+ [key: `x-${string}`]: unknown;
87
+ };
88
+ /**
89
+ * UISchema - User Interface Schema configuration.
90
+ *
91
+ * Defines how form fields should be rendered and behave in the UI.
92
+ * Separate from JSON Schema (which defines data structure) and focuses
93
+ * on presentation, widgets, and user interaction.
94
+ */
95
+ interface UISchema {
96
+ /** Widget name (string) or direct component reference */
97
+ "x-ui-widget"?: string | object;
98
+ "x-ui-options"?: UISchemaOptions;
99
+ "x-ui-globalOptions"?: UISchemaOptions;
100
+ "x-ui-order"?: string[];
101
+ "x-ui-title"?: string | false;
102
+ "x-ui-description"?: string;
103
+ "x-ui-placeholder"?: string;
104
+ "x-ui-help"?: string;
105
+ /** Column override -- merged over schema-level x-ui-column, UI schema wins. */
106
+ "x-ui-column"?: JsonSchemaProperty["x-ui-column"] & {
107
+ label?: string;
108
+ };
109
+ /** Cell renderer override. */
110
+ "x-ui-cell"?: string;
111
+ /** Cell renderer options override -- shallow-merged over schema-level x-ui-cell-options. */
112
+ "x-ui-cell-options"?: Record<string, unknown>;
113
+ /** Per-field UISchema for nested sub-forms (ObjectWidget / ArrayWidget) */
114
+ "x-ui-fields"?: {
115
+ [fieldName: string]: UISchema;
116
+ };
117
+ /** Layout config forwarded to the active template (ObjectWidget / ArrayWidget) */
118
+ "x-ui-layout"?: UILayoutConfig;
119
+ /** Extension keys not yet enumerated above */
120
+ [key: `x-ui-${string}`]: unknown;
121
+ }
122
+ /**
123
+ * UILayoutRowConfig -- one horizontal row in a multi-column object form layout.
124
+ *
125
+ * `fields` lists the field names to place in this row, left to right.
126
+ * `columns` is a raw CSS grid-template-columns string (default: equal 1fr
127
+ * per field). `gap` overrides the parent layout gap for this row only.
128
+ *
129
+ * Examples:
130
+ * { fields: ["first_name", "last_name"], columns: "1fr 1fr" }
131
+ * { fields: ["npi", "specialty", "email"], columns: "20% 1fr 1fr" }
132
+ * { fields: ["start_date", "end_date"], columns: "200px 1fr", gap: 8 }
133
+ */
134
+ interface UILayoutRowConfig {
135
+ fields: string[];
136
+ columns?: string;
137
+ gap?: number;
138
+ }
139
+ /**
140
+ * UILayoutConfig -- layout configuration for ObjectWidget and ArrayWidget.
141
+ *
142
+ * Configures the built-in default templates. Has no effect when a custom
143
+ * template is registered for the field.
144
+ */
145
+ interface UILayoutConfig {
146
+ direction?: "vertical" | "horizontal";
147
+ rows?: UILayoutRowConfig[];
148
+ columns?: string;
149
+ gap?: number;
150
+ labels?: boolean;
151
+ compact?: boolean;
152
+ }
153
+ /**
154
+ * ValueChange -- a single scalar or nested-object field that changed value.
155
+ *
156
+ * `path` is a dot-separated field path: "name", "address.city".
157
+ */
158
+ type ValueChange = {
159
+ type: "value";
160
+ path: string;
161
+ from: unknown;
162
+ to: unknown;
163
+ };
164
+ /**
165
+ * ArrayItemChange -- one item-level change within an array field.
166
+ *
167
+ * Items are identified by their form-internal `_fid` (never the server ID).
168
+ * A single item can appear as both "moved" and "modified" if it was
169
+ * repositioned and its values also changed -- these are emitted as two
170
+ * separate entries with the same `fid`.
171
+ */
172
+ type ArrayItemChange = {
173
+ type: "added";
174
+ fid: string;
175
+ index: number;
176
+ item: unknown;
177
+ } | {
178
+ type: "removed";
179
+ fid: string;
180
+ index: number;
181
+ item: unknown;
182
+ } | {
183
+ type: "moved";
184
+ fid: string;
185
+ from: number;
186
+ to: number;
187
+ } | {
188
+ type: "modified";
189
+ fid: string;
190
+ index: number;
191
+ changes: ValueChange[];
192
+ };
193
+ /**
194
+ * FieldDiff -- the diff for one top-level form field.
195
+ */
196
+ type FieldDiff = ValueChange | {
197
+ type: "array";
198
+ field: string;
199
+ changes: ArrayItemChange[];
200
+ };
201
+ /**
202
+ * FormDiff -- structured diff produced by diffFormData().
203
+ *
204
+ * One entry per changed top-level field. Fields whose value is deeply
205
+ * equal between original and updated are omitted.
206
+ */
207
+ type FormDiff = FieldDiff[];
208
+ /**
209
+ * UISchemaOptions - Widget configuration options.
210
+ */
211
+ interface UISchemaOptions {
212
+ label?: string | false;
213
+ disabled?: boolean;
214
+ readonly?: boolean;
215
+ compact?: boolean;
216
+ enumNames?: Record<string | number, string>;
217
+ inputType?: string;
218
+ rows?: number;
219
+ searchable?: boolean;
220
+ clearable?: boolean;
221
+ [key: string]: any;
222
+ }
223
+
224
+ /**
225
+ * Schema Service - Pure functions for JSON Schema manipulation.
226
+ *
227
+ * Generic, domain-agnostic utilities for reading and inspecting JSON
228
+ * Schema structures. No React, no HTTP, no side effects.
229
+ *
230
+ * Domain-specific accessors (x-entity, x-graph) live in the consumer's
231
+ * own service layer and import from here.
232
+ */
233
+
234
+ declare function getSchemaProperties(schema: JsonSchema): Record<string, JsonSchemaProperty>;
235
+ declare function isFieldRequired(schema: JsonSchema, fieldName: string): boolean;
236
+ declare function getFieldDefault(property: JsonSchemaProperty): unknown;
237
+ type JsonSchemaType = "string" | "number" | "integer" | "boolean" | "object" | "array" | "null";
238
+ /**
239
+ * Get the effective type of a property.
240
+ *
241
+ * Unwraps the nullable anyOf pattern produced by Zod / Pydantic Optional[T]:
242
+ * `{ anyOf: [{ type: T }, { type: "null" }] }` → T.
243
+ * Falls back to "string" when no type can be determined.
244
+ */
245
+ declare function getFieldType(property: JsonSchemaProperty): JsonSchemaType;
246
+ /**
247
+ * Returns true when a property uses the Pydantic Optional[bool] pattern:
248
+ * anyOf containing both a boolean member and a null member.
249
+ * Used for tri-state checkbox rendering.
250
+ */
251
+ declare function isNullableBoolean(property: JsonSchemaProperty): boolean;
252
+ /**
253
+ * Extract enum values from a property schema.
254
+ *
255
+ * Handles both the simple case (`{ enum: [...] }`) and the nullable
256
+ * pattern produced by Zod (`{ anyOf: [{ type: "string", enum: [...] }, { type: "null" }] }`).
257
+ *
258
+ * Returns the enum array if found, or an empty array.
259
+ */
260
+ declare function extractEnumValues(property: JsonSchemaProperty): (string | number | boolean)[];
261
+ /** Priority: x-ui-title > title > field name. */
262
+ declare function getFieldTitle(fieldName: string, property: JsonSchemaProperty): string;
263
+ /** Priority: x-ui-description > x-ui-help (legacy alias). */
264
+ declare function getFieldHelpText(property: JsonSchemaProperty): string | undefined;
265
+ declare function getFieldPlaceholder(property: JsonSchemaProperty): string | undefined;
266
+ declare function getFieldWidget(property: JsonSchemaProperty): string | undefined;
267
+ declare function isFieldOptional(schema: JsonSchema, fieldName: string, property: JsonSchemaProperty): boolean;
268
+ declare function getFieldConstraints(property: JsonSchemaProperty): {
269
+ minLength?: number;
270
+ maxLength?: number;
271
+ minimum?: number;
272
+ maximum?: number;
273
+ pattern?: string;
274
+ format?: string;
275
+ };
276
+ declare function getRequiredFields(schema: JsonSchema): string[];
277
+ declare function getOptionalFields(schema: JsonSchema): string[];
278
+ declare function hasProperties(schema: JsonSchema): boolean;
279
+
280
+ /**
281
+ * Schema Resolver - Resolves conditional schemas (if-then-else, allOf, etc.)
282
+ *
283
+ * This service handles JSON Schema conditionals by evaluating conditions
284
+ * based on form data and merging the appropriate schema branches.
285
+ *
286
+ * Based on react-jsonschema-form's retrieveSchema implementation.
287
+ *
288
+ * Supported features:
289
+ * - if-then-else conditionals
290
+ * - allOf schema merging
291
+ * - Nested conditionals (if-then-else inside allOf)
292
+ *
293
+ * @module services/schemaResolver
294
+ */
295
+
296
+ /**
297
+ * Resolve conditional schema based on form data
298
+ *
299
+ * Evaluates if-then-else conditions and merges allOf schemas to produce
300
+ * a final schema with only the properties that should be visible given
301
+ * the current form data.
302
+ *
303
+ * @param schema - Original JSON Schema with conditionals
304
+ * @param formData - Current form data to evaluate conditions against
305
+ * @returns Resolved schema with conditionals merged
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * const schema = {
310
+ * properties: { taxonomy: { type: 'string', enum: ['Tech', 'Arch'] } },
311
+ * allOf: [
312
+ * {
313
+ * if: { properties: { taxonomy: { const: 'Tech' } } },
314
+ * then: { properties: { tech_field: { type: 'string' } } }
315
+ * }
316
+ * ]
317
+ * };
318
+ *
319
+ * const resolved = resolveSchema(schema, { taxonomy: 'Tech' });
320
+ * // Result: { properties: { taxonomy: {...}, tech_field: {...} } }
321
+ * ```
322
+ */
323
+ declare function resolveSchema(schema: JsonSchema, formData: Record<string, any>): JsonSchema;
324
+ /**
325
+ * Validate formData against a schema
326
+ *
327
+ * Useful for checking if form data meets schema requirements,
328
+ * including type validation and format checking.
329
+ *
330
+ * @param schema - JSON Schema to validate against
331
+ * @param formData - Form data to validate
332
+ * @returns Validation result with errors
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * const result = validateSchema(schema, { name: 'John', age: 'invalid' });
337
+ * if (!result.valid) {
338
+ * errorLog(result.errors); // AJV error objects
339
+ * }
340
+ * ```
341
+ */
342
+ declare function validateSchema(schema: JsonSchema, formData: Record<string, any>): {
343
+ valid: boolean;
344
+ errors: any[];
345
+ };
346
+ /**
347
+ * Check if a value matches a const constraint
348
+ *
349
+ * Helper for evaluating const conditions in conditionals.
350
+ *
351
+ * @param value - Value to check
352
+ * @param constValue - Expected const value
353
+ * @returns true if value matches const
354
+ */
355
+ declare function matchesConst(value: any, constValue: any): boolean;
356
+ /**
357
+ * Check if a schema has conditionals
358
+ *
359
+ * Useful for determining if schema resolution is needed.
360
+ *
361
+ * @param schema - JSON Schema to check
362
+ * @returns true if schema contains if-then-else or allOf
363
+ */
364
+ declare function hasConditionals(schema: JsonSchema): boolean;
365
+
366
+ /**
367
+ * Schema utilities
368
+ *
369
+ * Pure functions for common JSON Schema operations:
370
+ * - Applying schema defaults to form data
371
+ * - Extracting default values from a schema
372
+ * - Generating a schema from a list of template variables
373
+ *
374
+ * All functions are side-effect free.
375
+ *
376
+ * @module services/schema/schemaUtils
377
+ */
378
+
379
+ /**
380
+ * Merge schema default values into an existing data object.
381
+ *
382
+ * For each property in the schema that has a `default` value, if the
383
+ * corresponding key is absent (`undefined`) in `data`, the default is applied.
384
+ * Existing values — including `null` and `false` — are never overwritten.
385
+ *
386
+ * @param data - Existing form data (may be partial)
387
+ * @param schema - JSON Schema containing property defaults
388
+ * @returns New object with defaults filled in for missing keys
389
+ */
390
+ declare function applySchemaDefaults(data: Record<string, unknown>, schema: JsonSchema | JsonSchemaProperty): Record<string, unknown>;
391
+ /**
392
+ * Extract all default values declared in a JSON Schema.
393
+ *
394
+ * Walks the schema's top-level properties and collects any `default` values.
395
+ * Properties without a `default` are omitted from the result.
396
+ *
397
+ * @param schema - JSON Schema to extract defaults from
398
+ * @returns Record of field names to their default values
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * extractSchemaDefaults({
403
+ * type: 'object',
404
+ * properties: {
405
+ * name: { type: 'string', default: 'John' },
406
+ * age: { type: 'number' },
407
+ * },
408
+ * });
409
+ * // => { name: 'John' }
410
+ * ```
411
+ */
412
+ declare function extractSchemaDefaults(schema: JsonSchema): Record<string, unknown>;
413
+ /**
414
+ * Generate a JSON Schema from a list of template variable names.
415
+ *
416
+ * Each variable becomes a required string property with a capitalised title.
417
+ * If an existing schema is provided, its property definitions are preserved
418
+ * for variables that still appear in the list; new variables are added as plain
419
+ * strings; properties no longer in the list are dropped.
420
+ *
421
+ * @param variables - Variable names to include in the schema
422
+ * @param existingSchema - Optional schema to merge with (preserves extensions)
423
+ * @returns A JSON Schema object with one property per variable
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * generateJsonSchemaFromVariables(['productId', 'limit']);
428
+ * // => {
429
+ * // type: 'object',
430
+ * // properties: {
431
+ * // productId: { type: 'string', title: 'ProductId' },
432
+ * // limit: { type: 'string', title: 'Limit' },
433
+ * // },
434
+ * // required: ['productId', 'limit'],
435
+ * // }
436
+ * ```
437
+ */
438
+ declare function generateJsonSchemaFromVariables(variables: string[], existingSchema?: {
439
+ properties?: Record<string, JsonSchemaProperty>;
440
+ }): JsonSchema;
441
+
442
+ /**
443
+ * Zod-to-JSON-Schema conversion with x-ui-* metadata passthrough.
444
+ *
445
+ * Wraps z.toJSONSchema() with an override that copies all .meta() keys
446
+ * into the generated JSON Schema output, including x-ui-* extension keys
447
+ * used by the form renderer.
448
+ *
449
+ * @module lib/schema/zodSchema
450
+ */
451
+
452
+ /**
453
+ * Convert a Zod schema to a JsonSchema compatible with the form renderer.
454
+ *
455
+ * All fields annotated with .meta() -- including x-ui-widget, x-ui-order,
456
+ * x-ui-options, title, description -- are passed through to the output.
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * const MySchema = z.object({
461
+ * name: z.string().meta({ title: "Name" }),
462
+ * notes: z.string().optional().meta({ "x-ui-widget": "textarea" }),
463
+ * });
464
+ *
465
+ * const jsonSchema = toJsonSchema(MySchema);
466
+ * type MyData = z.infer<typeof MySchema>;
467
+ * ```
468
+ */
469
+ declare function toJsonSchema(schema: z.ZodType): JsonSchema;
470
+
471
+ /**
472
+ * $ref Resolution -- resolves local JSON Schema references.
473
+ *
474
+ * Walks the schema tree and replaces { "$ref": "#/$defs/name" } nodes
475
+ * with the corresponding sub-schema from the document's $defs (Draft
476
+ * 2020-12) or definitions (Draft 7) block.
477
+ *
478
+ * Properties on the referencing node are merged on top of the resolved
479
+ * target, so context-specific overrides (e.g. a custom title) work
480
+ * without duplicating the definition.
481
+ *
482
+ * Only local refs (starting with "#/") are supported. External URIs
483
+ * are left as-is -- a future pluggable SchemaResolver interface will
484
+ * handle those.
485
+ */
486
+
487
+ /**
488
+ * Resolve all local $ref pointers in a JSON Schema document.
489
+ *
490
+ * Returns the schema unchanged if it contains no $defs/definitions
491
+ * or no $ref nodes. The original schema object is never mutated.
492
+ */
493
+ declare function resolveRefs(schema: JsonSchema): JsonSchema;
494
+
495
+ /**
496
+ * Recursive form data validation against a JSON Schema.
497
+ *
498
+ * Validates required fields at every nesting level: top-level properties,
499
+ * nested objects (type: "object"), and array items (type: "array" with
500
+ * items.type: "object"). Returns a flat list of human-readable error
501
+ * strings and a boolean summary.
502
+ *
503
+ * Pure function -- no React, no side effects.
504
+ */
505
+
506
+ interface ValidationResult {
507
+ errors: string[];
508
+ isValid: boolean;
509
+ }
510
+ /**
511
+ * Validate form data against a JSON Schema, recursing into nested objects
512
+ * and array items.
513
+ */
514
+ declare function validateFormData(schema: JsonSchema, data: Record<string, unknown>): ValidationResult;
515
+
516
+ /**
517
+ * diffFormData -- structural diff between two form data snapshots.
518
+ *
519
+ * Produces a FormDiff that classifies every change at field level (value
520
+ * changes, nested object changes) and at array item level (add, remove,
521
+ * move, modify). Array items are matched by their form-internal `_fid`
522
+ * field, which is assigned by ArrayWidget and never sent to the server.
523
+ *
524
+ * Items that lack a `_fid` (e.g. items that arrived before the form
525
+ * system assigned IDs) are matched by position as a fallback.
526
+ */
527
+
528
+ /**
529
+ * Compute a structural diff between two form data snapshots.
530
+ */
531
+ declare function diffFormData(original: Record<string, unknown>, updated: Record<string, unknown>): FormDiff;
532
+
533
+ export { type ArrayItemChange, type FieldDiff, type FormDiff, type JsonSchema, type JsonSchemaProperty, type UILayoutConfig, type UILayoutRowConfig, type UISchema, type UISchemaOptions, type ValidationResult, type ValueChange, applySchemaDefaults, diffFormData, extractEnumValues, extractSchemaDefaults, generateJsonSchemaFromVariables, getFieldConstraints, getFieldDefault, getFieldHelpText, getFieldPlaceholder, getFieldTitle, getFieldType, getFieldWidget, getOptionalFields, getRequiredFields, getSchemaProperties, hasConditionals, hasProperties, isFieldOptional, isFieldRequired, isNullableBoolean, matchesConst, resolveRefs, resolveSchema, toJsonSchema, validateFormData, validateSchema };