@petrarca/sonnet-core 0.5.0 → 0.7.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.
@@ -1,3 +1,4 @@
1
+ import Ajv from 'ajv';
1
2
  import { z } from 'zod';
2
3
 
3
4
  /**
@@ -7,6 +8,37 @@ import { z } from 'zod';
7
8
  * for form/widget integration. They are domain-agnostic -- no
8
9
  * application-specific entity concepts live here.
9
10
  */
11
+ /**
12
+ * A declarative presentation rule (JSONForms-style) attached to a field.
13
+ *
14
+ * The rule lives on the field it controls (the target). Its `condition` points
15
+ * at another field's value via a JSON Pointer (RFC 6901) into the form DATA
16
+ * (rooted at the whole form), and validates that value against `condition.schema`.
17
+ * When the condition holds, `effect` is applied to the target:
18
+ * - "show" -- render only when the condition holds
19
+ * - "hide" -- render only when the condition does NOT hold
20
+ * - "enable" -- interactive only when the condition holds
21
+ * - "disable"-- non-interactive when the condition holds
22
+ *
23
+ * Example: show `pregnancy_status` when `gender` equals "female":
24
+ * ```json
25
+ * { "effect": "show",
26
+ * "condition": { "scope": "/gender", "schema": { "const": "female" } } }
27
+ * ```
28
+ * Unlike if/then/else, the target field stays declared at its own position, so
29
+ * layout placement is stable.
30
+ */
31
+ interface UIRuleCondition {
32
+ /** JSON Pointer (RFC 6901) into the form data, e.g. "/gender" or "/patient/gender". */
33
+ scope: string;
34
+ /** Schema the value at `scope` is validated against (e.g. { const: "female" }). */
35
+ schema: JsonSchemaProperty;
36
+ }
37
+ type UIRuleEffect = "show" | "hide" | "enable" | "disable";
38
+ interface UIRule {
39
+ effect: UIRuleEffect;
40
+ condition: UIRuleCondition;
41
+ }
10
42
  /**
11
43
  * JSON Schema property definition following Draft 2020-12 specification.
12
44
  * Supports basic types, validation constraints, conditionals, and custom extensions.
@@ -24,29 +56,45 @@ type JsonSchemaProperty = {
24
56
  maxLength?: number;
25
57
  minimum?: number;
26
58
  maximum?: number;
59
+ exclusiveMinimum?: number;
60
+ exclusiveMaximum?: number;
27
61
  items?: JsonSchemaProperty;
28
62
  minItems?: number;
29
63
  maxItems?: number;
30
64
  properties?: Record<string, JsonSchemaProperty>;
31
65
  required?: string[];
32
66
  $ref?: string;
67
+ $defs?: Record<string, JsonSchemaProperty>;
68
+ definitions?: Record<string, JsonSchemaProperty>;
33
69
  allOf?: JsonSchemaProperty[];
34
70
  anyOf?: JsonSchemaProperty[];
35
71
  oneOf?: JsonSchemaProperty[];
36
72
  not?: JsonSchemaProperty;
73
+ /**
74
+ * Discriminator for a `oneOf` union (OpenAPI convention). `propertyName` is
75
+ * the shared property whose `const` value tags the active branch. When
76
+ * absent, the discriminator is auto-detected as the property that is const
77
+ * in every branch.
78
+ */
79
+ discriminator?: {
80
+ propertyName: string;
81
+ };
37
82
  if?: JsonSchemaProperty;
38
83
  then?: JsonSchemaProperty;
39
84
  else?: JsonSchemaProperty;
40
85
  "x-ui-order"?: string[];
86
+ "x-ui-rule"?: UIRule;
41
87
  "x-ui-widget"?: string;
42
88
  "x-ui-options"?: Record<string, any>;
43
89
  "x-ui-layout"?: UILayoutConfig;
44
90
  "x-ui-title"?: string;
45
- "x-ui-help"?: string;
46
91
  "x-ui-placeholder"?: string;
47
92
  "x-ui-description"?: string;
