@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.
Files changed (54) hide show
  1. package/README.md +22 -34
  2. package/dist/cjs/HttpApi.js +15 -6
  3. package/dist/cjs/HttpApi.js.map +1 -1
  4. package/dist/cjs/HttpApiBuilder.js +1 -1
  5. package/dist/cjs/HttpApiBuilder.js.map +1 -1
  6. package/dist/cjs/HttpApiClient.js.map +1 -1
  7. package/dist/cjs/HttpApiError.js +109 -1
  8. package/dist/cjs/HttpApiError.js.map +1 -1
  9. package/dist/cjs/HttpApiGroup.js.map +1 -1
  10. package/dist/cjs/HttpApiSchema.js +46 -2
  11. package/dist/cjs/HttpApiSchema.js.map +1 -1
  12. package/dist/cjs/OpenApi.js +20 -4
  13. package/dist/cjs/OpenApi.js.map +1 -1
  14. package/dist/cjs/OpenApiJsonSchema.js +18 -477
  15. package/dist/cjs/OpenApiJsonSchema.js.map +1 -1
  16. package/dist/dts/HttpApi.d.ts +13 -12
  17. package/dist/dts/HttpApi.d.ts.map +1 -1
  18. package/dist/dts/HttpApiBuilder.d.ts +8 -8
  19. package/dist/dts/HttpApiBuilder.d.ts.map +1 -1
  20. package/dist/dts/HttpApiClient.d.ts +3 -3
  21. package/dist/dts/HttpApiClient.d.ts.map +1 -1
  22. package/dist/dts/HttpApiError.d.ts +85 -0
  23. package/dist/dts/HttpApiError.d.ts.map +1 -1
  24. package/dist/dts/HttpApiGroup.d.ts +3 -2
  25. package/dist/dts/HttpApiGroup.d.ts.map +1 -1
  26. package/dist/dts/HttpApiSchema.d.ts +19 -1
  27. package/dist/dts/HttpApiSchema.d.ts.map +1 -1
  28. package/dist/dts/OpenApi.d.ts +8 -0
  29. package/dist/dts/OpenApi.d.ts.map +1 -1
  30. package/dist/dts/OpenApiJsonSchema.d.ts +34 -5
  31. package/dist/dts/OpenApiJsonSchema.d.ts.map +1 -1
  32. package/dist/esm/HttpApi.js +13 -5
  33. package/dist/esm/HttpApi.js.map +1 -1
  34. package/dist/esm/HttpApiBuilder.js +1 -1
  35. package/dist/esm/HttpApiBuilder.js.map +1 -1
  36. package/dist/esm/HttpApiClient.js.map +1 -1
  37. package/dist/esm/HttpApiError.js +96 -0
  38. package/dist/esm/HttpApiError.js.map +1 -1
  39. package/dist/esm/HttpApiGroup.js.map +1 -1
  40. package/dist/esm/HttpApiSchema.js +45 -2
  41. package/dist/esm/HttpApiSchema.js.map +1 -1
  42. package/dist/esm/OpenApi.js +17 -2
  43. package/dist/esm/OpenApi.js.map +1 -1
  44. package/dist/esm/OpenApiJsonSchema.js +18 -477
  45. package/dist/esm/OpenApiJsonSchema.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/HttpApi.ts +35 -19
  48. package/src/HttpApiBuilder.ts +17 -15
  49. package/src/HttpApiClient.ts +8 -6
  50. package/src/HttpApiError.ts +108 -0
  51. package/src/HttpApiGroup.ts +4 -3
  52. package/src/HttpApiSchema.ts +63 -5
  53. package/src/OpenApi.ts +19 -3
  54. 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> = {
@@ -1,13 +1,10 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import * as Arr from "effect/Array"
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 $defs: Record<string, any> = {}
218
- const out = makeWithDefs(schema, { defs: $defs })
219
- if (!Record.isEmptyRecord($defs)) {
220
- out.$defs = $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, any>
257
+ readonly defs: Record<string, JsonSchema>
231
258
  readonly defsPath?: string
232
- }): Root => {
233
- const defsPath = options.defsPath ?? "#/$defs/"
234
- const getRef = (id: string) => `${defsPath}${id}`
235
- const out = go(schema.ast, options.defs, true, [], { getRef }) as Root
236
- for (const id in options.defs) {
237
- if (options.defs[id]["$ref"] === getRef(id)) {
238
- delete options.defs[id]
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)