@fougere/schema 0.12.0-alpha.0 → 0.12.2-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 (348) hide show
  1. package/dist/FougereFieldAxes.d.ts +3 -0
  2. package/dist/FougereFieldAxes.d.ts.map +1 -0
  3. package/dist/FougereFieldAxes.js +2 -0
  4. package/dist/FougereFieldAxes.js.map +1 -0
  5. package/dist/Schema.d.ts +4 -3
  6. package/dist/Schema.d.ts.map +1 -1
  7. package/dist/Schema.js +3 -0
  8. package/dist/Schema.js.map +1 -1
  9. package/dist/SchemaConstraints.d.ts +13 -10
  10. package/dist/SchemaConstraints.d.ts.map +1 -1
  11. package/dist/SchemaConstraints.js +32 -20
  12. package/dist/SchemaConstraints.js.map +1 -1
  13. package/dist/SchemaDefinition.d.ts.map +1 -1
  14. package/dist/SchemaDefinition.js +6 -5
  15. package/dist/SchemaDefinition.js.map +1 -1
  16. package/dist/SchemaError.d.ts +4 -1
  17. package/dist/SchemaError.d.ts.map +1 -1
  18. package/dist/SchemaError.js +6 -3
  19. package/dist/SchemaError.js.map +1 -1
  20. package/dist/SchemaView.d.ts +3 -2
  21. package/dist/SchemaView.d.ts.map +1 -1
  22. package/dist/axis/Axes.d.ts +16 -0
  23. package/dist/axis/Axes.d.ts.map +1 -0
  24. package/dist/axis/Axes.js +27 -0
  25. package/dist/axis/Axes.js.map +1 -0
  26. package/dist/axis/Axis.d.ts +15 -9
  27. package/dist/axis/Axis.d.ts.map +1 -1
  28. package/dist/axis/Axis.js +1 -10
  29. package/dist/axis/Axis.js.map +1 -1
  30. package/dist/axis/boundary/Boundary.d.ts +38 -5
  31. package/dist/axis/boundary/Boundary.d.ts.map +1 -1
  32. package/dist/axis/boundary/Boundary.js +18 -11
  33. package/dist/axis/boundary/Boundary.js.map +1 -1
  34. package/dist/axis/boundary/Decoder.d.ts +8 -2
  35. package/dist/axis/boundary/Decoder.d.ts.map +1 -1
  36. package/dist/axis/boundary/Decoder.js.map +1 -1
  37. package/dist/axis/lifecycle/Lifecycle.d.ts +29 -8
  38. package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
  39. package/dist/axis/lifecycle/Lifecycle.js +26 -11
  40. package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
  41. package/dist/axis/lifecycle/apply.d.ts.map +1 -1
  42. package/dist/axis/lifecycle/apply.js +3 -4
  43. package/dist/axis/lifecycle/apply.js.map +1 -1
  44. package/dist/axis/role/Role.d.ts +31 -12
  45. package/dist/axis/role/Role.d.ts.map +1 -1
  46. package/dist/axis/role/Role.js +56 -22
  47. package/dist/axis/role/Role.js.map +1 -1
  48. package/dist/axis/shape/Shape.d.ts +32 -9
  49. package/dist/axis/shape/Shape.d.ts.map +1 -1
  50. package/dist/axis/shape/Shape.js +43 -12
  51. package/dist/axis/shape/Shape.js.map +1 -1
  52. package/dist/axis/shape/ShapeFormat.d.ts +10 -0
  53. package/dist/axis/shape/ShapeFormat.d.ts.map +1 -0
  54. package/dist/axis/shape/ShapeFormat.js +32 -0
  55. package/dist/axis/shape/ShapeFormat.js.map +1 -0
  56. package/dist/axis/shape/{Formats.d.ts → StringFormats.d.ts} +3 -3
  57. package/dist/axis/shape/StringFormats.d.ts.map +1 -0
  58. package/dist/axis/shape/{Formats.js → StringFormats.js} +3 -3
  59. package/dist/axis/shape/StringFormats.js.map +1 -0
  60. package/dist/entity/EntityDeclarations.d.ts +3 -2
  61. package/dist/entity/EntityDeclarations.d.ts.map +1 -1
  62. package/dist/entity/FieldGroups.d.ts +9 -0
  63. package/dist/entity/FieldGroups.d.ts.map +1 -0
  64. package/dist/entity/FieldGroups.js +2 -0
  65. package/dist/entity/FieldGroups.js.map +1 -0
  66. package/dist/field/Field.d.ts +37 -12
  67. package/dist/field/Field.d.ts.map +1 -1
  68. package/dist/field/Field.js +55 -8
  69. package/dist/field/Field.js.map +1 -1
  70. package/dist/field/FieldFormat.d.ts +9 -0
  71. package/dist/field/FieldFormat.d.ts.map +1 -0
  72. package/dist/field/FieldFormat.js +12 -0
  73. package/dist/field/FieldFormat.js.map +1 -0
  74. package/dist/field/FieldSet.d.ts +12 -3
  75. package/dist/field/FieldSet.d.ts.map +1 -1
  76. package/dist/field/FieldSet.js +22 -15
  77. package/dist/field/FieldSet.js.map +1 -1
  78. package/dist/index.d.ts +7 -3
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +4 -3
  81. package/dist/index.js.map +1 -1
  82. package/dist/lib/Format.d.ts +75 -0
  83. package/dist/lib/Format.d.ts.map +1 -0
  84. package/dist/lib/Format.js +123 -0
  85. package/dist/lib/Format.js.map +1 -0
  86. package/dist/lib/JsonSchema.d.ts +2 -0
  87. package/dist/lib/JsonSchema.d.ts.map +1 -0
  88. package/dist/lib/JsonSchema.js +2 -0
  89. package/dist/lib/JsonSchema.js.map +1 -0
  90. package/dist/lib/Registry.d.ts +4 -2
  91. package/dist/lib/Registry.d.ts.map +1 -1
  92. package/dist/lib/Registry.js +11 -5
  93. package/dist/lib/Registry.js.map +1 -1
  94. package/dist/lib/UnionToIntersection.d.ts +7 -0
  95. package/dist/lib/UnionToIntersection.d.ts.map +1 -0
  96. package/dist/lib/UnionToIntersection.js +2 -0
  97. package/dist/lib/UnionToIntersection.js.map +1 -0
  98. package/dist/lib/ValidationResult.d.ts +2 -0
  99. package/dist/lib/ValidationResult.d.ts.map +1 -1
  100. package/dist/lib/ValidationResult.js +6 -0
  101. package/dist/lib/ValidationResult.js.map +1 -1
  102. package/dist/lib/utils.d.ts +1 -1
  103. package/dist/lib/utils.d.ts.map +1 -1
  104. package/dist/lib/utils.js +1 -1
  105. package/dist/lib/utils.js.map +1 -1
  106. package/dist/projection/Cases.d.ts +1 -1
  107. package/dist/projection/Cases.d.ts.map +1 -1
  108. package/dist/projection/Cases.js +20 -8
  109. package/dist/projection/Cases.js.map +1 -1
  110. package/dist/projection/Visibility.d.ts +11 -0
  111. package/dist/projection/Visibility.d.ts.map +1 -1
  112. package/dist/projection/Visibility.js +28 -6
  113. package/dist/projection/Visibility.js.map +1 -1
  114. package/dist/projection/card/Bundle.d.ts.map +1 -1
  115. package/dist/projection/card/Bundle.js.map +1 -1
  116. package/dist/projection/card/Card.d.ts.map +1 -1
  117. package/dist/projection/card/Card.js +24 -30
  118. package/dist/projection/card/Card.js.map +1 -1
  119. package/dist/projection/card/CardForm.d.ts +7 -0
  120. package/dist/projection/card/CardForm.d.ts.map +1 -0
  121. package/dist/projection/card/CardForm.js +2 -0
  122. package/dist/projection/card/CardForm.js.map +1 -0
  123. package/dist/projection/card/CardForms.d.ts +10 -0
  124. package/dist/projection/card/CardForms.d.ts.map +1 -0
  125. package/dist/projection/card/CardForms.js +12 -0
  126. package/dist/projection/card/CardForms.js.map +1 -0
  127. package/dist/projection/card/Change.d.ts +2 -2
  128. package/dist/projection/card/Change.d.ts.map +1 -1
  129. package/dist/projection/card/Diff.js +2 -2
  130. package/dist/projection/card/Diff.js.map +1 -1
  131. package/dist/projection/card/FieldExtension.d.ts +4 -2
  132. package/dist/projection/card/FieldExtension.d.ts.map +1 -1
  133. package/dist/projection/card/RoleDescriptor.d.ts +15 -3
  134. package/dist/projection/card/RoleDescriptor.d.ts.map +1 -1
  135. package/dist/projection/card/RoleDescriptor.js +40 -1
  136. package/dist/projection/card/RoleDescriptor.js.map +1 -1
  137. package/dist/projection/card/ShapeKeywords.d.ts +1 -1
  138. package/dist/projection/card/ShapeKeywords.d.ts.map +1 -1
  139. package/dist/projection/card/admission.d.ts +0 -14
  140. package/dist/projection/card/admission.d.ts.map +1 -1
  141. package/dist/projection/card/admission.js +0 -20
  142. package/dist/projection/card/admission.js.map +1 -1
  143. package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
  144. package/dist/validator/FieldDeclarationValidator.js +10 -25
  145. package/dist/validator/FieldDeclarationValidator.js.map +1 -1
  146. package/dist/validator/FieldValueValidator.d.ts.map +1 -1
  147. package/dist/validator/FieldValueValidator.js +9 -29
  148. package/dist/validator/FieldValueValidator.js.map +1 -1
  149. package/dist/validator/InputValidator.d.ts +7 -1
  150. package/dist/validator/InputValidator.d.ts.map +1 -1
  151. package/dist/validator/InputValidator.js +15 -14
  152. package/dist/validator/InputValidator.js.map +1 -1
  153. package/dist/validator/JsonSchemaValidator.d.ts +23 -0
  154. package/dist/validator/JsonSchemaValidator.d.ts.map +1 -0
  155. package/dist/validator/JsonSchemaValidator.js +113 -0
  156. package/dist/validator/JsonSchemaValidator.js.map +1 -0
  157. package/dist/vocabulary/preset/created.d.ts +2 -2
  158. package/dist/vocabulary/preset/created.d.ts.map +1 -1
  159. package/dist/vocabulary/preset/created.js +2 -2
  160. package/dist/vocabulary/preset/created.js.map +1 -1
  161. package/dist/vocabulary/preset/updated.d.ts +2 -2
  162. package/dist/vocabulary/preset/updated.d.ts.map +1 -1
  163. package/dist/vocabulary/preset/updated.js +2 -2
  164. package/dist/vocabulary/preset/updated.js.map +1 -1
  165. package/dist/vocabulary/primitive/bool.d.ts +2 -5
  166. package/dist/vocabulary/primitive/bool.d.ts.map +1 -1
  167. package/dist/vocabulary/primitive/bool.js +1 -5
  168. package/dist/vocabulary/primitive/bool.js.map +1 -1
  169. package/dist/vocabulary/primitive/date.d.ts +2 -3
  170. package/dist/vocabulary/primitive/date.d.ts.map +1 -1
  171. package/dist/vocabulary/primitive/date.js +2 -3
  172. package/dist/vocabulary/primitive/date.js.map +1 -1
  173. package/dist/vocabulary/primitive/json.d.ts +3 -3
  174. package/dist/vocabulary/primitive/json.d.ts.map +1 -1
  175. package/dist/vocabulary/primitive/json.js +11 -11
  176. package/dist/vocabulary/primitive/json.js.map +1 -1
  177. package/dist/vocabulary/primitive/list.d.ts +6 -5
  178. package/dist/vocabulary/primitive/list.d.ts.map +1 -1
  179. package/dist/vocabulary/primitive/list.js +13 -7
  180. package/dist/vocabulary/primitive/list.js.map +1 -1
  181. package/dist/vocabulary/primitive/many.d.ts +2 -4
  182. package/dist/vocabulary/primitive/many.d.ts.map +1 -1
  183. package/dist/vocabulary/primitive/many.js +2 -4
  184. package/dist/vocabulary/primitive/many.js.map +1 -1
  185. package/dist/vocabulary/primitive/number.d.ts +2 -4
  186. package/dist/vocabulary/primitive/number.d.ts.map +1 -1
  187. package/dist/vocabulary/primitive/number.js +1 -3
  188. package/dist/vocabulary/primitive/number.js.map +1 -1
  189. package/dist/vocabulary/primitive/oneOf.d.ts +4 -7
  190. package/dist/vocabulary/primitive/oneOf.d.ts.map +1 -1
  191. package/dist/vocabulary/primitive/oneOf.js +19 -11
  192. package/dist/vocabulary/primitive/oneOf.js.map +1 -1
  193. package/dist/vocabulary/primitive/primary.d.ts +2 -2
  194. package/dist/vocabulary/primitive/primary.d.ts.map +1 -1
  195. package/dist/vocabulary/primitive/primary.js +2 -3
  196. package/dist/vocabulary/primitive/primary.js.map +1 -1
  197. package/dist/vocabulary/primitive/ref.d.ts +2 -6
  198. package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
  199. package/dist/vocabulary/primitive/ref.js +1 -1
  200. package/dist/vocabulary/primitive/ref.js.map +1 -1
  201. package/dist/vocabulary/primitive/text.d.ts +2 -4
  202. package/dist/vocabulary/primitive/text.d.ts.map +1 -1
  203. package/dist/vocabulary/primitive/text.js +1 -3
  204. package/dist/vocabulary/primitive/text.js.map +1 -1
  205. package/dist/vocabulary/rule/nullable.d.ts +0 -1
  206. package/dist/vocabulary/rule/nullable.d.ts.map +1 -1
  207. package/dist/vocabulary/rule/nullable.js +0 -1
  208. package/dist/vocabulary/rule/nullable.js.map +1 -1
  209. package/dist/vocabulary/rule/optional.d.ts +0 -1
  210. package/dist/vocabulary/rule/optional.d.ts.map +1 -1
  211. package/dist/vocabulary/rule/optional.js +0 -1
  212. package/dist/vocabulary/rule/optional.js.map +1 -1
  213. package/dist/vocabulary/rule/readOnly.d.ts +0 -2
  214. package/dist/vocabulary/rule/readOnly.d.ts.map +1 -1
  215. package/dist/vocabulary/rule/readOnly.js +0 -2
  216. package/dist/vocabulary/rule/readOnly.js.map +1 -1
  217. package/dist/vocabulary/rule/writeOnly.d.ts +0 -2
  218. package/dist/vocabulary/rule/writeOnly.d.ts.map +1 -1
  219. package/dist/vocabulary/rule/writeOnly.js +0 -2
  220. package/dist/vocabulary/rule/writeOnly.js.map +1 -1
  221. package/dist/vocabulary/vocabulary.d.ts +2 -3
  222. package/dist/vocabulary/vocabulary.d.ts.map +1 -1
  223. package/dist/vocabulary/vocabulary.js +18 -20
  224. package/dist/vocabulary/vocabulary.js.map +1 -1
  225. package/package.json +1 -1
  226. package/src/FougereFieldAxes.ts +2 -0
  227. package/src/Schema.ts +11 -8
  228. package/src/SchemaConstraints.ts +40 -22
  229. package/src/SchemaDefinition.ts +7 -5
  230. package/src/SchemaError.ts +9 -3
  231. package/src/SchemaView.ts +3 -2
  232. package/src/axis/Axes.ts +34 -0
  233. package/src/axis/Axis.ts +15 -19
  234. package/src/axis/boundary/Boundary.ts +39 -13
  235. package/src/axis/boundary/Decoder.ts +3 -1
  236. package/src/axis/lifecycle/Lifecycle.ts +42 -14
  237. package/src/axis/lifecycle/apply.ts +12 -6
  238. package/src/axis/role/Role.ts +73 -25
  239. package/src/axis/shape/Shape.ts +89 -23
  240. package/src/axis/shape/ShapeFormat.ts +37 -0
  241. package/src/axis/shape/{Formats.ts → StringFormats.ts} +2 -2
  242. package/src/entity/EntityDeclarations.ts +3 -2
  243. package/src/entity/FieldGroups.ts +9 -0
  244. package/src/field/Field.ts +89 -16
  245. package/src/field/FieldFormat.ts +18 -0
  246. package/src/field/FieldSet.ts +27 -19
  247. package/src/index.ts +7 -3
  248. package/src/lib/Format.ts +172 -0
  249. package/src/lib/JsonSchema.ts +1 -0
  250. package/src/lib/Registry.ts +14 -6
  251. package/src/lib/UnionToIntersection.ts +10 -0
  252. package/src/lib/ValidationResult.ts +8 -0
  253. package/src/lib/utils.ts +5 -1
  254. package/src/projection/Cases.ts +25 -8
  255. package/src/projection/Visibility.ts +31 -6
  256. package/src/projection/card/Bundle.ts +2 -0
  257. package/src/projection/card/Card.ts +29 -27
  258. package/src/projection/card/CardForm.ts +8 -0
  259. package/src/projection/card/CardForms.ts +13 -0
  260. package/src/projection/card/Change.ts +2 -2
  261. package/src/projection/card/Diff.ts +6 -2
  262. package/src/projection/card/FieldExtension.ts +5 -2
  263. package/src/projection/card/RoleDescriptor.ts +54 -3
  264. package/src/projection/card/ShapeKeywords.ts +1 -1
  265. package/src/projection/card/admission.ts +0 -33
  266. package/src/validator/FieldDeclarationValidator.ts +10 -25
  267. package/src/validator/FieldValueValidator.ts +13 -36
  268. package/src/validator/InputValidator.ts +17 -12
  269. package/src/validator/JsonSchemaValidator.ts +142 -0
  270. package/src/vocabulary/preset/created.ts +3 -3
  271. package/src/vocabulary/preset/updated.ts +4 -3
  272. package/src/vocabulary/primitive/bool.ts +3 -8
  273. package/src/vocabulary/primitive/date.ts +5 -4
  274. package/src/vocabulary/primitive/json.ts +27 -12
  275. package/src/vocabulary/primitive/list.ts +16 -9
  276. package/src/vocabulary/primitive/many.ts +3 -4
  277. package/src/vocabulary/primitive/number.ts +3 -8
  278. package/src/vocabulary/primitive/oneOf.ts +40 -24
  279. package/src/vocabulary/primitive/primary.ts +9 -5
  280. package/src/vocabulary/primitive/ref.ts +3 -7
  281. package/src/vocabulary/primitive/text.ts +3 -8
  282. package/src/vocabulary/rule/nullable.ts +0 -1
  283. package/src/vocabulary/rule/optional.ts +0 -1
  284. package/src/vocabulary/rule/readOnly.ts +0 -2
  285. package/src/vocabulary/rule/writeOnly.ts +0 -2
  286. package/src/vocabulary/vocabulary.ts +19 -21
  287. package/dist/axis/boundary/BoundaryAxis.d.ts +0 -4
  288. package/dist/axis/boundary/BoundaryAxis.d.ts.map +0 -1
  289. package/dist/axis/boundary/BoundaryAxis.js +0 -30
  290. package/dist/axis/boundary/BoundaryAxis.js.map +0 -1
  291. package/dist/axis/boundary/BoundaryRef.d.ts +0 -3
  292. package/dist/axis/boundary/BoundaryRef.d.ts.map +0 -1
  293. package/dist/axis/boundary/BoundaryRef.js +0 -2
  294. package/dist/axis/boundary/BoundaryRef.js.map +0 -1
  295. package/dist/axis/boundary/BoundaryRules.d.ts +0 -9
  296. package/dist/axis/boundary/BoundaryRules.d.ts.map +0 -1
  297. package/dist/axis/boundary/BoundaryRules.js +0 -2
  298. package/dist/axis/boundary/BoundaryRules.js.map +0 -1
  299. package/dist/axis/lifecycle/Clock.d.ts +0 -10
  300. package/dist/axis/lifecycle/Clock.d.ts.map +0 -1
  301. package/dist/axis/lifecycle/Clock.js +0 -19
  302. package/dist/axis/lifecycle/Clock.js.map +0 -1
  303. package/dist/axis/lifecycle/LifecycleAxis.d.ts +0 -4
  304. package/dist/axis/lifecycle/LifecycleAxis.d.ts.map +0 -1
  305. package/dist/axis/lifecycle/LifecycleAxis.js +0 -52
  306. package/dist/axis/lifecycle/LifecycleAxis.js.map +0 -1
  307. package/dist/axis/lifecycle/LifecycleRules.d.ts +0 -12
  308. package/dist/axis/lifecycle/LifecycleRules.d.ts.map +0 -1
  309. package/dist/axis/lifecycle/LifecycleRules.js +0 -3
  310. package/dist/axis/lifecycle/LifecycleRules.js.map +0 -1
  311. package/dist/axis/role/RoleAxis.d.ts +0 -5
  312. package/dist/axis/role/RoleAxis.d.ts.map +0 -1
  313. package/dist/axis/role/RoleAxis.js +0 -93
  314. package/dist/axis/role/RoleAxis.js.map +0 -1
  315. package/dist/axis/role/RoleRules.d.ts +0 -8
  316. package/dist/axis/role/RoleRules.d.ts.map +0 -1
  317. package/dist/axis/role/RoleRules.js +0 -2
  318. package/dist/axis/role/RoleRules.js.map +0 -1
  319. package/dist/axis/shape/Formats.d.ts.map +0 -1
  320. package/dist/axis/shape/Formats.js.map +0 -1
  321. package/dist/entity/CompositeUnique.d.ts +0 -4
  322. package/dist/entity/CompositeUnique.d.ts.map +0 -1
  323. package/dist/entity/CompositeUnique.js +0 -2
  324. package/dist/entity/CompositeUnique.js.map +0 -1
  325. package/dist/field/Meta.d.ts +0 -4
  326. package/dist/field/Meta.d.ts.map +0 -1
  327. package/dist/field/Meta.js +0 -2
  328. package/dist/field/Meta.js.map +0 -1
  329. package/dist/projection/card/RelationDescriptor.d.ts +0 -5
  330. package/dist/projection/card/RelationDescriptor.d.ts.map +0 -1
  331. package/dist/projection/card/RelationDescriptor.js +0 -2
  332. package/dist/projection/card/RelationDescriptor.js.map +0 -1
  333. package/dist/validator/AdapterFieldValidator.d.ts +0 -14
  334. package/dist/validator/AdapterFieldValidator.d.ts.map +0 -1
  335. package/dist/validator/AdapterFieldValidator.js +0 -37
  336. package/dist/validator/AdapterFieldValidator.js.map +0 -1
  337. package/src/axis/boundary/BoundaryAxis.ts +0 -32
  338. package/src/axis/boundary/BoundaryRef.ts +0 -3
  339. package/src/axis/boundary/BoundaryRules.ts +0 -4
  340. package/src/axis/lifecycle/Clock.ts +0 -22
  341. package/src/axis/lifecycle/LifecycleAxis.ts +0 -55
  342. package/src/axis/lifecycle/LifecycleRules.ts +0 -9
  343. package/src/axis/role/RoleAxis.ts +0 -104
  344. package/src/axis/role/RoleRules.ts +0 -8
  345. package/src/entity/CompositeUnique.ts +0 -6
  346. package/src/field/Meta.ts +0 -3
  347. package/src/projection/card/RelationDescriptor.ts +0 -3
  348. package/src/validator/AdapterFieldValidator.ts +0 -41
