@fougere/schema 0.12.0-alpha.0 → 0.12.1-alpha.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.
Files changed (192) hide show
  1. package/dist/SchemaError.js +1 -1
  2. package/dist/SchemaError.js.map +1 -1
  3. package/dist/axis/Axes.d.ts +16 -0
  4. package/dist/axis/Axes.d.ts.map +1 -0
  5. package/dist/axis/Axes.js +27 -0
  6. package/dist/axis/Axes.js.map +1 -0
  7. package/dist/axis/Axis.d.ts +5 -9
  8. package/dist/axis/Axis.d.ts.map +1 -1
  9. package/dist/axis/Axis.js +1 -10
  10. package/dist/axis/Axis.js.map +1 -1
  11. package/dist/axis/boundary/Boundary.d.ts +29 -3
  12. package/dist/axis/boundary/Boundary.d.ts.map +1 -1
  13. package/dist/axis/boundary/Boundary.js +12 -9
  14. package/dist/axis/boundary/Boundary.js.map +1 -1
  15. package/dist/axis/boundary/Decoder.d.ts +8 -2
  16. package/dist/axis/boundary/Decoder.d.ts.map +1 -1
  17. package/dist/axis/lifecycle/Lifecycle.d.ts +16 -4
  18. package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
  19. package/dist/axis/lifecycle/Lifecycle.js +18 -7
  20. package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
  21. package/dist/axis/lifecycle/apply.d.ts.map +1 -1
  22. package/dist/axis/lifecycle/apply.js +2 -3
  23. package/dist/axis/lifecycle/apply.js.map +1 -1
  24. package/dist/axis/role/Role.d.ts +18 -6
  25. package/dist/axis/role/Role.d.ts.map +1 -1
  26. package/dist/axis/role/Role.js +30 -16
  27. package/dist/axis/role/Role.js.map +1 -1
  28. package/dist/axis/shape/{Formats.d.ts → StringFormats.d.ts} +3 -3
  29. package/dist/axis/shape/StringFormats.d.ts.map +1 -0
  30. package/dist/axis/shape/{Formats.js → StringFormats.js} +3 -3
  31. package/dist/axis/shape/StringFormats.js.map +1 -0
  32. package/dist/field/Field.d.ts +11 -5
  33. package/dist/field/Field.d.ts.map +1 -1
  34. package/dist/field/Field.js +5 -3
  35. package/dist/field/Field.js.map +1 -1
  36. package/dist/field/FieldFormat.d.ts +9 -0
  37. package/dist/field/FieldFormat.d.ts.map +1 -0
  38. package/dist/field/FieldFormat.js +12 -0
  39. package/dist/field/FieldFormat.js.map +1 -0
  40. package/dist/field/FougereFieldAxes.d.ts +3 -0
  41. package/dist/field/FougereFieldAxes.d.ts.map +1 -0
  42. package/dist/field/FougereFieldAxes.js +2 -0
  43. package/dist/field/FougereFieldAxes.js.map +1 -0
  44. package/dist/field/Meta.d.ts +5 -3
  45. package/dist/field/Meta.d.ts.map +1 -1
  46. package/dist/field/Meta.js +2 -1
  47. package/dist/field/Meta.js.map +1 -1
  48. package/dist/index.d.ts +6 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -3
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/Format.d.ts +56 -0
  53. package/dist/lib/Format.d.ts.map +1 -0
  54. package/dist/lib/Format.js +95 -0
  55. package/dist/lib/Format.js.map +1 -0
  56. package/dist/lib/JsonSchema.d.ts +2 -0
  57. package/dist/lib/JsonSchema.d.ts.map +1 -0
  58. package/dist/lib/JsonSchema.js +2 -0
  59. package/dist/lib/JsonSchema.js.map +1 -0
  60. package/dist/lib/Registry.d.ts +4 -2
  61. package/dist/lib/Registry.d.ts.map +1 -1
  62. package/dist/lib/Registry.js +11 -5
  63. package/dist/lib/Registry.js.map +1 -1
  64. package/dist/lib/ValidationResult.d.ts +2 -0
  65. package/dist/lib/ValidationResult.d.ts.map +1 -1
  66. package/dist/lib/ValidationResult.js +6 -0
  67. package/dist/lib/ValidationResult.js.map +1 -1
  68. package/dist/projection/Visibility.d.ts +11 -0
  69. package/dist/projection/Visibility.d.ts.map +1 -1
  70. package/dist/projection/Visibility.js +22 -0
  71. package/dist/projection/Visibility.js.map +1 -1
  72. package/dist/projection/card/Card.d.ts.map +1 -1
  73. package/dist/projection/card/Card.js +12 -9
  74. package/dist/projection/card/Card.js.map +1 -1
  75. package/dist/projection/card/CardForm.d.ts +7 -0
  76. package/dist/projection/card/CardForm.d.ts.map +1 -0
  77. package/dist/projection/card/CardForm.js +2 -0
  78. package/dist/projection/card/CardForm.js.map +1 -0
  79. package/dist/projection/card/CardForms.d.ts +10 -0
  80. package/dist/projection/card/CardForms.d.ts.map +1 -0
  81. package/dist/projection/card/CardForms.js +12 -0
  82. package/dist/projection/card/CardForms.js.map +1 -0
  83. package/dist/projection/card/Change.d.ts +2 -2
  84. package/dist/projection/card/Change.d.ts.map +1 -1
  85. package/dist/projection/card/Diff.js +2 -2
  86. package/dist/projection/card/Diff.js.map +1 -1
  87. package/dist/projection/card/FieldExtension.d.ts +4 -2
  88. package/dist/projection/card/FieldExtension.d.ts.map +1 -1
  89. package/dist/projection/card/RoleDescriptor.d.ts +9 -2
  90. package/dist/projection/card/RoleDescriptor.d.ts.map +1 -1
  91. package/dist/projection/card/RoleDescriptor.js +40 -1
  92. package/dist/projection/card/RoleDescriptor.js.map +1 -1
  93. package/dist/projection/card/admission.d.ts +0 -14
  94. package/dist/projection/card/admission.d.ts.map +1 -1
  95. package/dist/projection/card/admission.js +0 -20
  96. package/dist/projection/card/admission.js.map +1 -1
  97. package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
  98. package/dist/validator/FieldDeclarationValidator.js +5 -17
  99. package/dist/validator/FieldDeclarationValidator.js.map +1 -1
  100. package/dist/validator/FieldValueValidator.d.ts.map +1 -1
  101. package/dist/validator/FieldValueValidator.js +9 -27
  102. package/dist/validator/FieldValueValidator.js.map +1 -1
  103. package/dist/validator/JsonSchemaValidator.d.ts +23 -0
  104. package/dist/validator/JsonSchemaValidator.d.ts.map +1 -0
  105. package/dist/validator/JsonSchemaValidator.js +113 -0
  106. package/dist/validator/JsonSchemaValidator.js.map +1 -0
  107. package/dist/vocabulary/vocabulary.js +7 -5
  108. package/dist/vocabulary/vocabulary.js.map +1 -1
  109. package/package.json +1 -1
  110. package/src/SchemaError.ts +1 -1
  111. package/src/axis/Axes.ts +34 -0
  112. package/src/axis/Axis.ts +5 -20
  113. package/src/axis/boundary/Boundary.ts +23 -11
  114. package/src/axis/boundary/Decoder.ts +1 -1
  115. package/src/axis/lifecycle/Lifecycle.ts +28 -11
  116. package/src/axis/lifecycle/apply.ts +11 -5
  117. package/src/axis/role/Role.ts +35 -19
  118. package/src/axis/shape/{Formats.ts → StringFormats.ts} +2 -2
  119. package/src/field/Field.ts +18 -8
  120. package/src/field/FieldFormat.ts +18 -0
  121. package/src/field/FougereFieldAxes.ts +2 -0
  122. package/src/field/Meta.ts +5 -3
  123. package/src/index.ts +6 -3
  124. package/src/lib/Format.ts +139 -0
  125. package/src/lib/JsonSchema.ts +1 -0
  126. package/src/lib/Registry.ts +14 -6
  127. package/src/lib/ValidationResult.ts +8 -0
  128. package/src/projection/Visibility.ts +22 -0
  129. package/src/projection/card/Card.ts +12 -9
  130. package/src/projection/card/CardForm.ts +8 -0
  131. package/src/projection/card/CardForms.ts +13 -0
  132. package/src/projection/card/Change.ts +2 -2
  133. package/src/projection/card/Diff.ts +2 -2
  134. package/src/projection/card/FieldExtension.ts +5 -2
  135. package/src/projection/card/RoleDescriptor.ts +48 -2
  136. package/src/projection/card/admission.ts +0 -33
  137. package/src/validator/FieldDeclarationValidator.ts +5 -17
  138. package/src/validator/FieldValueValidator.ts +10 -35
  139. package/src/validator/JsonSchemaValidator.ts +142 -0
  140. package/src/vocabulary/vocabulary.ts +7 -5
  141. package/dist/axis/boundary/BoundaryAxis.d.ts +0 -4
  142. package/dist/axis/boundary/BoundaryAxis.d.ts.map +0 -1
  143. package/dist/axis/boundary/BoundaryAxis.js +0 -30
  144. package/dist/axis/boundary/BoundaryAxis.js.map +0 -1
  145. package/dist/axis/boundary/BoundaryRef.d.ts +0 -3
  146. package/dist/axis/boundary/BoundaryRef.d.ts.map +0 -1
  147. package/dist/axis/boundary/BoundaryRef.js +0 -2
  148. package/dist/axis/boundary/BoundaryRef.js.map +0 -1
  149. package/dist/axis/boundary/BoundaryRules.d.ts +0 -9
  150. package/dist/axis/boundary/BoundaryRules.d.ts.map +0 -1
  151. package/dist/axis/boundary/BoundaryRules.js +0 -2
  152. package/dist/axis/boundary/BoundaryRules.js.map +0 -1
  153. package/dist/axis/lifecycle/Clock.d.ts +0 -10
  154. package/dist/axis/lifecycle/Clock.d.ts.map +0 -1
  155. package/dist/axis/lifecycle/Clock.js +0 -19
  156. package/dist/axis/lifecycle/Clock.js.map +0 -1
  157. package/dist/axis/lifecycle/LifecycleAxis.d.ts +0 -4
  158. package/dist/axis/lifecycle/LifecycleAxis.d.ts.map +0 -1
  159. package/dist/axis/lifecycle/LifecycleAxis.js +0 -52
  160. package/dist/axis/lifecycle/LifecycleAxis.js.map +0 -1
  161. package/dist/axis/lifecycle/LifecycleRules.d.ts +0 -12
  162. package/dist/axis/lifecycle/LifecycleRules.d.ts.map +0 -1
  163. package/dist/axis/lifecycle/LifecycleRules.js +0 -3
  164. package/dist/axis/lifecycle/LifecycleRules.js.map +0 -1
  165. package/dist/axis/role/RoleAxis.d.ts +0 -5
  166. package/dist/axis/role/RoleAxis.d.ts.map +0 -1
  167. package/dist/axis/role/RoleAxis.js +0 -93
  168. package/dist/axis/role/RoleAxis.js.map +0 -1
  169. package/dist/axis/role/RoleRules.d.ts +0 -8
  170. package/dist/axis/role/RoleRules.d.ts.map +0 -1
  171. package/dist/axis/role/RoleRules.js +0 -2
  172. package/dist/axis/role/RoleRules.js.map +0 -1
  173. package/dist/axis/shape/Formats.d.ts.map +0 -1
  174. package/dist/axis/shape/Formats.js.map +0 -1
  175. package/dist/projection/card/RelationDescriptor.d.ts +0 -5
  176. package/dist/projection/card/RelationDescriptor.d.ts.map +0 -1
  177. package/dist/projection/card/RelationDescriptor.js +0 -2
  178. package/dist/projection/card/RelationDescriptor.js.map +0 -1
  179. package/dist/validator/AdapterFieldValidator.d.ts +0 -14
  180. package/dist/validator/AdapterFieldValidator.d.ts.map +0 -1
  181. package/dist/validator/AdapterFieldValidator.js +0 -37
  182. package/dist/validator/AdapterFieldValidator.js.map +0 -1
  183. package/src/axis/boundary/BoundaryAxis.ts +0 -32
  184. package/src/axis/boundary/BoundaryRef.ts +0 -3
  185. package/src/axis/boundary/BoundaryRules.ts +0 -4
  186. package/src/axis/lifecycle/Clock.ts +0 -22
  187. package/src/axis/lifecycle/LifecycleAxis.ts +0 -55
  188. package/src/axis/lifecycle/LifecycleRules.ts +0 -9
  189. package/src/axis/role/RoleAxis.ts +0 -104
  190. package/src/axis/role/RoleRules.ts +0 -8
  191. package/src/projection/card/RelationDescriptor.ts +0 -3
  192. package/src/validator/AdapterFieldValidator.ts +0 -41
