@fougere/schema 0.3.0-alpha.0 → 0.4.0-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 (334) hide show
  1. package/README.md +1 -1
  2. package/dist/EntityAdapters.d.ts +26 -0
  3. package/dist/EntityAdapters.d.ts.map +1 -0
  4. package/dist/EntityAdapters.js +71 -0
  5. package/dist/EntityAdapters.js.map +1 -0
  6. package/dist/EntityDeclarations.d.ts +7 -14
  7. package/dist/EntityDeclarations.d.ts.map +1 -1
  8. package/dist/EntityDeclarations.js +1 -18
  9. package/dist/EntityDeclarations.js.map +1 -1
  10. package/dist/axis/Axis.d.ts +2 -0
  11. package/dist/axis/Axis.d.ts.map +1 -1
  12. package/dist/axis/Axis.js +5 -3
  13. package/dist/axis/Axis.js.map +1 -1
  14. package/dist/axis/boundary/Boundary.d.ts +1 -1
  15. package/dist/axis/boundary/Boundary.d.ts.map +1 -1
  16. package/dist/axis/boundary/{axis.d.ts → BoundaryAxis.d.ts} +1 -1
  17. package/dist/axis/boundary/BoundaryAxis.d.ts.map +1 -0
  18. package/dist/axis/boundary/{axis.js → BoundaryAxis.js} +3 -2
  19. package/dist/axis/boundary/BoundaryAxis.js.map +1 -0
  20. package/dist/axis/lifecycle/Clock.d.ts +18 -0
  21. package/dist/axis/lifecycle/Clock.d.ts.map +1 -0
  22. package/dist/axis/lifecycle/Clock.js +25 -0
  23. package/dist/axis/lifecycle/Clock.js.map +1 -0
  24. package/dist/axis/lifecycle/Generators.d.ts +16 -2
  25. package/dist/axis/lifecycle/Generators.d.ts.map +1 -1
  26. package/dist/axis/lifecycle/Generators.js +35 -5
  27. package/dist/axis/lifecycle/Generators.js.map +1 -1
  28. package/dist/axis/lifecycle/{axis.d.ts → LifecycleAxis.d.ts} +1 -1
  29. package/dist/axis/lifecycle/LifecycleAxis.d.ts.map +1 -0
  30. package/dist/axis/lifecycle/{axis.js → LifecycleAxis.js} +3 -2
  31. package/dist/axis/lifecycle/LifecycleAxis.js.map +1 -0
  32. package/dist/axis/lifecycle/apply.d.ts +1 -3
  33. package/dist/axis/lifecycle/apply.d.ts.map +1 -1
  34. package/dist/axis/lifecycle/apply.js +5 -43
  35. package/dist/axis/lifecycle/apply.js.map +1 -1
  36. package/dist/axis/role/Relation.d.ts +11 -1
  37. package/dist/axis/role/Relation.d.ts.map +1 -1
  38. package/dist/axis/role/Relation.js +18 -3
  39. package/dist/axis/role/Relation.js.map +1 -1
  40. package/dist/axis/role/Role.d.ts +5 -17
  41. package/dist/axis/role/Role.d.ts.map +1 -1
  42. package/dist/axis/role/Role.js +9 -19
  43. package/dist/axis/role/Role.js.map +1 -1
  44. package/dist/axis/role/{axis.d.ts → RoleAxis.d.ts} +1 -1
  45. package/dist/axis/role/RoleAxis.d.ts.map +1 -0
  46. package/dist/axis/role/{axis.js → RoleAxis.js} +5 -4
  47. package/dist/axis/role/RoleAxis.js.map +1 -0
  48. package/dist/card/Bundle.d.ts +15 -0
  49. package/dist/card/Bundle.d.ts.map +1 -0
  50. package/dist/card/Bundle.js +67 -0
  51. package/dist/card/Bundle.js.map +1 -0
  52. package/dist/card/Card.d.ts +27 -0
  53. package/dist/card/Card.d.ts.map +1 -0
  54. package/dist/card/Card.js +230 -0
  55. package/dist/card/Card.js.map +1 -0
  56. package/dist/card/EntityTypeSource.d.ts +12 -0
  57. package/dist/card/EntityTypeSource.d.ts.map +1 -0
  58. package/dist/card/{typescript.js → EntityTypeSource.js} +20 -35
  59. package/dist/card/EntityTypeSource.js.map +1 -0
  60. package/dist/card/FacadeTypeSource.d.ts +19 -0
  61. package/dist/card/FacadeTypeSource.d.ts.map +1 -0
  62. package/dist/card/FacadeTypeSource.js +40 -0
  63. package/dist/card/FacadeTypeSource.js.map +1 -0
  64. package/dist/{judge/form.d.ts → card/admission.d.ts} +2 -4
  65. package/dist/card/admission.d.ts.map +1 -0
  66. package/dist/{judge/form.js → card/admission.js} +1 -3
  67. package/dist/card/admission.js.map +1 -0
  68. package/dist/card/diff.d.ts +11 -55
  69. package/dist/card/diff.d.ts.map +1 -1
  70. package/dist/card/diff.js +1 -122
  71. package/dist/card/diff.js.map +1 -1
  72. package/dist/constraint/FieldGroup.d.ts +2 -2
  73. package/dist/constraint/FieldGroup.d.ts.map +1 -1
  74. package/dist/constraint/FieldGroup.js.map +1 -1
  75. package/dist/entity.d.ts +3 -3
  76. package/dist/entity.d.ts.map +1 -1
  77. package/dist/entity.js +8 -13
  78. package/dist/entity.js.map +1 -1
  79. package/dist/fields/Field.d.ts +24 -0
  80. package/dist/fields/Field.d.ts.map +1 -0
  81. package/dist/{Field.js → fields/Field.js} +17 -15
  82. package/dist/fields/Field.js.map +1 -0
  83. package/dist/fields/FieldSet.d.ts +28 -0
  84. package/dist/fields/FieldSet.d.ts.map +1 -0
  85. package/dist/fields/FieldSet.js +55 -0
  86. package/dist/fields/FieldSet.js.map +1 -0
  87. package/dist/index.d.ts +23 -18
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +21 -17
  90. package/dist/index.js.map +1 -1
  91. package/dist/judge/{cases.d.ts → Cases.d.ts} +22 -22
  92. package/dist/judge/Cases.d.ts.map +1 -0
  93. package/dist/judge/{cases.js → Cases.js} +35 -27
  94. package/dist/judge/Cases.js.map +1 -0
  95. package/dist/judge/FieldJudge.d.ts +9 -0
  96. package/dist/judge/FieldJudge.d.ts.map +1 -0
  97. package/dist/judge/FieldJudge.js +54 -0
  98. package/dist/judge/FieldJudge.js.map +1 -0
  99. package/dist/judge/RowJudge.d.ts +12 -0
  100. package/dist/judge/RowJudge.d.ts.map +1 -0
  101. package/dist/judge/RowJudge.js +81 -0
  102. package/dist/judge/RowJudge.js.map +1 -0
  103. package/dist/judge/RowRefusal.d.ts +17 -0
  104. package/dist/judge/RowRefusal.d.ts.map +1 -0
  105. package/dist/judge/RowRefusal.js +16 -0
  106. package/dist/judge/RowRefusal.js.map +1 -0
  107. package/dist/judge/ValueForm.d.ts +3 -0
  108. package/dist/judge/ValueForm.d.ts.map +1 -0
  109. package/dist/judge/ValueForm.js +3 -0
  110. package/dist/judge/ValueForm.js.map +1 -0
  111. package/dist/judge/ValueJudge.d.ts +12 -0
  112. package/dist/judge/ValueJudge.d.ts.map +1 -0
  113. package/dist/judge/ValueJudge.js +60 -0
  114. package/dist/judge/ValueJudge.js.map +1 -0
  115. package/dist/projection/SchemaOrCard.d.ts +7 -0
  116. package/dist/projection/SchemaOrCard.d.ts.map +1 -0
  117. package/dist/projection/{source.js → SchemaOrCard.js} +3 -3
  118. package/dist/projection/SchemaOrCard.js.map +1 -0
  119. package/dist/projection/Visibility.d.ts +10 -0
  120. package/dist/projection/Visibility.d.ts.map +1 -0
  121. package/dist/projection/Visibility.js +55 -0
  122. package/dist/projection/Visibility.js.map +1 -0
  123. package/dist/projection/standard.d.ts +2 -2
  124. package/dist/projection/standard.d.ts.map +1 -1
  125. package/dist/{Schema.d.ts → schema/Schema.d.ts} +30 -34
  126. package/dist/schema/Schema.d.ts.map +1 -0
  127. package/dist/{Schema.js → schema/Schema.js} +25 -48
  128. package/dist/schema/Schema.js.map +1 -0
  129. package/dist/schema/SchemaDefinition.d.ts +46 -0
  130. package/dist/schema/SchemaDefinition.d.ts.map +1 -0
  131. package/dist/schema/SchemaDefinition.js +64 -0
  132. package/dist/schema/SchemaDefinition.js.map +1 -0
  133. package/dist/schema/SchemaDerivation.d.ts +28 -0
  134. package/dist/schema/SchemaDerivation.d.ts.map +1 -0
  135. package/dist/schema/SchemaDerivation.js +37 -0
  136. package/dist/schema/SchemaDerivation.js.map +1 -0
  137. package/dist/schema/SchemaView.d.ts +23 -0
  138. package/dist/schema/SchemaView.d.ts.map +1 -0
  139. package/dist/{SchemaView.js.map → schema/SchemaView.js.map} +1 -1
  140. package/dist/utils.d.ts +4 -0
  141. package/dist/utils.d.ts.map +1 -0
  142. package/dist/utils.js +13 -0
  143. package/dist/utils.js.map +1 -0
  144. package/dist/vocabulary/bool.d.ts +1 -1
  145. package/dist/vocabulary/bool.d.ts.map +1 -1
  146. package/dist/vocabulary/bool.js +1 -1
  147. package/dist/vocabulary/bool.js.map +1 -1
  148. package/dist/vocabulary/created.d.ts +1 -1
  149. package/dist/vocabulary/created.d.ts.map +1 -1
  150. package/dist/vocabulary/date.d.ts +1 -1
  151. package/dist/vocabulary/date.d.ts.map +1 -1
  152. package/dist/vocabulary/date.js +1 -1
  153. package/dist/vocabulary/date.js.map +1 -1
  154. package/dist/vocabulary/email.d.ts +1 -1
  155. package/dist/vocabulary/email.d.ts.map +1 -1
  156. package/dist/vocabulary/immutable.d.ts +1 -1
  157. package/dist/vocabulary/immutable.d.ts.map +1 -1
  158. package/dist/vocabulary/indexed.d.ts +1 -1
  159. package/dist/vocabulary/indexed.d.ts.map +1 -1
  160. package/dist/vocabulary/json.d.ts +2 -2
  161. package/dist/vocabulary/json.d.ts.map +1 -1
  162. package/dist/vocabulary/json.js +6 -4
  163. package/dist/vocabulary/json.js.map +1 -1
  164. package/dist/vocabulary/list.d.ts +1 -1
  165. package/dist/vocabulary/list.d.ts.map +1 -1
  166. package/dist/vocabulary/list.js +1 -1
  167. package/dist/vocabulary/list.js.map +1 -1
  168. package/dist/vocabulary/many.d.ts +1 -1
  169. package/dist/vocabulary/many.d.ts.map +1 -1
  170. package/dist/vocabulary/many.js +3 -3
  171. package/dist/vocabulary/many.js.map +1 -1
  172. package/dist/vocabulary/nullable.d.ts +1 -1
  173. package/dist/vocabulary/nullable.d.ts.map +1 -1
  174. package/dist/vocabulary/number.d.ts +1 -1
  175. package/dist/vocabulary/number.d.ts.map +1 -1
  176. package/dist/vocabulary/number.js +1 -1
  177. package/dist/vocabulary/number.js.map +1 -1
  178. package/dist/vocabulary/oneOf.d.ts +1 -1
  179. package/dist/vocabulary/oneOf.d.ts.map +1 -1
  180. package/dist/vocabulary/oneOf.js +1 -1
  181. package/dist/vocabulary/oneOf.js.map +1 -1
  182. package/dist/vocabulary/optional.d.ts +1 -1
  183. package/dist/vocabulary/optional.d.ts.map +1 -1
  184. package/dist/vocabulary/primary.d.ts +1 -1
  185. package/dist/vocabulary/primary.d.ts.map +1 -1
  186. package/dist/vocabulary/primary.js +4 -5
  187. package/dist/vocabulary/primary.js.map +1 -1
  188. package/dist/vocabulary/readOnly.d.ts +1 -1
  189. package/dist/vocabulary/readOnly.d.ts.map +1 -1
  190. package/dist/vocabulary/ref.d.ts +1 -1
  191. package/dist/vocabulary/ref.d.ts.map +1 -1
  192. package/dist/vocabulary/ref.js +3 -9
  193. package/dist/vocabulary/ref.js.map +1 -1
  194. package/dist/vocabulary/text.d.ts +1 -1
  195. package/dist/vocabulary/text.d.ts.map +1 -1
  196. package/dist/vocabulary/text.js +1 -1
  197. package/dist/vocabulary/text.js.map +1 -1
  198. package/dist/vocabulary/unique.d.ts +1 -1
  199. package/dist/vocabulary/unique.d.ts.map +1 -1
  200. package/dist/vocabulary/updated.d.ts +1 -1
  201. package/dist/vocabulary/updated.d.ts.map +1 -1
  202. package/dist/vocabulary/url.d.ts +1 -1
  203. package/dist/vocabulary/url.d.ts.map +1 -1
  204. package/dist/vocabulary/vocabulary.d.ts +2 -3
  205. package/dist/vocabulary/vocabulary.d.ts.map +1 -1
  206. package/dist/vocabulary/vocabulary.js +4 -4
  207. package/dist/vocabulary/vocabulary.js.map +1 -1
  208. package/dist/vocabulary/writeOnly.d.ts +1 -1
  209. package/dist/vocabulary/writeOnly.d.ts.map +1 -1
  210. package/package.json +8 -4
  211. package/src/EntityAdapters.ts +95 -0
  212. package/src/EntityDeclarations.ts +19 -0
  213. package/src/axis/Axis.ts +25 -0
  214. package/src/axis/Meta.ts +3 -0
  215. package/src/axis/boundary/Boundaries.ts +44 -0
  216. package/src/axis/boundary/Boundary.ts +69 -0
  217. package/src/axis/boundary/BoundaryAxis.ts +32 -0
  218. package/src/axis/lifecycle/Clock.ts +26 -0
  219. package/src/axis/lifecycle/Generators.ts +46 -0
  220. package/src/axis/lifecycle/Lifecycle.ts +53 -0
  221. package/src/axis/lifecycle/LifecycleAxis.ts +49 -0
  222. package/src/axis/lifecycle/apply.ts +38 -0
  223. package/src/axis/role/Relation.ts +31 -0
  224. package/src/axis/role/Role.ts +56 -0
  225. package/src/axis/role/RoleAxis.ts +115 -0
  226. package/src/axis/shape/Formats.ts +22 -0
  227. package/src/axis/shape/Shape.ts +91 -0
  228. package/src/card/Bundle.ts +80 -0
  229. package/src/card/Card.ts +256 -0
  230. package/src/card/Descriptor.ts +68 -0
  231. package/src/card/EntityTypeSource.ts +90 -0
  232. package/src/card/FacadeTypeSource.ts +55 -0
  233. package/src/card/admission.ts +25 -0
  234. package/src/card/diff.ts +56 -0
  235. package/src/constraint/FieldGroup.ts +91 -0
  236. package/src/constraint/Unique.ts +3 -0
  237. package/src/entity.ts +15 -0
  238. package/src/fields/Field.ts +80 -0
  239. package/src/fields/FieldSet.ts +62 -0
  240. package/src/index.ts +64 -0
  241. package/src/judge/Cases.ts +139 -0
  242. package/src/judge/FieldJudge.ts +61 -0
  243. package/src/judge/RowJudge.ts +84 -0
  244. package/src/judge/RowRefusal.ts +17 -0
  245. package/src/judge/ValueForm.ts +7 -0
  246. package/src/judge/ValueJudge.ts +72 -0
  247. package/src/judge/options.ts +3 -0
  248. package/src/judge/result.ts +10 -0
  249. package/src/projection/SchemaOrCard.ts +18 -0
  250. package/src/projection/Visibility.ts +47 -0
  251. package/src/projection/standard.ts +47 -0
  252. package/src/schema/Schema.ts +167 -0
  253. package/src/schema/SchemaDefinition.ts +87 -0
  254. package/src/schema/SchemaDerivation.ts +46 -0
  255. package/src/schema/SchemaView.ts +25 -0
  256. package/src/utils.ts +13 -0
  257. package/src/vocabulary/bool.ts +9 -0
  258. package/src/vocabulary/created.ts +8 -0
  259. package/src/vocabulary/date.ts +5 -0
  260. package/src/vocabulary/email.ts +6 -0
  261. package/src/vocabulary/immutable.ts +6 -0
  262. package/src/vocabulary/indexed.ts +4 -0
  263. package/src/vocabulary/json.ts +22 -0
  264. package/src/vocabulary/list.ts +13 -0
  265. package/src/vocabulary/many.ts +9 -0
  266. package/src/vocabulary/nullable.ts +7 -0
  267. package/src/vocabulary/number.ts +17 -0
  268. package/src/vocabulary/oneOf.ts +24 -0
  269. package/src/vocabulary/optional.ts +8 -0
  270. package/src/vocabulary/primary.ts +33 -0
  271. package/src/vocabulary/readOnly.ts +7 -0
  272. package/src/vocabulary/ref.ts +13 -0
  273. package/src/vocabulary/text.ts +19 -0
  274. package/src/vocabulary/unique.ts +10 -0
  275. package/src/vocabulary/updated.ts +7 -0
  276. package/src/vocabulary/url.ts +6 -0
  277. package/src/vocabulary/vocabulary.ts +40 -0
  278. package/src/vocabulary/writeOnly.ts +7 -0
  279. package/dist/Field.d.ts +0 -21
  280. package/dist/Field.d.ts.map +0 -1
  281. package/dist/Field.js.map +0 -1
  282. package/dist/Schema.d.ts.map +0 -1
  283. package/dist/Schema.js.map +0 -1
  284. package/dist/SchemaView.d.ts +0 -18
  285. package/dist/SchemaView.d.ts.map +0 -1
  286. package/dist/axis/boundary/axis.d.ts.map +0 -1
  287. package/dist/axis/boundary/axis.js.map +0 -1
  288. package/dist/axis/lifecycle/axis.d.ts.map +0 -1
  289. package/dist/axis/lifecycle/axis.js.map +0 -1
  290. package/dist/axis/role/axis.d.ts.map +0 -1
  291. package/dist/axis/role/axis.js.map +0 -1
  292. package/dist/card/describe.d.ts +0 -12
  293. package/dist/card/describe.d.ts.map +0 -1
  294. package/dist/card/describe.js +0 -83
  295. package/dist/card/describe.js.map +0 -1
  296. package/dist/card/reconstruct.d.ts +0 -11
  297. package/dist/card/reconstruct.d.ts.map +0 -1
  298. package/dist/card/reconstruct.js +0 -83
  299. package/dist/card/reconstruct.js.map +0 -1
  300. package/dist/card/typescript.d.ts +0 -17
  301. package/dist/card/typescript.d.ts.map +0 -1
  302. package/dist/card/typescript.js.map +0 -1
  303. package/dist/clean.d.ts +0 -2
  304. package/dist/clean.d.ts.map +0 -1
  305. package/dist/clean.js +0 -7
  306. package/dist/clean.js.map +0 -1
  307. package/dist/judge/Judge.d.ts +0 -14
  308. package/dist/judge/Judge.d.ts.map +0 -1
  309. package/dist/judge/Judge.js +0 -161
  310. package/dist/judge/Judge.js.map +0 -1
  311. package/dist/judge/cases.d.ts.map +0 -1
  312. package/dist/judge/cases.js.map +0 -1
  313. package/dist/judge/form.d.ts.map +0 -1
  314. package/dist/judge/form.js.map +0 -1
  315. package/dist/name.d.ts +0 -3
  316. package/dist/name.d.ts.map +0 -1
  317. package/dist/name.js +0 -7
  318. package/dist/name.js.map +0 -1
  319. package/dist/projection/encode.d.ts +0 -3
  320. package/dist/projection/encode.d.ts.map +0 -1
  321. package/dist/projection/encode.js +0 -19
  322. package/dist/projection/encode.js.map +0 -1
  323. package/dist/projection/io.d.ts +0 -4
  324. package/dist/projection/io.d.ts.map +0 -1
  325. package/dist/projection/io.js +0 -29
  326. package/dist/projection/io.js.map +0 -1
  327. package/dist/projection/source.d.ts +0 -7
  328. package/dist/projection/source.d.ts.map +0 -1
  329. package/dist/projection/source.js.map +0 -1
  330. package/dist/same.d.ts +0 -6
  331. package/dist/same.d.ts.map +0 -1
  332. package/dist/same.js +0 -35
  333. package/dist/same.js.map +0 -1
  334. /package/dist/{SchemaView.js → schema/SchemaView.js} +0 -0
