effect 3.17.9 → 3.17.10

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 (49) hide show
  1. package/dist/cjs/Arbitrary.js +5 -2
  2. package/dist/cjs/Arbitrary.js.map +1 -1
  3. package/dist/cjs/Either.js.map +1 -1
  4. package/dist/cjs/JSONSchema.js +307 -364
  5. package/dist/cjs/JSONSchema.js.map +1 -1
  6. package/dist/cjs/ParseResult.js +1 -1
  7. package/dist/cjs/ParseResult.js.map +1 -1
  8. package/dist/cjs/Schema.js +17 -14
  9. package/dist/cjs/Schema.js.map +1 -1
  10. package/dist/cjs/SchemaAST.js +27 -25
  11. package/dist/cjs/SchemaAST.js.map +1 -1
  12. package/dist/cjs/internal/core-effect.js +6 -6
  13. package/dist/cjs/internal/core-effect.js.map +1 -1
  14. package/dist/cjs/internal/version.js +1 -1
  15. package/dist/cjs/internal/version.js.map +1 -1
  16. package/dist/dts/Effect.d.ts +4 -4
  17. package/dist/dts/Either.d.ts +96 -96
  18. package/dist/dts/Either.d.ts.map +1 -1
  19. package/dist/dts/JSONSchema.d.ts.map +1 -1
  20. package/dist/dts/STM.d.ts +2 -2
  21. package/dist/dts/Schema.d.ts.map +1 -1
  22. package/dist/dts/SchemaAST.d.ts +2 -0
  23. package/dist/dts/SchemaAST.d.ts.map +1 -1
  24. package/dist/esm/Arbitrary.js +5 -2
  25. package/dist/esm/Arbitrary.js.map +1 -1
  26. package/dist/esm/Either.js.map +1 -1
  27. package/dist/esm/JSONSchema.js +307 -364
  28. package/dist/esm/JSONSchema.js.map +1 -1
  29. package/dist/esm/ParseResult.js +1 -1
  30. package/dist/esm/ParseResult.js.map +1 -1
  31. package/dist/esm/Schema.js +17 -14
  32. package/dist/esm/Schema.js.map +1 -1
  33. package/dist/esm/SchemaAST.js +27 -25
  34. package/dist/esm/SchemaAST.js.map +1 -1
  35. package/dist/esm/internal/core-effect.js +6 -6
  36. package/dist/esm/internal/core-effect.js.map +1 -1
  37. package/dist/esm/internal/version.js +1 -1
  38. package/dist/esm/internal/version.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/Arbitrary.ts +4 -2
  41. package/src/Effect.ts +4 -4
  42. package/src/Either.ts +132 -132
  43. package/src/JSONSchema.ts +373 -332
  44. package/src/ParseResult.ts +1 -1
  45. package/src/STM.ts +2 -2
  46. package/src/Schema.ts +19 -12
  47. package/src/SchemaAST.ts +24 -31
  48. package/src/internal/core-effect.ts +6 -6
  49. package/src/internal/version.ts +1 -1
package/src/JSONSchema.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as Arr from "./Array.js"
6
6
  import * as errors_ from "./internal/schema/errors.js"
7
+ import * as schemaId_ from "./internal/schema/schemaId.js"
7
8
  import * as Option from "./Option.js"
8
9
  import * as ParseResult from "./ParseResult.js"
9
10
  import * as Predicate from "./Predicate.js"
