@smthrs/plan 1.0.0-rc.4 → 1.0.0-rc.6

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 (47) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +5 -5
  3. package/THIRD_PARTY_NOTICES.md +48 -0
  4. package/dist/cjs/KeyMaterial.d.ts +1 -1
  5. package/dist/cjs/KeyMaterial.js.map +1 -1
  6. package/dist/cjs/Node.d.ts +71 -0
  7. package/dist/cjs/Node.d.ts.map +1 -1
  8. package/dist/cjs/Node.js +7 -0
  9. package/dist/cjs/Node.js.map +2 -2
  10. package/dist/cjs/StepKey.d.ts +1 -1
  11. package/dist/cjs/StepKey.js.map +1 -1
  12. package/dist/cjs/internal/SchemaIdentity.d.ts +140 -0
  13. package/dist/cjs/internal/SchemaIdentity.d.ts.map +1 -0
  14. package/dist/cjs/internal/SchemaIdentity.js +718 -0
  15. package/dist/cjs/internal/SchemaIdentity.js.map +7 -0
  16. package/dist/cjs/internal/node.d.ts +1 -1
  17. package/dist/cjs/internal/node.d.ts.map +1 -1
  18. package/dist/cjs/internal/node.js +2 -1
  19. package/dist/cjs/internal/node.js.map +2 -2
  20. package/dist/esm/KeyMaterial.d.ts +1 -1
  21. package/dist/esm/KeyMaterial.js +1 -1
  22. package/dist/esm/KeyMaterial.js.map +1 -1
  23. package/dist/esm/Node.d.ts +71 -0
  24. package/dist/esm/Node.d.ts.map +1 -1
  25. package/dist/esm/Node.js +46 -0
  26. package/dist/esm/Node.js.map +1 -1
  27. package/dist/esm/StepKey.d.ts +1 -1
  28. package/dist/esm/StepKey.js +1 -1
  29. package/dist/esm/StepKey.js.map +1 -1
  30. package/dist/esm/internal/SchemaIdentity.d.ts +140 -0
  31. package/dist/esm/internal/SchemaIdentity.d.ts.map +1 -0
  32. package/dist/esm/internal/SchemaIdentity.js +1002 -0
  33. package/dist/esm/internal/SchemaIdentity.js.map +1 -0
  34. package/dist/esm/internal/node.d.ts +1 -1
  35. package/dist/esm/internal/node.d.ts.map +1 -1
  36. package/dist/esm/internal/node.js +3 -2
  37. package/dist/esm/internal/node.js.map +1 -1
  38. package/docs/README.md +1 -1
  39. package/docs/api.md +11 -1
  40. package/docs/concepts/authoring-ast.md +2 -1
  41. package/docs/installation.md +3 -3
  42. package/package.json +9 -8
  43. package/src/KeyMaterial.ts +1 -1
  44. package/src/Node.ts +79 -0
  45. package/src/StepKey.ts +1 -1
  46. package/src/internal/SchemaIdentity.ts +1096 -0
  47. package/src/internal/node.ts +3 -2