@@ -0,0 +1,139 @@
1
+ import { Anatomy } from '../axis/shape/Shape.js';
2
+ import { Boundary } from '../axis/boundary/Boundary.js';
3
+ import { Lifecycle } from '../axis/lifecycle/Lifecycle.js';
4
+ import { Role } from '../axis/role/Role.js';
5
+ import type { Field } from '../fields/Field.js';
6
+ import type { SchemaView } from '../schema/SchemaView.js';
7
+ import { RowJudge } from './RowJudge.js';
8
+ import { RowRefusal } from './RowRefusal.js';
9
+
10
+ /**
11
+ * One input, and what the judge must answer.
12
+ *
13
+ * `expect` carries the PATH of the field at fault and never the message. We know which
14
+ * field must be refused because we are the ones who broke it; naming the message would
15
+ * make this list a second judge, and the two would then have to be kept in step by hand —
16
+ * which is the duplication the whole thing exists to remove.
17
+ */
18
+ export interface Case {
19
+ /** What this input does, in one clause — carried into the assertion so a failure reads. */
20
+ why: string;
21
+ body: unknown;
22
+ /**
23
+ * Judged as an update rather than a creation. The two modes refuse different things:
24
+ * an absence is `Required` on create and legal on patch, an immutable field is legal
25
+ * on create and refused on patch.
26
+ */
27
+ patch: boolean;
28
+ expect: 'accept' | { reject: string };
29
+ }
30
+
31
+ /** A value of the wrong type for the declared shape — the one violation every field has. */
32
+ function wrongTypeFor(field: Field): unknown {
33
+ const declared = (field.shape as { type?: string | readonly string[] }).type;
34
+ const names = Array.isArray(declared) ? declared : [declared];
35
+ return names.includes('string') ? 42 : 'not-a-value-of-this-shape';
36
+ }
37
+
38
+ /** Values that break a stated bound. Empty when the field states none. */
39
+ function outOfBoundsFor(field: Field): { why: string; value: unknown }[] {
40
+ // Through `Anatomy`, not a cast: the cast admitted any keyword, and it declared
41
+ // `maxItems` — which no branch below ever produced. In the one function whose job is
42
+ // to be exhaustive, a bound the type does not carry is a case nobody notices missing.
43
+ const shape = Anatomy.of(field.shape).base;
44
+ const out: { why: string; value: unknown }[] = [];
45
+ if (shape?.type === 'string') {
46
+ if (typeof shape.minLength === 'number' && shape.minLength > 0)
47
+ out.push({ why: 'shorter than min', value: 'x'.repeat(shape.minLength - 1) });
48
+ if (typeof shape.maxLength === 'number')
49
+ out.push({ why: 'longer than max', value: 'x'.repeat(shape.maxLength + 1) });
50
+ if (shape.enum) out.push({ why: 'outside the stated set', value: '__not-in-enum__' });
51
+ }
52
+ if (shape?.type === 'number' || shape?.type === 'integer') {
53
+ if (typeof shape.minimum === 'number') out.push({ why: 'below minimum', value: shape.minimum - 1 });
54
+ if (typeof shape.maximum === 'number') out.push({ why: 'above maximum', value: shape.maximum + 1 });
55
+ }
56
+ if (shape?.type === 'array') {
57
+ if (typeof shape.minItems === 'number' && shape.minItems > 0)
58
+ out.push({ why: 'fewer items than min', value: [] });
59
+ if (typeof shape.maxItems === 'number')
60
+ out.push({ why: 'more items than max', value: Array.from({ length: shape.maxItems + 1 }, () => null) });
61
+ }
62
+ return out;
63
+ }
64
+
65
+ /**
66
+ * The decision table, enumerated on the entity's own fields.
67
+ *
68
+ * Use the complete entity so a derived view cannot hide a divergent field. The caller
69
+ * supplies a valid row; generating data belongs to the testing package.
70
+ */
71
+ export class Cases {
72
+ private constructor(
73
+ readonly all: readonly Case[],
74
+ ) {}
75
+
76
+ static of(entity: SchemaView, valid: Record<string, unknown>): Cases {
77
+ return new Cases(enumerate(entity, valid));
78
+ }
79
+
80
+ /**
81
+ * Whether a verdict is the one a case expected.
82
+ *
83
+ * Takes the RESULT rather than the input so the same reader serves the local judge, the
84
+ * façade and a door — the three that have to agree.
85
+ */
86
+ static holds(expected: Case['expect'], result: { success: boolean; errors?: { path: string }[] }): boolean {
87
+ if (expected === 'accept') return result.success;
88
+ if (result.success) return false;
89
+ return (result.errors ?? []).some((error) => error.path === expected.reject);
90
+ }
91
+
92
+ /** Every way a row is refused for its structure — the closed set this table must cover. */
93
+ static get refusals(): string[] {
94
+ return Object.values(RowRefusal);
95
+ }
96
+
97
+ [Symbol.iterator](): Iterator<Case> {
98
+ return this.all[Symbol.iterator]();
99
+ }
100
+ }
101
+
102
+ function enumerate(entity: SchemaView, valid: Record<string, unknown>): Case[] {
103
+ const fields = entity.getFields();
104
+ const judge = RowJudge.of(fields);
105
+ const cases: Case[] = [];
106
+ const withField = (name: string, value: unknown) => ({ ...valid, [name]: value });
107
+
108
+ cases.push({ why: 'a valid body', body: valid, patch: false, expect: 'accept' });
109
+ cases.push({ why: 'a key outside the contract', body: { ...valid, __unknown__: 'x' }, patch: false, expect: { reject: '__unknown__' } });
110
+ cases.push({ why: 'not an object at all', body: 'a string', patch: false, expect: { reject: '.' } });
111
+
112
+ for (const [name, field] of Object.entries(fields) as [string, Field][]) {
113
+ // A reference names a row that must exist; the caller supplied its id and we do not
114
+ // get to invent a second one, so the only case we can state about it is the bound one.
115
+ const isRef = Role.of(field).isReference;
116
+
117
+ if (judge.onAbsent(field) === null && name in valid) {
118
+ const body = { ...valid };
119
+ delete body[name];
120
+ cases.push({ why: `${name} absent`, body, patch: false, expect: { reject: name } });
121
+ }
122
+
123
+ if (Boundary.of(field).readOnly) {
124
+ cases.push({ why: `${name} supplied although read-only`, body: withField(name, wrongTypeFor(field)), patch: false, expect: { reject: name } });
125
+ }
126
+
127
+ if (Lifecycle.of(field).immutable && !Role.of(field).isPrimary) {
128
+ cases.push({ why: `${name} supplied on an update`, body: { [name]: valid[name] ?? wrongTypeFor(field) }, patch: true, expect: { reject: name } });
129
+ }
130
+
131
+ if (name in valid && !isRef) {
132
+ cases.push({ why: `${name} of the wrong type`, body: withField(name, wrongTypeFor(field)), patch: false, expect: { reject: name } });
133
+ for (const { why, value } of outOfBoundsFor(field))
134
+ cases.push({ why: `${name} ${why}`, body: withField(name, value), patch: false, expect: { reject: name } });
135
+ }
136
+ }
137
+
138
+ return cases;
139
+ }
@@ -0,0 +1,61 @@
1
+ import { EXTENSION_AXES } from "../axis/Axis.js";
2
+ import { Anatomy } from "../axis/shape/Shape.js";
3
+ import type { Field } from "../fields/Field.js";
4
+ import { isObject } from "./ValueForm.js";
5
+ import type { ValidationError, ValidationResult } from "./result.js";
6
+
7
+ export class FieldJudge {
8
+ private constructor(private readonly declaration: unknown) {}
9
+
10
+ static of(declaration: unknown): FieldJudge {
11
+ return new FieldJudge(declaration);
12
+ }
13
+
14
+ get verdict(): ValidationResult<Field> {
15
+ const declaration = this.declaration;
16
+
17
+ if (!isObject(declaration)) {
18
+ return {
19
+ success: false,
20
+ errors: [
21
+ {
22
+ path: ".",
23
+ message: `Expected an object — got ${JSON.stringify(declaration)}`,
24
+ },
25
+ ],
26
+ };
27
+ }
28
+
29
+ const errors: ValidationError[] = [];
30
+
31
+ if (!Anatomy.is(declaration.shape)) {
32
+ errors.push({
33
+ path: "shape",
34
+ message: `Every field states a shape — got ${JSON.stringify(declaration.shape)}`,
35
+ });
36
+ }
37
+
38
+ for (const axis of EXTENSION_AXES) {
39
+ const declared = declaration[axis.slot];
40
+ if (declared !== undefined) axis.judge(declared, errors);
41
+ }
42
+
43
+ if (declaration.meta !== undefined) {
44
+ if (!isObject(declaration.meta)) {
45
+ errors.push({
46
+ path: "meta",
47
+ message: `Expected an object — got ${JSON.stringify(declaration.meta)}`,
48
+ });
49
+ } else if (
50
+ declaration.meta.description !== undefined &&
51
+ typeof declaration.meta.description !== "string"
52
+ ) {
53
+ errors.push({ path: "meta.description", message: "Expected a string" });
54
+ }
55
+ }
56
+
57
+ return errors.length
58
+ ? { success: false, errors }
59
+ : { success: true, data: declaration as unknown as Field };
60
+ }
61
+ }
@@ -0,0 +1,84 @@
1
+ import { Boundary } from '../axis/boundary/Boundary.js';
2
+ import { Lifecycle } from '../axis/lifecycle/Lifecycle.js';
3
+ import { Role } from '../axis/role/Role.js';
4
+ import type { Field, Fields } from '../fields/Field.js';
5
+ import type { ValidateOptions } from './options.js';
6
+ import type { ValidationError, ValidationResult } from './result.js';
7
+ import { ValueJudge } from './ValueJudge.js';
8
+ import { RowRefusal } from './RowRefusal.js';
9
+
10
+ export class RowJudge {
11
+ private constructor(
12
+ private readonly fields: Fields,
13
+ private readonly options: ValidateOptions,
14
+ ) {}
15
+
16
+ static of(fields: Fields, options: ValidateOptions = {}): RowJudge {
17
+ return new RowJudge(fields, options);
18
+ }
19
+
20
+ onAbsent(field: Field): 'skip' | 'empty-list' | null {
21
+ if (Boundary.of(field).readOnly) return 'skip';
22
+ if (!Lifecycle.of(field).requiredAtCreate) return 'skip';
23
+ if (Role.of(field).isCollection) return 'empty-list';
24
+ return null;
25
+ }
26
+
27
+ check(input: unknown): ValidationResult<Record<string, unknown>> {
28
+ if (typeof input !== 'object' || input === null) {
29
+ return { success: false, errors: [{ path: '.', message: RowRefusal.notAnObject }] };
30
+ }
31
+
32
+ const data = input as Record<string, unknown>;
33
+ const errors: ValidationError[] = [];
34
+ const out: Record<string, unknown> = {};
35
+
36
+ for (const key of Object.keys(data)) {
37
+ if (!(key in this.fields)) {
38
+ errors.push({ path: key, message: RowRefusal.unknownField });
39
+ }
40
+ }
41
+
42
+ for (const [key, field] of Object.entries(this.fields)) {
43
+ const path = key;
44
+ const value = data[key];
45
+
46
+ if (value === undefined) {
47
+ if (this.options.patch) continue;
48
+ const absence = this.onAbsent(field);
49
+ if (absence === null) {
50
+ errors.push({ path, message: RowRefusal.required });
51
+ continue;
52
+ }
53
+ if (absence === 'empty-list') out[key] = [];
54
+ continue;
55
+ }
56
+
57
+ const boundary = Boundary.of(field);
58
+ if (boundary.readOnly) {
59
+ errors.push({ path, message: RowRefusal.readOnly });
60
+ continue;
61
+ }
62
+ if (this.options.patch && Lifecycle.of(field).immutable) {
63
+ errors.push({ path, message: RowRefusal.immutable });
64
+ continue;
65
+ }
66
+
67
+ const checked = ValueJudge.of(field).check(value);
68
+ if ('error' in checked) {
69
+ errors.push({ path, message: checked.error });
70
+ continue;
71
+ }
72
+ if (checked.value === null) {
73
+ out[key] = null;
74
+ continue;
75
+ }
76
+ const decoded = boundary.decode(checked.value);
77
+ if ('error' in decoded) errors.push({ path, message: decoded.error });
78
+ else out[key] = decoded.value;
79
+ }
80
+
81
+ if (errors.length > 0) return { success: false, errors };
82
+ return { success: true, data: out };
83
+ }
84
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Every way a row is refused for its STRUCTURE — the closed set, stated once.
3
+ *
4
+ * A value refused by its own shape carries the engine's message and a decoder refused by
5
+ * user code carries that code's; neither is structural and neither belongs here. These
6
+ * five are `RowJudge`'s own, and a table of cases enumerates them instead of reading the
7
+ * text of a function — which a minifier rewrites without a word.
8
+ */
9
+ export const RowRefusal = {
10
+ notAnObject: 'Expected an object',
11
+ unknownField: 'Unknown field',
12
+ required: 'Required',
13
+ readOnly: 'Read-only',
14
+ immutable: 'Immutable',
15
+ } as const;
16
+
17
+ export type RowRefusal = (typeof RowRefusal)[keyof typeof RowRefusal];
@@ -0,0 +1,7 @@
1
+ export const isObject = (value: unknown): value is Record<string, unknown> =>
2
+ typeof value === 'object' && value !== null && !Array.isArray(value);
3
+
4
+ export const oneOfTokens = <T extends readonly string[]>(
5
+ value: unknown,
6
+ tokens: T,
7
+ ): value is T[number] => typeof value === 'string' && (tokens as readonly string[]).includes(value);
@@ -0,0 +1,72 @@
1
+ import { Validator, format as engineFormats } from '@cfworker/json-schema';
2
+ import { Formats, type FormatPredicate } from '../axis/shape/Formats.js';
3
+ import { Anatomy, type Shape } from '../axis/shape/Shape.js';
4
+ import type { Field } from '../fields/Field.js';
5
+ import type { Checked } from './result.js';
6
+
7
+ interface ShapePlan {
8
+ validator: Validator;
9
+ custom?: FormatPredicate;
10
+ formatName?: string;
11
+ }
12
+
13
+ export class ValueJudge {
14
+ private static readonly plans = new WeakMap<object, ShapePlan>();
15
+
16
+ private constructor(private readonly field: Field) {}
17
+
18
+ static of(field: Field): ValueJudge {
19
+ return new ValueJudge(field);
20
+ }
21
+
22
+ check(value: unknown): Checked {
23
+ const shape = this.field.shape;
24
+ const base = Anatomy.of(shape).base;
25
+ if (value !== null) {
26
+ if (base?.type === 'object' && !base.properties) return { value };
27
+ if (base?.type === 'string' && base.format === 'date-time' && value instanceof Date) {
28
+ return Number.isNaN(value.getTime()) ? { error: 'Invalid date' } : { value };
29
+ }
30
+ if (
31
+ (base?.type === 'number' || base?.type === 'integer') &&
32
+ typeof value === 'number' &&
33
+ Number.isNaN(value)
34
+ ) {
35
+ return { error: 'Expected a number' };
36
+ }
37
+ }
38
+ const plan = ValueJudge.planFor(shape);
39
+ const result = plan.validator.validate(value);
40
+ if (!result.valid) return { error: result.errors[0]?.error ?? 'Invalid value' };
41
+ if (plan.custom && typeof value === 'string' && !plan.custom(value)) {
42
+ return { error: `String does not match format "${plan.formatName}".` };
43
+ }
44
+ return { value };
45
+ }
46
+
47
+ private static planFor(shape: Shape): ShapePlan {
48
+ let plan = this.plans.get(shape);
49
+ if (!plan) {
50
+ const base = Anatomy.of(shape).base;
51
+ const formatName = base?.type === 'string' ? base.format : undefined;
52
+ plan = {
53
+ validator: new Validator(shape as object, '2020-12', true),
54
+ custom: formatName === undefined ? undefined : this.customFormatOf(formatName),
55
+ formatName,
56
+ };
57
+ this.plans.set(shape, plan);
58
+ }
59
+ return plan;
60
+ }
61
+
62
+ private static customFormatOf(name: string): FormatPredicate | undefined {
63
+ const custom = Formats.resolve(name);
64
+ if (!custom && !(name in engineFormats)) {
65
+ throw new Error(
66
+ `Unknown format: '${name}'. Register it with Formats.register('${name}', …) — ` +
67
+ `the engine judges ${Object.keys(engineFormats).length} formats natively and this is not one of them.`,
68
+ );
69
+ }
70
+ return custom;
71
+ }
72
+ }
@@ -0,0 +1,3 @@
1
+ export interface ValidateOptions {
2
+ patch?: boolean;
3
+ }
@@ -0,0 +1,10 @@
1
+ export interface ValidationError {
2
+ path: string;
3
+ message: string;
4
+ }
5
+
6
+ export type ValidationResult<T> =
7
+ | { success: true; data: T }
8
+ | { success: false; errors: ValidationError[] };
9
+
10
+ export type Checked = { value: unknown } | { error: string };
@@ -0,0 +1,18 @@
1
+ import type { Fields } from '../fields/Field.js';
2
+ import type { SchemaView } from '../schema/SchemaView.js';
3
+ import type { SchemaDescriptor } from '../card/Descriptor.js';
4
+ import { Card } from '../card/Card.js';
5
+
6
+ export type SchemaOrCard = SchemaView | SchemaDescriptor;
7
+
8
+ function isDescriptor(source: SchemaOrCard): source is SchemaDescriptor {
9
+ return typeof (source as SchemaView).getFields !== 'function';
10
+ }
11
+
12
+ export function schemaOf(source: SchemaOrCard): SchemaView {
13
+ return isDescriptor(source) ? Card.fromDescriptor(source).toSchema() : source;
14
+ }
15
+
16
+ export function fieldsOf(source: SchemaOrCard): Fields {
17
+ return schemaOf(source).getFields();
18
+ }
@@ -0,0 +1,47 @@
1
+ import { Boundary } from '../axis/boundary/Boundary.js';
2
+ import { Lifecycle } from '../axis/lifecycle/Lifecycle.js';
3
+ import { Role } from '../axis/role/Role.js';
4
+ import type { Fields } from '../fields/Field.js';
5
+
6
+ export class Visibility {
7
+ private constructor(private readonly fields: Fields) {}
8
+
9
+ static of(fields: Fields): Visibility {
10
+ return new Visibility(fields);
11
+ }
12
+
13
+ get input(): Fields {
14
+ const result: Fields = {};
15
+ for (const [name, field] of Object.entries(this.fields)) {
16
+ const role = Role.of(field);
17
+ if (role.isPrimary) continue;
18
+ if (Lifecycle.of(field).create === 'now') continue;
19
+ if (role.isCollection) continue;
20
+ if (Boundary.of(field).readOnly) continue;
21
+ result[name] = field;
22
+ }
23
+ return result;
24
+ }
25
+
26
+ get output(): Fields {
27
+ const result: Fields = {};
28
+ for (const [name, field] of Object.entries(this.fields)) {
29
+ if (Boundary.of(field).writeOnly) continue;
30
+ result[name] = field;
31
+ }
32
+ return result;
33
+ }
34
+
35
+ encode(record: Record<string, unknown>): Record<string, unknown> {
36
+ const out: Record<string, unknown> = { ...record };
37
+ for (const [key, field] of Object.entries(this.fields)) {
38
+ if (!(key in record)) continue;
39
+ const boundary = Boundary.of(field);
40
+ if (boundary.writeOnly) { delete out[key]; continue; }
41
+ const value = record[key];
42
+ if (value === null || value === undefined) continue;
43
+ out[key] = boundary.encode(value);
44
+ }
45
+ return out;
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
2
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
3
+ }
4
+
5
+ export declare namespace StandardSchemaV1 {
6
+ interface Props<Input = unknown, Output = Input> {
7
+ readonly version: 1;
8
+ readonly vendor: string;
9
+ readonly validate: (
10
+ value: unknown,
11
+ ) => Result<Output> | Promise<Result<Output>>;
12
+ readonly types?: Types<Input, Output> | undefined;
13
+ }
14
+
15
+ type Result<Output> = SuccessResult<Output> | FailureResult;
16
+
17
+ interface SuccessResult<Output> {
18
+ readonly value: Output;
19
+ readonly issues?: undefined;
20
+ }
21
+
22
+ interface FailureResult {
23
+ readonly issues: readonly Issue[];
24
+ }
25
+
26
+ interface Issue {
27
+ readonly message: string;
28
+ readonly path?: readonly (PropertyKey | PathSegment)[] | undefined;
29
+ }
30
+
31
+ interface PathSegment {
32
+ readonly key: PropertyKey;
33
+ }
34
+
35
+ interface Types<Input = unknown, Output = Input> {
36
+ readonly input: Input;
37
+ readonly output: Output;
38
+ }
39
+
40
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<
41
+ Schema['~standard']['types']
42
+ >['input'];
43
+
44
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
45
+ Schema['~standard']['types']
46
+ >['output'];
47
+ }
@@ -0,0 +1,167 @@
1
+ import { Boundary } from '../axis/boundary/Boundary.js';
2
+ import { type Fields } from '../fields/Field.js';
3
+ import { type PreviousNames } from '../EntityDeclarations.js';
4
+ import { type EntityAdapters } from '../EntityAdapters.js';
5
+ import { FieldGroup } from '../constraint/FieldGroup.js';
6
+ import { Unique } from '../constraint/Unique.js';
7
+ import { type CompositeUnique } from '../EntityDeclarations.js';
8
+ import { RowJudge } from '../judge/RowJudge.js';
9
+ import { SchemaDerivation } from './SchemaDerivation.js';
10
+ import { SchemaDefinition } from './SchemaDefinition.js';
11
+ import { type ValidateOptions } from '../judge/options.js';
12
+ import type { StandardSchemaV1 } from '../projection/standard.js';
13
+ import type { PartialRow, Row, SchemaView } from './SchemaView.js';
14
+
15
+ export const ANONYMOUS_SCHEMA_NAME = "Schema";
16
+
17
+ export interface SchemaConstructor<TFields extends Fields> extends SchemaView<TFields> {
18
+ new (data: PartialRow<TFields>): Row<TFields>;
19
+ readonly "~standard": StandardSchemaV1.Props<Record<string, unknown>, Row<TFields>>;
20
+ readonly derivation?: SchemaDerivation;
21
+ readonly previous?: PreviousNames<TFields>;
22
+ from(data: Record<string, unknown>): Row<TFields>;
23
+ pick<K extends string & keyof TFields>(...keys: K[]): SchemaConstructor<Pick<TFields, K>>;
24
+ omit<K extends string & keyof TFields>(...keys: K[]): SchemaConstructor<Omit<TFields, K>>;
25
+ partial(): SchemaConstructor<TFields>;
26
+ extend<E extends Fields>(extra: E): SchemaConstructor<TFields & E>;
27
+ named(name: string): SchemaConstructor<TFields>;
28
+ rename(mapping: Partial<Record<string & keyof TFields, string>>): SchemaConstructor<Fields>;
29
+ }
30
+
31
+ export class Schema {
32
+ /** The one place a schema holds what it is. The five readings below are its projections. */
33
+ static definition: SchemaDefinition = SchemaDefinition.stated({ fields: {} });
34
+
35
+ static get fields(): Fields { return this.definition.fields; }
36
+ static get adapters(): EntityAdapters<Fields> | undefined { return this.definition.adapters; }
37
+ static get opts(): ValidateOptions { return this.definition.opts; }
38
+ static get derivation(): SchemaDerivation | undefined { return this.definition.derivation; }
39
+ static get previous(): PreviousNames<Fields> | undefined { return this.definition.previous; }
40
+
41
+ constructor(data?: Record<string, unknown>) {
42
+ if (!data) return;
43
+ for (const [key, value] of Object.entries(data)) {
44
+ Object.defineProperty(this, key, { value, writable: true, enumerable: true, configurable: true });
45
+ }
46
+ }
47
+
48
+ static getFields() { return this.fields; }
49
+ static getAdapters() { return this.adapters; }
50
+ static getUnique(): CompositeUnique<Fields> | undefined {
51
+ const groups = FieldGroup.groupsOf(this.fields, Unique);
52
+ return groups.length ? groups.map((group) => [...group.members]) : undefined;
53
+ }
54
+ static getOpts() { return this.opts; }
55
+
56
+ static validate(input: unknown) {
57
+ return RowJudge.of(this.fields, this.opts).check(input);
58
+ }
59
+
60
+ static from(data: Record<string, unknown>) {
61
+ const out: Record<string, unknown> = {};
62
+ for (const [key, field] of Object.entries(this.fields)) {
63
+ if (!(key in data)) continue;
64
+ const value = data[key];
65
+ if (value === null || value === undefined) { out[key] = value; continue; }
66
+ const decoded = Boundary.of(field).decode(value);
67
+ out[key] = "error" in decoded ? value : decoded.value;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ static get ["~standard"](): StandardSchemaV1.Props<Record<string, unknown>> {
73
+ const { fields, opts } = this;
74
+ return {
75
+ version: 1,
76
+ vendor: "fougere",
77
+ validate(value: unknown) {
78
+ const result = RowJudge.of(fields, opts).check(value);
79
+ if (result.success) return { value: result.data };
80
+ return {
81
+ issues: result.errors.map((e) => ({
82
+ message: e.message,
83
+ path: e.path && e.path !== "." ? [{ key: e.path }] : undefined,
84
+ })),
85
+ };
86
+ },
87
+ };
88
+ }
89
+
90
+ static pick(...keys: string[]) {
91
+ assertKnownKeys("pick", keys, this.fields);
92
+ const picked: Fields = {};
93
+ for (const key of keys) if (this.fields[key]) picked[key] = this.fields[key];
94
+ return this.derive(picked, (k) => (keys.includes(k) ? k : undefined));
95
+ }
96
+
97
+ static omit(...keys: string[]) {
98
+ assertKnownKeys("omit", keys, this.fields);
99
+ const kept: Fields = {};
100
+ for (const [key, field] of Object.entries(this.fields)) if (!keys.includes(key)) kept[key] = field;
101
+ return this.derive(kept, (k) => (keys.includes(k) ? undefined : k));
102
+ }
103
+
104
+ static rename(mapping: Record<string, string>) {
105
+ assertKnownKeys("rename", Object.keys(mapping), this.fields);
106
+ const renamed: Fields = {};
107
+ for (const [key, field] of Object.entries(this.fields)) renamed[mapping[key] ?? key] = field;
108
+ return this.derive(renamed, (k) => mapping[k] ?? k);
109
+ }
110
+
111
+ static partial() {
112
+ return Schema.subclass(this.definition.patched(this));
113
+ }
114
+
115
+ static extend(extra: Fields) {
116
+ return Schema.subclass(this.definition.extended(extra, this));
117
+ }
118
+
119
+ static named(name: string) {
120
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
121
+ throw new Error(`named(): \`${name}\` is not a valid class name.`);
122
+ }
123
+ if (this.name !== ANONYMOUS_SCHEMA_NAME) {
124
+ throw new Error(`named(): \`${this.name}\` is already named by its class declaration.`);
125
+ }
126
+ Object.defineProperty(this, "name", { value: name, configurable: true });
127
+ return this;
128
+ }
129
+
130
+ static compose<T extends SchemaView[]>(...sources: T): SchemaConstructor<Merged<T>> {
131
+ return Schema.subclass(SchemaDefinition.merged(sources)) as unknown as SchemaConstructor<Merged<T>>;
132
+ }
133
+
134
+ static of<TFields extends Fields>(stated: {
135
+ fields: TFields;
136
+ derivation?: SchemaDerivation;
137
+ adapters?: EntityAdapters<TFields>;
138
+ opts?: ValidateOptions;
139
+ previous?: PreviousNames<TFields>;
140
+ }): SchemaConstructor<TFields> {
141
+ return Schema.subclass(SchemaDefinition.stated(stated)) as unknown as SchemaConstructor<TFields>;
142
+ }
143
+
144
+ /** The class a definition becomes. The one place a Schema subclass is born. */
145
+ private static subclass(definition: SchemaDefinition): SchemaConstructor<Fields> {
146
+ class Derived extends Schema {}
147
+ Derived.definition = definition;
148
+ Object.defineProperty(Derived, "name", { value: ANONYMOUS_SCHEMA_NAME, configurable: true });
149
+ return Derived as unknown as SchemaConstructor<Fields>;
150
+ }
151
+
152
+ private static derive(fields: Fields, survives: (key: string) => string | undefined) {
153
+ return Schema.subclass(this.definition.cut(fields, survives, this));
154
+ }
155
+ }
156
+
157
+ function assertKnownKeys(operation: string, keys: string[], fields: Fields): void {
158
+ const strangers = keys.filter((key) => !(key in fields));
159
+ if (strangers.length === 0) return;
160
+ throw new Error(
161
+ `${operation}(): unknown field ${strangers.map((s) => `\`${s}\``).join(", ")}. ` +
162
+ `This schema carries ${Object.keys(fields).join(", ")}.`,
163
+ );
164
+ }
165
+
166
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
167
+ type Merged<T extends SchemaView[]> = UnionToIntersection<T[number] extends { getFields(): infer F } ? F : Fields> & Fields;