48
93
  "x-ui-column"?: {
49
- /** Display order (ascending). Properties without order are appended sorted by key. */
94
+ /**
95
+ * Display order (ascending). Properties without an order are appended
96
+ * after the ordered ones, keeping their schema declaration order.
97
+ */
50
98
  order?: number;
51
99
  /** Exclude this column from the table entirely. */
52
100
  hidden?: boolean;
@@ -113,10 +161,11 @@ interface UISchema {
113
161
  "x-ui-options"?: UISchemaOptions;
114
162
  "x-ui-globalOptions"?: UISchemaOptions;
115
163
  "x-ui-order"?: string[];
164
+ /** Declarative show/hide or enable/disable rule (see UIRule). */
165
+ "x-ui-rule"?: UIRule;
116
166
  "x-ui-title"?: string | false;
117
167
  "x-ui-description"?: string;
118
168
  "x-ui-placeholder"?: string;
119
- "x-ui-help"?: string;
120
169
  /** Semantic field-slot width (default "full"). */
121
170
  "x-ui-width"?: WidthToken;
122
171
  /** Column override -- merged over schema-level x-ui-column, UI schema wins. */
@@ -180,6 +229,31 @@ interface UILayoutRowConfig {
180
229
  */
181
230
  arrange?: "grid" | "flow";
182
231
  }
232
+ /**
233
+ * One named group of a grouped form layout. A group holds rows of fields under
234
+ * a label; the data stays flat (grouping is presentation only, like `group` on
235
+ * a single row). How groups are shown -- tabs, wizard steps, stacked sections
236
+ * -- is chosen by {@link UILayoutConfig.variant}, not by this structure.
237
+ *
238
+ * A group may carry an `x-ui-rule`-style {@link UIRule} to appear conditionally
239
+ * (e.g. show an "Address" group only when "provide address" is checked). The
240
+ * rule is evaluated against the form data exactly as a field-level rule is.
241
+ */
242
+ interface UILayoutGroupConfig {
243
+ /** Label shown on the group's trigger or heading. */
244
+ label: string;
245
+ /** Rows of fields in this group (same shape as UILayoutConfig.rows). */
246
+ rows: UILayoutRowConfig[];
247
+ /**
248
+ * Optional show/hide rule for the whole group: a hidden group disappears
249
+ * along with its fields. Narrowed to the visibility effects -- a disabled
250
+ * group has no meaning, and permitting the effect would let it be authored
251
+ * and then silently ignored.
252
+ */
253
+ rule?: UIRule & {
254
+ effect: "show" | "hide";
255
+ };
256
+ }
183
257
  /**
184
258
  * UILayoutConfig -- layout configuration for ObjectWidget and ArrayWidget.
185
259
  *
@@ -189,6 +263,8 @@ interface UILayoutRowConfig {
189
263
  interface UILayoutConfig {
190
264
  direction?: "vertical" | "horizontal";
191
265
  rows?: UILayoutRowConfig[];
266
+ groups?: UILayoutGroupConfig[];
267
+ variant?: "tabs" | "stepper" | "sections" | (string & {});
192
268
  columns?: string;
193
269
  gap?: number;
194
270
  labels?: boolean;
@@ -263,6 +339,8 @@ interface UISchemaOptions {
263
339
  rows?: number;
264
340
  searchable?: boolean;
265
341
  clearable?: boolean;
342
+ /** Allow the user to type and submit a value not in the enum list (combobox / open-choice). */
343
+ creatable?: boolean;
266
344
  /** Label placement: "top" (default, above) or "start" (inline, RTL-safe). */
267
345
  labelPlacement?: "top" | "start";
268
346
  /** Semantic field-slot width (default "full"). */
@@ -309,7 +387,14 @@ declare function isNullableBoolean(property: JsonSchemaProperty): boolean;
309
387
  declare function extractEnumValues(property: JsonSchemaProperty): (string | number | boolean)[];
310
388
  /** Priority: x-ui-title > title > field name. */
311
389
  declare function getFieldTitle(fieldName: string, property: JsonSchemaProperty): string;
312
- /** Priority: x-ui-description > x-ui-help (legacy alias). */
390
+ /**
391
+ * Help text for a field, rendered under the control.
392
+ *
393
+ * `x-ui-description` overrides the standard JSON Schema `description`. There is
394
+ * one help-text slot per field, so a schema that only carries `description` --
395
+ * anything generated from a typed model, or authored in the designer's Data tab
396
+ * -- shows it without needing a UI hint.
397
+ */
313
398
  declare function getFieldHelpText(property: JsonSchemaProperty): string | undefined;
314
399
  declare function getFieldPlaceholder(property: JsonSchemaProperty): string | undefined;
315
400
  declare function getFieldWidget(property: JsonSchemaProperty): string | undefined;
@@ -326,6 +411,38 @@ declare function getRequiredFields(schema: JsonSchema): string[];
326
411
  declare function getOptionalFields(schema: JsonSchema): string[];
327
412
  declare function hasProperties(schema: JsonSchema): boolean;
328
413
 
414
+ /**
415
+ * oneOf (discriminated-union) helpers.
416
+ *
417
+ * A oneOf property is an object whose shape is one of several branches, modeled
418
+ * as `oneOf: [branchSchema, ...]`. Each branch shares a discriminator property
419
+ * -- a field with a `const` value that tags which branch is active. The
420
+ * discriminator is named by `discriminator.propertyName` (OpenAPI convention),
421
+ * or auto-detected as the property that carries a `const` in every branch.
422
+ *
423
+ * These helpers are pure and domain-agnostic.
424
+ */
425
+
426
+ /** Whether a property is a oneOf union (has a non-empty `oneOf`). */
427
+ declare function isOneOf(property: JsonSchemaProperty): boolean;
428
+ /**
429
+ * Resolve the discriminator property name for a oneOf union.
430
+ *
431
+ * Uses `discriminator.propertyName` when set; otherwise auto-detects the single
432
+ * property that carries a `const` in every branch. Returns undefined when no
433
+ * consistent discriminator can be determined.
434
+ */
435
+ declare function getDiscriminator(property: JsonSchemaProperty): string | undefined;
436
+ /** The discriminator const value for a branch, or undefined. */
437
+ declare function getBranchValue(branch: JsonSchemaProperty, discriminator: string): string | number | boolean | undefined;
438
+ /** Human label for a branch: its `title`, else the discriminator value. */
439
+ declare function getBranchLabel(branch: JsonSchemaProperty, discriminator: string | undefined, index: number): string;
440
+ /**
441
+ * Find the branch that matches the current form data by comparing the
442
+ * discriminator value. Returns the branch index, or -1 when none match.
443
+ */
444
+ declare function findActiveBranch(property: JsonSchemaProperty, data: Record<string, unknown> | undefined, discriminator: string | undefined): number;
445
+
329
446
  /**
330
447
  * Schema Resolver - Resolves conditional schemas (if-then-else, allOf, etc.)
331
448
  *
@@ -342,6 +459,7 @@ declare function hasProperties(schema: JsonSchema): boolean;
342
459
  * @module services/schemaResolver
343
460
  */
344
461
 
462
+ declare const ajv: Ajv;
345
463
  /**
346
464
  * Resolve conditional schema based on form data
347
465
  *
@@ -412,6 +530,45 @@ declare function matchesConst(value: any, constValue: any): boolean;
412
530
  */
413
531
  declare function hasConditionals(schema: JsonSchema): boolean;
414
532
 
533
+ /**
534
+ * Declarative UI rules (x-ui-rule): show/hide or enable/disable a field based
535
+ * on another field's value.
536
+ *
537
+ * The rule lives on the target field. Its condition references a value elsewhere
538
+ * in the form data via a JSON Pointer (RFC 6901) rooted at the whole form, and
539
+ * validates that value against a schema. Evaluation yields presentation flags
540
+ * (visible / enabled) the renderer applies -- the field itself stays declared at
541
+ * its own position, so layout is stable (unlike if/then/else).
542
+ *
543
+ * Scopes are root-anchored at every depth: a rule on a nested field reads its
544
+ * pointer from the top of the form, so `/country` means the form's `country`
545
+ * even from inside `address`, and a sibling is reached by its full path
546
+ * (`/address/city`). This matches every other JSON Pointer in the vocabulary
547
+ * (`editablePaths`, validation error pointers). The renderer is responsible for
548
+ * passing the root form data in -- see `useRootFormData` in sonnet-forms.
549
+ *
550
+ * A consequence worth knowing when authoring inside `$defs`: a definition is
551
+ * reusable, but a root-anchored scope is not. A rule inside a definition that
552
+ * references `/country` only holds for forms that have a top-level `country`.
553
+ * This is inherent to root-anchoring and is the same in comparable libraries.
554
+ */
555
+
556
+ /** The presentation state a rule resolves to for its target field. */
557
+ interface RuleState {
558
+ visible: boolean;
559
+ enabled: boolean;
560
+ }
561
+ /**
562
+ * Resolve a JSON Pointer against a data object. Returns the value at the pointer,
563
+ * or undefined when any segment is missing. "" (empty pointer) returns the root.
564
+ */
565
+ declare function getAtPointer(data: unknown, pointer: string): unknown;
566
+ /**
567
+ * Resolve a field's presentation state from its `x-ui-rule` (if any) against the
568
+ * current form data. Fields without a rule are visible and enabled.
569
+ */
570
+ declare function evaluateUiRule(rule: UIRule | undefined, formData: unknown): RuleState;
571
+
415
572
  /**
416
573
  * Schema utilities
417
574
  *
@@ -518,42 +675,67 @@ declare function generateJsonSchemaFromVariables(variables: string[], existingSc
518
675
  declare function toJsonSchema(schema: z.ZodType): JsonSchema;
519
676
 
520
677
  /**
521
- * $ref Resolution -- resolves local JSON Schema references.
678
+ * $ref Resolution -- resolves JSON Schema references.
522
679
  *
523
- * Walks the schema tree and replaces { "$ref": "#/$defs/name" } nodes
524
- * with the corresponding sub-schema from the document's $defs (Draft
525
- * 2020-12) or definitions (Draft 7) block.
680
+ * Walks the schema tree and replaces `{ "$ref": ... }` nodes with the
681
+ * corresponding sub-schema. By default it resolves local, in-document refs:
682
+ * `#/$defs/name` (Draft 2020-12) or `#/definitions/name` (Draft 7).
526
683
  *
527
- * Properties on the referencing node are merged on top of the resolved
528
- * target, so context-specific overrides (e.g. a custom title) work
529
- * without duplicating the definition.
684
+ * A caller may inject a `RefResolver` to resolve ANY ref string (e.g. a schema
685
+ * registry id like `sdf_...`, or a canonical URI). The injected resolver is
686
+ * tried first; the in-document `$defs`/`definitions` lookup is the fallback, so
687
+ * both ref styles work together. The resolver is synchronous: consumers that
688
+ * fetch schemas asynchronously (e.g. from a registry over HTTP) pre-load them
689
+ * into a map and pass a `(ref) => map[ref]` lookup. This keeps the toolkit pure
690
+ * (no HTTP, no async).
530
691
  *
531
- * Only local refs (starting with "#/") are supported. External URIs
532
- * are left as-is -- a future pluggable SchemaResolver interface will
533
- * handle those.
692
+ * Properties on the referencing node are merged on top of the resolved target,
693
+ * so context-specific overrides (e.g. a custom title) work without duplicating
694
+ * the definition. Refs that no resolver can satisfy are left as-is.
534
695
  */
535
696
 
697
+ /** Resolve a `$ref` string to its target sub-schema, or undefined if unknown. */
698
+ type RefResolver = (ref: string) => JsonSchemaProperty | undefined;
536
699
  /**
537
- * Resolve all local $ref pointers in a JSON Schema document.
700
+ * Resolve `$ref` pointers in a JSON Schema document.
538
701
  *
539
- * Returns the schema unchanged if it contains no $defs/definitions
540
- * or no $ref nodes. The original schema object is never mutated.
702
+ * By default resolves in-document `#/$defs` / `#/definitions` refs. Pass a
703
+ * `resolve` to also resolve registry/URI refs; it is tried first and the
704
+ * in-document lookup is the fallback. The original schema is never mutated;
705
+ * a document with no refs to resolve is returned unchanged.
541
706
  */
542
- declare function resolveRefs(schema: JsonSchema): JsonSchema;
707
+ declare function resolveRefs(schema: JsonSchema, resolve?: RefResolver): JsonSchema;
543
708
 
544
709
  /**
545
710
  * Recursive form data validation against a JSON Schema.
546
711
  *
547
712
  * Validates required fields at every nesting level: top-level properties,
548
713
  * nested objects (type: "object"), and array items (type: "array" with
549
- * items.type: "object"). Returns a flat list of human-readable error
550
- * strings and a boolean summary.
714
+ * items.type: "object"). Returns human-readable error strings, the same
715
+ * errors keyed by JSON Pointer, and a boolean summary.
716
+ *
717
+ * The pointer form lets callers attribute an error to a specific field --
718
+ * needed to surface errors that are not currently on screen (e.g. a badge on
719
+ * a form tab whose fields are hidden). `errors` remains the flat message list.
551
720
  *
552
721
  * Pure function -- no React, no side effects.
553
722
  */
554
723
 
724
+ /** A validation error attributed to the field that produced it. */
725
+ interface FieldError {
726
+ /**
727
+ * JSON Pointer (RFC 6901) to the offending field, e.g. `/email`,
728
+ * `/contact/email`, or `/tags/2/label` for the third array item.
729
+ */
730
+ pointer: string;
731
+ /** Human-readable message, e.g. "Contact > Email is required". */
732
+ message: string;
733
+ }
555
734
  interface ValidationResult {
735
+ /** Flat, human-readable messages (in field declaration order). */
556
736
  errors: string[];
737
+ /** The same errors, each attributed to its field via a JSON Pointer. */
738
+ fieldErrors: FieldError[];
557
739
  isValid: boolean;
558
740
  }
559
741
  /**
@@ -579,4 +761,29 @@ declare function validateFormData(schema: JsonSchema, data: Record<string, unkno
579
761
  */
580
762
  declare function diffFormData(original: Record<string, unknown>, updated: Record<string, unknown>): FormDiff;
581
763
 
582
- export { type ArrayItemChange, type FieldDiff, type FormDiff, type JsonSchema, type JsonSchemaProperty, type UILayoutConfig, type UILayoutRowConfig, type UISchema, type UISchemaOptions, type ValidationResult, type ValueChange, WIDTH_TOKEN_REMS, type WidthToken, 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 };
764
+ /**
765
+ * ensureFormItemIds -- stamp a stable `_fid` onto every array-of-object item
766
+ * in a form data snapshot, guided by the schema.
767
+ *
768
+ * The form system matches array items across a diff by their `_fid` (see
769
+ * diffFormData). ArrayWidget assigns one when it renders, but that only reaches
770
+ * the working copy. The dirty-tracking baseline is snapshotted from the
771
+ * incoming `data`, which has none -- so a loaded array diffs as a wholesale
772
+ * replace after a single-item edit.
773
+ *
774
+ * Stamping the incoming data once, before both the working copy and the
775
+ * baseline are derived from it, gives the two sides the *same* ids, so
776
+ * identity survives the diff. Items that already carry a `_fid` keep it.
777
+ *
778
+ * Only array-of-object fields declared in the schema are touched; scalar
779
+ * arrays and everything else pass through unchanged. Recurses into nested
780
+ * objects and into the items of nested arrays.
781
+ */
782
+
783
+ /**
784
+ * Return `data` with a `_fid` on every array-of-object item the schema
785
+ * declares. Returns the input unchanged when nothing needed stamping.
786
+ */
787
+ declare function ensureFormItemIds(data: Record<string, unknown>, schema: JsonSchema): Record<string, unknown>;
788
+
789
+ export { type ArrayItemChange, type FieldDiff, type FieldError, type FormDiff, type JsonSchema, type JsonSchemaProperty, type RefResolver, type RuleState, type UILayoutConfig, type UILayoutGroupConfig, type UILayoutRowConfig, type UIRule, type UIRuleCondition, type UIRuleEffect, type UISchema, type UISchemaOptions, type ValidationResult, type ValueChange, WIDTH_TOKEN_REMS, type WidthToken, ajv, applySchemaDefaults, diffFormData, ensureFormItemIds, evaluateUiRule, extractEnumValues, extractSchemaDefaults, findActiveBranch, generateJsonSchemaFromVariables, getAtPointer, getBranchLabel, getBranchValue, getDiscriminator, getFieldConstraints, getFieldDefault, getFieldHelpText, getFieldPlaceholder, getFieldTitle, getFieldType, getFieldWidget, getOptionalFields, getRequiredFields, getSchemaProperties, hasConditionals, hasProperties, isFieldOptional, isFieldRequired, isNullableBoolean, isOneOf, matchesConst, resolveRefs, resolveSchema, toJsonSchema, validateFormData, validateSchema };