@@ -33,6 +33,28 @@ export class Visibility {
33
33
  return result;
34
34
  }
35
35
 
36
+ /**
37
+ * The dual of `encode`, read where a caller RECEIVES a row.
38
+ *
39
+ * `date-time` means a `Date` on both sides, and only the outgoing half was ever applied: a
40
+ * handler answered `new Date(0)`, the facade encoded it, and the caller was handed
41
+ * `"1970-01-01T00:00:00.000Z"` while its type said `Date` — locally as well as across a wire.
42
+ *
43
+ * It converts and never judges: what arrives was validated where it was produced, so a value
44
+ * the codec refuses is kept as it came rather than replaced or thrown over.
45
+ */
46
+ decode(record: Record<string, unknown>): Record<string, unknown> {
47
+ const out: Record<string, unknown> = { ...record };
48
+ for (const [key, field] of Object.entries(this.fields)) {
49
+ const value = record[key];
50
+ if (!(key in record) || value === null || value === undefined) continue;
51
+ const verdict = Boundary.of(field).decode(value);
52
+ if ('value' in verdict) out[key] = verdict.value;
53
+ }
54
+
55
+ return out;
56
+ }
57
+
36
58
  encode(record: Record<string, unknown>): Record<string, unknown> {
37
59
  const out: Record<string, unknown> = { ...record };
38
60
  for (const [key, field] of Object.entries(this.fields)) {
@@ -1,4 +1,5 @@
1
- import { EXTENSION_AXES } from '../../axis/Axis.js';
1
+ import { Axes } from '../../axis/Axes.js';
2
+ import { CardForms } from './CardForms.js';
2
3
  import { type Resolver } from '../../axis/Resolver.js';
3
4
  import { clean, isObject } from '../../lib/utils.js';
4
5
  import { Field } from '../../field/Field.js';
@@ -126,11 +127,12 @@ export class Card<T = Values<Fields>> {
126
127
  */
127
128
  function describeExtension(field: Field, key: string): FieldExtension | undefined {
128
129
  const extension: Record<string, unknown> = {};
129
- for (const axis of EXTENSION_AXES) {
130
- const declared = (field as unknown as Record<string, unknown>)[axis.slot];
130
+ for (const slot of Axes.names) {
131
+ const declared = (field as unknown as Record<string, unknown>)[slot];
131
132
  if (declared === undefined) continue;
132
- const wire = axis.describe(declared, key);
133
- if (wire !== undefined) extension[axis.slot] = wire;
133
+ const form = CardForms.find(slot);
134
+ const wire = form ? form.describe(declared, key) : declared;
135
+ if (wire !== undefined) extension[slot] = wire;
134
136
  }
135
137
  clean(extension);
136
138
  return Object.keys(extension).length ? (extension as FieldExtension) : undefined;
@@ -201,9 +203,10 @@ function reconstructField(
201
203
  admitPatterns(shape, `Field '${key}'`);
202
204
  const extension = property['x-fougere'];
203
205
  const axes: Record<string, unknown> = {};
204
- for (const axis of EXTENSION_AXES) {
205
- const wire = (extension as Record<string, unknown> | undefined)?.[axis.slot];
206
- if (wire !== undefined) axes[axis.slot] = axis.reconstruct(wire, resolve);
206
+ for (const slot of Axes.names) {
207
+ const wire = (extension as Record<string, unknown> | undefined)?.[slot];
208
+ const form = CardForms.find(slot);
209
+ if (wire !== undefined) axes[slot] = form ? form.reconstruct(wire, resolve) : wire;
207
210
  }
208
211
  return new Field({
209
212
  shape,
@@ -212,7 +215,7 @@ function reconstructField(
212
215
  property.description !== undefined
213
216
  ? { description: property.description }
214
217
  : undefined,
215
- } as never);
218
+ } as never, key);
216
219
  }
217
220
 
218
221
  /**
@@ -0,0 +1,8 @@
1
+ import type { Resolver } from '../../axis/Resolver.js';
2
+
3
+ /** How a card writes what an axis declared, and reads it back — stated by the card, not the axis. */
4
+ export interface CardForm<Declared = unknown, Wire = unknown> {
5
+ describe(value: Declared, key: string): Wire | undefined;
6
+
7
+ reconstruct(wire: Wire, resolve?: Resolver): Declared;
8
+ }
@@ -0,0 +1,13 @@
1
+ import { Registry } from '../../lib/Registry.js';
2
+ import type { CardForm } from './CardForm.js';
3
+ import { roleOnCard } from './RoleDescriptor.js';
4
+
5
+ /**
6
+ * The axes a card writes differently from their declaration. An axis absent here travels as
7
+ * itself, which is what `lifecycle`, `boundary` and anything registered from outside do.
8
+ * FR : les axes qu'une carte écrit autrement que déclarés ; les autres voyagent tels quels.
9
+ * `CardForms.register('tenancy', tenancyOnCard)`
10
+ */
11
+ export const CardForms = new Registry<CardForm>('card form', 'call CardForms.register(slot, form)', [
12
+ ['role', roleOnCard as CardForm],
13
+ ]);
@@ -1,5 +1,5 @@
1
- import type { BoundaryRef } from '../../axis/boundary/BoundaryRef.js';
2
- import type { LifecycleRules } from '../../axis/lifecycle/LifecycleRules.js';
1
+ import type { BoundaryRef } from '../../axis/boundary/Boundary.js';
2
+ import type { LifecycleRules } from '../../axis/lifecycle/Lifecycle.js';
3
3
  import type { FieldDescriptor } from './FieldDescriptor.js';
4
4
  import type { RoleDescriptor } from './RoleDescriptor.js';
5
5
  import type { TypeSet } from './TypeSet.js';
@@ -1,5 +1,5 @@
1
1
  import { dequal } from 'dequal';
2
- import { EXTENSION_SLOTS } from '../../axis/Axis.js';
2
+ import { Axes } from '../../axis/Axes.js';
3
3
  import type { FieldDescriptor } from './FieldDescriptor.js';
4
4
  import type { FieldExtension } from './FieldExtension.js';
5
5
 
@@ -93,7 +93,7 @@ function restated(
93
93
  before: FieldExtension | undefined,
94
94
  after: FieldExtension | undefined,
95
95
  ): Change[] {
96
- return EXTENSION_SLOTS.filter((axis) => !dequal(before?.[axis], after?.[axis])).map(
96
+ return Axes.names.filter((axis) => !dequal(before?.[axis], after?.[axis])).map(
97
97
  (axis) =>
98
98
  ({
99
99
  kind: 'restated',
@@ -1,9 +1,12 @@
1
- import type { BoundaryRef } from '../../axis/boundary/BoundaryRef.js';
2
- import type { LifecycleRules } from '../../axis/lifecycle/LifecycleRules.js';
1
+ import type { BoundaryRef } from '../../axis/boundary/Boundary.js';
2
+ import type { LifecycleRules } from '../../axis/lifecycle/Lifecycle.js';
3
3
  import type { RoleDescriptor } from './RoleDescriptor.js';
4
4
 
5
5
  export interface FieldExtension {
6
6
  role?: RoleDescriptor;
7
7
  lifecycle?: LifecycleRules;
8
8
  boundary?: BoundaryRef;
9
+
10
+ /** An axis registered from outside writes its own slot here, under its own name. */
11
+ [slot: string]: unknown;
9
12
  }
@@ -1,7 +1,53 @@
1
- import type { RoleRules } from '../../axis/role/RoleRules.js';
2
- import type { RelationDescriptor } from './RelationDescriptor.js';
1
+ import type { EntityConstructor } from '../../axis/role/EntityConstructor.js';
2
+ import type { Relation } from '../../axis/role/Relation.js';
3
+ import type { RoleRules } from '../../axis/role/Role.js';
4
+ import type { Resolver } from '../../axis/Resolver.js';
5
+ import { lowerFirst } from '../../lib/utils.js';
6
+ import type { CardForm } from './CardForm.js';
3
7
 
8
+ /** What a role becomes on a card: a target is a NAME there, and a unique is a group. */
4
9
  export type RoleDescriptor = Pick<RoleRules, 'primary' | 'index'> & {
5
10
  unique?: string[][];
6
11
  relation?: RelationDescriptor;
7
12
  };
13
+
14
+ export type RelationDescriptor = Pick<Relation, 'kind' | 'onDelete'> & { to: string };
15
+
16
+ type Mutable<T> = { -readonly [K in keyof T]: T[K] };
17
+
18
+ /** A card carries the target's NAME: a class cannot cross a process boundary. */
19
+ export const roleOnCard: CardForm<RoleRules, RoleDescriptor> = {
20
+ describe(role, key) {
21
+ const descriptor: Mutable<RoleDescriptor> = {};
22
+ if (role.primary) descriptor.primary = true;
23
+ if (role.unique) descriptor.unique = [[key]];
24
+ if (role.index) descriptor.index = true;
25
+ if (role.relation) {
26
+ const target = role.relation.to() as { name?: string };
27
+ descriptor.relation = {
28
+ to: lowerFirst(target.name ?? ''),
29
+ kind: role.relation.kind,
30
+ ...(role.relation.onDelete ? { onDelete: role.relation.onDelete } : {}),
31
+ };
32
+ }
33
+
34
+ return Object.keys(descriptor).length ? descriptor : undefined;
35
+ },
36
+
37
+ reconstruct(wire, resolve?: Resolver) {
38
+ const rules: Mutable<RoleRules> = {};
39
+ if (wire.primary) rules.primary = true;
40
+ if (wire.unique?.some((group) => group.length === 1)) rules.unique = true;
41
+ if (wire.index) rules.index = true;
42
+ if (wire.relation) {
43
+ const name = wire.relation.to;
44
+ rules.relation = {
45
+ to: () => (resolve?.(name) ?? ({ name } as unknown)) as EntityConstructor,
46
+ kind: wire.relation.kind,
47
+ ...(wire.relation.onDelete ? { onDelete: wire.relation.onDelete } : {}),
48
+ };
49
+ }
50
+
51
+ return rules;
52
+ },
53
+ };
@@ -1,6 +1,4 @@
1
- import type { ValidationError } from '../../lib/ValidationError.js';
2
1
  import { Shapes } from '../../axis/shape/Shape.js';
3
- import { dotted } from '../../lib/ValidationResult.js';
4
2
  import { SchemaError } from '../../SchemaError.js';
5
3
 
6
4
  /**
@@ -12,37 +10,6 @@ export function refuse(what: string, fix: string): never {
12
10
  throw new SchemaError(`This card cannot be read: ${what}.\n ${fix}`);
13
11
  }
14
12
 
15
- /**
16
- * Runs a card's axis through the validator a hand-written field goes through, so there is
17
- * one judge and not two.
18
- * FR : passe l'axe d'une carte par le juge d'un champ écrit à la main : un juge, pas deux.
19
- * `{ update: 'maybe' }` → `lifecycle is malformed — lifecycle.update: Expected 'now' or 'forbidden'`
20
- */
21
- export function admitWire(
22
- validator: (value: unknown, errors: ValidationError[]) => void,
23
- value: unknown,
24
- slot: string,
25
- ): void {
26
- const errors: ValidationError[] = [];
27
- validator(value, errors);
28
- if (errors.length) {
29
- refuse(
30
- `${slot} is malformed — ${errors.map((e) => `${dotted(e.path)}: ${e.message}`).join('; ')}`,
31
- 'A card states an axis the way a declaration does.',
32
- );
33
- }
34
- }
35
-
36
- /**
37
- * Checks a token against the list that declares it, never against a copy written here.
38
- * FR : vérifie un mot contre la liste qui le déclare, jamais contre une copie locale.
39
- * `oneOfTokens('many', RELATION_KINDS)` → `true`
40
- */
41
- export const oneOfTokens = <T extends readonly string[]>(
42
- value: unknown,
43
- tokens: T,
44
- ): value is T[number] => typeof value === 'string' && (tokens as readonly string[]).includes(value);
45
-
46
13
  /**
47
14
  * Compiles every `pattern` at the facade, so a bad one is refused here and not at the first row.
48
15
  * FR : compile chaque `pattern` à la porte : un mauvais est refusé ici, pas au premier row.
@@ -1,9 +1,10 @@
1
- import { EXTENSION_AXES } from '../axis/Axis.js';
1
+ import { Axes } from '../axis/Axes.js';
2
2
  import { Shapes } from '../axis/shape/Shape.js';
3
3
  import type { Field } from '../field/Field.js';
4
4
  import { isObject, shown } from '../lib/utils.js';
5
5
  import type { ValidationError } from '../lib/ValidationError.js';
6
6
  import type { ValidationResult } from '../lib/ValidationResult.js';
7
+ import { JsonSchemaValidator } from './JsonSchemaValidator.js';
7
8
 
8
9
  export class FieldDeclarationValidator {
9
10
  private constructor(private readonly declaration: unknown) {}
@@ -36,23 +37,10 @@ export class FieldDeclarationValidator {
36
37
  });
37
38
  }
38
39
 
39
- for (const axis of EXTENSION_AXES) {
40
- const declared = declaration[axis.slot];
41
- if (declared !== undefined) axis.validator(declared, errors);
42
- }
40
+ errors.push(...JsonSchemaValidator.of(Axes.fieldFormat).refusalsOf(declaration, []));
43
41
 
44
- if (declaration.meta !== undefined) {
45
- if (!isObject(declaration.meta)) {
46
- errors.push({
47
- path: ['meta'],
48
- message: `Expected an object — got ${shown(declaration.meta)}`,
49
- });
50
- } else if (
51
- declaration.meta.description !== undefined &&
52
- typeof declaration.meta.description !== 'string'
53
- ) {
54
- errors.push({ path: ['meta', 'description'], message: 'Expected a string' });
55
- }
42
+ for (const [slot, axis] of Axes.entries) {
43
+ errors.push(...(axis.refusals?.(declaration[slot]) ?? []));
56
44
  }
57
45
 
58
46
  return errors.length
@@ -1,16 +1,16 @@
1
- import { Validator, format as engineFormats } from '@cfworker/json-schema';
1
+ import { format as engineFormats } from '@cfworker/json-schema';
2
2
  import { Boundary } from '../axis/boundary/Boundary.js';
3
3
  import { type FormatPredicate } from '../axis/shape/FormatPredicate.js';
4
- import { Formats } from '../axis/shape/Formats.js';
4
+ import { StringFormats } from '../axis/shape/StringFormats.js';
5
5
  import { type Shape } from '../axis/shape/Shape.js';
6
6
  import { Shapes } from '../axis/shape/Shape.js';
7
7
  import type { Field } from '../field/Field.js';
8
8
  import type { Verdict } from '../lib/Verdict.js';
9
- import type { OutputUnit } from '@cfworker/json-schema';
10
9
  import { SchemaError } from '../SchemaError.js';
10
+ import type { JsonSchema } from '../lib/JsonSchema.js';
11
+ import { JsonSchemaValidator } from './JsonSchemaValidator.js';
11
12
 
12
13
  interface ShapePlan {
13
- validator: Validator;
14
14
  custom?: FormatPredicate;
15
15
  formatName?: string;
16
16
  }
@@ -39,8 +39,10 @@ export class FieldValueValidator {
39
39
  }
40
40
  }
41
41
  const plan = FieldValueValidator.planFor(shape);
42
- const result = plan.validator.validate(value);
43
- if (!result.valid) return refusalOf(result.errors);
42
+ const refusal = JsonSchemaValidator.of(shape as JsonSchema).refusalOf(value, []);
43
+ if (refusal) {
44
+ return refusal.path.length > 0 ? { message: refusal.message, path: refusal.path } : { message: refusal.message };
45
+ }
44
46
  if (plan.custom && typeof value === 'string' && !plan.custom(value)) {
45
47
  return { message: `String does not match format "${plan.formatName}".` };
46
48
  }
@@ -65,7 +67,6 @@ export class FieldValueValidator {
65
67
  const base = Shapes.of(shape).base;
66
68
  const formatName = base?.type === 'string' ? base.format : undefined;
67
69
  plan = {
68
- validator: new Validator(shape as object, '2020-12', true),
69
70
  custom: formatName === undefined ? undefined : this.customFormatOf(formatName),
70
71
  formatName,
71
72
  };
@@ -75,39 +76,13 @@ export class FieldValueValidator {
75
76
  }
76
77
 
77
78
  private static customFormatOf(name: string): FormatPredicate | undefined {
78
- const custom = Formats.find(name);
79
+ const custom = StringFormats.find(name);
79
80
  if (!custom && !(name in engineFormats)) {
80
81
  throw new SchemaError(
81
- `Unknown format: '${name}'. Register it with Formats.register('${name}', …) — ` +
82
+ `Unknown format: '${name}'. Register it with StringFormats.register('${name}', …) — ` +
82
83
  `the engine validates ${Object.keys(engineFormats).length} formats natively and this is not one of them.`,
83
84
  );
84
85
  }
85
86
  return custom;
86
87
  }
87
88
  }
88
-
89
- /**
90
- * The engine states its refusals outermost first, so `errors[0]` on a nested shape is the
91
- * parent's `Property "street" does not match schema.` — true, and never the reason. The
92
- * DEEPEST one is the reason, and it is the one that says where.
93
- */
94
- function refusalOf(errors: readonly OutputUnit[]): Verdict {
95
- const deepest = errors.reduce<OutputUnit | undefined>(
96
- (held, one) => (held && depthOf(held) >= depthOf(one) ? held : one),
97
- undefined,
98
- );
99
- if (!deepest) return { message: 'Invalid value' };
100
- const path = locationOf(deepest.instanceLocation);
101
-
102
- return path.length > 0 ? { message: deepest.error, path } : { message: deepest.error };
103
- }
104
-
105
- const depthOf = (unit: OutputUnit): number => locationOf(unit.instanceLocation).length;
106
-
107
- /** `#/city/zip` — a JSON Pointer fragment, and `~1`/`~0` are how it spells `/` and `~`. */
108
- const locationOf = (instanceLocation: string): string[] =>
109
- instanceLocation
110
- .replace(/^#/, '')
111
- .split('/')
112
- .filter(Boolean)
113
- .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
@@ -0,0 +1,142 @@
1
+ import { Validator, type OutputUnit } from '@cfworker/json-schema';
2
+ import type { JsonSchema } from '../lib/JsonSchema.js';
3
+ import { isObject } from '../lib/utils.js';
4
+ import type { ValidationError } from '../lib/ValidationError.js';
5
+ import { locationOf } from '../lib/ValidationResult.js';
6
+
7
+ /**
8
+ * The one door onto the engine: a field's value against its shape, a declaration against the
9
+ * format of its key. The format is DATA, not a TypeScript interface: an interface is erased
10
+ * before a JS caller, a config or a card from another language could be measured against it.
11
+ */
12
+ export class JsonSchemaValidator {
13
+ private static readonly held = new WeakMap<JsonSchema, JsonSchemaValidator>();
14
+
15
+ private first?: Validator;
16
+ private every?: Validator;
17
+
18
+ private constructor(private readonly schema: JsonSchema) {}
19
+
20
+ static of(schema: JsonSchema): JsonSchemaValidator {
21
+ let validator = this.held.get(schema);
22
+
23
+ if (!validator) {
24
+ validator = new JsonSchemaValidator(schema);
25
+ this.held.set(schema, validator);
26
+ }
27
+
28
+ return validator;
29
+ }
30
+
31
+ /** `refusalOf({ update: 'maybe' }, ['lifecycle'])` → `lifecycle.update: Instance does not match any of ["now","forbidden"].` */
32
+ refusalOf(value: unknown, path: readonly string[]): ValidationError | undefined {
33
+ this.first ??= new Validator(this.schema, '2020-12', true);
34
+
35
+ const refused = refusedBy(this.first, value);
36
+
37
+ if (!Array.isArray(refused)) return under(path, refused);
38
+ if (!refused.length) return undefined;
39
+
40
+ const reasons = named(refused);
41
+
42
+ return at(path, reasons.length ? deepestOf(reasons) : refused[0]);
43
+ }
44
+
45
+ /**
46
+ * One refusal per key, so a declaration hears every fault at once. It reads through an engine
47
+ * of its own: the other stops at the first, which is what a single value wants.
48
+ */
49
+ refusalsOf(value: unknown, path: readonly string[]): ValidationError[] {
50
+ this.every ??= new Validator(this.schema, '2020-12', false);
51
+
52
+ const refused = refusedBy(this.every, value);
53
+
54
+ if (!Array.isArray(refused)) return [under(path, refused)];
55
+ if (!refused.length) return [];
56
+
57
+ const faults = [...byKey(named(refused)).values()];
58
+
59
+ return faults.length ? faults.map((units) => at(path, deepestOf(units))) : [at(path, refused[0])];
60
+ }
61
+ }
62
+
63
+ /** What it refused on, or the member JSON cannot hold — the engine throws on one rather than refusing. */
64
+ function refusedBy(engine: Validator, value: unknown): OutputUnit[] | ValidationError {
65
+ const json = stated(value);
66
+
67
+ try {
68
+ const verdict = engine.validate(json);
69
+
70
+ return verdict.valid ? [] : verdict.errors;
71
+ } catch {
72
+ const where = unreadableIn(json) ?? { path: [], value: json };
73
+
74
+ return { path: where.path, message: `Expected a JSON value — got ${typeof where.value}` };
75
+ }
76
+ }
77
+
78
+ /** `False boolean schema` names nothing: the unit above it carries the offending key. */
79
+ const named = (units: readonly OutputUnit[]): OutputUnit[] =>
80
+ units.filter((unit) => unit.keyword !== 'false');
81
+
82
+ /** The engine states its refusals outermost first: the deepest says where, the last one is why. */
83
+ function deepestOf(units: readonly OutputUnit[]): OutputUnit {
84
+ return units.reduce((held, unit) => (depthOf(held) > depthOf(unit) ? held : unit));
85
+ }
86
+
87
+ /**
88
+ * The units of one fault, addressed by the key they happened under. A unit on the value ITSELF
89
+ * points at a key when another one is deeper — `Property "lifecycle" does not match schema.` —
90
+ * so it is kept only when it is all there is.
91
+ */
92
+ function byKey(units: readonly OutputUnit[]): Map<string, OutputUnit[]> {
93
+ const held = new Map<string, OutputUnit[]>();
94
+
95
+ for (const unit of units) {
96
+ const key = locationOf(unit.instanceLocation)[0] ?? '';
97
+ held.set(key, [...(held.get(key) ?? []), unit]);
98
+ }
99
+
100
+ if (held.size > 1) held.delete('');
101
+
102
+ return held;
103
+ }
104
+
105
+ const at = (path: readonly string[], unit: OutputUnit): ValidationError => ({
106
+ path: [...path, ...locationOf(unit.instanceLocation)],
107
+ message: unit.error,
108
+ });
109
+
110
+ const under = (path: readonly string[], refusal: ValidationError): ValidationError => ({
111
+ path: [...path, ...refusal.path],
112
+ message: refusal.message,
113
+ });
114
+
115
+ const depthOf = (unit: OutputUnit): number => locationOf(unit.instanceLocation).length;
116
+
117
+ /** A key set to `undefined` states nothing, as in JSON — and the engine refuses to read one. */
118
+ function stated(value: unknown): unknown {
119
+ if (Array.isArray(value)) return value.map(stated);
120
+
121
+ if (!isObject(value)) return value;
122
+
123
+ const present = Object.entries(value).filter(([, member]) => member !== undefined);
124
+
125
+ return Object.fromEntries(present.map(([key, member]) => [key, stated(member)]));
126
+ }
127
+
128
+ /** Where the engine stopped: a function, a symbol or a bigint is not a value JSON holds. */
129
+ function unreadableIn(
130
+ value: unknown,
131
+ path: readonly string[] = [],
132
+ ): { path: readonly string[]; value: unknown } | undefined {
133
+ if (['function', 'symbol', 'bigint'].includes(typeof value)) return { path, value };
134
+
135
+ if (Array.isArray(value)) {
136
+ return value.flatMap((member, index) => unreadableIn(member, [...path, String(index)]) ?? [])[0];
137
+ }
138
+
139
+ if (!isObject(value)) return undefined;
140
+
141
+ return Object.entries(value).flatMap(([key, member]) => unreadableIn(member, [...path, key]) ?? [])[0];
142
+ }
@@ -1,9 +1,9 @@
1
1
  import { Field } from '../field/Field.js';
2
- import { EXTENSION_SLOTS } from '../axis/Axis.js';
2
+ import { Axes } from '../axis/Axes.js';
3
3
  import { dequal } from 'dequal';
4
4
  import { SchemaError } from '../SchemaError.js';
5
5
 
6
- const MEMBER_SLOTS = [...EXTENSION_SLOTS, 'meta'] as const;
6
+ const memberSlots = (): string[] => [...Axes.names, 'meta'];
7
7
 
8
8
  /**
9
9
  * Builds a `rule/` word, which states members on the field it receives.
@@ -26,12 +26,14 @@ export type FieldWord = (field: Field<any>) => Field<any>;
26
26
  */
27
27
  function merge(name: string, field: Field, given: Partial<Field>): Partial<Field> {
28
28
  const merged: Record<string, unknown> = {};
29
+ const stated = given as Record<string, unknown>;
30
+ const held = field as unknown as Record<string, unknown>;
29
31
  if ('shape' in given) merged.shape = given.shape;
30
32
 
31
- for (const slot of MEMBER_SLOTS) {
32
- const members = given[slot];
33
+ for (const slot of memberSlots()) {
34
+ const members = stated[slot];
33
35
  if (members === undefined) continue;
34
- const already = field[slot] as Record<string, unknown> | undefined;
36
+ const already = held[slot] as Record<string, unknown> | undefined;
35
37
  if (typeof members !== 'object' || members === null || typeof already !== 'object') {
36
38
  merged[slot] = members;
37
39
  continue;
@@ -1,4 +0,0 @@
1
- import type { Axis } from '../Axis.js';
2
- import type { BoundaryRef } from './BoundaryRef.js';
3
- export declare const boundaryAxis: Axis<BoundaryRef, BoundaryRef>;
4
- //# sourceMappingURL=BoundaryAxis.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BoundaryAxis.d.ts","sourceRoot":"","sources":["../../../src/axis/boundary/BoundaryAxis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,eAAO,MAAM,YAAY,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,CA0BvD,CAAC"}
@@ -1,30 +0,0 @@
1
- import { admitWire } from '../../projection/card/admission.js';
2
- import { isObject, shown } from '../../lib/utils.js';
3
- export const boundaryAxis = {
4
- slot: 'boundary',
5
- validator(value, errors) {
6
- if (typeof value === 'string')
7
- return;
8
- if (!isObject(value)) {
9
- errors.push({
10
- path: ['boundary'],
11
- message: `Expected an alias name or { in, out } — got ${shown(value)}`,
12
- });
13
- return;
14
- }
15
- for (const [side, verb] of [['in', 'decode'], ['out', 'encode']]) {
16
- const rule = value[side];
17
- if (rule === undefined || rule === 'closed')
18
- continue;
19
- if (!isObject(rule) || typeof rule[verb] !== 'string') {
20
- errors.push({ path: ['boundary', side], message: `Expected 'closed' or { ${verb}: <name> }` });
21
- }
22
- }
23
- },
24
- describe: (value) => value,
25
- reconstruct: (wire) => {
26
- admitWire(boundaryAxis.validator, wire, 'boundary');
27
- return wire;
28
- },
29
- };
30
- //# sourceMappingURL=BoundaryAxis.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BoundaryAxis.js","sourceRoot":"","sources":["../../../src/axis/boundary/BoundaryAxis.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,CAAC,MAAM,YAAY,GAAmC;IAC1D,IAAI,EAAE,UAAU;IAEhB,SAAS,CAAC,KAAK,EAAE,MAAM;QACrB,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO;QACtC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,CAAC,UAAU,CAAC;gBAClB,OAAO,EAAE,+CAA+C,KAAK,CAAC,KAAK,CAAC,EAAE;aACvE,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAU,EAAE,CAAC;YAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,QAAQ;gBAAE,SAAS;YACtD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACtD,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,0BAA0B,IAAI,YAAY,EAAE,CAAC,CAAC;YACjG,CAAC;QACH,CAAC;IACH,CAAC;IAED,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IAC1B,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE;QACpB,SAAS,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -1,3 +0,0 @@
1
- import type { BoundaryRules } from './BoundaryRules.js';
2
- export type BoundaryRef = 'isoDate' | (string & {}) | BoundaryRules;
3
- //# sourceMappingURL=BoundaryRef.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BoundaryRef.d.ts","sourceRoot":"","sources":["../../../src/axis/boundary/BoundaryRef.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,aAAa,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=BoundaryRef.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BoundaryRef.js","sourceRoot":"","sources":["../../../src/axis/boundary/BoundaryRef.ts"],"names":[],"mappings":""}
@@ -1,9 +0,0 @@
1
- export interface BoundaryRules {
2
- in?: 'closed' | {
3
- decode: string;
4
- };
5
- out?: 'closed' | {
6
- encode: string;
7
- };
8
- }
9
- //# sourceMappingURL=BoundaryRules.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BoundaryRules.d.ts","sourceRoot":"","sources":["../../../src/axis/boundary/BoundaryRules.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,QAAQ,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,GAAG,CAAC,EAAE,QAAQ,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=BoundaryRules.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BoundaryRules.js","sourceRoot":"","sources":["../../../src/axis/boundary/BoundaryRules.ts"],"names":[],"mappings":""}
@@ -1,10 +0,0 @@
1
- export declare class Clock {
2
- private static reading;
3
- static now(): number;
4
- /**
5
- *
6
- * @returns function to restore the clock
7
- */
8
- static freeze(at: number | Date): () => void;
9
- }
10
- //# sourceMappingURL=Clock.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Clock.d.ts","sourceRoot":"","sources":["../../../src/axis/lifecycle/Clock.ts"],"names":[],"mappings":"AAAA,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAC,OAAO,CAA0B;IAEhD,MAAM,CAAC,GAAG,IAAI,MAAM,CAEnB;IAED;;;OAGG;IACH,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,IAAI,CAS3C;CACF"}