@@ -313,42 +314,50 @@ export const fromAST = (ast: AST.AST, options: {
313
314
  const definitionPath = options.definitionPath ?? "#/$defs/"
314
315
  const getRef = (id: string) => definitionPath + id
315
316
  const target = options.target ?? "jsonSchema7"
316
- const handleIdentifier = options.topLevelReferenceStrategy !== "skip"
317
+ const handleIdentifier = options.topLevelReferenceStrategy !== "skip" ? "handle-identifier" : "ignore-identifier"
317
318
  const additionalPropertiesStrategy = options.additionalPropertiesStrategy ?? "strict"
318
- return go(ast, options.definitions, handleIdentifier, [], {
319
- getRef,
320
- target,
321
- additionalPropertiesStrategy
322
- })
323
- }
324
-
325
- const constNever: JsonSchema7 = {
326
- "$id": "/schemas/never",
327
- "not": {}
319
+ return go(
320
+ ast,
321
+ options.definitions,
322
+ handleIdentifier,
323
+ [],
324
+ {
325
+ getRef,
326
+ target,
327
+ additionalPropertiesStrategy
328
+ },
329
+ "handle-annotation",
330
+ "handle-errors"
331
+ )
332
+ }
333
+
334
+ const constNever: JsonSchema7Never = {
335
+ $id: "/schemas/never",
336
+ not: {}
328
337
  }
329
338
 
330
- const constAny: JsonSchema7 = {
331
- "$id": "/schemas/any"
339
+ const constAny: JsonSchema7Any = {
340
+ $id: "/schemas/any"
332
341
  }
333
342
 
334
- const constUnknown: JsonSchema7 = {
335
- "$id": "/schemas/unknown"
343
+ const constUnknown: JsonSchema7Unknown = {
344
+ $id: "/schemas/unknown"
336
345
  }
337
346
 
338
- const constVoid: JsonSchema7 = {
339
- "$id": "/schemas/void"
347
+ const constVoid: JsonSchema7Void = {
348
+ $id: "/schemas/void"
340
349
  }
341
350
 
342
- const constAnyObject: JsonSchema7 = {
343
- "$id": "/schemas/object",
351
+ const constObject: JsonSchema7object = {
352
+ $id: "/schemas/object",
344
353
  "anyOf": [
345
354
  { "type": "object" },
346
355
  { "type": "array" }
347
356
  ]
348
357
  }
349
358
 
350
- const constEmpty: JsonSchema7 = {
351
- "$id": "/schemas/%7B%7D",
359
+ const constEmptyStruct: JsonSchema7empty = {
360
+ $id: "/schemas/%7B%7D",
352
361
  "anyOf": [
353
362
  { "type": "object" },
354
363
  { "type": "array" }
@@ -357,49 +366,97 @@ const constEmpty: JsonSchema7 = {
357
366
 
358
367
  const $schema = "http://json-schema.org/draft-07/schema#"
359
368
 
360
- const getJsonSchemaAnnotations = (ast: AST.AST, annotated?: AST.Annotated): JsonSchemaAnnotations => {
361
- annotated ??= ast
362
- const out: JsonSchemaAnnotations = Record.getSomes({
363
- description: AST.getDescriptionAnnotation(annotated),
364
- title: AST.getTitleAnnotation(annotated),
365
- default: AST.getDefaultAnnotation(annotated)
366
- })
367
- const oexamples = AST.getExamplesAnnotation(annotated)
368
- if (Option.isSome(oexamples) && oexamples.value.length > 0) {
369
- const getOption = ParseResult.getOption(ast, false)
370
- const examples = Arr.filterMap(oexamples.value, (e) => getOption(e))
371
- if (examples.length > 0) {
372
- out.examples = examples
369
+ function getRawDescription(annotated: AST.Annotated | undefined): string | undefined {
370
+ if (annotated !== undefined) return Option.getOrUndefined(AST.getDescriptionAnnotation(annotated))
371
+ }
372
+
373
+ function getRawTitle(annotated: AST.Annotated | undefined): string | undefined {
374
+ if (annotated !== undefined) return Option.getOrUndefined(AST.getTitleAnnotation(annotated))
375
+ }
376
+
377
+ function getRawDefault(annotated: AST.Annotated | undefined): Option.Option<unknown> {
378
+ if (annotated !== undefined) return AST.getDefaultAnnotation(annotated)
379
+ return Option.none()
380
+ }
381
+
382
+ function getRawExamples(annotated: AST.Annotated | undefined): ReadonlyArray<unknown> | undefined {
383
+ if (annotated !== undefined) return Option.getOrUndefined(AST.getExamplesAnnotation(annotated))
384
+ }
385
+
386
+ function encodeExamples(ast: AST.AST, examples: ReadonlyArray<unknown>): Array<unknown> | undefined {
387
+ const getOption = ParseResult.getOption(ast, false)
388
+ const out = Arr.filterMap(examples, (e) => getOption(e))
389
+ return out.length > 0 ? out : undefined
390
+ }
391
+
392
+ function filterBuiltIn(ast: AST.AST, annotation: string | undefined, key: symbol): string | undefined {
393
+ if (annotation !== undefined) {
394
+ switch (ast._tag) {
395
+ case "StringKeyword":
396
+ return annotation !== AST.stringKeyword.annotations[key] ? annotation : undefined
397
+ case "NumberKeyword":
398
+ return annotation !== AST.numberKeyword.annotations[key] ? annotation : undefined
399
+ case "BooleanKeyword":
400
+ return annotation !== AST.booleanKeyword.annotations[key] ? annotation : undefined
373
401
  }
374
402
  }
375
- return out
403
+ return annotation
376
404
  }
377
405
 
378
- const removeDefaultJsonSchemaAnnotations = (
379
- jsonSchemaAnnotations: JsonSchemaAnnotations,
380
- ast: AST.AST
381
- ): JsonSchemaAnnotations => {
382
- if (jsonSchemaAnnotations["title"] === ast.annotations[AST.TitleAnnotationId]) {
383
- delete jsonSchemaAnnotations["title"]
406
+ function pruneJsonSchemaAnnotations(
407
+ ast: AST.AST,
408
+ description: string | undefined,
409
+ title: string | undefined,
410
+ def: Option.Option<unknown>,
411
+ examples: ReadonlyArray<unknown> | undefined
412
+ ): JsonSchemaAnnotations | undefined {
413
+ const out: JsonSchemaAnnotations = {}
414
+ if (description !== undefined) out.description = description
415
+ if (title !== undefined) out.title = title
416
+ if (Option.isSome(def)) out.default = def.value
417
+ if (examples !== undefined) {
418
+ const encodedExamples = encodeExamples(ast, examples)
419
+ if (encodedExamples !== undefined) {
420
+ out.examples = encodedExamples
421
+ }
384
422
  }
385
- if (jsonSchemaAnnotations["description"] === ast.annotations[AST.DescriptionAnnotationId]) {
386
- delete jsonSchemaAnnotations["description"]
423
+ if (Object.keys(out).length === 0) {
424
+ return undefined
387
425
  }
388
- return jsonSchemaAnnotations
426
+ return out
389
427
  }
390
428
 
391
- const getASTJsonSchemaAnnotations = (ast: AST.AST): JsonSchemaAnnotations => {
392
- const jsonSchemaAnnotations = getJsonSchemaAnnotations(ast)
393
- switch (ast._tag) {
394
- case "StringKeyword":
395
- return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.stringKeyword)
396
- case "NumberKeyword":
397
- return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.numberKeyword)
398
- case "BooleanKeyword":
399
- return removeDefaultJsonSchemaAnnotations(jsonSchemaAnnotations, AST.booleanKeyword)
400
- default:
401
- return jsonSchemaAnnotations
429
+ function getContextJsonSchemaAnnotations(ast: AST.AST, annotated: AST.Annotated): JsonSchemaAnnotations | undefined {
430
+ return pruneJsonSchemaAnnotations(
431
+ ast,
432
+ getRawDescription(annotated),
433
+ getRawTitle(annotated),
434
+ getRawDefault(annotated),
435
+ getRawExamples(annotated)
436
+ )
437
+ }
438
+
439
+ function getJsonSchemaAnnotations(ast: AST.AST): JsonSchemaAnnotations | undefined {
440
+ return pruneJsonSchemaAnnotations(
441
+ ast,
442
+ filterBuiltIn(ast, getRawDescription(ast), AST.DescriptionAnnotationId),
443
+ filterBuiltIn(ast, getRawTitle(ast), AST.TitleAnnotationId),
444
+ getRawDefault(ast),
445
+ getRawExamples(ast)
446
+ )
447
+ }
448
+
449
+ function mergeJsonSchemaAnnotations(
450
+ jsonSchema: JsonSchema7,
451
+ jsonSchemaAnnotations: JsonSchemaAnnotations | undefined
452
+ ): JsonSchema7 {
453
+ if (jsonSchemaAnnotations) {
454
+ if ("$ref" in jsonSchema) {
455
+ return { allOf: [jsonSchema], ...jsonSchemaAnnotations } as any
456
+ }
457
+ return { ...jsonSchema, ...jsonSchemaAnnotations }
402
458
  }
459
+ return jsonSchema
403
460
  }
404
461
 
405
462
  const pruneUndefined = (ast: AST.AST): AST.AST | undefined => {
@@ -411,39 +468,18 @@ const pruneUndefined = (ast: AST.AST): AST.AST | undefined => {
411
468
  const isParseJsonTransformation = (ast: AST.AST): boolean =>
412
469
  ast.annotations[AST.SchemaIdAnnotationId] === AST.ParseJsonSchemaId
413
470
 
414
- const isOverrideAnnotation = (jsonSchema: JsonSchema7): boolean => {
415
- return ("type" in jsonSchema) || ("oneOf" in jsonSchema) || ("anyOf" in jsonSchema) || ("const" in jsonSchema) ||
416
- ("enum" in jsonSchema) || ("$ref" in jsonSchema)
417
- }
418
-
419
- // Returns true if the schema is an enum with no other properties other than the
420
- // optional "type". This is used to merge enums together.
421
- const isMergeableEnum = (jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Enum => {
422
- const len = Object.keys(jsonSchema).length
423
- return "enum" in jsonSchema && (len === 1 || ("type" in jsonSchema && len === 2))
424
- }
425
-
426
- // Some validators do not support enums without a type keyword. This function
427
- // adds a type keyword to the schema if it is missing and the enum values are
428
- // homogeneous.
429
- const addEnumType = (jsonSchema: JsonSchema7): JsonSchema7 => {
430
- if ("enum" in jsonSchema && !("type" in jsonSchema)) {
431
- const type: "string" | "number" | "boolean" | undefined = jsonSchema.enum.every(Predicate.isString) ?
432
- "string" :
433
- jsonSchema.enum.every(Predicate.isNumber) ?
434
- "number" :
435
- jsonSchema.enum.every(Predicate.isBoolean) ?
436
- "boolean" :
437
- undefined
438
- if (type !== undefined) {
439
- return { type, ...jsonSchema }
471
+ const isOverrideAnnotation = (ast: AST.AST, jsonSchema: JsonSchema7): boolean => {
472
+ if (AST.isRefinement(ast)) {
473
+ const schemaId = ast.annotations[AST.SchemaIdAnnotationId]
474
+ if (schemaId === schemaId_.IntSchemaId) {
475
+ return "type" in jsonSchema && jsonSchema.type !== "integer"
440
476
  }
441
477
  }
442
- return jsonSchema
478
+ return ("type" in jsonSchema) || ("oneOf" in jsonSchema) || ("anyOf" in jsonSchema) || ("$ref" in jsonSchema)
443
479
  }
444
480
 
445
- const mergeRefinements = (from: any, jsonSchema: any, annotations: any): any => {
446
- const out: any = { ...from, ...annotations, ...jsonSchema }
481
+ const mergeRefinements = (from: any, jsonSchema: any, ast: AST.AST): any => {
482
+ const out: any = { ...from, ...getJsonSchemaAnnotations(ast), ...jsonSchema }
447
483
  out.allOf ??= []
448
484
 
449
485
  const handle = (name: string, filter: (i: any) => boolean) => {
@@ -486,27 +522,6 @@ function isContentSchemaSupported(options: GoOptions): boolean {
486
522
  }
487
523
  }
488
524
 
489
- function isNullTypeKeywordSupported(options: GoOptions): boolean {
490
- switch (options.target) {
491
- case "jsonSchema7":
492
- case "jsonSchema2019-09":
493
- return true
494
- case "openApi3.1":
495
- return false
496
- }
497
- }
498
-
499
- // https://swagger.io/docs/specification/v3_0/data-models/data-types/#null
500
- function isNullableKeywordSupported(options: GoOptions): boolean {
501
- switch (options.target) {
502
- case "jsonSchema7":
503
- case "jsonSchema2019-09":
504
- return false
505
- case "openApi3.1":
506
- return true
507
- }
508
- }
509
-
510
525
  function getAdditionalProperties(options: GoOptions): boolean {
511
526
  switch (options.additionalPropertiesStrategy) {
512
527
  case "allow":
@@ -516,139 +531,169 @@ function getAdditionalProperties(options: GoOptions): boolean {
516
531
  }
517
532
  }
518
533
 
519
- const isNeverJSONSchema = (jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Never =>
520
- "$id" in jsonSchema && jsonSchema.$id === "/schemas/never"
521
-
522
- const isAnyJSONSchema = (jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Any =>
523
- "$id" in jsonSchema && jsonSchema.$id === "/schemas/any"
524
-
525
- const isUnknownJSONSchema = (jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Unknown =>
526
- "$id" in jsonSchema && jsonSchema.$id === "/schemas/unknown"
527
-
528
- const isVoidJSONSchema = (jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Void =>
529
- "$id" in jsonSchema && jsonSchema.$id === "/schemas/void"
534
+ function addASTAnnotations(jsonSchema: JsonSchema7, ast: AST.AST): JsonSchema7 {
535
+ return addAnnotations(jsonSchema, getJsonSchemaAnnotations(ast))
536
+ }
530
537
 
531
- const shrink = (members: Array<JsonSchema7>): Array<JsonSchema7> => {
532
- let i = members.findIndex(isAnyJSONSchema)
533
- if (i !== -1) {
534
- members = [members[i]]
538
+ function addAnnotations(jsonSchema: JsonSchema7, annotations: JsonSchemaAnnotations | undefined): JsonSchema7 {
539
+ if (annotations === undefined || Object.keys(annotations).length === 0) {
540
+ return jsonSchema
535
541
  }
536
- i = members.findIndex(isUnknownJSONSchema)
537
- if (i !== -1) {
538
- members = [members[i]]
542
+ if ("$ref" in jsonSchema) {
543
+ return { allOf: [jsonSchema], ...annotations } as any
539
544
  }
540
- i = members.findIndex(isVoidJSONSchema)
541
- if (i !== -1) {
542
- members = [members[i]]
545
+ return { ...jsonSchema, ...annotations }
546
+ }
547
+
548
+ function getIdentifierAnnotation(ast: AST.AST): string | undefined {
549
+ const identifier = Option.getOrUndefined(AST.getJSONIdentifier(ast))
550
+ if (identifier === undefined) {
551
+ if (AST.isSuspend(ast)) {
552
+ return getIdentifierAnnotation(ast.f())
553
+ }
554
+ if (AST.isTransformation(ast) && AST.isTypeLiteral(ast.from) && AST.isDeclaration(ast.to)) {
555
+ const to = ast.to
556
+ const surrogate = AST.getSurrogateAnnotation(to)
557
+ if (Option.isSome(surrogate)) {
558
+ return getIdentifierAnnotation(to)
559
+ }
560
+ }
543
561
  }
544
- return members
562
+ return identifier
545
563
  }
546
564
 
547
- const go = (
565
+ function go(
548
566
  ast: AST.AST,
549
567
  $defs: Record<string, JsonSchema7>,
550
- handleIdentifier: boolean,
568
+ identifier: "handle-identifier" | "ignore-identifier",
551
569
  path: ReadonlyArray<PropertyKey>,
552
- options: GoOptions
553
- ): JsonSchema7 => {
554
- if (handleIdentifier) {
555
- const identifier = AST.getJSONIdentifier(ast)
556
- if (Option.isSome(identifier)) {
557
- const id = identifier.value
570
+ options: GoOptions,
571
+ annotation: "handle-annotation" | "ignore-annotation",
572
+ errors: "handle-errors" | "ignore-errors"
573
+ ): JsonSchema7 {
574
+ if (identifier === "handle-identifier") {
575
+ const id = getIdentifierAnnotation(ast)
576
+ if (id !== undefined) {
558
577
  const escapedId = id.replace(/~/ig, "~0").replace(/\//ig, "~1")
559
578
  const out = { $ref: options.getRef(escapedId) }
560
579
  if (!Record.has($defs, id)) {
561
580
  $defs[id] = out
562
- $defs[id] = go(ast, $defs, false, path, options)
581
+ $defs[id] = go(ast, $defs, "ignore-identifier", path, options, "handle-annotation", errors)
563
582
  }
564
583
  return out
565
584
  }
566
585
  }
567
- const hook = AST.getJSONSchemaAnnotation(ast)
568
- if (Option.isSome(hook)) {
569
- const handler = hook.value as JsonSchema7
570
- if (AST.isRefinement(ast)) {
571
- const t = AST.getTransformationFrom(ast)
572
- if (t === undefined) {
573
- return mergeRefinements(
574
- go(ast.from, $defs, handleIdentifier, path, options),
575
- handler,
576
- getJsonSchemaAnnotations(ast)
577
- )
578
- } else if (!isOverrideAnnotation(handler)) {
579
- return go(t, $defs, handleIdentifier, path, options)
586
+ if (annotation === "handle-annotation") {
587
+ const hook = AST.getJSONSchemaAnnotation(ast)
588
+ if (Option.isSome(hook)) {
589
+ const handler = hook.value as JsonSchema7
590
+ if (isOverrideAnnotation(ast, handler)) {
591
+ switch (ast._tag) {
592
+ case "Declaration":
593
+ return addASTAnnotations(handler, ast)
594
+ default:
595
+ return handler
596
+ }
597
+ } else {
598
+ switch (ast._tag) {
599
+ case "Refinement": {
600
+ const t = AST.getTransformationFrom(ast)
601
+ if (t === undefined) {
602
+ return mergeRefinements(
603
+ go(ast.from, $defs, identifier, path, options, "handle-annotation", errors),
604
+ handler,
605
+ ast
606
+ )
607
+ } else {
608
+ return go(t, $defs, identifier, path, options, "handle-annotation", errors)
609
+ }
610
+ }
611
+ default:
612
+ return {
613
+ ...go(ast, $defs, identifier, path, options, "ignore-annotation", errors),
614
+ ...handler
615
+ } as any
616
+ }
580
617
  }
581
618
  }
582
- if (AST.isDeclaration(ast)) {
583
- return { ...handler, ...getJsonSchemaAnnotations(ast) }
584
- }
585
- return handler
586
619
  }
587
620
  const surrogate = AST.getSurrogateAnnotation(ast)
588
621
  if (Option.isSome(surrogate)) {
589
- return go(surrogate.value, $defs, handleIdentifier, path, options)
622
+ return go(surrogate.value, $defs, identifier, path, options, "handle-annotation", errors)
590
623
  }
591
624
  switch (ast._tag) {
625
+ // Unsupported
592
626
  case "Declaration":
627
+ case "UndefinedKeyword":
628
+ case "BigIntKeyword":
629
+ case "UniqueSymbol":
630
+ case "SymbolKeyword": {
631
+ if (errors === "ignore-errors") return addASTAnnotations(constAny, ast)
593
632
  throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast))
594
- case "Literal": {
595
- const literal = ast.literal
596
- if (literal === null) {
597
- if (isNullTypeKeywordSupported(options)) {
598
- // https://json-schema.org/draft-07/draft-handrews-json-schema-validation-00.pdf
599
- // Section 6.1.1
600
- return { type: "null", ...getJsonSchemaAnnotations(ast) }
601
- } else {
602
- // OpenAPI 3.1 does not support the "null" type keyword
603
- // https://swagger.io/docs/specification/v3_0/data-models/data-types/#null
604
- return {
605
- // @ts-expect-error
606
- enum: [null],
607
- ...getJsonSchemaAnnotations(ast)
608
- }
609
- }
610
- } else if (Predicate.isString(literal)) {
611
- return { type: "string", enum: [literal], ...getJsonSchemaAnnotations(ast) }
612
- } else if (Predicate.isNumber(literal)) {
613
- return { type: "number", enum: [literal], ...getJsonSchemaAnnotations(ast) }
614
- } else if (Predicate.isBoolean(literal)) {
615
- return { type: "boolean", enum: [literal], ...getJsonSchemaAnnotations(ast) }
633
+ }
634
+ case "Suspend": {
635
+ if (identifier === "handle-identifier") {
636
+ if (errors === "ignore-errors") return addASTAnnotations(constAny, ast)
637
+ throw new Error(errors_.getJSONSchemaMissingIdentifierAnnotationErrorMessage(path, ast))
616
638
  }
617
- throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast))
639
+ return go(ast.f(), $defs, "ignore-identifier", path, options, "handle-annotation", errors)
618
640
  }
619
- case "UniqueSymbol":
620
- throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast))
621
- case "UndefinedKeyword":
622
- throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast))
623
- case "VoidKeyword":
624
- return { ...constVoid, ...getJsonSchemaAnnotations(ast) }
641
+ // Primitives
625
642
  case "NeverKeyword":
626
- return { ...constNever, ...getJsonSchemaAnnotations(ast) }
643
+ return addASTAnnotations(constNever, ast)
644
+ case "VoidKeyword":
645
+ return addASTAnnotations(constVoid, ast)
627
646
  case "UnknownKeyword":
628
- return { ...constUnknown, ...getJsonSchemaAnnotations(ast) }
647
+ return addASTAnnotations(constUnknown, ast)
629
648
  case "AnyKeyword":
630
- return { ...constAny, ...getJsonSchemaAnnotations(ast) }
649
+ return addASTAnnotations(constAny, ast)
631
650
  case "ObjectKeyword":
632
- return { ...constAnyObject, ...getJsonSchemaAnnotations(ast) }
651
+ return addASTAnnotations(constObject, ast)
633
652
  case "StringKeyword":
634
- return { type: "string", ...getASTJsonSchemaAnnotations(ast) }
653
+ return addASTAnnotations({ type: "string" }, ast)
635
654
  case "NumberKeyword":
636
- return { type: "number", ...getASTJsonSchemaAnnotations(ast) }
655
+ return addASTAnnotations({ type: "number" }, ast)
637
656
  case "BooleanKeyword":
638
- return { type: "boolean", ...getASTJsonSchemaAnnotations(ast) }
639
- case "BigIntKeyword":
640
- throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast))
641
- case "SymbolKeyword":
657
+ return addASTAnnotations({ type: "boolean" }, ast)
658
+ case "Literal": {
659
+ const literal = ast.literal
660
+ if (literal === null) {
661
+ return addASTAnnotations({ type: "null" }, ast)
662
+ } else if (Predicate.isString(literal)) {
663
+ return addASTAnnotations({ type: "string", enum: [literal] }, ast)
664
+ } else if (Predicate.isNumber(literal)) {
665
+ return addASTAnnotations({ type: "number", enum: [literal] }, ast)
666
+ } else if (Predicate.isBoolean(literal)) {
667
+ return addASTAnnotations({ type: "boolean", enum: [literal] }, ast)
668
+ }
669
+ if (errors === "ignore-errors") return addASTAnnotations(constAny, ast)
642
670
  throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast))
671
+ }
672
+ case "Enums": {
673
+ const anyOf = ast.enums.map((e) => {
674
+ const type: "string" | "number" = Predicate.isNumber(e[1]) ? "number" : "string"
675
+ return { type, title: e[0], enum: [e[1]] }
676
+ })
677
+ return anyOf.length >= 1 ?
678
+ addASTAnnotations({
679
+ $comment: "/schemas/enums",
680
+ anyOf
681
+ }, ast) :
682
+ addASTAnnotations(constNever, ast)
683
+ }
643
684
  case "TupleType": {
644
- const elements = ast.elements.map((e, i) => ({
645
- ...go(e.type, $defs, true, path.concat(i), options),
646
- ...getJsonSchemaAnnotations(e.type, e)
647
- }))
648
- const rest = ast.rest.map((annotatedAST) => ({
649
- ...go(annotatedAST.type, $defs, true, path, options),
650
- ...getJsonSchemaAnnotations(annotatedAST.type, annotatedAST)
651
- }))
685
+ const elements = ast.elements.map((e, i) =>
686
+ mergeJsonSchemaAnnotations(
687
+ go(e.type, $defs, "handle-identifier", path.concat(i), options, "handle-annotation", errors),
688
+ getContextJsonSchemaAnnotations(e.type, e)
689
+ )
690
+ )
691
+ const rest = ast.rest.map((type) =>
692
+ mergeJsonSchemaAnnotations(
693
+ go(type.type, $defs, "handle-identifier", path, options, "handle-annotation", errors),
694
+ getContextJsonSchemaAnnotations(type.type, type)
695
+ )
696
+ )
652
697
  const output: JsonSchema7Array = { type: "array" }
653
698
  // ---------------------------------------------
654
699
  // handle elements
@@ -675,6 +720,7 @@ const go = (
675
720
  // handle post rest elements
676
721
  // ---------------------------------------------
677
722
  if (restLength > 1) {
723
+ if (errors === "ignore-errors") return addASTAnnotations(constAny, ast)
678
724
  throw new Error(errors_.getJSONSchemaUnsupportedPostRestElementsErrorMessage(path))
679
725
  }
680
726
  } else {
@@ -685,11 +731,11 @@ const go = (
685
731
  }
686
732
  }
687
733
 
688
- return { ...output, ...getJsonSchemaAnnotations(ast) }
734
+ return addASTAnnotations(output, ast)
689
735
  }
690
736
  case "TypeLiteral": {
691
737
  if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
692
- return { ...constEmpty, ...getJsonSchemaAnnotations(ast) }
738
+ return addASTAnnotations(constEmptyStruct, ast)
693
739
  }
694
740
  const output: JsonSchema7Object = {
695
741
  type: "object",
@@ -704,11 +750,19 @@ const go = (
704
750
  const parameter = is.parameter
705
751
  switch (parameter._tag) {
706
752
  case "StringKeyword": {
707
- output.additionalProperties = go(pruned, $defs, true, path, options)
753
+ output.additionalProperties = go(
754
+ pruned,
755
+ $defs,
756
+ "handle-identifier",
757
+ path,
758
+ options,
759
+ "handle-annotation",
760
+ errors
761
+ )
708
762
  break
709
763
  }
710
764
  case "TemplateLiteral": {
711
- patternProperties = go(pruned, $defs, true, path, options)
765
+ patternProperties = go(pruned, $defs, "handle-identifier", path, options, "handle-annotation", errors)
712
766
  propertyNames = {
713
767
  type: "string",
714
768
  pattern: AST.getTemplateLiteralRegExp(parameter).source
@@ -716,14 +770,30 @@ const go = (
716
770
  break
717
771
  }
718
772
  case "Refinement": {
719
- patternProperties = go(pruned, $defs, true, path, options)
720
- propertyNames = go(parameter, $defs, true, path, options)
773
+ patternProperties = go(pruned, $defs, "handle-identifier", path, options, "handle-annotation", errors)
774
+ propertyNames = go(parameter, $defs, "handle-identifier", path, options, "handle-annotation", errors)
721
775
  break
722
776
  }
723
777
  case "SymbolKeyword": {
724
778
  const indexSignaturePath = path.concat("[symbol]")
725
- output.additionalProperties = go(pruned, $defs, true, indexSignaturePath, options)
726
- propertyNames = go(parameter, $defs, true, indexSignaturePath, options)
779
+ output.additionalProperties = go(
780
+ pruned,
781
+ $defs,
782
+ "handle-identifier",
783
+ indexSignaturePath,
784
+ options,
785
+ "handle-annotation",
786
+ errors
787
+ )
788
+ propertyNames = go(
789
+ parameter,
790
+ $defs,
791
+ "handle-identifier",
792
+ indexSignaturePath,
793
+ options,
794
+ "handle-annotation",
795
+ errors
796
+ )
727
797
  break
728
798
  }
729
799
  }
@@ -737,10 +807,10 @@ const go = (
737
807
  if (Predicate.isString(name)) {
738
808
  const pruned = pruneUndefined(ps.type)
739
809
  const type = pruned ?? ps.type
740
- output.properties[name] = {
741
- ...go(type, $defs, true, path.concat(ps.name), options),
742
- ...getJsonSchemaAnnotations(type, ps)
743
- }
810
+ output.properties[name] = mergeJsonSchemaAnnotations(
811
+ go(type, $defs, "handle-identifier", path.concat(ps.name), options, "handle-annotation", errors),
812
+ getContextJsonSchemaAnnotations(type, ps)
813
+ )
744
814
  // ---------------------------------------------
745
815
  // handle optional property signatures
746
816
  // ---------------------------------------------
@@ -748,6 +818,7 @@ const go = (
748
818
  output.required.push(name)
749
819
  }
750
820
  } else {
821
+ if (errors === "ignore-errors") return addASTAnnotations(constAny, ast)
751
822
  throw new Error(errors_.getJSONSchemaUnsupportedKeyErrorMessage(name, path))
752
823
  }
753
824
  }
@@ -762,117 +833,32 @@ const go = (
762
833
  output.propertyNames = propertyNames
763
834
  }
764
835
 
765
- return { ...output, ...getJsonSchemaAnnotations(ast) }
836
+ return addASTAnnotations(output, ast)
766
837
  }
767
838
  case "Union": {
768
- const members: Array<JsonSchema7> = []
769
- for (const type of ast.types) {
770
- const jsonSchema = go(type, $defs, true, path, options)
771
- if (!isNeverJSONSchema(jsonSchema)) {
772
- const last = members[members.length - 1]
773
- if (isMergeableEnum(jsonSchema) && last !== undefined && isMergeableEnum(last)) {
774
- members[members.length - 1] = { enum: last.enum.concat(jsonSchema.enum) }
775
- } else {
776
- members.push(jsonSchema)
777
- }
778
- }
779
- }
780
-
781
- const anyOf = shrink(members)
782
-
783
- const finalize = (anyOf: Array<JsonSchema7>) => {
784
- switch (anyOf.length) {
785
- case 0:
786
- return {
787
- ...constNever,
788
- ...getJsonSchemaAnnotations(ast)
789
- }
790
- case 1: {
791
- return {
792
- ...addEnumType(anyOf[0]),
793
- ...getJsonSchemaAnnotations(ast)
794
- }
795
- }
796
- default:
797
- return {
798
- anyOf: anyOf.map(addEnumType),
799
- ...getJsonSchemaAnnotations(ast)
800
- }
801
- }
802
- }
803
-
804
- if (isNullableKeywordSupported(options)) {
805
- let nullable = false
806
- const nonNullables: Array<JsonSchema7> = []
807
- for (const s of anyOf) {
808
- if ("nullable" in s) {
809
- nullable = true
810
- const nn = { ...s }
811
- delete nn.nullable
812
- nonNullables.push(nn)
813
- } else if (isMergeableEnum(s)) {
814
- const nnes = s.enum.filter((e) => e !== null)
815
- if (nnes.length < s.enum.length) {
816
- nullable = true
817
- if (nnes.length === 0) {
818
- continue
819
- }
820
- const nn = { ...s }
821
- nn.enum = nnes
822
- nonNullables.push(nn)
823
- }
824
- } else {
825
- nonNullables.push(s)
826
- }
827
- }
828
- if (nullable) {
829
- const out = finalize(nonNullables)
830
- if (!isAnyJSONSchema(out) && !isUnknownJSONSchema(out)) {
831
- // @ts-expect-error
832
- out.nullable = nullable
833
- }
834
- return out
835
- }
839
+ const members: Array<JsonSchema7> = ast.types.map((t) =>
840
+ go(t, $defs, "handle-identifier", path, options, "handle-annotation", errors)
841
+ )
842
+ const anyOf = compactUnion(members)
843
+ switch (anyOf.length) {
844
+ case 0:
845
+ return constNever
846
+ case 1:
847
+ return addASTAnnotations(anyOf[0], ast)
848
+ default:
849
+ return addASTAnnotations({ anyOf }, ast)
836
850
  }
837
-
838
- return finalize(anyOf)
839
- }
840
- case "Enums": {
841
- const anyOf = ast.enums.map((e) => addEnumType({ title: e[0], enum: [e[1]] }))
842
- return anyOf.length >= 1 ?
843
- {
844
- $comment: "/schemas/enums",
845
- anyOf,
846
- ...getJsonSchemaAnnotations(ast)
847
- } :
848
- {
849
- ...constNever,
850
- ...getJsonSchemaAnnotations(ast)
851
- }
852
- }
853
- case "Refinement": {
854
- // The jsonSchema annotation is required only if the refinement does not have a transformation
855
- if (AST.getTransformationFrom(ast) === undefined) {
856
- throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast))
857
- }
858
- return go(ast.from, $defs, handleIdentifier, path, options)
859
851
  }
852
+ case "Refinement":
853
+ return go(ast.from, $defs, identifier, path, options, "handle-annotation", errors)
860
854
  case "TemplateLiteral": {
861
855
  const regex = AST.getTemplateLiteralRegExp(ast)
862
- return {
856
+ return addASTAnnotations({
863
857
  type: "string",
864
858
  title: String(ast),
865
859
  description: "a template literal",
866
- pattern: regex.source,
867
- ...getJsonSchemaAnnotations(ast)
868
- }
869
- }
870
- case "Suspend": {
871
- const identifier = Option.orElse(AST.getJSONIdentifier(ast), () => AST.getJSONIdentifier(ast.f()))
872
- if (Option.isNone(identifier)) {
873
- throw new Error(errors_.getJSONSchemaMissingIdentifierAnnotationErrorMessage(path, ast))
874
- }
875
- return go(ast.f(), $defs, handleIdentifier, path, options)
860
+ pattern: regex.source
861
+ }, ast)
876
862
  }
877
863
  case "Transformation": {
878
864
  if (isParseJsonTransformation(ast.from)) {
@@ -881,28 +867,83 @@ const go = (
881
867
  "contentMediaType": "application/json"
882
868
  }
883
869
  if (isContentSchemaSupported(options)) {
884
- out["contentSchema"] = go(ast.to, $defs, handleIdentifier, path, options)
870
+ out["contentSchema"] = go(ast.to, $defs, identifier, path, options, "handle-annotation", errors)
885
871
  }
886
872
  return out
887
873
  }
888
- let next = ast.from
889
- if (AST.isTypeLiteralTransformation(ast.transformation)) {
890
- // Annotations from the transformation are applied unless there are user-defined annotations on the form side,
891
- // ensuring that the user's intended annotations are included in the generated schema.
892
- const identifier = AST.getIdentifierAnnotation(ast)
893
- if (Option.isSome(identifier) && Option.isNone(AST.getIdentifierAnnotation(next))) {
894
- next = AST.annotations(next, { [AST.IdentifierAnnotationId]: identifier.value })
895
- }
896
- const title = AST.getTitleAnnotation(ast)
897
- if (Option.isSome(title) && Option.isNone(AST.getTitleAnnotation(next))) {
898
- next = AST.annotations(next, { [AST.TitleAnnotationId]: title.value })
899
- }
900
- const description = AST.getDescriptionAnnotation(ast)
901
- if (Option.isSome(description) && Option.isNone(AST.getDescriptionAnnotation(next))) {
902
- next = AST.annotations(next, { [AST.DescriptionAnnotationId]: description.value })
874
+ const from = go(ast.from, $defs, identifier, path, options, "handle-annotation", errors)
875
+ if (
876
+ ast.transformation._tag === "TypeLiteralTransformation" &&
877
+ isJsonSchema7Object(from)
878
+ ) {
879
+ const to = go(ast.to, {}, "ignore-identifier", path, options, "handle-annotation", "ignore-errors")
880
+ if (isJsonSchema7Object(to)) {
881
+ for (const t of ast.transformation.propertySignatureTransformations) {
882
+ const toKey = t.to
883
+ const fromKey = t.from
884
+ if (Predicate.isString(toKey) && Predicate.isString(fromKey)) {
885
+ const toProperty = to.properties[toKey]
886
+ if (Predicate.isRecord(toProperty)) {
887
+ const fromProperty = from.properties[fromKey]
888
+ if (Predicate.isRecord(fromProperty)) {
889
+ const annotations: JsonSchemaAnnotations = {}
890
+ if (Predicate.isString(toProperty.title)) annotations.title = toProperty.title
891
+ if (Predicate.isString(toProperty.description)) annotations.description = toProperty.description
892
+ if (Array.isArray(toProperty.examples)) annotations.examples = toProperty.examples
893
+ if (Object.hasOwn(toProperty, "default")) annotations.default = toProperty.default
894
+ from.properties[fromKey] = addAnnotations(fromProperty, annotations)
895
+ }
896
+ }
897
+ }
898
+ }
903
899
  }
904
900
  }
905
- return go(next, $defs, handleIdentifier, path, options)
901
+ return addASTAnnotations(from, ast)
902
+ }
903
+ }
904
+ }
905
+
906
+ function isJsonSchema7Object(jsonSchema: unknown): jsonSchema is JsonSchema7Object {
907
+ return Predicate.isRecord(jsonSchema) && jsonSchema.type === "object" && Predicate.isRecord(jsonSchema.properties)
908
+ }
909
+
910
+ function isNeverWithoutCustomAnnotations(jsonSchema: JsonSchema7): boolean {
911
+ return jsonSchema === constNever || (Predicate.hasProperty(jsonSchema, "$id") && jsonSchema.$id === constNever.$id &&
912
+ Object.keys(jsonSchema).length === 3 && jsonSchema.title === AST.neverKeyword.annotations[AST.TitleAnnotationId])
913
+ }
914
+
915
+ function isAny(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Any {
916
+ return "$id" in jsonSchema && jsonSchema.$id === constAny.$id
917
+ }
918
+
919
+ function isUnknown(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Unknown {
920
+ return "$id" in jsonSchema && jsonSchema.$id === constUnknown.$id
921
+ }
922
+
923
+ function isVoid(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Void {
924
+ return "$id" in jsonSchema && jsonSchema.$id === constVoid.$id
925
+ }
926
+
927
+ function isCompactableLiteral(jsonSchema: JsonSchema7 | undefined): jsonSchema is JsonSchema7Enum {
928
+ return Predicate.hasProperty(jsonSchema, "enum") && "type" in jsonSchema && Object.keys(jsonSchema).length === 2
929
+ }
930
+
931
+ function compactUnion(members: Array<JsonSchema7>): Array<JsonSchema7> {
932
+ const out: Array<JsonSchema7> = []
933
+ for (const m of members) {
934
+ if (isNeverWithoutCustomAnnotations(m)) continue
935
+ if (isAny(m) || isUnknown(m) || isVoid(m)) return [m]
936
+ if (isCompactableLiteral(m) && out.length > 0) {
937
+ const last = out[out.length - 1]
938
+ if (isCompactableLiteral(last) && last.type === m.type) {
939
+ out[out.length - 1] = {
940
+ type: last.type,
941
+ enum: [...last.enum, ...m.enum]
942
+ } as JsonSchema7Enum
943
+ continue
944
+ }
906
945
  }
946
+ out.push(m)
907
947
  }
948
+ return out
908
949
  }