@@ -4,27 +4,58 @@ import type { ShapeType } from './ShapeType.js';
4
4
 
5
5
  type Nullably<T extends string> = T | readonly [T, 'null'];
6
6
 
7
- interface StringConstraints { minLength?: number; maxLength?: number; pattern?: string; enum?: readonly (string | null)[]; format?: StringFormat }
7
+ /** The sentence a field carries, where JSON Schema already holds one. */
8
+ interface Documented {
9
+ description?: string;
10
+ }
11
+
12
+ interface StringConstraints extends Documented {
13
+ minLength?: number;
14
+ maxLength?: number;
15
+ pattern?: string;
16
+ enum?: readonly (string | null)[];
17
+ format?: StringFormat;
18
+ }
8
19
 
9
- interface NumericConstraints { minimum?: number; maximum?: number }
20
+ interface NumericConstraints extends Documented {
21
+ minimum?: number;
22
+ maximum?: number;
23
+ enum?: readonly (number | null)[];
24
+ }
10
25
 
11
- interface ArrayConstraints { items?: Shape; minItems?: number; maxItems?: number }
26
+ interface ArrayConstraints extends Documented {
27
+ items?: Shape;
28
+ minItems?: number;
29
+ maxItems?: number;
30
+ }
12
31
 
13
- interface ObjectConstraints { properties?: Record<string, unknown>; required?: readonly string[]; additionalProperties?: boolean | Shape; propertyNames?: Shape }
32
+ interface ObjectConstraints extends Documented {
33
+ properties?: Record<string, unknown>;
34
+ required?: readonly string[];
35
+ additionalProperties?: boolean | Shape;
36
+ propertyNames?: Shape;
37
+ }
14
38
 
