@effect/platform 0.66.0 → 0.66.2
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/OpenApiJsonSchema/package.json +6 -0
- package/dist/cjs/OpenApi.js +7 -12
- package/dist/cjs/OpenApi.js.map +1 -1
- package/dist/cjs/OpenApiJsonSchema.js +492 -0
- package/dist/cjs/OpenApiJsonSchema.js.map +1 -0
- package/dist/cjs/index.js +3 -1
- package/dist/dts/OpenApi.d.ts +5 -10
- package/dist/dts/OpenApi.d.ts.map +1 -1
- package/dist/dts/OpenApiJsonSchema.d.ts +183 -0
- package/dist/dts/OpenApiJsonSchema.d.ts.map +1 -0
- package/dist/dts/index.d.ts +4 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/OpenApi.js +7 -12
- package/dist/esm/OpenApi.js.map +1 -1
- package/dist/esm/OpenApiJsonSchema.js +482 -0
- package/dist/esm/OpenApiJsonSchema.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +11 -3
- package/src/OpenApi.ts +11 -23
- package/src/OpenApiJsonSchema.ts +714 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0
|
|
3
|
+
*/
|
|
4
|
+
import * as AST from "@effect/schema/AST"
|
|
5
|
+
import type * as ParseResult from "@effect/schema/ParseResult"
|
|
6
|
+
import type * as Schema from "@effect/schema/Schema"
|
|
7
|
+
import * as Arr from "effect/Array"
|
|
8
|
+
import * as Option from "effect/Option"
|
|
9
|
+
import * as Predicate from "effect/Predicate"
|
|
10
|
+
import * as Record from "effect/Record"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @category model
|
|
14
|
+
* @since 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
export interface Annotations {
|
|
17
|
+
title?: string
|
|
18
|
+
description?: string
|
|
19
|
+
default?: unknown
|
|
20
|
+
examples?: globalThis.Array<unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @category model
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
*/
|
|
27
|
+
export interface Any extends Annotations {
|
|
28
|
+
$id: "/schemas/any"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @category model
|
|
33
|
+
* @since 1.0.0
|
|
34
|
+
*/
|
|
35
|
+
export interface Unknown extends Annotations {
|
|
36
|
+
$id: "/schemas/unknown"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @category model
|
|
41
|
+
* @since 0.69.0
|
|
42
|
+
*/
|
|
43
|
+
export interface Void extends Annotations {
|
|
44
|
+
$id: "/schemas/void"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @category model
|
|
49
|
+
* @since 0.71.0
|
|
50
|
+
*/
|
|
51
|
+
export interface AnyObject extends Annotations {
|
|
52
|
+
$id: "/schemas/object"
|
|
53
|
+
anyOf: [
|
|
54
|
+
{ type: "object" },
|
|
55
|
+
{ type: "array" }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @category model
|
|
61
|
+
* @since 0.71.0
|
|
62
|
+
*/
|
|
63
|
+
export interface Empty extends Annotations {
|
|
64
|
+
$id: "/schemas/{}"
|
|
65
|
+
anyOf: [
|
|
66
|
+
{ type: "object" },
|
|
67
|
+
{ type: "array" }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @category model
|
|
73
|
+
* @since 1.0.0
|
|
74
|
+
*/
|
|
75
|
+
export interface Ref extends Annotations {
|
|
76
|
+
$ref: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @category model
|
|
81
|
+
* @since 1.0.0
|
|
82
|
+
*/
|
|
83
|
+
export interface String extends Annotations {
|
|
84
|
+
type: "string"
|
|
85
|
+
minLength?: number
|
|
86
|
+
maxLength?: number
|
|
87
|
+
pattern?: string
|
|
88
|
+
contentEncoding?: string
|
|
89
|
+
contentMediaType?: string
|
|
90
|
+
contentSchema?: JsonSchema
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @category model
|
|
95
|
+
* @since 1.0.0
|
|
96
|
+
*/
|
|
97
|
+
export interface Numeric extends Annotations {
|
|
98
|
+
minimum?: number
|
|
99
|
+
exclusiveMinimum?: number
|
|
100
|
+
maximum?: number
|
|
101
|
+
exclusiveMaximum?: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @category model
|
|
106
|
+
* @since 1.0.0
|
|
107
|
+
*/
|
|
108
|
+
export interface Number extends Numeric {
|
|
109
|
+
type: "number"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @category model
|
|
114
|
+
* @since 1.0.0
|
|
115
|
+
*/
|
|
116
|
+
export interface Integer extends Numeric {
|
|
117
|
+
type: "integer"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @category model
|
|
122
|
+
* @since 1.0.0
|
|
123
|
+
*/
|
|
124
|
+
export interface Boolean extends Annotations {
|
|
125
|
+
type: "boolean"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @category model
|
|
130
|
+
* @since 1.0.0
|
|
131
|
+
*/
|
|
132
|
+
export interface Array extends Annotations {
|
|
133
|
+
type: "array"
|
|
134
|
+
items?: JsonSchema | globalThis.Array<JsonSchema>
|
|
135
|
+
minItems?: number
|
|
136
|
+
maxItems?: number
|
|
137
|
+
additionalItems?: JsonSchema | boolean
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @category model
|
|
142
|
+
* @since 1.0.0
|
|
143
|
+
*/
|
|
144
|
+
export interface Enum extends Annotations {
|
|
145
|
+
enum: globalThis.Array<AST.LiteralValue>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @category model
|
|
150
|
+
* @since 0.71.0
|
|
151
|
+
*/
|
|
152
|
+
export interface Enums extends Annotations {
|
|
153
|
+
$comment: "/schemas/enums"
|
|
154
|
+
anyOf: globalThis.Array<{
|
|
155
|
+
title: string
|
|
156
|
+
enum: [string | number]
|
|
157
|
+
}>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @category model
|
|
162
|
+
* @since 1.0.0
|
|
163
|
+
*/
|
|
164
|
+
export interface AnyOf extends Annotations {
|
|
165
|
+
anyOf: globalThis.Array<JsonSchema>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @category model
|
|
170
|
+
* @since 1.0.0
|
|
171
|
+
*/
|
|
172
|
+
export interface Object extends Annotations {
|
|
173
|
+
type: "object"
|
|
174
|
+
required: globalThis.Array<string>
|
|
175
|
+
properties: Record<string, JsonSchema>
|
|
176
|
+
additionalProperties?: boolean | JsonSchema
|
|
177
|
+
patternProperties?: Record<string, JsonSchema>
|
|
178
|
+
propertyNames?: JsonSchema
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @category model
|
|
183
|
+
* @since 0.71.0
|
|
184
|
+
*/
|
|
185
|
+
export type JsonSchema =
|
|
186
|
+
| Any
|
|
187
|
+
| Unknown
|
|
188
|
+
| Void
|
|
189
|
+
| AnyObject
|
|
190
|
+
| Empty
|
|
191
|
+
| Ref
|
|
192
|
+
| String
|
|
193
|
+
| Number
|
|
194
|
+
| Integer
|
|
195
|
+
| Boolean
|
|
196
|
+
| Array
|
|
197
|
+
| Enum
|
|
198
|
+
| Enums
|
|
199
|
+
| AnyOf
|
|
200
|
+
| Object
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @category model
|
|
204
|
+
* @since 1.0.0
|
|
205
|
+
*/
|
|
206
|
+
export type Root = JsonSchema & {
|
|
207
|
+
$defs?: Record<string, JsonSchema>
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @category encoding
|
|
212
|
+
* @since 1.0.0
|
|
213
|
+
*/
|
|
214
|
+
export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): Root => {
|
|
215
|
+
const $defs: Record<string, any> = {}
|
|
216
|
+
const out = go(schema.ast, $defs, true, []) as Root
|
|
217
|
+
// clean up self-referencing entries
|
|
218
|
+
for (const id in $defs) {
|
|
219
|
+
if ($defs[id]["$ref"] === get$ref(id)) {
|
|
220
|
+
delete $defs[id]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!Record.isEmptyRecord($defs)) {
|
|
224
|
+
out.$defs = $defs
|
|
225
|
+
}
|
|
226
|
+
return out
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const constAny: JsonSchema = { $id: "/schemas/any" }
|
|
230
|
+
|
|
231
|
+
const constUnknown: JsonSchema = { $id: "/schemas/unknown" }
|
|
232
|
+
|
|
233
|
+
const constVoid: JsonSchema = { $id: "/schemas/void" }
|
|
234
|
+
|
|
235
|
+
const constAnyObject: JsonSchema = {
|
|
236
|
+
"$id": "/schemas/object",
|
|
237
|
+
"anyOf": [
|
|
238
|
+
{ "type": "object" },
|
|
239
|
+
{ "type": "array" }
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const constEmpty: JsonSchema = {
|
|
244
|
+
"$id": "/schemas/{}",
|
|
245
|
+
"anyOf": [
|
|
246
|
+
{ "type": "object" },
|
|
247
|
+
{ "type": "array" }
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const getJsonSchemaAnnotations = (annotated: AST.Annotated): Annotations =>
|
|
252
|
+
Record.getSomes({
|
|
253
|
+
description: AST.getDescriptionAnnotation(annotated),
|
|
254
|
+
title: AST.getTitleAnnotation(annotated),
|
|
255
|
+
examples: AST.getExamplesAnnotation(annotated),
|
|
256
|
+
default: AST.getDefaultAnnotation(annotated)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const removeDefaultJsonSchemaAnnotations = (
|
|
260
|
+
jsonSchemaAnnotations: Annotations,
|
|
261
|
+
ast: AST.AST
|
|
262
|
+
): Annotations => {
|
|
263
|
+
if (jsonSchemaAnnotations["title"] === ast.annotations[AST.TitleAnnotationId]) {
|
|
264
|
+
delete jsonSchemaAnnotations["title"]
|
|
265
|
+
}
|
|
266
|
+
if (jsonSchemaAnnotations["description"] === ast.annotations[AST.DescriptionAnnotationId]) {
|
|
267
|
+
delete jsonSchemaAnnotations["description"]
|
|
268
|
+
}
|
|
269
|
+
return jsonSchemaAnnotations
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const getASTJsonSchemaAnnotations = (ast: AST.AST): Annotations => {
|
|
273
|
+
const jsonSchemaAnnotations = getJsonSchemaAnnotations(ast)
|
|
274
|
+
switch (ast._tag) {
|
|
275
|
+
case "StringKeyword":
|
|
276
|
+
return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.stringKeyword)
|
|
277
|
+
case "NumberKeyword":
|
|
278
|
+
return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.numberKeyword)
|
|
279
|
+
case "BooleanKeyword":
|
|
280
|
+
return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.booleanKeyword)
|
|
281
|
+
default:
|
|
282
|
+
return jsonSchemaAnnotations
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pruneUndefinedKeyword = (ps: AST.PropertySignature): AST.AST | undefined => {
|
|
287
|
+
const type = ps.type
|
|
288
|
+
if (AST.isUnion(type) && Option.isNone(AST.getJSONSchemaAnnotation(type))) {
|
|
289
|
+
const types = type.types.filter((type) => !AST.isUndefinedKeyword(type))
|
|
290
|
+
if (types.length < type.types.length) {
|
|
291
|
+
return AST.Union.make(types, type.annotations)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const DEFINITION_PREFIX = "#/$defs/"
|
|
297
|
+
|
|
298
|
+
const get$ref = (id: string): string => `${DEFINITION_PREFIX}${id}`
|
|
299
|
+
|
|
300
|
+
const getRefinementInnerTransformation = (ast: AST.Refinement): AST.AST | undefined => {
|
|
301
|
+
switch (ast.from._tag) {
|
|
302
|
+
case "Transformation":
|
|
303
|
+
return ast.from
|
|
304
|
+
case "Refinement":
|
|
305
|
+
return getRefinementInnerTransformation(ast.from)
|
|
306
|
+
case "Suspend": {
|
|
307
|
+
const from = ast.from.f()
|
|
308
|
+
if (AST.isRefinement(from)) {
|
|
309
|
+
return getRefinementInnerTransformation(from)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const isParseJsonTransformation = (ast: AST.AST): boolean => ast.annotations[AST.TypeAnnotationId] === ParseJsonTypeId
|
|
316
|
+
|
|
317
|
+
const isOverrideAnnotation = (jsonSchema: JsonSchema): boolean => {
|
|
318
|
+
return ("type" in jsonSchema) || ("oneOf" in jsonSchema) || ("anyOf" in jsonSchema) || ("const" in jsonSchema) ||
|
|
319
|
+
("enum" in jsonSchema) || ("$ref" in jsonSchema)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const go = (
|
|
323
|
+
ast: AST.AST,
|
|
324
|
+
$defs: Record<string, JsonSchema>,
|
|
325
|
+
handleIdentifier: boolean,
|
|
326
|
+
path: ReadonlyArray<PropertyKey>
|
|
327
|
+
): JsonSchema => {
|
|
328
|
+
const hook = AST.getJSONSchemaAnnotation(ast)
|
|
329
|
+
if (Option.isSome(hook)) {
|
|
330
|
+
const handler = hook.value as JsonSchema
|
|
331
|
+
if (AST.isRefinement(ast)) {
|
|
332
|
+
const t = getRefinementInnerTransformation(ast)
|
|
333
|
+
if (t === undefined) {
|
|
334
|
+
try {
|
|
335
|
+
return {
|
|
336
|
+
...go(ast.from, $defs, true, path),
|
|
337
|
+
...getJsonSchemaAnnotations(ast),
|
|
338
|
+
...handler
|
|
339
|
+
}
|
|
340
|
+
} catch (e) {
|
|
341
|
+
return {
|
|
342
|
+
...getJsonSchemaAnnotations(ast),
|
|
343
|
+
...handler
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else if (!isOverrideAnnotation(handler)) {
|
|
347
|
+
return {
|
|
348
|
+
...go(t, $defs, true, path),
|
|
349
|
+
...getJsonSchemaAnnotations(ast)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return handler
|
|
354
|
+
}
|
|
355
|
+
const surrogate = getSurrogateAnnotation(ast)
|
|
356
|
+
if (Option.isSome(surrogate)) {
|
|
357
|
+
return {
|
|
358
|
+
...(ast._tag === "Transformation" ? getJsonSchemaAnnotations(ast.to) : {}),
|
|
359
|
+
...go(surrogate.value, $defs, handleIdentifier, path),
|
|
360
|
+
...getJsonSchemaAnnotations(ast)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (handleIdentifier && !AST.isTransformation(ast) && !AST.isRefinement(ast)) {
|
|
364
|
+
const identifier = getJSONIdentifier(ast)
|
|
365
|
+
if (Option.isSome(identifier)) {
|
|
366
|
+
const id = identifier.value
|
|
367
|
+
const out = { $ref: get$ref(id) }
|
|
368
|
+
if (!Record.has($defs, id)) {
|
|
369
|
+
$defs[id] = out
|
|
370
|
+
$defs[id] = go(ast, $defs, false, path)
|
|
371
|
+
}
|
|
372
|
+
return out
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
switch (ast._tag) {
|
|
376
|
+
case "Declaration":
|
|
377
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
378
|
+
case "Literal": {
|
|
379
|
+
const literal = ast.literal
|
|
380
|
+
if (literal === null) {
|
|
381
|
+
return {
|
|
382
|
+
enum: [null],
|
|
383
|
+
...getJsonSchemaAnnotations(ast)
|
|
384
|
+
}
|
|
385
|
+
} else if (Predicate.isString(literal) || Predicate.isNumber(literal) || Predicate.isBoolean(literal)) {
|
|
386
|
+
return {
|
|
387
|
+
enum: [literal],
|
|
388
|
+
...getJsonSchemaAnnotations(ast)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
392
|
+
}
|
|
393
|
+
case "UniqueSymbol":
|
|
394
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
395
|
+
case "UndefinedKeyword":
|
|
396
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
397
|
+
case "VoidKeyword":
|
|
398
|
+
return {
|
|
399
|
+
...constVoid,
|
|
400
|
+
...getJsonSchemaAnnotations(ast)
|
|
401
|
+
}
|
|
402
|
+
case "NeverKeyword":
|
|
403
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
404
|
+
case "UnknownKeyword":
|
|
405
|
+
return {
|
|
406
|
+
...constUnknown,
|
|
407
|
+
...getJsonSchemaAnnotations(ast)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case "AnyKeyword":
|
|
411
|
+
return {
|
|
412
|
+
...constAny,
|
|
413
|
+
...getJsonSchemaAnnotations(ast)
|
|
414
|
+
}
|
|
415
|
+
case "ObjectKeyword":
|
|
416
|
+
return {
|
|
417
|
+
...constAnyObject,
|
|
418
|
+
...getJsonSchemaAnnotations(ast)
|
|
419
|
+
}
|
|
420
|
+
case "StringKeyword":
|
|
421
|
+
return { type: "string", ...getASTJsonSchemaAnnotations(ast) }
|
|
422
|
+
case "NumberKeyword":
|
|
423
|
+
return { type: "number", ...getASTJsonSchemaAnnotations(ast) }
|
|
424
|
+
case "BooleanKeyword":
|
|
425
|
+
return { type: "boolean", ...getASTJsonSchemaAnnotations(ast) }
|
|
426
|
+
case "BigIntKeyword":
|
|
427
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
428
|
+
case "SymbolKeyword":
|
|
429
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
430
|
+
case "TupleType": {
|
|
431
|
+
const elements = ast.elements.map((e, i) => ({
|
|
432
|
+
...go(e.type, $defs, true, path.concat(i)),
|
|
433
|
+
...getJsonSchemaAnnotations(e)
|
|
434
|
+
}))
|
|
435
|
+
const rest = ast.rest.map((annotatedAST) => ({
|
|
436
|
+
...go(annotatedAST.type, $defs, true, path),
|
|
437
|
+
...getJsonSchemaAnnotations(annotatedAST)
|
|
438
|
+
}))
|
|
439
|
+
const output: Array = { type: "array" }
|
|
440
|
+
// ---------------------------------------------
|
|
441
|
+
// handle elements
|
|
442
|
+
// ---------------------------------------------
|
|
443
|
+
const len = ast.elements.length
|
|
444
|
+
if (len > 0) {
|
|
445
|
+
output.minItems = len - ast.elements.filter((element) => element.isOptional).length
|
|
446
|
+
output.items = elements
|
|
447
|
+
}
|
|
448
|
+
// ---------------------------------------------
|
|
449
|
+
// handle rest element
|
|
450
|
+
// ---------------------------------------------
|
|
451
|
+
const restLength = rest.length
|
|
452
|
+
if (restLength > 0) {
|
|
453
|
+
const head = rest[0]
|
|
454
|
+
const isHomogeneous = restLength === 1 && ast.elements.every((e) => e.type === ast.rest[0].type)
|
|
455
|
+
if (isHomogeneous) {
|
|
456
|
+
output.items = head
|
|
457
|
+
} else {
|
|
458
|
+
output.additionalItems = head
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------
|
|
462
|
+
// handle post rest elements
|
|
463
|
+
// ---------------------------------------------
|
|
464
|
+
if (restLength > 1) {
|
|
465
|
+
throw new Error(getJSONSchemaUnsupportedPostRestElementsErrorMessage(path))
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
if (len > 0) {
|
|
469
|
+
output.additionalItems = false
|
|
470
|
+
} else {
|
|
471
|
+
output.maxItems = 0
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
...output,
|
|
477
|
+
...getJsonSchemaAnnotations(ast)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
case "TypeLiteral": {
|
|
481
|
+
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
|
|
482
|
+
return {
|
|
483
|
+
...constEmpty,
|
|
484
|
+
...getJsonSchemaAnnotations(ast)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let patternProperties: JsonSchema | undefined = undefined
|
|
488
|
+
let propertyNames: JsonSchema | undefined = undefined
|
|
489
|
+
for (const is of ast.indexSignatures) {
|
|
490
|
+
const parameter = is.parameter
|
|
491
|
+
switch (parameter._tag) {
|
|
492
|
+
case "StringKeyword": {
|
|
493
|
+
patternProperties = go(is.type, $defs, true, path)
|
|
494
|
+
break
|
|
495
|
+
}
|
|
496
|
+
case "TemplateLiteral": {
|
|
497
|
+
patternProperties = go(is.type, $defs, true, path)
|
|
498
|
+
propertyNames = {
|
|
499
|
+
type: "string",
|
|
500
|
+
pattern: AST.getTemplateLiteralRegExp(parameter).source
|
|
501
|
+
}
|
|
502
|
+
break
|
|
503
|
+
}
|
|
504
|
+
case "Refinement": {
|
|
505
|
+
patternProperties = go(is.type, $defs, true, path)
|
|
506
|
+
propertyNames = go(parameter, $defs, true, path)
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
case "SymbolKeyword":
|
|
510
|
+
throw new Error(getJSONSchemaUnsupportedParameterErrorMessage(path, parameter))
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const output: Object = {
|
|
514
|
+
type: "object",
|
|
515
|
+
required: [],
|
|
516
|
+
properties: {},
|
|
517
|
+
additionalProperties: false
|
|
518
|
+
}
|
|
519
|
+
// ---------------------------------------------
|
|
520
|
+
// handle property signatures
|
|
521
|
+
// ---------------------------------------------
|
|
522
|
+
for (let i = 0; i < ast.propertySignatures.length; i++) {
|
|
523
|
+
const ps = ast.propertySignatures[i]
|
|
524
|
+
const name = ps.name
|
|
525
|
+
if (Predicate.isString(name)) {
|
|
526
|
+
const pruned = pruneUndefinedKeyword(ps)
|
|
527
|
+
output.properties[name] = {
|
|
528
|
+
...go(pruned ? pruned : ps.type, $defs, true, path.concat(ps.name)),
|
|
529
|
+
...getJsonSchemaAnnotations(ps)
|
|
530
|
+
}
|
|
531
|
+
// ---------------------------------------------
|
|
532
|
+
// handle optional property signatures
|
|
533
|
+
// ---------------------------------------------
|
|
534
|
+
if (!ps.isOptional && pruned === undefined) {
|
|
535
|
+
output.required.push(name)
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
throw new Error(getJSONSchemaUnsupportedKeyErrorMessage(name, path))
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// ---------------------------------------------
|
|
542
|
+
// handle index signatures
|
|
543
|
+
// ---------------------------------------------
|
|
544
|
+
if (patternProperties !== undefined) {
|
|
545
|
+
delete output.additionalProperties
|
|
546
|
+
output.patternProperties = { "": patternProperties }
|
|
547
|
+
}
|
|
548
|
+
if (propertyNames !== undefined) {
|
|
549
|
+
output.propertyNames = propertyNames
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
...output,
|
|
554
|
+
...getJsonSchemaAnnotations(ast)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
case "Union": {
|
|
558
|
+
const enums: globalThis.Array<AST.LiteralValue> = []
|
|
559
|
+
const anyOf: globalThis.Array<JsonSchema> = []
|
|
560
|
+
for (const type of ast.types) {
|
|
561
|
+
const schema = go(type, $defs, true, path)
|
|
562
|
+
if ("enum" in schema) {
|
|
563
|
+
if (Object.keys(schema).length > 1) {
|
|
564
|
+
anyOf.push(schema)
|
|
565
|
+
} else {
|
|
566
|
+
for (const e of schema.enum) {
|
|
567
|
+
enums.push(e)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
anyOf.push(schema)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (anyOf.length === 0) {
|
|
575
|
+
return { enum: enums, ...getJsonSchemaAnnotations(ast) }
|
|
576
|
+
} else {
|
|
577
|
+
if (enums.length >= 1) {
|
|
578
|
+
anyOf.push({ enum: enums })
|
|
579
|
+
}
|
|
580
|
+
return { anyOf, ...getJsonSchemaAnnotations(ast) }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
case "Enums": {
|
|
584
|
+
return {
|
|
585
|
+
$comment: "/schemas/enums",
|
|
586
|
+
anyOf: ast.enums.map((e) => ({ title: e[0], enum: [e[1]] })),
|
|
587
|
+
...getJsonSchemaAnnotations(ast)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
case "Refinement": {
|
|
591
|
+
if (AST.encodedBoundAST(ast) === ast) {
|
|
592
|
+
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
593
|
+
}
|
|
594
|
+
return go(ast.from, $defs, true, path)
|
|
595
|
+
}
|
|
596
|
+
case "TemplateLiteral": {
|
|
597
|
+
const regex = AST.getTemplateLiteralRegExp(ast)
|
|
598
|
+
return {
|
|
599
|
+
type: "string",
|
|
600
|
+
description: "a template literal",
|
|
601
|
+
pattern: regex.source,
|
|
602
|
+
...getJsonSchemaAnnotations(ast)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
case "Suspend": {
|
|
606
|
+
const identifier = Option.orElse(getJSONIdentifier(ast), () => getJSONIdentifier(ast.f()))
|
|
607
|
+
if (Option.isNone(identifier)) {
|
|
608
|
+
throw new Error(getJSONSchemaMissingIdentifierAnnotationErrorMessage(path, ast))
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
...go(ast.f(), $defs, true, path),
|
|
612
|
+
...getJsonSchemaAnnotations(ast)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
case "Transformation": {
|
|
616
|
+
// Properly handle S.parseJson transformations by focusing on
|
|
617
|
+
// the 'to' side of the AST. This approach prevents the generation of useless schemas
|
|
618
|
+
// derived from the 'from' side (type: string), ensuring the output matches the intended
|
|
619
|
+
// complex schema type.
|
|
620
|
+
if (isParseJsonTransformation(ast.from)) {
|
|
621
|
+
return {
|
|
622
|
+
type: "string",
|
|
623
|
+
contentMediaType: "application/json",
|
|
624
|
+
contentSchema: go(ast.to, $defs, true, path),
|
|
625
|
+
...getJsonSchemaAnnotations(ast)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
...getASTJsonSchemaAnnotations(ast.to),
|
|
630
|
+
...go(ast.from, $defs, true, path),
|
|
631
|
+
...getJsonSchemaAnnotations(ast)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const getJSONSchemaMissingAnnotationErrorMessage = (
|
|
638
|
+
path: ReadonlyArray<PropertyKey>,
|
|
639
|
+
ast: AST.AST
|
|
640
|
+
) =>
|
|
641
|
+
getMissingAnnotationErrorMessage(
|
|
642
|
+
`Generating a JSON Schema for this schema requires a "jsonSchema" annotation`,
|
|
643
|
+
path,
|
|
644
|
+
ast
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
const getJSONSchemaMissingIdentifierAnnotationErrorMessage = (
|
|
648
|
+
path: ReadonlyArray<PropertyKey>,
|
|
649
|
+
ast: AST.AST
|
|
650
|
+
) =>
|
|
651
|
+
getMissingAnnotationErrorMessage(
|
|
652
|
+
`Generating a JSON Schema for this schema requires an "identifier" annotation`,
|
|
653
|
+
path,
|
|
654
|
+
ast
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
const getJSONSchemaUnsupportedParameterErrorMessage = (
|
|
658
|
+
path: ReadonlyArray<PropertyKey>,
|
|
659
|
+
parameter: AST.AST
|
|
660
|
+
): string => getErrorMessage("Unsupported index signature parameter", undefined, path, parameter)
|
|
661
|
+
|
|
662
|
+
const getJSONSchemaUnsupportedPostRestElementsErrorMessage = (path: ReadonlyArray<PropertyKey>): string =>
|
|
663
|
+
getErrorMessage(
|
|
664
|
+
"Generating a JSON Schema for post-rest elements is not currently supported. You're welcome to contribute by submitting a Pull Request",
|
|
665
|
+
undefined,
|
|
666
|
+
path
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
const getJSONSchemaUnsupportedKeyErrorMessage = (key: PropertyKey, path: ReadonlyArray<PropertyKey>): string =>
|
|
670
|
+
getErrorMessage("Unsupported key", `Cannot encode ${formatPropertyKey(key)} key to JSON Schema`, path)
|
|
671
|
+
|
|
672
|
+
const getMissingAnnotationErrorMessage = (details?: string, path?: ReadonlyArray<PropertyKey>, ast?: AST.AST): string =>
|
|
673
|
+
getErrorMessage("Missing annotation", details, path, ast)
|
|
674
|
+
|
|
675
|
+
const getErrorMessage = (
|
|
676
|
+
reason: string,
|
|
677
|
+
details?: string,
|
|
678
|
+
path?: ReadonlyArray<PropertyKey>,
|
|
679
|
+
ast?: AST.AST
|
|
680
|
+
): string => {
|
|
681
|
+
let out = reason
|
|
682
|
+
|
|
683
|
+
if (path && Arr.isNonEmptyReadonlyArray(path)) {
|
|
684
|
+
out += `\nat path: ${formatPath(path)}`
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (details !== undefined) {
|
|
688
|
+
out += `\ndetails: ${details}`
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (ast) {
|
|
692
|
+
out += `\nschema (${ast._tag}): ${ast}`
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return out
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const formatPathKey = (key: PropertyKey): string => `[${formatPropertyKey(key)}]`
|
|
699
|
+
|
|
700
|
+
const formatPath = (path: ParseResult.Path): string =>
|
|
701
|
+
isNonEmpty(path) ? path.map(formatPathKey).join("") : formatPathKey(path)
|
|
702
|
+
|
|
703
|
+
const isNonEmpty = <A>(x: ParseResult.SingleOrNonEmpty<A>): x is Arr.NonEmptyReadonlyArray<A> => Array.isArray(x)
|
|
704
|
+
|
|
705
|
+
const formatPropertyKey = (name: PropertyKey): string => typeof name === "string" ? JSON.stringify(name) : String(name)
|
|
706
|
+
|
|
707
|
+
const ParseJsonTypeId: unique symbol = Symbol.for("@effect/schema/TypeId/ParseJson")
|
|
708
|
+
const SurrogateAnnotationId = Symbol.for("@effect/schema/annotation/Surrogate")
|
|
709
|
+
const JSONIdentifierAnnotationId = Symbol.for("@effect/schema/annotation/JSONIdentifier")
|
|
710
|
+
|
|
711
|
+
const getSurrogateAnnotation = AST.getAnnotation<AST.AST>(SurrogateAnnotationId)
|
|
712
|
+
const getJSONIdentifierAnnotation = AST.getAnnotation<string>(JSONIdentifierAnnotationId)
|
|
713
|
+
const getJSONIdentifier = (annotated: AST.Annotated) =>
|
|
714
|
+
Option.orElse(getJSONIdentifierAnnotation(annotated), () => AST.getIdentifierAnnotation(annotated))
|
package/src/index.ts
CHANGED