@smthrs/plan 0.0.0-stage → 1.0.0-rc.13
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.
- package/CHANGELOG.md +189 -0
- package/LICENSE +21 -0
- package/README.md +230 -2
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/dist/cjs/CachePolicy.d.ts +131 -0
- package/dist/cjs/CachePolicy.d.ts.map +1 -0
- package/dist/cjs/CachePolicy.js +50 -0
- package/dist/cjs/CachePolicy.js.map +7 -0
- package/dist/cjs/Effects.d.ts +258 -0
- package/dist/cjs/Effects.d.ts.map +1 -0
- package/dist/cjs/Effects.js +76 -0
- package/dist/cjs/Effects.js.map +7 -0
- package/dist/cjs/FileSet.d.ts +366 -0
- package/dist/cjs/FileSet.d.ts.map +1 -0
- package/dist/cjs/FileSet.js +196 -0
- package/dist/cjs/FileSet.js.map +7 -0
- package/dist/cjs/GraphBuildError.d.ts +120 -0
- package/dist/cjs/GraphBuildError.d.ts.map +1 -0
- package/dist/cjs/GraphBuildError.js +71 -0
- package/dist/cjs/GraphBuildError.js.map +7 -0
- package/dist/cjs/KeyMaterial.d.ts +107 -0
- package/dist/cjs/KeyMaterial.d.ts.map +1 -0
- package/dist/cjs/KeyMaterial.js +64 -0
- package/dist/cjs/KeyMaterial.js.map +7 -0
- package/dist/cjs/Node.d.ts +685 -0
- package/dist/cjs/Node.d.ts.map +1 -0
- package/dist/cjs/Node.js +230 -0
- package/dist/cjs/Node.js.map +7 -0
- package/dist/cjs/Placement.d.ts +117 -0
- package/dist/cjs/Placement.d.ts.map +1 -0
- package/dist/cjs/Placement.js +46 -0
- package/dist/cjs/Placement.js.map +7 -0
- package/dist/cjs/Plan.d.ts +489 -0
- package/dist/cjs/Plan.d.ts.map +1 -0
- package/dist/cjs/Plan.js +511 -0
- package/dist/cjs/Plan.js.map +7 -0
- package/dist/cjs/PlanDiff.d.ts +77 -0
- package/dist/cjs/PlanDiff.d.ts.map +1 -0
- package/dist/cjs/PlanDiff.js +149 -0
- package/dist/cjs/PlanDiff.js.map +7 -0
- package/dist/cjs/Planned.d.ts +121 -0
- package/dist/cjs/Planned.d.ts.map +1 -0
- package/dist/cjs/Planned.js +72 -0
- package/dist/cjs/Planned.js.map +7 -0
- package/dist/cjs/Repetition.d.ts +31 -0
- package/dist/cjs/Repetition.d.ts.map +1 -0
- package/dist/cjs/Repetition.js +36 -0
- package/dist/cjs/Repetition.js.map +7 -0
- package/dist/cjs/Scheduling.d.ts +89 -0
- package/dist/cjs/Scheduling.d.ts.map +1 -0
- package/dist/cjs/Scheduling.js +76 -0
- package/dist/cjs/Scheduling.js.map +7 -0
- package/dist/cjs/StepKey.d.ts +354 -0
- package/dist/cjs/StepKey.d.ts.map +1 -0
- package/dist/cjs/StepKey.js +313 -0
- package/dist/cjs/StepKey.js.map +7 -0
- package/dist/cjs/index.d.ts +91 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +59 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ConflictAnnotation.d.ts +59 -0
- package/dist/cjs/internal/ConflictAnnotation.d.ts.map +1 -0
- package/dist/cjs/internal/ConflictAnnotation.js +247 -0
- package/dist/cjs/internal/ConflictAnnotation.js.map +7 -0
- package/dist/cjs/internal/EffectCandidates.d.ts +19 -0
- package/dist/cjs/internal/EffectCandidates.d.ts.map +1 -0
- package/dist/cjs/internal/EffectCandidates.js +92 -0
- package/dist/cjs/internal/EffectCandidates.js.map +7 -0
- package/dist/cjs/internal/JsonMirror.d.ts +34 -0
- package/dist/cjs/internal/JsonMirror.d.ts.map +1 -0
- package/dist/cjs/internal/JsonMirror.js +195 -0
- package/dist/cjs/internal/JsonMirror.js.map +7 -0
- package/dist/cjs/internal/SchemaIdentity.d.ts +140 -0
- package/dist/cjs/internal/SchemaIdentity.d.ts.map +1 -0
- package/dist/cjs/internal/SchemaIdentity.js +718 -0
- package/dist/cjs/internal/SchemaIdentity.js.map +7 -0
- package/dist/cjs/internal/effects.d.ts +245 -0
- package/dist/cjs/internal/effects.d.ts.map +1 -0
- package/dist/cjs/internal/effects.js +290 -0
- package/dist/cjs/internal/effects.js.map +7 -0
- package/dist/cjs/internal/node.d.ts +526 -0
- package/dist/cjs/internal/node.d.ts.map +1 -0
- package/dist/cjs/internal/node.js +313 -0
- package/dist/cjs/internal/node.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/PlanFixtures.d.ts +127 -0
- package/dist/cjs/test/PlanFixtures.d.ts.map +1 -0
- package/dist/cjs/test/PlanFixtures.js +59 -0
- package/dist/cjs/test/PlanFixtures.js.map +7 -0
- package/dist/esm/CachePolicy.d.ts +131 -0
- package/dist/esm/CachePolicy.d.ts.map +1 -0
- package/dist/esm/CachePolicy.js +117 -0
- package/dist/esm/CachePolicy.js.map +1 -0
- package/dist/esm/Effects.d.ts +258 -0
- package/dist/esm/Effects.d.ts.map +1 -0
- package/dist/esm/Effects.js +178 -0
- package/dist/esm/Effects.js.map +1 -0
- package/dist/esm/FileSet.d.ts +366 -0
- package/dist/esm/FileSet.d.ts.map +1 -0
- package/dist/esm/FileSet.js +398 -0
- package/dist/esm/FileSet.js.map +1 -0
- package/dist/esm/GraphBuildError.d.ts +120 -0
- package/dist/esm/GraphBuildError.d.ts.map +1 -0
- package/dist/esm/GraphBuildError.js +145 -0
- package/dist/esm/GraphBuildError.js.map +1 -0
- package/dist/esm/KeyMaterial.d.ts +107 -0
- package/dist/esm/KeyMaterial.d.ts.map +1 -0
- package/dist/esm/KeyMaterial.js +92 -0
- package/dist/esm/KeyMaterial.js.map +1 -0
- package/dist/esm/Node.d.ts +685 -0
- package/dist/esm/Node.d.ts.map +1 -0
- package/dist/esm/Node.js +591 -0
- package/dist/esm/Node.js.map +1 -0
- package/dist/esm/Placement.d.ts +117 -0
- package/dist/esm/Placement.d.ts.map +1 -0
- package/dist/esm/Placement.js +65 -0
- package/dist/esm/Placement.js.map +1 -0
- package/dist/esm/Plan.d.ts +489 -0
- package/dist/esm/Plan.d.ts.map +1 -0
- package/dist/esm/Plan.js +718 -0
- package/dist/esm/Plan.js.map +1 -0
- package/dist/esm/PlanDiff.d.ts +77 -0
- package/dist/esm/PlanDiff.d.ts.map +1 -0
- package/dist/esm/PlanDiff.js +205 -0
- package/dist/esm/PlanDiff.js.map +1 -0
- package/dist/esm/Planned.d.ts +121 -0
- package/dist/esm/Planned.d.ts.map +1 -0
- package/dist/esm/Planned.js +147 -0
- package/dist/esm/Planned.js.map +1 -0
- package/dist/esm/Repetition.d.ts +31 -0
- package/dist/esm/Repetition.d.ts.map +1 -0
- package/dist/esm/Repetition.js +24 -0
- package/dist/esm/Repetition.js.map +1 -0
- package/dist/esm/Scheduling.d.ts +89 -0
- package/dist/esm/Scheduling.d.ts.map +1 -0
- package/dist/esm/Scheduling.js +82 -0
- package/dist/esm/Scheduling.js.map +1 -0
- package/dist/esm/StepKey.d.ts +354 -0
- package/dist/esm/StepKey.d.ts.map +1 -0
- package/dist/esm/StepKey.js +503 -0
- package/dist/esm/StepKey.js.map +1 -0
- package/dist/esm/index.d.ts +91 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +91 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ConflictAnnotation.d.ts +59 -0
- package/dist/esm/internal/ConflictAnnotation.d.ts.map +1 -0
- package/dist/esm/internal/ConflictAnnotation.js +332 -0
- package/dist/esm/internal/ConflictAnnotation.js.map +1 -0
- package/dist/esm/internal/EffectCandidates.d.ts +19 -0
- package/dist/esm/internal/EffectCandidates.d.ts.map +1 -0
- package/dist/esm/internal/EffectCandidates.js +86 -0
- package/dist/esm/internal/EffectCandidates.js.map +1 -0
- package/dist/esm/internal/JsonMirror.d.ts +34 -0
- package/dist/esm/internal/JsonMirror.d.ts.map +1 -0
- package/dist/esm/internal/JsonMirror.js +204 -0
- package/dist/esm/internal/JsonMirror.js.map +1 -0
- package/dist/esm/internal/SchemaIdentity.d.ts +140 -0
- package/dist/esm/internal/SchemaIdentity.d.ts.map +1 -0
- package/dist/esm/internal/SchemaIdentity.js +1002 -0
- package/dist/esm/internal/SchemaIdentity.js.map +1 -0
- package/dist/esm/internal/effects.d.ts +245 -0
- package/dist/esm/internal/effects.d.ts.map +1 -0
- package/dist/esm/internal/effects.js +459 -0
- package/dist/esm/internal/effects.js.map +1 -0
- package/dist/esm/internal/node.d.ts +526 -0
- package/dist/esm/internal/node.d.ts.map +1 -0
- package/dist/esm/internal/node.js +565 -0
- package/dist/esm/internal/node.js.map +1 -0
- package/dist/esm/test/PlanFixtures.d.ts +127 -0
- package/dist/esm/test/PlanFixtures.d.ts.map +1 -0
- package/dist/esm/test/PlanFixtures.js +60 -0
- package/dist/esm/test/PlanFixtures.js.map +1 -0
- package/docs/README.md +167 -0
- package/docs/api.md +1485 -0
- package/docs/concepts/authoring-ast.md +204 -0
- package/docs/concepts/effects-and-conflicts.md +135 -0
- package/docs/concepts/plan-value.md +168 -0
- package/docs/concepts/step-keys.md +179 -0
- package/docs/guides/author-a-node-graph.md +182 -0
- package/docs/guides/compile-a-plan.md +146 -0
- package/docs/guides/declare-file-effects.md +143 -0
- package/docs/guides/diff-two-plans.md +310 -0
- package/docs/installation.md +75 -0
- package/docs/quickstart.md +128 -0
- package/docs/testing.md +73 -0
- package/docs/troubleshooting.md +314 -0
- package/package.json +255 -3
- package/src/CachePolicy.ts +143 -0
- package/src/Effects.ts +302 -0
- package/src/FileSet.ts +493 -0
- package/src/GraphBuildError.ts +156 -0
- package/src/KeyMaterial.ts +111 -0
- package/src/Node.ts +974 -0
- package/src/Placement.ts +99 -0
- package/src/Plan.ts +911 -0
- package/src/PlanDiff.ts +244 -0
- package/src/Planned.ts +208 -0
- package/src/Repetition.ts +33 -0
- package/src/Scheduling.ts +154 -0
- package/src/StepKey.ts +736 -0
- package/src/index.ts +103 -0
- package/src/internal/ConflictAnnotation.ts +374 -0
- package/src/internal/EffectCandidates.ts +89 -0
- package/src/internal/JsonMirror.ts +255 -0
- package/src/internal/SchemaIdentity.ts +1096 -0
- package/src/internal/effects.ts +543 -0
- package/src/internal/node.ts +859 -0
- package/src/test/PlanFixtures.ts +76 -0
|
@@ -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)))
|