15
39
  export type Shape =
16
40
  | ({ type: Nullably<'string'> } & StringConstraints)
17
41
  | ({ type: Nullably<'number'> | Nullably<'integer'> } & NumericConstraints)
18
- | { type: Nullably<'boolean'> }
42
+ | ({ type: Nullably<'boolean'> } & Documented)
19
43
  | ({ type: Nullably<'array'> } & ArrayConstraints)
20
44
  | ({ type: Nullably<'object'> } & ObjectConstraints);
21
45
 
22
- const SHAPE_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object'] as const;
46
+ export const SHAPE_TYPES = [
47
+ 'string',
48
+ 'number',
49
+ 'integer',
50
+ 'boolean',
51
+ 'array',
52
+ 'object',
53
+ ] as const;
23
54
 
24
55
  type BaseShape =
25
56
  | ({ type: 'string' } & StringConstraints)
26
57
  | ({ type: 'number' | 'integer' } & NumericConstraints)
27
- | { type: 'boolean' }
58
+ | ({ type: 'boolean' } & Documented)
28
59
  | ({ type: 'array' } & ArrayConstraints)
29
60
  | ({ type: 'object' } & ObjectConstraints);
30
61
 
@@ -42,26 +73,21 @@ type _ShapeConformsToJsonSchema = Assert<
42
73
  >;