@@ -0,0 +1,1096 @@
1
+ /**
2
+ * The schema identity projection: the JSON-Schema-shaped document a schema
3
+ * contributes to a step key.
4
+ *
5
+ * A call node folds what its callee accepts and produces into the material a
6
+ * step key is derived from, and a catch node folds its filter the same way. A
7
+ * projection that printed differently under another `effect` release would
8
+ * re-key every recorded step in every journal at once. `effect` is a peer
9
+ * range, so the application picks the release; the projection therefore
10
+ * cannot be the installed `Schema.toJsonSchemaDocument`. `effect@4.0.4`
11
+ * already changed what that emitter prints for `Schema.Int`, `isMinLength`,
12
+ * `isPattern`, repeated bounds, `oneOf` unions and shared definitions.
13
+ *
14
+ * This module owns the bytes instead. It reads the schema through two public
15
+ * functions, `Schema.toCodecJson` and `Schema.toRepresentation`, and prints
16
+ * the result with the emitter and the check fragments fixed below, which
17
+ * reproduce `Schema.toJsonSchemaDocument(schema, options)` of
18
+ * `effect@4.0.0-rc.115` for both values of `onExcessProperty`.
19
+ *
20
+ * It is for identity only. Where a JSON Schema is shown to a model or a
21
+ * person, the installed emitter is the right one.
22
+ *
23
+ * What still moves an identity, by design: a release that changes a schema's
24
+ * JSON codec or its representation changes what the schema accepts on the
25
+ * wire. Three limits follow from what the representation carries:
26
+ *
27
+ * - A built-in check contributes the constraint its id and payload name. A
28
+ * `toJsonSchema` annotation passed to a built-in check constructor is not
29
+ * consulted; one on a `makeFilter` check is.
30
+ * - A built-in check this module does not list falls back to the installed
31
+ * check's own `toJsonSchema` annotation.
32
+ * - `Schema.brand` is not part of the representation from `effect@4.0.4` on.
33
+ * Under rc.115 a schema that takes a definition (an `identifier` or a
34
+ * recursive schema) and appeared both branded and unbranded in one document
35
+ * got two definitions; it now gets one.
36
+ *
37
+ * Provenance: {@link fromRepresentation} and its helpers are adapted from
38
+ * `effect@4.0.0-rc.115` (MIT, Copyright (c) 2023 Effectful Technologies Inc;
39
+ * the notice is in this package's `THIRD_PARTY_NOTICES.md`), files
40
+ * `src/internal/schema/toJsonSchemaDocument.ts`, `src/JsonSchema.ts`
41
+ * (`rewriteRefs`), `src/JsonPointer.ts` and `src/SchemaAST.ts` (`isJson`).
42
+ * Changes: `onExcessProperty` is the only option; one root; a built-in
43
+ * check's fragment comes from {@link builtinChecks}, not from
44
+ * the installed annotation; an approximate check result contributes its
45
+ * schema.
46
+ *
47
+ * @since 1.0.0
48
+ */
49
+
50
+ import type * as JsonSchema from "effect/JsonSchema"
51
+ import * as Schema from "effect/Schema"
52
+ import type * as SchemaAST from "effect/SchemaAST"
53
+ import * as SchemaRepresentation from "effect/SchemaRepresentation"
54
+
55
+ /** @private */
56
+ type Fragment = JsonSchema.JsonSchema
57
+
58
+ /** @private */
59
+ type Path = ReadonlyArray<string | number>
60
+
61
+ /** @private */
62
+ type Representation = SchemaRepresentation.Representation
63
+
64
+ /** @private */
65
+ type Check = SchemaRepresentation.Check
66
+
67
+ /** @private */
68
+ type CheckInput = SchemaRepresentation.ToJsonSchema.CheckInput
69
+
70
+ /**
71
+ * The document a schema's identity is: exactly what rc.115 returned from
72
+ * `Schema.toJsonSchemaDocument`, key order included.
73
+ *
74
+ * @since 1.0.0
75
+ * @private
76
+ */
77
+ export type Document = JsonSchema.Document<"draft-2020-12">
78
+
79
+ /**
80
+ * How an object schema treats a key it does not declare, which is the one
81
+ * emitter option a declaration's identity is taken under. It has rc.115's
82
+ * meaning and rc.115's default: `"ignore"` leaves such keys open, `"error"`
83
+ * closes them.
84
+ *
85
+ * @since 1.0.0
86
+ * @private
87
+ */
88
+ export interface Options {
89
+ readonly onExcessProperty?: "ignore" | "error" | undefined
90
+ }
91
+
92
+ /**
93
+ * A schema a step key cannot be derived from: one with no JSON Schema form or
94
+ * no representation, or one whose built-in check the installed `effect`
95
+ * describes differently from the releases this package supports. The message
96
+ * is the one `effect@4.0.0-rc.115` gave for the same schema, where it gave one.
97
+ *
98
+ * @since 1.0.0
99
+ * @category errors
100
+ */
101
+ export class SchemaIdentityError extends Schema.TaggedError<SchemaIdentityError>()("@smthrs/plan/SchemaIdentityError", {
102
+ message: Schema.String
103
+ }) {}
104
+
105
+ /** @private */
106
+ const invalid = (message: string, path: Path): SchemaIdentityError =>
107
+ new SchemaIdentityError({
108
+ message: path.length === 0
109
+ ? message
110
+ : `${message}\n at ${path.map((key) => `[${typeof key === "string" ? JSON.stringify(key) : key}]`).join("")}`
111
+ })
112
+
113
+ /** @private */
114
+ const isObject = (value: unknown): value is Record<string, unknown> =>
115
+ typeof value === "object" && value !== null && !Array.isArray(value)
116
+
117
+ /**
118
+ * Assigns an own property, `__proto__` included: a struct may declare a field
119
+ * by that name, and plain assignment would set the prototype instead.
120
+ *
121
+ * @private
122
+ */
123
+ const assignProperty = (target: Record<string, unknown>, key: string, value: unknown): void => {
124
+ if (key === "__proto__") {
125
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true })
126
+ } else {
127
+ target[key] = value
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Whether a value is JSON: finite numbers, plain objects, no cycles. An
133
+ * annotation that is not JSON never reaches the document.
134
+ *
135
+ * @private
136
+ */
137
+ const isJson = (value: unknown, path: ReadonlySet<object> = new Set()): boolean => {
138
+ if (typeof value !== "object" || value === null) {
139
+ return value === null || typeof value === "string" || typeof value === "boolean" ||
140
+ (typeof value === "number" && Number.isFinite(value))
141
+ }
142
+ if (path.has(value)) return false
143
+ const members = Array.isArray(value) ? Array.from<unknown>(value) : plainValues(value)
144
+ if (members === undefined) return false
145
+ const inner = new Set(path).add(value)
146
+ return members.every((member) => isJson(member, inner))
147
+ }
148
+
149
+ /** @private */
150
+ const plainValues = (value: object): ReadonlyArray<unknown> | undefined => {
151
+ const prototype: unknown = Object.getPrototypeOf(value)
152
+ // A plain object from another realm has a different `Object.prototype`,
153
+ // but that prototype still has a null prototype.
154
+ return prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null
155
+ ? Object.values(value)
156
+ : undefined
157
+ }
158
+
159
+ /**
160
+ * Structural equality of two emitted schemas, ignoring key order. `NaN`
161
+ * equals itself: an author's fragment may carry one, and two definitions that
162
+ * differ in nothing else are one definition.
163
+ *
164
+ * @private
165
+ */
166
+ const sameJson = (left: unknown, right: unknown): boolean => {
167
+ if (Array.isArray(left) && Array.isArray(right)) {
168
+ return left.length === right.length && left.every((member, index) => sameJson(member, right[index]))
169
+ }
170
+ if (isObject(left) && isObject(right)) {
171
+ const keys = Object.keys(left)
172
+ return keys.length === Object.keys(right).length &&
173
+ keys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]))
174
+ }
175
+ return left === right || (left !== left && right !== right)
176
+ }
177
+
178
+ /** @private */
179
+ const escapeRegExp = (text: string): string => text.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&")
180
+
181
+ /** @private */
182
+ const unescapeToken = (token: string): string => token.replace(/~1/g, "/").replace(/~0/g, "~")
183
+
184
+ /** @private */
185
+ const definitionReference = (key: string): string =>
186
+ `#/$defs/${encodeURI(key.replace(/~/g, "~0").replace(/\//g, "~1")).replace(/#/g, "%23")}`
187
+
188
+ /**
189
+ * The JSON Pointer a URI fragment names, or `undefined` when it names none.
190
+ *
191
+ * @private
192
+ */
193
+ const decodeUriFragment = (fragment: string): string | undefined => {
194
+ if (fragment.length === 0 || fragment === "#") return ""
195
+ if (!fragment.startsWith("#")) return undefined
196
+ const encoded = fragment.slice(1)
197
+ try {
198
+ if (encodeURI(encoded).replace(/%25/g, "%").replace(/#/g, "%23") !== encoded) return undefined
199
+ const pointer = decodeURIComponent(encoded)
200
+ return pointer.startsWith("/") && !/~(?:[^01]|$)/.test(pointer) ? pointer : undefined
201
+ } catch {
202
+ return undefined
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Copies a schema, rewriting every `$ref` and `$dynamicRef` in a schema
208
+ * position. Values that are data (`enum`, `const`, `default`, `examples`) are
209
+ * not walked.
210
+ *
211
+ * @private
212
+ */
213
+ const rewriteRefs = (node: Fragment, rewrite: (reference: string) => string): Fragment => {
214
+ const walk = (value: unknown): unknown => isObject(value) ? rewriteRefs(value, rewrite) : value
215
+ const out: Fragment = {}
216
+ for (const key of Object.keys(node)) {
217
+ const value = node[key]
218
+ let transformed = value
219
+ switch (key) {
220
+ case "$defs":
221
+ case "properties":
222
+ case "patternProperties":
223
+ case "dependentSchemas":
224
+ if (isObject(value)) {
225
+ const mapped: Fragment = {}
226
+ for (const name of Object.keys(value)) assignProperty(mapped, name, walk(value[name]))
227
+ transformed = mapped
228
+ }
229
+ break
230
+ case "allOf":
231
+ case "anyOf":
232
+ case "oneOf":
233
+ case "prefixItems":
234
+ if (Array.isArray(value)) transformed = value.map(walk)
235
+ break
236
+ case "not":
237
+ case "additionalProperties":
238
+ case "propertyNames":
239
+ case "unevaluatedProperties":
240
+ case "items":
241
+ case "contains":
242
+ case "unevaluatedItems":
243
+ case "if":
244
+ case "then":
245
+ case "else":
246
+ case "contentSchema":
247
+ transformed = walk(value)
248
+ }
249
+ assignProperty(out, key, transformed)
250
+ }
251
+ if (typeof out["$ref"] === "string") out["$ref"] = rewrite(out["$ref"])
252
+ if (typeof out["$dynamicRef"] === "string") out["$dynamicRef"] = rewrite(out["$dynamicRef"])
253
+ return out
254
+ }
255
+
256
+ /**
257
+ * The annotations a JSON Schema states, in the order rc.115 printed them.
258
+ *
259
+ * @private
260
+ */
261
+ const ordinaryAnnotations = (annotations: Schema.Annotations.Annotations | undefined): Fragment | undefined => {
262
+ if (annotations === undefined) return undefined
263
+ const out: Fragment = {}
264
+ if (typeof annotations["title"] === "string") out["title"] = annotations["title"]
265
+ if (typeof annotations["description"] === "string") out["description"] = annotations["description"]
266
+ if (isJson(annotations["default"])) out["default"] = annotations["default"]
267
+ if (Array.isArray(annotations["examples"]) && isJson(annotations["examples"])) {
268
+ out["examples"] = annotations["examples"]
269
+ }
270
+ if (typeof annotations["readOnly"] === "boolean") out["readOnly"] = annotations["readOnly"]
271
+ if (typeof annotations["writeOnly"] === "boolean") out["writeOnly"] = annotations["writeOnly"]
272
+ if (typeof annotations["format"] === "string") out["format"] = annotations["format"]
273
+ if (typeof annotations["contentEncoding"] === "string") out["contentEncoding"] = annotations["contentEncoding"]
274
+ if (typeof annotations["contentMediaType"] === "string") out["contentMediaType"] = annotations["contentMediaType"]
275
+ if (isJson(annotations["contentSchema"])) out["contentSchema"] = annotations["contentSchema"]
276
+ return Object.keys(out).length === 0 ? undefined : out
277
+ }
278
+
279
+ /** @private */
280
+ const finitePattern = "[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?"
281
+
282
+ /** @private */
283
+ const stringPattern = "[\\s\\S]*?"
284
+
285
+ /** @private */
286
+ const bigIntPattern = "^-?\\d+$"
287
+
288
+ /** @private */
289
+ const symbolPattern = "^Symbol\\(([\\s\\S]*)\\)$"
290
+
291
+ /** @private */
292
+ const uuidPattern = (version: number | null): string =>
293
+ version === null
294
+ ? "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|" +
295
+ "00000000-0000-0000-0000-000000000000|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$"
296
+ : `^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`
297
+
298
+ /**
299
+ * One field of a built-in check's payload. A release that renamed or retyped
300
+ * it would otherwise print `undefined` into an identity, so it is refused.
301
+ *
302
+ * @private
303
+ */
304
+ const field = <A>(id: string, payload: unknown, name: string, is: (value: unknown) => value is A): A => {
305
+ const value = isObject(payload) ? payload[name] : undefined
306
+ if (!is(value)) {
307
+ throw new SchemaIdentityError({
308
+ message: `Cannot derive a schema identity: check ${id} carries no usable "${name}" in its payload. ` +
309
+ "The installed effect release describes this check differently from the releases @smthrs/plan supports."
310
+ })
311
+ }
312
+ return value
313
+ }
314
+
315
+ /** @private */
316
+ const isNumber = (value: unknown): value is number => typeof value === "number"
317
+
318
+ /** @private */
319
+ const isString = (value: unknown): value is string => typeof value === "string"
320
+
321
+ /** @private */
322
+ const isVersion = (value: unknown): value is number | null => value === null || typeof value === "number"
323
+
324
+ /** @private */
325
+ const isFlag = (value: unknown): value is boolean | undefined => value === undefined || typeof value === "boolean"
326
+
327
+ /**
328
+ * What a built-in check prints: its payload and the JSON type it refines in,
329
+ * a fragment out. `null` is a check that prints nothing.
330
+ *
331
+ * @private
332
+ */
333
+ type BuiltinCheck = ((id: string, payload: unknown, input: CheckInput) => Fragment) | null
334
+
335
+ /** @private */
336
+ const pattern = (source: string): BuiltinCheck => () => ({ pattern: source })
337
+
338
+ /** @private */
339
+ const literalPattern = (name: string, source: (escaped: string) => string): BuiltinCheck => (id, payload) => ({
340
+ pattern: new RegExp(source(escapeRegExp(field(id, payload, name, isString)))).source
341
+ })
342
+
343
+ /** @private */
344
+ const bound = (keyword: string, name: string = keyword): BuiltinCheck => (id, payload) => ({
345
+ [keyword]: field(id, payload, name, isNumber)
346
+ })
347
+
348
+ /** @private */
349
+ const length = (name: string, array: string, string: string): BuiltinCheck => (id, payload, input) => ({
350
+ [input.type === "array" ? array : string]: field(id, payload, name, isNumber)
351
+ })
352
+
353
+ /** @private */
354
+ const empty: BuiltinCheck = () => ({})
355
+
356
+ /**
357
+ * Every built-in check, keyed by the id its representation carries, with the
358
+ * fragment `effect@4.0.0-rc.115` printed for it.
359
+ *
360
+ * The three code-point checks did not exist under rc.115; they print what
361
+ * `effect@4.0.4` introduced them with. The date, big integer and size checks
362
+ * print nothing but still carry their own annotations, which is what the
363
+ * empty fragment keeps.
364
+ *
365
+ * @private
366
+ */
367
+ const builtinChecks: ReadonlyMap<string, BuiltinCheck> = new Map<string, BuiltinCheck>([
368
+ ["effect/schema/isTrimmed", pattern("^\\S[\\s\\S]*\\S$|^\\S$|^$")],
369
+ ["effect/schema/isPattern", (id, payload) => ({ pattern: field(id, payload, "source", isString) })],
370
+ ["effect/schema/isStringFinite", pattern(`^${finitePattern}$`)],
371
+ ["effect/schema/isStringBigInt", pattern(bigIntPattern)],
372
+ ["effect/schema/isStringSymbol", pattern(symbolPattern)],
373
+ ["effect/schema/isUUID", (id, payload) => ({
374
+ pattern: uuidPattern(field(id, payload, "version", isVersion)),
375
+ format: "uuid"
376
+ })],
377
+ ["effect/schema/isGUID", pattern("^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")],
378
+ ["effect/schema/isULID", pattern("^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$")],
379
+ ["effect/schema/isBase64", pattern("^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$")],
380
+ ["effect/schema/isBase64Url", pattern("^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$")],
381
+ ["effect/schema/isStartingWith", literalPattern("startsWith", (escaped) => `^${escaped}`)],
382
+ ["effect/schema/isEndingWith", literalPattern("endsWith", (escaped) => `${escaped}$`)],
383
+ ["effect/schema/isIncluding", literalPattern("includes", (escaped) => escaped)],
384
+ ["effect/schema/isUppercased", pattern("^[^a-z]*$")],
385
+ ["effect/schema/isLowercased", pattern("^[^A-Z]*$")],
386
+ ["effect/schema/isCapitalized", pattern("^(?:[^a-z][\\s\\S]*)?$")],
387
+ ["effect/schema/isUncapitalized", pattern("^(?:[^A-Z][\\s\\S]*)?$")],
388
+ ["effect/schema/isFinite", () => ({ type: "number" })],
389
+ ["effect/schema/isInt", () => ({ type: "integer" })],
390
+ ["effect/schema/isGreaterThan", bound("exclusiveMinimum")],
391
+ ["effect/schema/isGreaterThanOrEqualTo", bound("minimum")],
392
+ ["effect/schema/isLessThan", bound("exclusiveMaximum")],
393
+ ["effect/schema/isLessThanOrEqualTo", bound("maximum")],
394
+ ["effect/schema/isBetween", (id, payload) => ({
395
+ [field(id, payload, "exclusiveMinimum", isFlag) ? "exclusiveMinimum" : "minimum"]: field(
396
+ id,
397
+ payload,
398
+ "minimum",
399
+ isNumber
400
+ ),
401
+ [field(id, payload, "exclusiveMaximum", isFlag) ? "exclusiveMaximum" : "maximum"]: field(
402
+ id,
403
+ payload,
404
+ "maximum",
405
+ isNumber
406
+ )
407
+ })],
408
+ ["effect/schema/isMultipleOf", bound("multipleOf", "divisor")],
409
+ ["effect/schema/isGreaterThanDate", empty],
410
+ ["effect/schema/isGreaterThanOrEqualToDate", empty],
411
+ ["effect/schema/isLessThanDate", empty],
412
+ ["effect/schema/isLessThanOrEqualToDate", empty],
413
+ ["effect/schema/isBetweenDate", empty],
414
+ ["effect/schema/isGreaterThanBigInt", empty],
415
+ ["effect/schema/isGreaterThanOrEqualToBigInt", empty],
416
+ ["effect/schema/isLessThanBigInt", empty],
417
+ ["effect/schema/isLessThanOrEqualToBigInt", empty],
418
+ ["effect/schema/isBetweenBigInt", empty],
419
+ ["effect/schema/isMinLength", length("minLength", "minItems", "minLength")],
420
+ ["effect/schema/isMaxLength", length("maxLength", "maxItems", "maxLength")],
421
+ ["effect/schema/isBetweenLength", (id, payload, input) => {
422
+ const minimum = field(id, payload, "minimum", isNumber)
423
+ const maximum = field(id, payload, "maximum", isNumber)
424
+ return input.type === "array"
425
+ ? { allOf: [{ minItems: minimum }, { maxItems: maximum }] }
426
+ : { allOf: [{ minLength: minimum }, { maxLength: maximum }] }
427
+ }],
428
+ ["effect/schema/isMinCodePoints", bound("minLength", "minCodePoints")],
429
+ ["effect/schema/isMaxCodePoints", bound("maxLength", "maxCodePoints")],
430
+ ["effect/schema/isBetweenCodePoints", (id, payload) => ({
431
+ allOf: [
432
+ { minLength: field(id, payload, "minimum", isNumber) },
433
+ { maxLength: field(id, payload, "maximum", isNumber) }
434
+ ]
435
+ })],
436
+ ["effect/schema/isMinSize", empty],
437
+ ["effect/schema/isMaxSize", empty],
438
+ ["effect/schema/isBetweenSize", empty],
439
+ ["effect/schema/isMinProperties", bound("minProperties")],
440
+ ["effect/schema/isMaxProperties", bound("maxProperties")],
441
+ ["effect/schema/isBetweenProperties", (id, payload) => ({
442
+ minProperties: field(id, payload, "minimum", isNumber),
443
+ maxProperties: field(id, payload, "maximum", isNumber)
444
+ })],
445
+ ["effect/schema/isPropertyNames", (_id, _payload, input) => ({ propertyNames: input.schemas[0] })],
446
+ ["effect/schema/isUnique", () => ({ uniqueItems: true })],
447
+ ["effect/schema/isUniqueKey", null]
448
+ ])
449
+
450
+ /**
451
+ * The ids {@link builtinChecks} lists, for the test that holds the list
452
+ * against the checks the installed `effect` can revive.
453
+ *
454
+ * @since 1.0.0
455
+ * @private
456
+ */
457
+ export const builtinCheckIds: ReadonlyArray<string> = [...builtinChecks.keys()]
458
+
459
+ /** @private */
460
+ const isCheckCallback = (value: unknown): value is SchemaRepresentation.ToJsonSchema.Check =>
461
+ typeof value === "function"
462
+
463
+ /** @private */
464
+ const isApproximate = (
465
+ output: SchemaRepresentation.ToJsonSchema.CheckOutput
466
+ ): output is readonly [schema: Fragment, approximate: true] => Array.isArray(output)
467
+
468
+ /**
469
+ * What prints a check, if anything does: the owned fragment for a built-in
470
+ * check, otherwise the author's own `toJsonSchema` annotation.
471
+ *
472
+ * @private
473
+ */
474
+ const fragmentSource = (check: Check): ((input: CheckInput) => Fragment) | undefined => {
475
+ const representation = check.representation
476
+ const builtin = representation === undefined ? undefined : builtinChecks.get(representation.id)
477
+ if (representation !== undefined && builtin !== undefined) {
478
+ return builtin === null ? undefined : (input) => builtin(representation.id, representation.payload, input)
479
+ }
480
+ const callback = check.annotations?.["toJsonSchema"]
481
+ if (!isCheckCallback(callback)) return undefined
482
+ return (input) => {
483
+ const output = callback(input)
484
+ return isApproximate(output) ? output[0] : output
485
+ }
486
+ }
487
+
488
+ /** @private */
489
+ type NumberType = "number" | "integer"
490
+
491
+ /** @private */
492
+ const extractNumberType = (schema: Fragment): { readonly type: NumberType | undefined; readonly schema: Fragment } => {
493
+ let type: NumberType | undefined = schema["type"] === "number" || schema["type"] === "integer"
494
+ ? schema["type"]
495
+ : undefined
496
+ let out = schema
497
+ if (type !== undefined) {
498
+ out = { ...schema }
499
+ delete out["type"]
500
+ }
501
+ const allOf = out["allOf"]
502
+ if (Array.isArray(allOf)) {
503
+ const members: Array<Fragment> = []
504
+ let changed = false
505
+ for (const member of allOf) {
506
+ const extracted = extractNumberType(member)
507
+ if (extracted.type !== undefined) {
508
+ changed = true
509
+ if (type === undefined || extracted.type === "integer") type = extracted.type
510
+ }
511
+ if (Object.keys(extracted.schema).length > 0) members.push(extracted.schema)
512
+ }
513
+ if (changed) {
514
+ const { allOf: _, ...rest } = out
515
+ out = members.length === 0 ? rest : { ...rest, allOf: members }
516
+ }
517
+ }
518
+ return { type, schema: out }
519
+ }
520
+
521
+ /** @private */
522
+ const isNumberEncoding = (schema: Fragment): boolean => {
523
+ const anyOf = schema["anyOf"]
524
+ if (!Array.isArray(anyOf) || anyOf.length !== 2 || !isObject(anyOf[0]) || anyOf[0]["type"] !== "number") return false
525
+ const special: unknown = anyOf[1]
526
+ if (!isObject(special) || special["type"] !== "string") return false
527
+ const values = special["enum"]
528
+ return Array.isArray(values) && values.length === 3 && values.includes("NaN") && values.includes("Infinity") &&
529
+ values.includes("-Infinity")
530
+ }
531
+
532
+ // Keep this allowlist closed: applicators and dependent keywords can change meaning when moved across schema objects.
533
+ /** @private */
534
+ const inlineableCheckKeywords =
535
+ "|type|format|pattern|multipleOf|minimum|maximum|exclusiveMinimum|exclusiveMaximum|minLength|maxLength|minItems|maxItems|uniqueItems|minProperties|maxProperties|propertyNames|"
536
+
537
+ // `format` and `content*` can affect validation, so they are not treated as pure annotations.
538
+ /** @private */
539
+ const promotableAnnotationKeywords = "|title|description|default|examples|readOnly|writeOnly|"
540
+
541
+ /** @private */
542
+ const inlineableAnnotatedCheckKeywords = inlineableCheckKeywords + promotableAnnotationKeywords
543
+
544
+ /** @private */
545
+ const hasOnlyKeywords = (schema: Fragment, allowed: string): boolean =>
546
+ Object.keys(schema).every((key) => allowed.includes(`|${key}|`))
547
+
548
+ /** @private */
549
+ const hasNoCollisions = (left: Fragment, rightKeys: ReadonlyArray<string>): boolean =>
550
+ typeof left["$ref"] !== "string" && rightKeys.every((key) => !Object.hasOwn(left, key))
551
+
552
+ /**
553
+ * Adds a constraint to a schema: inline when no keyword collides, otherwise
554
+ * under `allOf`. A repeated bound is never merged into the tighter one.
555
+ *
556
+ * @private
557
+ */
558
+ const appendJsonSchema = (left: Fragment, right: Fragment, inlineCheck?: true): Fragment => {
559
+ if (Object.keys(left).length === 0) return right
560
+ const rightKeys = Object.keys(right)
561
+ if (rightKeys.length === 0) return left
562
+ const leftType = left["type"] === "number" || left["type"] === "integer" ? left["type"] : undefined
563
+ const numberEncoding = isNumberEncoding(left)
564
+ if (leftType !== undefined || numberEncoding) {
565
+ const extracted = extractNumberType(right)
566
+ if (extracted.type !== undefined) {
567
+ const type = leftType === "integer" || extracted.type === "integer" ? "integer" : "number"
568
+ const base: Fragment = { ...left, type }
569
+ if (numberEncoding) delete base["anyOf"]
570
+ const extractedKeys = Object.keys(extracted.schema)
571
+ if (extractedKeys.length === 0) return base
572
+ return hasOnlyKeywords(extracted.schema, promotableAnnotationKeywords) && hasNoCollisions(base, extractedKeys)
573
+ ? { ...base, ...extracted.schema }
574
+ : appendJsonSchema(base, extracted.schema, inlineCheck)
575
+ }
576
+ }
577
+ if (inlineCheck && hasNoCollisions(left, rightKeys)) return { ...left, ...right }
578
+ const rightAllOf = right["allOf"]
579
+ const members: ReadonlyArray<unknown> = Array.isArray(rightAllOf) && rightKeys.length === 1 ? rightAllOf : [right]
580
+ const leftAllOf = left["allOf"]
581
+ if (Array.isArray(leftAllOf)) return { ...left, allOf: [...leftAllOf, ...members] }
582
+ if (typeof left["$ref"] === "string") return { allOf: [left, ...members] }
583
+ return { ...left, allOf: members }
584
+ }
585
+
586
+ /** @private */
587
+ const jsonSchemaTypes: ReadonlySet<unknown> = new Set([
588
+ "string",
589
+ "number",
590
+ "boolean",
591
+ "array",
592
+ "object",
593
+ "null",
594
+ "integer"
595
+ ])
596
+
597
+ /** @private */
598
+ const isJsonSchemaType = (input: unknown): input is JsonSchema.Type => jsonSchemaTypes.has(input)
599
+
600
+ /**
601
+ * Collapses a union of single-type enums into one enum, the way a union of
602
+ * literals reads.
603
+ *
604
+ * @private
605
+ */
606
+ const compactEnums = (schemas: ReadonlyArray<Fragment>): Fragment | undefined => {
607
+ let sharedType: unknown = undefined
608
+ const values: Array<unknown> = []
609
+ for (const schema of schemas) {
610
+ const members = schema["enum"]
611
+ if (
612
+ Object.keys(schema).length !== 2 || schema["type"] === undefined || !Array.isArray(members) ||
613
+ members.length === 0
614
+ ) {
615
+ return undefined
616
+ }
617
+ if (sharedType === undefined) sharedType = schema["type"]
618
+ else if (schema["type"] !== sharedType) return undefined
619
+ for (const value of members) {
620
+ if (values.includes(value)) return undefined
621
+ values.push(value)
622
+ }
623
+ }
624
+ return { type: sharedType, enum: values }
625
+ }
626
+
627
+ /** @private */
628
+ const partPattern = (part: Representation): string => {
629
+ switch (part._tag) {
630
+ case "Literal":
631
+ return escapeRegExp(String(part.literal))
632
+ case "String":
633
+ return stringPattern
634
+ case "Number":
635
+ return finitePattern
636
+ case "TemplateLiteral":
637
+ return part.parts.map(partPattern).join("")
638
+ case "Union":
639
+ return `(?:${part.types.map(partPattern).join("|")})`
640
+ default:
641
+ throw invalid("Invalid schema representation document", [])
642
+ }
643
+ }
644
+
645
+ /** @private */
646
+ const neverSchema: Fragment = { not: {} }
647
+
648
+ /**
649
+ * Refuses a representation kind this module does not print. A later `effect`
650
+ * release that adds one fails here by name, not as a missing property.
651
+ *
652
+ * @private
653
+ */
654
+ const unsupported = (representation: never, path: Path): never => {
655
+ throw invalid(`Unsupported schema representation ${(representation as { readonly _tag: string })._tag}`, path)
656
+ }
657
+
658
+ /**
659
+ * Prints a representation document as the JSON Schema document rc.115
660
+ * printed for it under the same options.
661
+ *
662
+ * Reserved for {@link schemaIdentity} and for the tests that hand it a
663
+ * representation no schema produces.
664
+ *
665
+ * @since 1.0.0
666
+ * @private
667
+ */
668
+ export const fromRepresentation = (document: SchemaRepresentation.Document, options?: Options): Document => {
669
+ const closed = options?.onExcessProperty === "error"
670
+ const references = document.references
671
+ // A key maps to its compiled schema, to the key it is an alias of, or to
672
+ // `null` while it is compiling.
673
+ const definitionStates = new Map<string, Fragment | string | null>()
674
+ const compiledRepresentations = new WeakMap<Representation, Fragment>()
675
+ const fallbackDefinitions = new Map<string, Array<string>>()
676
+
677
+ const compileDefinition = (key: string, path: Path): string => {
678
+ const compiled = definitionStates.get(key)
679
+ if (compiled !== undefined) return typeof compiled === "string" ? compiled : key
680
+ const representation = Object.hasOwn(references, key) ? references[key] : undefined
681
+ if (representation === undefined) throw invalid(`Invalid reference ${key}`, [...path, "$ref"])
682
+
683
+ definitionStates.set(key, null)
684
+ const schema = recur(representation, ["references", key])
685
+
686
+ const fallback = identifierFallback(representation)
687
+ if (fallback !== undefined) {
688
+ const candidates = fallbackDefinitions.get(fallback)
689
+ const match = candidates?.find((candidate) => sameJson(definitionStates.get(candidate), schema))
690
+ if (match === undefined) {
691
+ if (candidates === undefined) fallbackDefinitions.set(fallback, [key])
692
+ else candidates.push(key)
693
+ } else {
694
+ definitionStates.set(key, match)
695
+ return match
696
+ }
697
+ }
698
+ definitionStates.set(key, schema)
699
+ return key
700
+ }
701
+
702
+ const finalize = (schema: Fragment): Fragment =>
703
+ rewriteRefs(schema, (reference) => {
704
+ const pointer = decodeUriFragment(reference)
705
+ if (pointer === undefined) {
706
+ if (/^#(?:\/|%2f)/i.test(reference)) {
707
+ throw new SchemaIdentityError({ message: `Invalid JSON Pointer URI fragment ${JSON.stringify(reference)}` })
708
+ }
709
+ return reference
710
+ }
711
+ if (!pointer.startsWith("/$defs/")) return reference
712
+ const separator = pointer.indexOf("/", 7)
713
+ const canonical = definitionStates.get(unescapeToken(pointer.slice(7, separator < 0 ? undefined : separator)))
714
+ if (typeof canonical !== "string") return reference
715
+ // URI-encoded slashes separate pointer tokens; only ~1 represents a slash within a token.
716
+ return reference.replace(/^#(?:\/|%2f).*?(?:\/|%2f).*?(?=\/|%2f|$)/i, definitionReference(canonical))
717
+ })
718
+
719
+ const identifierFallback = (representation: Representation): string | undefined => {
720
+ if (representation._tag === "Reference") return undefined
721
+ const last = representation.checks.at(-1)
722
+ const annotations = last === undefined ? representation.annotations : last.annotations
723
+ const fallback = annotations?.["~identifier"]
724
+ return typeof annotations?.["identifier"] !== "string" && typeof fallback === "string" ? fallback : undefined
725
+ }
726
+
727
+ const compileCheck = (
728
+ check: Check,
729
+ type: JsonSchema.Type | undefined,
730
+ path: Path
731
+ ): readonly [schema: Fragment, inline?: true] | undefined => {
732
+ const source = fragmentSource(check)
733
+ if (source !== undefined) {
734
+ const schemas = check.representation?.schemas?.map((schema, index) =>
735
+ recur(schema, [...path, "representation", "schemas", index])
736
+ ) ?? []
737
+ const fragment = source({ type, schemas })
738
+ const ordinary = ordinaryAnnotations(check.annotations)
739
+ const schema = ordinary === undefined ? fragment : { ...fragment, ...ordinary }
740
+ const allowed = ordinary === undefined ? inlineableCheckKeywords : inlineableAnnotatedCheckKeywords
741
+ return check._tag === "Filter" && hasOnlyKeywords(schema, allowed) &&
742
+ (ordinary === undefined || hasOnlyKeywords(ordinary, promotableAnnotationKeywords))
743
+ ? [schema, true]
744
+ : [schema]
745
+ }
746
+ if (check._tag === "Filter") return undefined
747
+
748
+ const allOf: Array<Fragment> = []
749
+ check.checks.forEach((child, index) => {
750
+ const compiled = compileCheck(child, type, [...path, "checks", index])
751
+ if (compiled !== undefined) allOf.push(compiled[0])
752
+ })
753
+ if (allOf.length === 0) return undefined
754
+ const ordinary = ordinaryAnnotations(check.annotations)
755
+ return [ordinary === undefined ? { allOf } : { allOf, ...ordinary }]
756
+ }
757
+
758
+ const recur = (representation: Representation, path: Path): Fragment => {
759
+ if (representation._tag === "Reference") {
760
+ return { $ref: definitionReference(compileDefinition(representation.$ref, path)) }
761
+ }
762
+ const cached = compiledRepresentations.get(representation)
763
+ if (cached !== undefined) return cached
764
+
765
+ let output = on(representation, path)
766
+ const ordinary = ordinaryAnnotations(representation.annotations)
767
+ if (ordinary !== undefined) output = { ...output, ...ordinary }
768
+ representation.checks.forEach((check, index) => {
769
+ const compiled = compileCheck(
770
+ check,
771
+ isJsonSchemaType(output["type"]) ? output["type"] : undefined,
772
+ [...path, "checks", index]
773
+ )
774
+ if (compiled !== undefined) output = appendJsonSchema(output, ...compiled)
775
+ })
776
+ compiledRepresentations.set(representation, output)
777
+ return output
778
+ }
779
+
780
+ const annotated = (compiled: Fragment, annotations: Schema.Annotations.Annotations | undefined): Fragment => {
781
+ const ordinary = ordinaryAnnotations(annotations)
782
+ return ordinary === undefined ? compiled : appendJsonSchema(compiled, ordinary)
783
+ }
784
+
785
+ const on = (representation: Exclude<Representation, SchemaRepresentation.Reference>, path: Path): Fragment => {
786
+ switch (representation._tag) {
787
+ case "Any":
788
+ case "Unknown":
789
+ case "Declaration":
790
+ return {}
791
+ case "ObjectKeyword":
792
+ return { anyOf: [{ type: "object" }, { type: "array" }] }
793
+ case "Void":
794
+ case "Undefined":
795
+ case "Null":
796
+ return { type: "null" }
797
+ case "BigInt":
798
+ return { type: "string", allOf: [{ pattern: bigIntPattern }] }
799
+ case "Symbol":
800
+ return { type: "string", allOf: [{ pattern: symbolPattern }] }
801
+ case "UniqueSymbol":
802
+ return Symbol.keyFor(representation.symbol) === undefined
803
+ ? { not: {} }
804
+ : { type: "string", enum: [String(representation.symbol)] }
805
+ case "Suspend":
806
+ return recur(representation.thunk, [...path, "thunk"])
807
+ case "Never":
808
+ return { not: {} }
809
+ case "String":
810
+ return { type: "string" }
811
+ case "Number":
812
+ return { anyOf: [{ type: "number" }, { type: "string", enum: ["NaN", "Infinity", "-Infinity"] }] }
813
+ case "Boolean":
814
+ return { type: "boolean" }
815
+ case "Literal": {
816
+ const literal = representation.literal
817
+ return typeof literal === "bigint"
818
+ ? { type: "string", enum: [String(literal)] }
819
+ : { type: typeof literal, enum: [literal] }
820
+ }
821
+ case "Enum": {
822
+ const types = representation.enums.map(([title, literal], index) => {
823
+ if (typeof literal === "number" && !Number.isFinite(literal)) {
824
+ throw invalid(`Invalid numeric enum value ${String(literal)}`, [...path, "enums", index, 1])
825
+ }
826
+ return { type: typeof literal, enum: [literal], title }
827
+ })
828
+ return types.length === 0 ? { not: {} } : { anyOf: types }
829
+ }
830
+ case "TemplateLiteral":
831
+ return { type: "string", pattern: `^${representation.parts.map(partPattern).join("")}$` }
832
+ case "Arrays": {
833
+ const rest = representation.rest[0]
834
+ if (representation.rest.length > 1) {
835
+ throw invalid("Invalid schema representation document", [...path, "rest"])
836
+ }
837
+ const out: Fragment = { type: "array" }
838
+ let minItems = representation.elements.length
839
+ const prefixItems = representation.elements.map((element, index) => {
840
+ if (element.isOptional) minItems--
841
+ return annotated(recur(element.type, [...path, "elements", index, "type"]), element.annotations)
842
+ })
843
+ if (prefixItems.length > 0) {
844
+ out["prefixItems"] = prefixItems
845
+ out["maxItems"] = representation.elements.length
846
+ if (minItems > 0) out["minItems"] = minItems
847
+ } else {
848
+ out["items"] = false
849
+ }
850
+ if (rest !== undefined) {
851
+ delete out["maxItems"]
852
+ const items = recur(rest, [...path, "rest", 0])
853
+ if (Object.keys(items).length > 0) out["items"] = items
854
+ else delete out["items"]
855
+ }
856
+ return out
857
+ }
858
+ case "Objects": {
859
+ if (representation.propertySignatures.length === 0 && representation.indexSignatures.length === 0) {
860
+ return { not: { type: "null" } }
861
+ }
862
+ const out: Fragment = { type: "object" }
863
+ const properties: Record<string, Fragment> = {}
864
+ const required: Array<string> = []
865
+ representation.propertySignatures.forEach((property, index) => {
866
+ const name = property.name
867
+ if (typeof name !== "string") {
868
+ throw invalid("Invalid schema representation document", [...path, "propertySignatures", index, "name"])
869
+ }
870
+ assignProperty(
871
+ properties,
872
+ name,
873
+ annotated(recur(property.type, [...path, "propertySignatures", index, "type"]), property.annotations)
874
+ )
875
+ if (!property.isOptional) required.push(name)
876
+ })
877
+ if (representation.propertySignatures.length > 0) out["properties"] = properties
878
+ if (required.length > 0) out["required"] = required
879
+ const patternProperties: Record<string, Fragment | false> = {}
880
+ const additionalPropertySchemas: Array<Fragment | false> = []
881
+ const indexValueSchemas: Array<Fragment | false> = []
882
+ let hasUnrepresentableIndexPattern = false
883
+ representation.indexSignatures.forEach((signature, index) => {
884
+ const compiled = recur(signature.type, [...path, "indexSignatures", index, "type"])
885
+ const type = sameJson(compiled, neverSchema) ? false : compiled
886
+ indexValueSchemas.push(type)
887
+ const patterns = parameterPatterns(
888
+ signature.parameter,
889
+ [...path, "indexSignatures", index, "parameter"],
890
+ new Set()
891
+ )
892
+ if (patterns === undefined) {
893
+ hasUnrepresentableIndexPattern = true
894
+ } else if (patterns.length === 0) {
895
+ additionalPropertySchemas.push(type)
896
+ } else {
897
+ for (const key of patterns) {
898
+ const previous = Object.hasOwn(patternProperties, key) ? patternProperties[key] : undefined
899
+ assignProperty(
900
+ patternProperties,
901
+ key,
902
+ previous === undefined
903
+ ? type
904
+ : previous === false || type === false
905
+ ? false
906
+ : appendJsonSchema(previous, type)
907
+ )
908
+ }
909
+ }
910
+ })
911
+ const hasPatternProperties = Object.keys(patternProperties).length > 0
912
+ if (hasPatternProperties) out["patternProperties"] = patternProperties
913
+ const additional = additionalPropertySchemas.map((type) => ({ type: "object", additionalProperties: type }))
914
+ if (hasUnrepresentableIndexPattern && closed) {
915
+ const propertyNames: Array<Fragment> = []
916
+ if (representation.propertySignatures.length > 0) propertyNames.push({ enum: Object.keys(properties) })
917
+ representation.indexSignatures.forEach((signature, index) => {
918
+ propertyNames.push(recur(signature.parameter, [...path, "indexSignatures", index, "parameter"]))
919
+ })
920
+ out["propertyNames"] = propertyNames.length === 1 ? propertyNames[0] : { anyOf: propertyNames }
921
+ out["additionalProperties"] = indexValueSchemas.length === 1
922
+ ? indexValueSchemas[0]
923
+ : { anyOf: indexValueSchemas }
924
+ if (additional.length > 0) out["allOf"] = additional
925
+ } else if (additional.length === 0) {
926
+ out["additionalProperties"] = hasUnrepresentableIndexPattern || !closed
927
+ } else if (
928
+ additionalPropertySchemas.length === 1 && representation.propertySignatures.length === 0 &&
929
+ !hasPatternProperties
930
+ ) {
931
+ out["additionalProperties"] = additionalPropertySchemas[0]
932
+ } else {
933
+ out["allOf"] = additional
934
+ }
935
+ const additionalProperties = out["additionalProperties"]
936
+ if (isObject(additionalProperties) && Object.keys(additionalProperties).length === 0) {
937
+ delete out["additionalProperties"]
938
+ }
939
+ return out
940
+ }
941
+ case "Union": {
942
+ const types = representation.types.map((type, index) => recur(type, [...path, "types", index]))
943
+ if (types.length === 0) return { not: {} }
944
+ const mode = representation.options?.mode ?? "anyOf"
945
+ if (mode === "anyOf" && types.length > 1) {
946
+ const compacted = compactEnums(types)
947
+ if (compacted !== undefined) return compacted
948
+ }
949
+ return mode === "anyOf" ? { anyOf: types } : { oneOf: types }
950
+ }
951
+ default:
952
+ return unsupported(representation, path)
953
+ }
954
+ }
955
+
956
+ const parameterPatterns = (
957
+ parameter: Representation,
958
+ path: Path,
959
+ seenReferences: ReadonlySet<string>
960
+ ): ReadonlyArray<string> | undefined => {
961
+ switch (parameter._tag) {
962
+ case "Reference": {
963
+ const target = Object.hasOwn(references, parameter.$ref) ? references[parameter.$ref] : undefined
964
+ if (target === undefined) throw invalid(`Invalid reference ${parameter.$ref}`, [...path, "$ref"])
965
+ compileDefinition(parameter.$ref, path)
966
+ if (seenReferences.has(parameter.$ref)) return undefined
967
+ return parameterPatterns(target, ["references", parameter.$ref], new Set(seenReferences).add(parameter.$ref))
968
+ }
969
+ case "String": {
970
+ const check = parameter.checks[0]
971
+ if (check === undefined) return []
972
+ if (parameter.checks.length !== 1 || check._tag !== "Filter" || fragmentSource(check) === undefined) {
973
+ return undefined
974
+ }
975
+ const schema = recur(parameter, path)
976
+ const key = schema["pattern"]
977
+ return schema["type"] === "string" && typeof key === "string" &&
978
+ hasOnlyKeywords(schema, "|type|pattern|title|description|default|examples|readOnly|writeOnly|")
979
+ ? [key]
980
+ : undefined
981
+ }
982
+ case "TemplateLiteral":
983
+ return parameter.checks.length === 0 ? [`^${parameter.parts.map(partPattern).join("")}$`] : undefined
984
+ case "Union": {
985
+ if (parameter.types.length === 0 || parameter.checks.length > 0 || parameter.options?.mode === "oneOf") {
986
+ return undefined
987
+ }
988
+ const patterns: Array<string> = []
989
+ for (const [index, member] of parameter.types.entries()) {
990
+ const memberPatterns = parameterPatterns(member, [...path, "types", index], seenReferences)
991
+ if (memberPatterns === undefined) return undefined
992
+ if (memberPatterns.length === 0) return []
993
+ patterns.push(...memberPatterns)
994
+ }
995
+ return patterns
996
+ }
997
+ default:
998
+ throw invalid("Invalid schema representation document", path)
999
+ }
1000
+ }
1001
+
1002
+ const referenceKeys = Object.keys(references)
1003
+ for (const key of referenceKeys) compileDefinition(key, ["references", key])
1004
+ const schema = finalize(recur(document.representation, ["representation"]))
1005
+ const definitions: Record<string, Fragment> = {}
1006
+ for (const key of referenceKeys) {
1007
+ const compiled = definitionStates.get(key)
1008
+ if (isObject(compiled)) assignProperty(definitions, key, finalize(compiled))
1009
+ }
1010
+ return { dialect: "draft-2020-12", schema, definitions }
1011
+ }
1012
+
1013
+ /**
1014
+ * The identity a schema contributes: a pure function of the schema and the
1015
+ * option, and of nothing the installed `effect` release prints. It takes the
1016
+ * place of `Schema.toJsonSchemaDocument(schema, options)` wherever the
1017
+ * document is hashed, compared or pinned.
1018
+ *
1019
+ * Throws what rc.115 threw for a schema with no JSON Schema form: more than
1020
+ * one rest element, a record key that is neither a string nor a template, a
1021
+ * `toJsonSchema` annotation that throws.
1022
+ *
1023
+ * @since 1.0.0
1024
+ * @private
1025
+ */
1026
+ export const schemaIdentity = (schema: Schema.Top, options?: Options): Document =>
1027
+ fromRepresentation(Schema.toRepresentation(Schema.toCodecJson(schema)), options)
1028
+
1029
+ /**
1030
+ * The six built-in checks `effect` renamed after `4.0.0-rc.115`, from the id
1031
+ * a later release writes to the id rc.115 wrote. Their payloads and
1032
+ * annotations did not change.
1033
+ *
1034
+ * @private
1035
+ */
1036
+ const rc115CheckIds: ReadonlyMap<string, string> = new Map([
1037
+ ["effect/schema/isStartingWith", "effect/schema/isStartsWith"],
1038
+ ["effect/schema/isEndingWith", "effect/schema/isEndsWith"],
1039
+ ["effect/schema/isIncluding", "effect/schema/isIncludes"],
1040
+ ["effect/schema/isBetweenLength", "effect/schema/isLengthBetween"],
1041
+ ["effect/schema/isBetweenSize", "effect/schema/isSizeBetween"],
1042
+ ["effect/schema/isBetweenProperties", "effect/schema/isPropertiesLengthBetween"]
1043
+ ])
1044
+
1045
+ /**
1046
+ * A copy of an encoded representation with every renamed check id written
1047
+ * back as rc.115 wrote it. Only an element of a `checks` array is a check.
1048
+ * `annotations` and `payload` hold an author's data, which may have any
1049
+ * shape, so they are copied as they are. Key order is kept, so the bytes
1050
+ * match.
1051
+ *
1052
+ * @private
1053
+ */
1054
+ const withRc115CheckIds = (value: Schema.Json, isCheck = false): Schema.Json => {
1055
+ if (Array.isArray(value)) return value.map((member) => withRc115CheckIds(member, isCheck))
1056
+ if (!isObject(value)) return value
1057
+ const out: Record<string, Schema.Json> = {}
1058
+ for (const [key, member] of Object.entries(value as Readonly<Record<string, Schema.Json>>)) {
1059
+ if (key === "annotations" || key === "payload") {
1060
+ assignProperty(out, key, member)
1061
+ continue
1062
+ }
1063
+ const renamed = isCheck && key === "representation" && isObject(member) && typeof member["id"] === "string"
1064
+ ? rc115CheckIds.get(member["id"])
1065
+ : undefined
1066
+ assignProperty(
1067
+ out,
1068
+ key,
1069
+ renamed === undefined
1070
+ ? withRc115CheckIds(member, key === "checks")
1071
+ : withRc115CheckIds({ ...(member as Readonly<Record<string, Schema.Json>>), id: renamed })
1072
+ )
1073
+ }
1074
+ return out
1075
+ }
1076
+
1077
+ /**
1078
+ * The representation identity of a schema AST: what
1079
+ * `SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(ast))`
1080
+ * returned under `effect@4.0.0-rc.115`, key order included. A sealed action's
1081
+ * step key hashes it, so it must not move with the installed `effect`.
1082
+ *
1083
+ * The installed release encodes the representation; this writes back the six
1084
+ * check ids renamed after rc.115. One difference cannot be undone: from
1085
+ * `effect@4.0.4` on, `Schema.brand` is not part of the AST, so the `brands`
1086
+ * annotation rc.115 wrote is absent and a declaration with a brand re-keys
1087
+ * once.
1088
+ *
1089
+ * Throws what rc.115 threw for an AST with no representation, such as one
1090
+ * whose check carries no `representation` annotation.
1091
+ *
1092
+ * @since 1.0.0
1093
+ * @private
1094
+ */
1095
+ export const representationIdentity = (ast: SchemaAST.AST): Schema.Json =>
1096
+ withRc115CheckIds(SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(ast)))