@effect/platform 0.70.6 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -34
- package/dist/cjs/HttpApi.js +15 -6
- package/dist/cjs/HttpApi.js.map +1 -1
- package/dist/cjs/HttpApiBuilder.js +1 -1
- package/dist/cjs/HttpApiBuilder.js.map +1 -1
- package/dist/cjs/HttpApiClient.js.map +1 -1
- package/dist/cjs/HttpApiError.js +109 -1
- package/dist/cjs/HttpApiError.js.map +1 -1
- package/dist/cjs/HttpApiGroup.js.map +1 -1
- package/dist/cjs/HttpApiSchema.js +46 -2
- package/dist/cjs/HttpApiSchema.js.map +1 -1
- package/dist/cjs/OpenApi.js +20 -4
- package/dist/cjs/OpenApi.js.map +1 -1
- package/dist/cjs/OpenApiJsonSchema.js +18 -477
- package/dist/cjs/OpenApiJsonSchema.js.map +1 -1
- package/dist/dts/HttpApi.d.ts +13 -12
- package/dist/dts/HttpApi.d.ts.map +1 -1
- package/dist/dts/HttpApiBuilder.d.ts +8 -8
- package/dist/dts/HttpApiBuilder.d.ts.map +1 -1
- package/dist/dts/HttpApiClient.d.ts +3 -3
- package/dist/dts/HttpApiClient.d.ts.map +1 -1
- package/dist/dts/HttpApiError.d.ts +85 -0
- package/dist/dts/HttpApiError.d.ts.map +1 -1
- package/dist/dts/HttpApiGroup.d.ts +3 -2
- package/dist/dts/HttpApiGroup.d.ts.map +1 -1
- package/dist/dts/HttpApiSchema.d.ts +19 -1
- package/dist/dts/HttpApiSchema.d.ts.map +1 -1
- package/dist/dts/OpenApi.d.ts +8 -0
- package/dist/dts/OpenApi.d.ts.map +1 -1
- package/dist/dts/OpenApiJsonSchema.d.ts +34 -5
- package/dist/dts/OpenApiJsonSchema.d.ts.map +1 -1
- package/dist/esm/HttpApi.js +13 -5
- package/dist/esm/HttpApi.js.map +1 -1
- package/dist/esm/HttpApiBuilder.js +1 -1
- package/dist/esm/HttpApiBuilder.js.map +1 -1
- package/dist/esm/HttpApiClient.js.map +1 -1
- package/dist/esm/HttpApiError.js +96 -0
- package/dist/esm/HttpApiError.js.map +1 -1
- package/dist/esm/HttpApiGroup.js.map +1 -1
- package/dist/esm/HttpApiSchema.js +45 -2
- package/dist/esm/HttpApiSchema.js.map +1 -1
- package/dist/esm/OpenApi.js +17 -2
- package/dist/esm/OpenApi.js.map +1 -1
- package/dist/esm/OpenApiJsonSchema.js +18 -477
- package/dist/esm/OpenApiJsonSchema.js.map +1 -1
- package/package.json +2 -2
- package/src/HttpApi.ts +35 -19
- package/src/HttpApiBuilder.ts +17 -15
- package/src/HttpApiClient.ts +8 -6
- package/src/HttpApiError.ts +108 -0
- package/src/HttpApiGroup.ts +4 -3
- package/src/HttpApiSchema.ts +63 -5
- package/src/OpenApi.ts +19 -3
- package/src/OpenApiJsonSchema.ts +45 -513
package/src/OpenApi.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @since 1.0.0
|
|
3
3
|
*/
|
|
4
4
|
import * as Context from "effect/Context"
|
|
5
|
+
import { constFalse } from "effect/Function"
|
|
5
6
|
import { globalValue } from "effect/GlobalValue"
|
|
6
7
|
import * as Option from "effect/Option"
|
|
7
8
|
import type { ReadonlyRecord } from "effect/Record"
|
|
@@ -84,6 +85,14 @@ export class Deprecated extends Context.Tag("@effect/platform/OpenApi/Deprecated
|
|
|
84
85
|
*/
|
|
85
86
|
export class Override extends Context.Tag("@effect/platform/OpenApi/Override")<Override, Record<string, unknown>>() {}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @since 1.0.0
|
|
90
|
+
* @category annotations
|
|
91
|
+
*/
|
|
92
|
+
export class Exclude extends Context.Reference<Exclude>()("@effect/platform/OpenApi/Exclude", {
|
|
93
|
+
defaultValue: constFalse
|
|
94
|
+
}) {}
|
|
95
|
+
|
|
87
96
|
/**
|
|
88
97
|
* Transforms the generated OpenAPI specification
|
|
89
98
|
* @since 1.0.0
|
|
@@ -127,6 +136,7 @@ export const annotations: (
|
|
|
127
136
|
readonly servers?: ReadonlyArray<OpenAPISpecServer> | undefined
|
|
128
137
|
readonly format?: string | undefined
|
|
129
138
|
readonly override?: Record<string, unknown> | undefined
|
|
139
|
+
readonly exclude?: boolean | undefined
|
|
130
140
|
readonly transform?: ((openApiSpec: Record<string, any>) => Record<string, any>) | undefined
|
|
131
141
|
}
|
|
132
142
|
) => Context.Context<never> = contextPartial({
|
|
@@ -140,6 +150,7 @@ export const annotations: (
|
|
|
140
150
|
servers: Servers,
|
|
141
151
|
format: Format,
|
|
142
152
|
override: Override,
|
|
153
|
+
exclude: Exclude,
|
|
143
154
|
transform: Transform
|
|
144
155
|
})
|
|
145
156
|
|
|
@@ -171,8 +182,7 @@ export const fromApi = <A extends HttpApi.HttpApi.Any>(self: A): OpenAPISpec =>
|
|
|
171
182
|
}
|
|
172
183
|
function makeJsonSchemaOrRef(schema: Schema.Schema.All): JsonSchema.JsonSchema {
|
|
173
184
|
return JsonSchema.makeWithDefs(schema as any, {
|
|
174
|
-
defs: jsonSchemaDefs
|
|
175
|
-
defsPath: "#/components/schemas/"
|
|
185
|
+
defs: jsonSchemaDefs
|
|
176
186
|
})
|
|
177
187
|
}
|
|
178
188
|
function registerSecurity(
|
|
@@ -214,6 +224,9 @@ export const fromApi = <A extends HttpApi.HttpApi.Any>(self: A): OpenAPISpec =>
|
|
|
214
224
|
})
|
|
215
225
|
HttpApi.reflect(api as any, {
|
|
216
226
|
onGroup({ group }) {
|
|
227
|
+
if (Context.get(group.annotations, Exclude)) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
217
230
|
let tag: Mutable<OpenAPISpecTag> = {
|
|
218
231
|
name: Context.getOrElse(group.annotations, Title, () => group.identifier)
|
|
219
232
|
}
|
|
@@ -231,7 +244,10 @@ export const fromApi = <A extends HttpApi.HttpApi.Any>(self: A): OpenAPISpec =>
|
|
|
231
244
|
})
|
|
232
245
|
spec.tags!.push(tag)
|
|
233
246
|
},
|
|
234
|
-
onEndpoint({ endpoint, errors, group, middleware, payloads, successes }) {
|
|
247
|
+
onEndpoint({ endpoint, errors, group, mergedAnnotations, middleware, payloads, successes }) {
|
|
248
|
+
if (Context.get(mergedAnnotations, Exclude)) {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
235
251
|
const path = endpoint.path.replace(/:(\w+)[^/]*/g, "{$1}")
|
|
236
252
|
const method = endpoint.method.toLowerCase() as OpenAPISpecMethodName
|
|
237
253
|
let op: DeepMutable<OpenAPISpecOperation> = {
|
package/src/OpenApiJsonSchema.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @since 1.0.0
|
|
3
3
|
*/
|
|
4
|
-
import * as
|
|
5
|
-
import * as Option from "effect/Option"
|
|
6
|
-
import type * as ParseResult from "effect/ParseResult"
|
|
7
|
-
import * as Predicate from "effect/Predicate"
|
|
4
|
+
import * as JSONSchema from "effect/JSONSchema"
|
|
8
5
|
import * as Record from "effect/Record"
|
|
9
6
|
import type * as Schema from "effect/Schema"
|
|
10
|
-
import * as AST from "effect/SchemaAST"
|
|
7
|
+
import type * as AST from "effect/SchemaAST"
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* @category model
|
|
@@ -20,6 +17,15 @@ export interface Annotations {
|
|
|
20
17
|
examples?: globalThis.Array<unknown>
|
|
21
18
|
}
|
|
22
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @category model
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
*/
|
|
24
|
+
export interface Never extends Annotations {
|
|
25
|
+
$id: "/schemas/never"
|
|
26
|
+
not: {}
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
/**
|
|
24
30
|
* @category model
|
|
25
31
|
* @since 1.0.0
|
|
@@ -86,9 +92,13 @@ export interface String extends Annotations {
|
|
|
86
92
|
maxLength?: number
|
|
87
93
|
pattern?: string
|
|
88
94
|
format?: string
|
|
89
|
-
contentEncoding?: string
|
|
90
95
|
contentMediaType?: string
|
|
91
96
|
contentSchema?: JsonSchema
|
|
97
|
+
allOf?: globalThis.Array<{
|
|
98
|
+
minLength?: number
|
|
99
|
+
maxLength?: number
|
|
100
|
+
pattern?: string
|
|
101
|
+
}>
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
/**
|
|
@@ -100,7 +110,15 @@ export interface Numeric extends Annotations {
|
|
|
100
110
|
exclusiveMinimum?: number
|
|
101
111
|
maximum?: number
|
|
102
112
|
exclusiveMaximum?: number
|
|
113
|
+
multipleOf?: number
|
|
103
114
|
format?: string
|
|
115
|
+
allOf?: globalThis.Array<{
|
|
116
|
+
minimum?: number
|
|
117
|
+
exclusiveMinimum?: number
|
|
118
|
+
maximum?: number
|
|
119
|
+
exclusiveMaximum?: number
|
|
120
|
+
multipleOf?: number
|
|
121
|
+
}>
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
/**
|
|
@@ -185,6 +203,7 @@ export interface Object extends Annotations {
|
|
|
185
203
|
* @since 0.71.0
|
|
186
204
|
*/
|
|
187
205
|
export type JsonSchema =
|
|
206
|
+
| Never
|
|
188
207
|
| Any
|
|
189
208
|
| Unknown
|
|
190
209
|
| Void
|
|
@@ -214,522 +233,35 @@ export type Root = JsonSchema & {
|
|
|
214
233
|
* @since 1.0.0
|
|
215
234
|
*/
|
|
216
235
|
export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): Root => {
|
|
217
|
-
const
|
|
218
|
-
const out = makeWithDefs(schema, { defs
|
|
219
|
-
if (!Record.isEmptyRecord(
|
|
220
|
-
out.$defs =
|
|
236
|
+
const defs: Record<string, JsonSchema> = {}
|
|
237
|
+
const out: Root = makeWithDefs(schema, { defs })
|
|
238
|
+
if (!Record.isEmptyRecord(defs)) {
|
|
239
|
+
out.$defs = defs
|
|
221
240
|
}
|
|
222
241
|
return out
|
|
223
242
|
}
|
|
224
243
|
|
|
225
244
|
/**
|
|
245
|
+
* Creates a schema with additional options and definitions.
|
|
246
|
+
*
|
|
247
|
+
* - `defs`: A record of definitions that are included in the schema.
|
|
248
|
+
* - `defsPath`: The path to the definitions within the schema (defaults to "#/$defs/").
|
|
249
|
+
* - `topLevelReferenceStrategy`: Controls the handling of the top-level reference. Possible values are:
|
|
250
|
+
* - `"keep"`: Keep the top-level reference (default behavior).
|
|
251
|
+
* - `"skip"`: Skip the top-level reference.
|
|
252
|
+
*
|
|
226
253
|
* @category encoding
|
|
227
254
|
* @since 1.0.0
|
|
228
255
|
*/
|
|
229
256
|
export const makeWithDefs = <A, I, R>(schema: Schema.Schema<A, I, R>, options: {
|
|
230
|
-
readonly defs: Record<string,
|
|
257
|
+
readonly defs: Record<string, JsonSchema>
|
|
231
258
|
readonly defsPath?: string
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return out
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const constAny: JsonSchema = { $id: "/schemas/any" }
|
|
245
|
-
|
|
246
|
-
const constUnknown: JsonSchema = { $id: "/schemas/unknown" }
|
|
247
|
-
|
|
248
|
-
const constVoid: JsonSchema = { $id: "/schemas/void" }
|
|
249
|
-
|
|
250
|
-
const constAnyObject: JsonSchema = {
|
|
251
|
-
"$id": "/schemas/object",
|
|
252
|
-
"anyOf": [
|
|
253
|
-
{ "type": "object" },
|
|
254
|
-
{ "type": "array" }
|
|
255
|
-
]
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const constEmpty: JsonSchema = {
|
|
259
|
-
"$id": "/schemas/{}",
|
|
260
|
-
"anyOf": [
|
|
261
|
-
{ "type": "object" },
|
|
262
|
-
{ "type": "array" }
|
|
263
|
-
]
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const getJsonSchemaAnnotations = (annotated: AST.Annotated): Annotations =>
|
|
267
|
-
Record.getSomes({
|
|
268
|
-
description: AST.getDescriptionAnnotation(annotated),
|
|
269
|
-
title: AST.getTitleAnnotation(annotated),
|
|
270
|
-
examples: AST.getExamplesAnnotation(annotated),
|
|
271
|
-
default: AST.getDefaultAnnotation(annotated)
|
|
259
|
+
readonly topLevelReferenceStrategy?: "skip" | "keep"
|
|
260
|
+
}): JsonSchema => {
|
|
261
|
+
return JSONSchema.fromAST(schema.ast, {
|
|
262
|
+
definitions: options.defs,
|
|
263
|
+
definitionPath: options.defsPath ?? "#/components/schemas/",
|
|
264
|
+
target: "openApi3.1",
|
|
265
|
+
topLevelReferenceStrategy: options.topLevelReferenceStrategy ?? "keep"
|
|
272
266
|
})
|
|
273
|
-
|
|
274
|
-
const removeDefaultJsonSchemaAnnotations = (
|
|
275
|
-
jsonSchemaAnnotations: Annotations,
|
|
276
|
-
ast: AST.AST
|
|
277
|
-
): Annotations => {
|
|
278
|
-
if (jsonSchemaAnnotations["title"] === ast.annotations[AST.TitleAnnotationId]) {
|
|
279
|
-
delete jsonSchemaAnnotations["title"]
|
|
280
|
-
}
|
|
281
|
-
if (jsonSchemaAnnotations["description"] === ast.annotations[AST.DescriptionAnnotationId]) {
|
|
282
|
-
delete jsonSchemaAnnotations["description"]
|
|
283
|
-
}
|
|
284
|
-
return jsonSchemaAnnotations
|
|
285
267
|
}
|
|
286
|
-
|
|
287
|
-
const getASTJsonSchemaAnnotations = (ast: AST.AST): Annotations => {
|
|
288
|
-
const jsonSchemaAnnotations = getJsonSchemaAnnotations(ast)
|
|
289
|
-
switch (ast._tag) {
|
|
290
|
-
case "StringKeyword":
|
|
291
|
-
return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.stringKeyword)
|
|
292
|
-
case "NumberKeyword":
|
|
293
|
-
return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.numberKeyword)
|
|
294
|
-
case "BooleanKeyword":
|
|
295
|
-
return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.booleanKeyword)
|
|
296
|
-
default:
|
|
297
|
-
return jsonSchemaAnnotations
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const pruneUndefinedFromPropertySignature = (ast: AST.AST): AST.AST | undefined => {
|
|
302
|
-
if (Option.isNone(AST.getJSONSchemaAnnotation(ast))) {
|
|
303
|
-
switch (ast._tag) {
|
|
304
|
-
case "Union": {
|
|
305
|
-
const types = ast.types.filter((type) => !AST.isUndefinedKeyword(type))
|
|
306
|
-
if (types.length < ast.types.length) {
|
|
307
|
-
return AST.Union.make(types, ast.annotations)
|
|
308
|
-
}
|
|
309
|
-
break
|
|
310
|
-
}
|
|
311
|
-
case "Transformation":
|
|
312
|
-
return pruneUndefinedFromPropertySignature(isParseJsonTransformation(ast.from) ? ast.to : ast.from)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const getRefinementInnerTransformation = (ast: AST.Refinement): AST.AST | undefined => {
|
|
318
|
-
switch (ast.from._tag) {
|
|
319
|
-
case "Transformation":
|
|
320
|
-
return ast.from
|
|
321
|
-
case "Refinement":
|
|
322
|
-
return getRefinementInnerTransformation(ast.from)
|
|
323
|
-
case "Suspend": {
|
|
324
|
-
const from = ast.from.f()
|
|
325
|
-
if (AST.isRefinement(from)) {
|
|
326
|
-
return getRefinementInnerTransformation(from)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const isParseJsonTransformation = (ast: AST.AST): boolean =>
|
|
333
|
-
ast.annotations[AST.SchemaIdAnnotationId] === AST.ParseJsonSchemaId
|
|
334
|
-
|
|
335
|
-
const isOverrideAnnotation = (jsonSchema: JsonSchema): boolean => {
|
|
336
|
-
return ("type" in jsonSchema) || ("oneOf" in jsonSchema) || ("anyOf" in jsonSchema) || ("const" in jsonSchema) ||
|
|
337
|
-
("enum" in jsonSchema) || ("$ref" in jsonSchema)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const go = (
|
|
341
|
-
ast: AST.AST,
|
|
342
|
-
$defs: Record<string, JsonSchema>,
|
|
343
|
-
handleIdentifier: boolean,
|
|
344
|
-
path: ReadonlyArray<PropertyKey>,
|
|
345
|
-
options: {
|
|
346
|
-
readonly getRef: (id: string) => string
|
|
347
|
-
}
|
|
348
|
-
): JsonSchema => {
|
|
349
|
-
const hook = AST.getJSONSchemaAnnotation(ast)
|
|
350
|
-
if (Option.isSome(hook)) {
|
|
351
|
-
const handler = hook.value as JsonSchema
|
|
352
|
-
if (AST.isRefinement(ast)) {
|
|
353
|
-
const t = getRefinementInnerTransformation(ast)
|
|
354
|
-
if (t === undefined) {
|
|
355
|
-
try {
|
|
356
|
-
return {
|
|
357
|
-
...go(ast.from, $defs, true, path, options),
|
|
358
|
-
...getJsonSchemaAnnotations(ast),
|
|
359
|
-
...handler
|
|
360
|
-
}
|
|
361
|
-
} catch (e) {
|
|
362
|
-
return {
|
|
363
|
-
...getJsonSchemaAnnotations(ast),
|
|
364
|
-
...handler
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
} else if (!isOverrideAnnotation(handler)) {
|
|
368
|
-
return {
|
|
369
|
-
...go(t, $defs, true, path, options),
|
|
370
|
-
...getJsonSchemaAnnotations(ast)
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return handler
|
|
375
|
-
}
|
|
376
|
-
const surrogate = AST.getSurrogateAnnotation(ast)
|
|
377
|
-
if (handleIdentifier && !AST.isRefinement(ast)) {
|
|
378
|
-
const identifier = AST.getJSONIdentifier(
|
|
379
|
-
Option.isSome(surrogate) ?
|
|
380
|
-
{
|
|
381
|
-
annotations: {
|
|
382
|
-
...(ast._tag === "Transformation" ? ast.to.annotations : {}),
|
|
383
|
-
...ast.annotations
|
|
384
|
-
}
|
|
385
|
-
} :
|
|
386
|
-
ast
|
|
387
|
-
)
|
|
388
|
-
if (Option.isSome(identifier)) {
|
|
389
|
-
const id = identifier.value
|
|
390
|
-
const out = { $ref: options.getRef(id) }
|
|
391
|
-
if (!Record.has($defs, id)) {
|
|
392
|
-
$defs[id] = out
|
|
393
|
-
$defs[id] = go(ast, $defs, false, path, options)
|
|
394
|
-
}
|
|
395
|
-
return out
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (Option.isSome(surrogate)) {
|
|
399
|
-
return {
|
|
400
|
-
...go(surrogate.value, $defs, handleIdentifier, path, options),
|
|
401
|
-
...(ast._tag === "Transformation" ? getJsonSchemaAnnotations(ast.to) : {}),
|
|
402
|
-
...getJsonSchemaAnnotations(ast)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
switch (ast._tag) {
|
|
406
|
-
case "Declaration":
|
|
407
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
408
|
-
case "Literal": {
|
|
409
|
-
const literal = ast.literal
|
|
410
|
-
if (literal === null) {
|
|
411
|
-
return {
|
|
412
|
-
enum: [null],
|
|
413
|
-
...getJsonSchemaAnnotations(ast)
|
|
414
|
-
}
|
|
415
|
-
} else if (Predicate.isString(literal) || Predicate.isNumber(literal) || Predicate.isBoolean(literal)) {
|
|
416
|
-
return {
|
|
417
|
-
enum: [literal],
|
|
418
|
-
...getJsonSchemaAnnotations(ast)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
422
|
-
}
|
|
423
|
-
case "UniqueSymbol":
|
|
424
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
425
|
-
case "UndefinedKeyword":
|
|
426
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
427
|
-
case "VoidKeyword":
|
|
428
|
-
return {
|
|
429
|
-
...constVoid,
|
|
430
|
-
...getJsonSchemaAnnotations(ast)
|
|
431
|
-
}
|
|
432
|
-
case "NeverKeyword":
|
|
433
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
434
|
-
case "UnknownKeyword":
|
|
435
|
-
return {
|
|
436
|
-
...constUnknown,
|
|
437
|
-
...getJsonSchemaAnnotations(ast)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
case "AnyKeyword":
|
|
441
|
-
return {
|
|
442
|
-
...constAny,
|
|
443
|
-
...getJsonSchemaAnnotations(ast)
|
|
444
|
-
}
|
|
445
|
-
case "ObjectKeyword":
|
|
446
|
-
return {
|
|
447
|
-
...constAnyObject,
|
|
448
|
-
...getJsonSchemaAnnotations(ast)
|
|
449
|
-
}
|
|
450
|
-
case "StringKeyword":
|
|
451
|
-
return { type: "string", ...getASTJsonSchemaAnnotations(ast) }
|
|
452
|
-
case "NumberKeyword":
|
|
453
|
-
return { type: "number", ...getASTJsonSchemaAnnotations(ast) }
|
|
454
|
-
case "BooleanKeyword":
|
|
455
|
-
return { type: "boolean", ...getASTJsonSchemaAnnotations(ast) }
|
|
456
|
-
case "BigIntKeyword":
|
|
457
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
458
|
-
case "SymbolKeyword":
|
|
459
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
460
|
-
case "TupleType": {
|
|
461
|
-
const elements = ast.elements.map((e, i) => ({
|
|
462
|
-
...go(e.type, $defs, true, path.concat(i), options),
|
|
463
|
-
...getJsonSchemaAnnotations(e)
|
|
464
|
-
}))
|
|
465
|
-
const rest = ast.rest.map((annotatedAST) => ({
|
|
466
|
-
...go(annotatedAST.type, $defs, true, path, options),
|
|
467
|
-
...getJsonSchemaAnnotations(annotatedAST)
|
|
468
|
-
}))
|
|
469
|
-
const output: Array = { type: "array" }
|
|
470
|
-
// ---------------------------------------------
|
|
471
|
-
// handle elements
|
|
472
|
-
// ---------------------------------------------
|
|
473
|
-
const len = ast.elements.length
|
|
474
|
-
if (len > 0) {
|
|
475
|
-
output.minItems = len - ast.elements.filter((element) => element.isOptional).length
|
|
476
|
-
output.items = elements
|
|
477
|
-
}
|
|
478
|
-
// ---------------------------------------------
|
|
479
|
-
// handle rest element
|
|
480
|
-
// ---------------------------------------------
|
|
481
|
-
const restLength = rest.length
|
|
482
|
-
if (restLength > 0) {
|
|
483
|
-
const head = rest[0]
|
|
484
|
-
const isHomogeneous = restLength === 1 && ast.elements.every((e) => e.type === ast.rest[0].type)
|
|
485
|
-
if (isHomogeneous) {
|
|
486
|
-
output.items = head
|
|
487
|
-
} else {
|
|
488
|
-
output.additionalItems = head
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// ---------------------------------------------
|
|
492
|
-
// handle post rest elements
|
|
493
|
-
// ---------------------------------------------
|
|
494
|
-
if (restLength > 1) {
|
|
495
|
-
throw new Error(getJSONSchemaUnsupportedPostRestElementsErrorMessage(path))
|
|
496
|
-
}
|
|
497
|
-
} else {
|
|
498
|
-
if (len > 0) {
|
|
499
|
-
output.additionalItems = false
|
|
500
|
-
} else {
|
|
501
|
-
output.maxItems = 0
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return {
|
|
506
|
-
...output,
|
|
507
|
-
...getJsonSchemaAnnotations(ast)
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
case "TypeLiteral": {
|
|
511
|
-
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
|
|
512
|
-
return {
|
|
513
|
-
...constEmpty,
|
|
514
|
-
...getJsonSchemaAnnotations(ast)
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
let patternProperties: JsonSchema | undefined = undefined
|
|
518
|
-
let propertyNames: JsonSchema | undefined = undefined
|
|
519
|
-
for (const is of ast.indexSignatures) {
|
|
520
|
-
const parameter = is.parameter
|
|
521
|
-
switch (parameter._tag) {
|
|
522
|
-
case "StringKeyword": {
|
|
523
|
-
patternProperties = go(is.type, $defs, true, path, options)
|
|
524
|
-
break
|
|
525
|
-
}
|
|
526
|
-
case "TemplateLiteral": {
|
|
527
|
-
patternProperties = go(is.type, $defs, true, path, options)
|
|
528
|
-
propertyNames = {
|
|
529
|
-
type: "string",
|
|
530
|
-
pattern: AST.getTemplateLiteralRegExp(parameter).source
|
|
531
|
-
}
|
|
532
|
-
break
|
|
533
|
-
}
|
|
534
|
-
case "Refinement": {
|
|
535
|
-
patternProperties = go(is.type, $defs, true, path, options)
|
|
536
|
-
propertyNames = go(parameter, $defs, true, path, options)
|
|
537
|
-
break
|
|
538
|
-
}
|
|
539
|
-
case "SymbolKeyword":
|
|
540
|
-
throw new Error(getJSONSchemaUnsupportedParameterErrorMessage(path, parameter))
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
const output: Object = {
|
|
544
|
-
type: "object",
|
|
545
|
-
required: [],
|
|
546
|
-
properties: {},
|
|
547
|
-
additionalProperties: false
|
|
548
|
-
}
|
|
549
|
-
// ---------------------------------------------
|
|
550
|
-
// handle property signatures
|
|
551
|
-
// ---------------------------------------------
|
|
552
|
-
for (let i = 0; i < ast.propertySignatures.length; i++) {
|
|
553
|
-
const ps = ast.propertySignatures[i]
|
|
554
|
-
const name = ps.name
|
|
555
|
-
if (Predicate.isString(name)) {
|
|
556
|
-
const pruned = pruneUndefinedFromPropertySignature(ps.type)
|
|
557
|
-
output.properties[name] = {
|
|
558
|
-
...go(pruned ? pruned : ps.type, $defs, true, path.concat(ps.name), options),
|
|
559
|
-
...getJsonSchemaAnnotations(ps)
|
|
560
|
-
}
|
|
561
|
-
// ---------------------------------------------
|
|
562
|
-
// handle optional property signatures
|
|
563
|
-
// ---------------------------------------------
|
|
564
|
-
if (!ps.isOptional && pruned === undefined) {
|
|
565
|
-
output.required.push(name)
|
|
566
|
-
}
|
|
567
|
-
} else {
|
|
568
|
-
throw new Error(getJSONSchemaUnsupportedKeyErrorMessage(name, path))
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// ---------------------------------------------
|
|
572
|
-
// handle index signatures
|
|
573
|
-
// ---------------------------------------------
|
|
574
|
-
if (patternProperties !== undefined) {
|
|
575
|
-
delete output.additionalProperties
|
|
576
|
-
output.patternProperties = { "": patternProperties }
|
|
577
|
-
}
|
|
578
|
-
if (propertyNames !== undefined) {
|
|
579
|
-
output.propertyNames = propertyNames
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return {
|
|
583
|
-
...output,
|
|
584
|
-
...getJsonSchemaAnnotations(ast)
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
case "Union": {
|
|
588
|
-
const enums: globalThis.Array<AST.LiteralValue> = []
|
|
589
|
-
const anyOf: globalThis.Array<JsonSchema> = []
|
|
590
|
-
for (const type of ast.types) {
|
|
591
|
-
const schema = go(type, $defs, true, path, options)
|
|
592
|
-
if ("enum" in schema) {
|
|
593
|
-
if (Object.keys(schema).length > 1) {
|
|
594
|
-
anyOf.push(schema)
|
|
595
|
-
} else {
|
|
596
|
-
for (const e of schema.enum) {
|
|
597
|
-
enums.push(e)
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
} else {
|
|
601
|
-
anyOf.push(schema)
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
if (anyOf.length === 0) {
|
|
605
|
-
return { enum: enums, ...getJsonSchemaAnnotations(ast) }
|
|
606
|
-
} else {
|
|
607
|
-
if (enums.length >= 1) {
|
|
608
|
-
anyOf.push({ enum: enums })
|
|
609
|
-
}
|
|
610
|
-
return { anyOf, ...getJsonSchemaAnnotations(ast) }
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
case "Enums": {
|
|
614
|
-
return {
|
|
615
|
-
$comment: "/schemas/enums",
|
|
616
|
-
anyOf: ast.enums.map((e) => ({ title: e[0], enum: [e[1]] })),
|
|
617
|
-
...getJsonSchemaAnnotations(ast)
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
case "Refinement": {
|
|
621
|
-
if (AST.encodedBoundAST(ast) === ast) {
|
|
622
|
-
throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast))
|
|
623
|
-
}
|
|
624
|
-
return go(ast.from, $defs, true, path, options)
|
|
625
|
-
}
|
|
626
|
-
case "TemplateLiteral": {
|
|
627
|
-
const regex = AST.getTemplateLiteralRegExp(ast)
|
|
628
|
-
return {
|
|
629
|
-
type: "string",
|
|
630
|
-
description: "a template literal",
|
|
631
|
-
pattern: regex.source,
|
|
632
|
-
...getJsonSchemaAnnotations(ast)
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
case "Suspend": {
|
|
636
|
-
const identifier = Option.orElse(AST.getJSONIdentifier(ast), () => AST.getJSONIdentifier(ast.f()))
|
|
637
|
-
if (Option.isNone(identifier)) {
|
|
638
|
-
throw new Error(getJSONSchemaMissingIdentifierAnnotationErrorMessage(path, ast))
|
|
639
|
-
}
|
|
640
|
-
return {
|
|
641
|
-
...go(ast.f(), $defs, true, path, options),
|
|
642
|
-
...getJsonSchemaAnnotations(ast)
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
case "Transformation": {
|
|
646
|
-
// Properly handle S.parseJson transformations by focusing on
|
|
647
|
-
// the 'to' side of the AST. This approach prevents the generation of useless schemas
|
|
648
|
-
// derived from the 'from' side (type: string), ensuring the output matches the intended
|
|
649
|
-
// complex schema type.
|
|
650
|
-
if (isParseJsonTransformation(ast.from)) {
|
|
651
|
-
return {
|
|
652
|
-
type: "string",
|
|
653
|
-
contentMediaType: "application/json",
|
|
654
|
-
contentSchema: go(ast.to, $defs, true, path, options),
|
|
655
|
-
...getJsonSchemaAnnotations(ast)
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
return {
|
|
659
|
-
...getASTJsonSchemaAnnotations(ast.to),
|
|
660
|
-
...go(ast.from, $defs, true, path, options),
|
|
661
|
-
...getJsonSchemaAnnotations(ast)
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const getJSONSchemaMissingAnnotationErrorMessage = (
|
|
668
|
-
path: ReadonlyArray<PropertyKey>,
|
|
669
|
-
ast: AST.AST
|
|
670
|
-
) =>
|
|
671
|
-
getMissingAnnotationErrorMessage(
|
|
672
|
-
`Generating a JSON Schema for this schema requires a "jsonSchema" annotation`,
|
|
673
|
-
path,
|
|
674
|
-
ast
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
const getJSONSchemaMissingIdentifierAnnotationErrorMessage = (
|
|
678
|
-
path: ReadonlyArray<PropertyKey>,
|
|
679
|
-
ast: AST.AST
|
|
680
|
-
) =>
|
|
681
|
-
getMissingAnnotationErrorMessage(
|
|
682
|
-
`Generating a JSON Schema for this schema requires an "identifier" annotation`,
|
|
683
|
-
path,
|
|
684
|
-
ast
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
const getJSONSchemaUnsupportedParameterErrorMessage = (
|
|
688
|
-
path: ReadonlyArray<PropertyKey>,
|
|
689
|
-
parameter: AST.AST
|
|
690
|
-
): string => getErrorMessage("Unsupported index signature parameter", undefined, path, parameter)
|
|
691
|
-
|
|
692
|
-
const getJSONSchemaUnsupportedPostRestElementsErrorMessage = (path: ReadonlyArray<PropertyKey>): string =>
|
|
693
|
-
getErrorMessage(
|
|
694
|
-
"Generating a JSON Schema for post-rest elements is not currently supported. You're welcome to contribute by submitting a Pull Request",
|
|
695
|
-
undefined,
|
|
696
|
-
path
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
const getJSONSchemaUnsupportedKeyErrorMessage = (key: PropertyKey, path: ReadonlyArray<PropertyKey>): string =>
|
|
700
|
-
getErrorMessage("Unsupported key", `Cannot encode ${formatPropertyKey(key)} key to JSON Schema`, path)
|
|
701
|
-
|
|
702
|
-
const getMissingAnnotationErrorMessage = (details?: string, path?: ReadonlyArray<PropertyKey>, ast?: AST.AST): string =>
|
|
703
|
-
getErrorMessage("Missing annotation", details, path, ast)
|
|
704
|
-
|
|
705
|
-
const getErrorMessage = (
|
|
706
|
-
reason: string,
|
|
707
|
-
details?: string,
|
|
708
|
-
path?: ReadonlyArray<PropertyKey>,
|
|
709
|
-
ast?: AST.AST
|
|
710
|
-
): string => {
|
|
711
|
-
let out = reason
|
|
712
|
-
|
|
713
|
-
if (path && Arr.isNonEmptyReadonlyArray(path)) {
|
|
714
|
-
out += `\nat path: ${formatPath(path)}`
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
if (details !== undefined) {
|
|
718
|
-
out += `\ndetails: ${details}`
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
if (ast) {
|
|
722
|
-
out += `\nschema (${ast._tag}): ${ast}`
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
return out
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const formatPathKey = (key: PropertyKey): string => `[${formatPropertyKey(key)}]`
|
|
729
|
-
|
|
730
|
-
const formatPath = (path: ParseResult.Path): string =>
|
|
731
|
-
isNonEmpty(path) ? path.map(formatPathKey).join("") : formatPathKey(path)
|
|
732
|
-
|
|
733
|
-
const isNonEmpty = <A>(x: ParseResult.SingleOrNonEmpty<A>): x is Arr.NonEmptyReadonlyArray<A> => Array.isArray(x)
|
|
734
|
-
|
|
735
|
-
const formatPropertyKey = (name: PropertyKey): string => typeof name === "string" ? JSON.stringify(name) : String(name)
|