43
74
 
44
75
  type _ShapeTypesAreTheStandardsLessNull = Assert<
45
- [Exclude<Exclude<JSONSchema7TypeName, 'null'>, (typeof SHAPE_TYPES)[number]>] extends [never]
76
+ [Exclude<Exclude<JSONSchema7TypeName, 'null'>, (typeof SHAPE_TYPES)[number]>] extends [
77
+ never,
78
+ ]
46
79
  ? true
47
80
  : false
48
81
  >;
49
82
 
83
+ /** Every type name a shape states, whether it states one or a union of them. */
84
+ const typesOf = (shape: Shape): readonly string[] =>
85
+ (Array.isArray(shape.type) ? shape.type : [shape.type]) as readonly string[];
86
+
50
87
  export class Shapes {
51
88
  private static readonly cache = new WeakMap<object, ShapeParts>();
52
89
  private static readonly none: ShapeParts = { base: undefined, nullable: false };
53
90
 
54
- static is(value: unknown): value is Shape {
55
- if (typeof value !== 'object' || value === null) return false;
56
- const type = (value as Shape).type;
57
- const names = Array.isArray(type) ? type : [type];
58
-
59
- return (
60
- names.some((name) => (SHAPE_TYPES as readonly unknown[]).includes(name)) &&
61
- names.every((name) => name === 'null' || (SHAPE_TYPES as readonly unknown[]).includes(name))
62
- );
63
- }
64
-
65
91
  /**
66
92
  * Every `pattern` a shape states, the nested ones included — `items`, `properties`.
67
93
  *
@@ -89,11 +115,28 @@ export class Shapes {
89
115
  for (const member of Object.values(value)) Shapes.collectPatterns(member, found);
90
116
  }
91
117
 
118
+ /**
119
+ * `null` enters the grammar, and every constraint stays. It is added when it is ABSENT,
120
+ * read off the shape rather than assumed from its form — a shape already stating it comes
121
+ * back as itself, so `optional(nullable(text()))` does the work once.
122
+ *
123
+ * `enum` is widened TOO, because JSON Schema checks it apart from `type`: a shape saying
124
+ * `['string','null']` and `enum: ['draft','live']` calls null a legal type and not a legal
125
+ * value, and the engine answers `Instance does not match any of ["draft","live"]`.
126
+ *
127
+ * `{ type: 'string', minLength: 3 }` → `{ type: ['string','null'], minLength: 3 }`
128
+ * `{ type: 'string', enum: ['draft'] }` → `{ type: ['string','null'], enum: ['draft', null] }`
129
+ */
92
130
  static nullable(shape: Shape): Shape {
93
- if (Array.isArray(shape.type)) return shape;
94
- const nullable = { ...shape, type: [shape.type, 'null'] } as unknown as Shape;
131
+ const types = typesOf(shape);
132
+ if (types.includes('null')) return shape;
133
+
134
+ const nullable = { ...shape, type: [...types, 'null'] } as unknown as Shape;
95
135
  if ('enum' in nullable && nullable.enum && !nullable.enum.includes(null)) {
96
- (nullable as { enum: readonly (string | null)[] }).enum = [...nullable.enum, null];
136
+ (nullable as { enum: readonly (string | number | null)[] }).enum = [
137
+ ...nullable.enum,
138
+ null,
139
+ ];
97
140
  }
98
141
 
99
142
  return nullable;
@@ -107,7 +150,9 @@ export class Shapes {
107
150
  const baseType = shape.type.find((t) => t !== 'null');
108
151
  const base = { ...shape, type: baseType } as BaseShape;
109
152
  if ('enum' in base && base.enum) {
110
- (base as { enum: readonly (string | null)[] }).enum = base.enum.filter((v) => v !== null);
153
+ (base as { enum: readonly (string | number | null)[] }).enum = base.enum.filter(
154
+ (v) => v !== null,
155
+ );
111
156
  }
112
157
  parts = { base, nullable: true };
113
158
  } else {
@@ -115,6 +160,7 @@ export class Shapes {
115
160
  }
116
161
  this.cache.set(shape, parts);
117
162
  }
163
+
118
164
  return parts;
119
165
  }
120
166
 
@@ -122,6 +168,26 @@ export class Shapes {
122
168
  return this.of(shape).nullable;
123
169
  }
124
170
 
171
+ /**
172
+ * A value that came in as text — a form control, a command-line flag — read as the shape
173
+ * declares it. What does not read that way stays as it came, for the judge to refuse by name.
174
+ * `fromText({ type: 'integer' }, '12')` → `12`, and `fromText({ type: 'integer' }, 'abc')` → `'abc'`
175
+ */
176
+ static fromText(shape: Shape | undefined, value: unknown): unknown {
177
+ const type = this.of(shape).base?.type;
178
+
179
+ if (
180
+ (type !== 'number' && type !== 'integer') ||
181
+ typeof value !== 'string' ||
182
+ value === ''
183
+ )
184
+ return value;
185
+
186
+ const number = Number(value);
187
+
188
+ return Number.isFinite(number) ? number : value;
189
+ }
190
+
125
191
  /**
126
192
  * `shape.type` read off the base, except that a `string` answers `date` or `choice`.
127
193
  * FR : `shape.type` lu sur la base, sauf qu'une chaîne répond `date` ou `choice`.
@@ -0,0 +1,37 @@
1
+ import { Format } from '../../lib/Format.js';
2
+ import { SHAPE_TYPES, type Shape } from './Shape.js';
3
+
4
+ const SHAPE = 'field/shape';
5
+
6
+ /** A word, or that word beside `null` — the second place is what makes a shape nullable. */
7
+ const TYPE = Format.either(
8
+ Format.tokens(SHAPE_TYPES),
9
+ Format.tuple(Format.tokens(SHAPE_TYPES), Format.tokens(['null'])),
10
+ );
11
+
12
+ /**
13
+ * What a shape may state — JSON Schema's own keywords, less the ones this package has no word
14
+ * for. `shape` used to be the one key of a field declaration nothing judged, so `minLenght: 1`
15
+ * passed and was dropped where `lifecycle: { craete: 'now' }` had been refused since `Format`.
16
+ * `properties` stays open because a projection folds `meta.description` into it.
17
+ */
18
+ export const SHAPE_FORMAT = Format.of(SHAPE)
19
+ .key('type', TYPE)
20
+ .key('description', Format.text)
21
+ .key('minLength', Format.count)
22
+ .key('maxLength', Format.count)
23
+ .key('pattern', Format.text)
24
+ .key('format', Format.text)
25
+ .key('enum', Format.listOf(Format.anything))
26
+ .key('minimum', Format.number)
27
+ .key('maximum', Format.number)
28
+ .key('items', Format.ref(SHAPE))
29
+ .key('minItems', Format.count)
30
+ .key('maxItems', Format.count)
31
+ .key('properties', Format.anything)
32
+ .key('required', Format.listOf(Format.text))
33
+ .key('additionalProperties', Format.anything)
34
+ .key('propertyNames', Format.ref(SHAPE))
35
+ .needs('type')
36
+ .closed()
37
+ .as<Shape>();
@@ -5,6 +5,6 @@ import type { FormatPredicate } from './FormatPredicate.js';
5
5
  * String formats the JSON Schema engine does not know. `find` answers `undefined` for
6
6
  * `email` or `uuid`, which it validates on its own.
7
7
  * FR : les formats de chaîne que le moteur ignore ; `find` rend `undefined` pour les siens.
8
- * `Formats.register('siret', (v) => /^\d{14}$/.test(v))`
8
+ * `StringFormats.register('siret', (v) => /^\d{14}$/.test(v))`
9
9
  */
10
- export const Formats = new Registry<FormatPredicate>('format');
10
+ export const StringFormats = new Registry<FormatPredicate>('format');
@@ -1,10 +1,11 @@
1
1
  import type { Fields } from '../field/Fields.js';
2
2
  import type { EntityAdapters } from './EntityAdapters.js';
3
- import type { CompositeUnique } from './CompositeUnique.js';
3
+ import type { FieldGroups } from './FieldGroups.js';
4
4
  import type { PreviousNames } from './PreviousNames.js';
5
5
 
6
6
  export interface EntityDeclarations<TFields extends Fields> {
7
7
  adapters?: EntityAdapters<TFields>;
8
8
  previous?: PreviousNames<TFields>;
9
- unique?: CompositeUnique<TFields>;
9
+ unique?: FieldGroups<TFields>;
10
+ index?: FieldGroups<TFields>;
10
11
  }
@@ -0,0 +1,9 @@
1
+ import type { FieldName } from '../field/FieldName.js';
2
+ import type { Fields } from '../field/Fields.js';
3
+
4
+ /**
5
+ * Field names grouped, as `unique` and `index` both are — `[['listId', 'docId'], ['slug']]`.
6
+ * The ORDER inside a group counts for both: a unique constraint on `(a, b)` is not the one on
7
+ * `(b, a)`, and an index on `(a, b)` serves a filter on `a` alone and never on `b` alone.
8
+ */
9
+ export type FieldGroups<TFields extends Fields> = readonly (readonly FieldName<TFields>[])[];
@@ -1,24 +1,49 @@
1
1
  import type { Shape } from '../axis/shape/Shape.js';
2
- import type { RoleRules } from '../axis/role/RoleRules.js';
3
- import type { LifecycleRules } from '../axis/lifecycle/LifecycleRules.js';
4
- import type { BoundaryRef } from '../axis/boundary/BoundaryRef.js';
5
- import type { Meta } from './Meta.js';
6
- import type { Axis } from '../axis/Axis.js';
2
+ import { Axes } from '../axis/Axes.js';
3
+ import type { FougereFieldAxes } from '../FougereFieldAxes.js';
7
4
  import { FieldDeclarationValidator } from '../validator/FieldDeclarationValidator.js';
8
5
  import { FieldValueValidator } from '../validator/FieldValueValidator.js';
6
+ import { isObject } from '../lib/utils.js';
9
7
  import { dotted } from '../lib/ValidationResult.js';
10
8
  import { SchemaError } from '../SchemaError.js';
11
9
 
12
- type FieldDeclaration = Pick<Field, 'shape' | Axis['slot'] | 'meta'>;
10
+ /** The sentence any word admits, whatever it shapes — a relation and a date carry one too. */
11
+ export interface Described {
12
+ description?: string;
13
+ }
14
+
15
+ /** And a value it is born with, for the words that shape one. `many()` has none to hold. */
16
+ export interface Shared<T> extends Described {
17
+ default?: T;
18
+ }
19
+
20
+ export interface FieldDeclaration extends FougereFieldAxes {
21
+ shape: Shape;
22
+ }
23
+
24
+ /**
25
+ * Every word ends on `setShared`, so the options are read in ONE place and refused there.
26
+ * `text(42)` used to pass for a bare `text()`, and nine of the twelve words took it.
27
+ */
28
+ const refuseLooseOptions = (opts: unknown): void => {
29
+ if (!isObject(opts))
30
+ throw new SchemaError('Options must go in an object, like { max: 200 }', { received: opts });
31
+ };
32
+
33
+ /** The registry answers strings, and a member of a declaration is what they name. */
34
+ const registered = (): (keyof FieldDeclaration)[] => Axes.names as (keyof FieldDeclaration)[];
35
+
36
+ /**
37
+ * The axes it carries are `FougereFieldAxes`, which each axis writes its own line in — so the
38
+ * three of the core are typed the way a fourth declared outside is, and the class names what
39
+ * the registry decides only once, in the constructor.
40
+ */
41
+ export interface Field<T = unknown> extends FougereFieldAxes {
42
+ readonly _type?: T;
43
+ }
13
44
 
14
45
  export class Field<T = unknown> {
15
46
  readonly shape: Shape;
16
- readonly role?: RoleRules;
17
- readonly lifecycle?: LifecycleRules;
18
- readonly boundary?: BoundaryRef;
19
- readonly meta?: Meta;
20
-
21
- declare readonly _type?: T;
22
47
 
23
48
  constructor(init: FieldDeclaration, key?: string) {
24
49
  const verdict = FieldDeclarationValidator.of(init).verdict;
@@ -31,10 +56,12 @@ export class Field<T = unknown> {
31
56
  }
32
57
 
33
58
  this.shape = init.shape;
34
- this.role = init.role;
35
- this.lifecycle = init.lifecycle;
36
- this.boundary = init.boundary;
37
- this.meta = init.meta;
59
+
60
+ for (const name of registered()) {
61
+ Object.defineProperty(this, name, {
62
+ value: init[name], writable: true, enumerable: true, configurable: true,
63
+ });
64
+ }
38
65
 
39
66
  const create = this.lifecycle?.create;
40
67
  if (typeof create === 'object' && create !== null && 'value' in create) {
@@ -51,7 +78,53 @@ export class Field<T = unknown> {
51
78
  return FieldDeclarationValidator.of(value).verdict.success;
52
79
  }
53
80
 
81
+ /**
82
+ * What this field declares on ONE axis, a fourth one registered outside included. An axis is
83
+ * a member, so it is read by NAME, and three callers reached for it through a cast of their
84
+ * own before this existed.
85
+ */
86
+ axis(name: keyof FieldDeclaration): unknown {
87
+ return (this satisfies FieldDeclaration)[name];
88
+ }
89
+
90
+ /** Every axis it declares, and nothing for the ones it says nothing on. */
91
+ get axes(): Record<string, unknown> {
92
+ return Object.fromEntries(
93
+ registered()
94
+ .map((name) => [name, this.axis(name)])
95
+ .filter(([, declared]) => declared !== undefined),
96
+ );
97
+ }
98
+
99
+ /** Whether it states anything at all beside its shape. */
100
+ hasAxes(): boolean {
101
+ return registered().some((name) => this.axis(name) !== undefined);
102
+ }
103
+
54
104
  with<U = T>(overrides: Partial<FieldDeclaration>): Field<U> {
55
105
  return new Field<U>({ ...this, ...overrides });
56
106
  }
107
+
108
+ /**
109
+ * What a word admits whatever its shape, and where it lands: `description` IS a JSON Schema
110
+ * keyword, `default` IS a lifecycle rule. Stated here alone, so no word writes the conversion.
111
+ * FR : ce que tout mot admet quelle que soit sa forme, et où ça atterrit.
112
+ * `text({ max: 200 }).setShared({ description: 'The title' })` → `shape.description`
113
+ */
114
+ setShared(opts?: Shared<T>): Field<T> {
115
+ if (opts === undefined) return this;
116
+
117
+ refuseLooseOptions(opts);
118
+
119
+ const overrides: Partial<FieldDeclaration> = {
120
+ ...(opts.description !== undefined
121
+ ? { shape: { ...this.shape, description: opts.description } }
122
+ : {}),
123
+ ...(opts.default !== undefined
124
+ ? { lifecycle: { ...this.lifecycle, create: { value: opts.default } } }
125
+ : {}),
126
+ };
127
+
128
+ return Object.keys(overrides).length ? this.with<T>(overrides) : this;
129
+ }
57
130
  }
@@ -0,0 +1,18 @@
1
+ import type { Axis } from '../axis/Axis.js';
2
+ import { Format } from '../lib/Format.js';
3
+ import type { JsonSchema } from '../lib/JsonSchema.js';
4
+ import { SHAPE_FORMAT } from '../axis/shape/ShapeFormat.js';
5
+
6
+ /**
7
+ * What a field may state, composed from the keys that state a format: each is carried whole and
8
+ * cited by its `$id`, so the document travels alone and a key keeps its own identity. `shape` is
9
+ * cited like the rest — it is JSON Schema, and JSON Schema is what states its keywords.
10
+ */
11
+ export function fieldFormat(axes: readonly [string, Axis][]): JsonSchema {
12
+ const document = axes.reduce(
13
+ (held, [name, axis]) => held.cites(name, axis.format),
14
+ Format.of('field').cites('shape', SHAPE_FORMAT),
15
+ );
16
+
17
+ return document.closed().schema as JsonSchema;
18
+ }
@@ -1,5 +1,5 @@
1
1
  import { Role } from '../axis/role/Role.js';
2
- import type { CompositeUnique } from '../entity/CompositeUnique.js';
2
+ import type { FieldGroups } from '../entity/FieldGroups.js';
3
3
  import { Field } from './Field.js';
4
4
  import { type FieldName } from './FieldName.js';
5
5
  import { type Fields } from './Fields.js';
@@ -17,39 +17,47 @@ export class FieldSet<TFields extends Fields = Fields> {
17
17
  * FR : partage un `unique` déclaré : un groupe d'un va au champ, plusieurs restent au schéma.
18
18
  * `declaring(fields, [['email'], ['listId', 'docId']])` → `email` carries it, the pair is a group
19
19
  */
20
+ /**
21
+ * A group of ONE is the word on the field — `unique: [['slug']]` is `unique(text())` — so it
22
+ * is written there and never kept as a group, which is what lets a derivation drop it with
23
+ * its field. A group of several is what no single field can state, and it is kept whole.
24
+ */
20
25
  static declaring<TFields extends Fields>(
21
26
  declared: TFields,
22
- unique?: CompositeUnique<TFields>,
23
- ): { fields: TFields; groups: CompositeUnique<TFields> } {
27
+ groups: { unique?: FieldGroups<TFields>; index?: FieldGroups<TFields> } = {},
28
+ ): { fields: TFields; unique: FieldGroups<TFields>; index: FieldGroups<TFields> } {
24
29
  const fields: Fields = {};
25
30
  for (const [key, field] of Object.entries(declared))
26
31
  fields[key] = new Field(field, key);
27
32
 
28
- const composite: FieldName<TFields>[][] = [];
29
- for (const group of unique ?? []) {
30
- const missing = group.filter((key) => !Object.hasOwn(fields, key));
31
- if (missing.length)
32
- throw new SchemaError(
33
- `unique: [${group.join(', ')}] names ` +
34
- `${missing.map((key) => `'${key}'`).join(', ')}, which the entity does not declare.`,
35
- );
33
+ const composite = { unique: [] as FieldName<TFields>[][], index: [] as FieldName<TFields>[][] };
34
+
35
+ for (const kind of ['unique', 'index'] as const) {
36
+ for (const group of groups[kind] ?? []) {
37
+ const missing = group.filter((key) => !Object.hasOwn(fields, key));
38
+ if (missing.length)
39
+ throw new SchemaError(
40
+ `${kind}: [${group.join(', ')}] names ` +
41
+ `${missing.map((key) => `'${key}'`).join(', ')}, which the entity does not declare.`,
42
+ );
36
43
 
37
- if (group.length === 1) {
38
- const key = group[0]!;
39
- const alone = fields[key]!;
40
- fields[key] = alone.with({ role: { ...alone.role, unique: true } });
41
- continue;
44
+ if (group.length === 1) {
45
+ const key = group[0]!;
46
+ const alone = fields[key]!;
47
+ fields[key] = alone.with({ role: { ...alone.role, [kind]: true } });
48
+ continue;
49
+ }
50
+ composite[kind].push([...group]);
42
51
  }
43
- composite.push([...group]);
44
52
  }
45
53
 
46
- return { fields: fields as TFields, groups: composite };
54
+ return { fields: fields as TFields, unique: composite.unique, index: composite.index };
47
55
  }
48
56
 
49
57
  /** A second `primary` is refused, naming both, rather than the first winning silently. */
50
58
  get primary(): FieldName<TFields> | undefined {
51
59
  const primaries = Object.entries(this.fields)
52
- .filter(([, field]) => Role.of(field).isPrimary)
60
+ .filter(([, field]) => Role.of(field).isPrimary())
53
61
  .map(([name]) => name);
54
62
 
55
63
  if (primaries.length > 1) {
package/src/index.ts CHANGED
@@ -37,11 +37,15 @@ export type { SetDiff } from './projection/card/SetDiff.js';
37
37
  export type { SetDiffOptions } from './projection/card/SetDiffOptions.js';
38
38
 
39
39
  export * from './lib/index.js';
40
- export { Formats } from './axis/shape/Formats.js';
40
+ export { StringFormats } from './axis/shape/StringFormats.js';
41
41
  export { Generators } from './axis/lifecycle/Generators.js';
42
42
  export { Boundaries } from './axis/boundary/Decoder.js';
43
- export { Clock } from './axis/lifecycle/Clock.js';
44
43
  export { type FougereEntityAdapters } from './entity/FougereEntityAdapters.js';
44
+ export { type FougereFieldAxes } from './FougereFieldAxes.js';
45
45
  export { InputValidator } from './validator/InputValidator.js';
46
46
  export { FieldValueValidator } from './validator/FieldValueValidator.js';
47
- export { AdapterFieldValidator } from './validator/AdapterFieldValidator.js';
47
+ export { JsonSchemaValidator } from './validator/JsonSchemaValidator.js';
48
+ export type { JsonSchema } from './lib/JsonSchema.js';
49
+ export { Axes } from './axis/Axes.js';
50
+ export { Format } from './lib/Format.js';
51
+ export type { Axis } from './axis/Axis.js';
@@ -0,0 +1,172 @@
1
+ import type { JsonSchema } from './JsonSchema.js';
2
+ import { SchemaError } from '../SchemaError.js';
3
+
4
+ type Stated = JsonSchema | boolean;
5
+
6
+ const HERE = 'https://fougere.dev/schema/';
7
+
8
+ const absolute = (id?: string): string | undefined =>
9
+ id === undefined || id.includes('://') ? id : `${HERE}${id}`;
10
+
11
+ /** The values a format lets in, as a type — `Accepted<typeof Lifecycle.format>`. */
12
+ export type Accepted<F> = F extends Format<infer T> ? T : never;
13
+
14
+ /**
15
+ * How a format is written here: its keys, never its JSON Schema. `$id`, `$ref`, `$defs` and the
16
+ * closing are produced, and what it accepts is carried as a TYPE, so a declaration is stated once.
17
+ * FR : on écrit les clés d'un format ; le document et le type sont produits.
18
+ * `Format.of('axis/tenancy').key('scope', Format.tokens(SCOPES)).closed()`
19
+ */
20
+ export class Format<T = unknown> {
21
+ static readonly anything = new Format<unknown>(true);
22
+ static readonly text = new Format<string>({ type: 'string' });
23
+ static readonly flag = new Format<boolean>({ type: 'boolean' });
24
+ static readonly number = new Format<number>({ type: 'number' });
25
+ static readonly count = new Format<number>({ type: 'integer', minimum: 0 });
26
+
27
+ declare readonly _admits?: T;
28
+
29
+ protected constructor(
30
+ private readonly stated: Stated,
31
+ readonly id?: string,
32
+ ) {}
33
+
34
+ /** `Format.tokens(['now', 'optional'])` → `Instance does not match any of ["now","optional"].` */
35
+ static tokens<const Words extends readonly string[]>(words: Words): Format<Words[number]> {
36
+ return new Format({ enum: [...words] });
37
+ }
38
+
39
+ /** `enum`, `required` — a list of one format, so `enum: 'draft'` is refused as the scalar it is. */
40
+ static listOf<Value>(format: Format<Value>): Format<Value[]> {
41
+ return new Format({ type: 'array', items: format.schema });
42
+ }
43
+
44
+ /**
45
+ * A list of a FIXED length, each place stating its own format — `['string', 'null']` and
46
+ * never `['string', 'number']`, which a list of one format cannot tell apart.
47
+ */
48
+ static tuple<const Places extends readonly Format<unknown>[]>(
49
+ ...places: Places
50
+ ): Format<{ -readonly [K in keyof Places]: Accepted<Places[K]> }> {
51
+ return new Format({
52
+ type: 'array',
53
+ prefixItems: places.map((place) => place.schema),
54
+ minItems: places.length,
55
+ maxItems: places.length,
56
+ });
57
+ }
58
+
59
+ /**
60
+ * A format naming ITSELF, which no value can do while it is being built: `items` holds a shape,
61
+ * and the shape is what declares `items`. The `$id` `of()` already posts is what it points at.
62
+ */
63
+ static ref(id: string): Format<unknown> {
64
+ return new Format({ $ref: absolute(id) as string });
65
+ }
66
+
67
+ /**
68
+ * A word or anything else, told apart by TYPE, so a refusal names the half the value belongs
69
+ * to — `create: 'nawak'` reads against the words, `type: ['string', 'null']` against the list.
70
+ */
71
+ static either<Word, Other>(word: Format<Word>, otherwise: Format<Other>): Format<Word | Other> {
72
+ return new Format({
73
+ if: { type: 'string' },
74
+ then: word.schema as JsonSchema,
75
+ else: otherwise.schema as JsonSchema,
76
+ });
77
+ }
78
+
79
+ /** One of these keys and no other — `{ value }` or `{ generate }`, never both and never neither. */
80
+ static exactlyOne<Keys extends Record<string, Format<unknown>>>(
81
+ keys: Keys,
82
+ ): Format<{ [K in keyof Keys]: { [P in K]: Accepted<Keys[K]> } }[keyof Keys]> {
83
+ return new Format({
84
+ type: 'object',
85
+ properties: Object.fromEntries(
86
+ Object.entries(keys).map(([key, format]) => [key, format.schema]),
87
+ ),
88
+ propertyNames: { enum: Object.keys(keys) },
89
+ minProperties: 1,
90
+ maxProperties: 1,
91
+ });
92
+ }
93
+
94
+ /** A name resolves under this project's own space; another one states its whole `$id`. */
95
+ static of(id?: string): ObjectFormat<object> {
96
+ return new ObjectFormat(absolute(id));
97
+ }
98
+
99
+ /** What `of` does for an object, for a format that is not one — `either`, a list of words. */
100
+ static named<Value>(id: string, format: Format<Value>): Format<Value> {
101
+ const named = absolute(id) as string;
102
+
103
+ return new Format({ $id: named, ...(format.schema as JsonSchema) }, named);
104
+ }
105
+
106
+ /** The type JSON Schema cannot carry: a generator's name, a thunk to an entity. */
107
+ as<Value>(): Format<Value> {
108
+ return this as unknown as Format<Value>;
109
+ }
110
+
111
+ get schema(): Stated {
112
+ return this.stated;
113
+ }
114
+ }
115
+
116
+ export class ObjectFormat<T extends object> extends Format<T> {
117
+ private readonly keys = new Map<string, Stated>();
118
+ private readonly cited = new Map<string, JsonSchema>();
119
+ private readonly needed: string[] = [];
120
+
121
+ constructor(id?: string) {
122
+ super(true, id);
123
+ }
124
+
125
+ key<Name extends string, Value>(
126
+ name: Name,
127
+ format: Format<Value>,
128
+ ): ObjectFormat<T & { [K in Name]?: Value }> {
129
+ this.keys.set(name, format.schema);
130
+
131
+ return this as ObjectFormat<T & { [K in Name]?: Value }>;
132
+ }
133
+
134
+ /** A key carried whole and pointed at by its `$id`, so it keeps an identity of its own. */
135
+ cites<Name extends string, Value>(
136
+ name: Name,
137
+ format: Format<Value>,
138
+ ): ObjectFormat<T & { [K in Name]?: Value }> {
139
+ if (!format.id) {
140
+ throw new SchemaError(
141
+ `A format cited as '${name}' states no \`$id\`, so nothing can point at it.`,
142
+ );
143
+ }
144
+ this.keys.set(name, { $ref: format.id });
145
+ this.cited.set(name, format.schema as JsonSchema);
146
+
147
+ return this as ObjectFormat<T & { [K in Name]?: Value }>;
148
+ }
149
+
150
+ needs<Name extends keyof T & string>(
151
+ ...names: Name[]
152
+ ): ObjectFormat<Omit<T, Name> & Required<Pick<T, Name>>> {
153
+ this.needed.push(...names);
154
+
155
+ return this as unknown as ObjectFormat<Omit<T, Name> & Required<Pick<T, Name>>>;
156
+ }
157
+
158
+ /** These keys and no other: a name nothing states is refused UNDER that name. */
159
+ closed(): Format<{ [K in keyof T]: T[K] }> {
160
+ return new Format(
161
+ {
162
+ ...(this.id ? { $id: this.id } : {}),
163
+ type: 'object',
164
+ properties: Object.fromEntries(this.keys),
165
+ propertyNames: { enum: [...this.keys.keys()] },
166
+ ...(this.needed.length ? { required: [...this.needed] } : {}),
167
+ ...(this.cited.size ? { $defs: Object.fromEntries(this.cited) } : {}),
168
+ },
169
+ this.id,
170
+ );
171
+ }
172
+ }
@@ -0,0 +1 @@
1
+ export type { Schema as JsonSchema } from '@cfworker/json-schema';