effect 4.0.0-rc.113 → 4.0.0-rc.114

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 (46) hide show
  1. package/dist/Effect.d.ts +268 -7
  2. package/dist/Effect.d.ts.map +1 -1
  3. package/dist/Effect.js +33 -2
  4. package/dist/Effect.js.map +1 -1
  5. package/dist/Effectable.d.ts +1 -2
  6. package/dist/Effectable.d.ts.map +1 -1
  7. package/dist/Effectable.js.map +1 -1
  8. package/dist/FileSystem.d.ts +3 -4
  9. package/dist/FileSystem.d.ts.map +1 -1
  10. package/dist/FileSystem.js.map +1 -1
  11. package/dist/Match.d.ts +1 -14
  12. package/dist/Match.d.ts.map +1 -1
  13. package/dist/Schema.d.ts +4 -4
  14. package/dist/Schema.d.ts.map +1 -1
  15. package/dist/SchemaRepresentation.d.ts +39 -24
  16. package/dist/SchemaRepresentation.d.ts.map +1 -1
  17. package/dist/SchemaRepresentation.js +37 -23
  18. package/dist/SchemaRepresentation.js.map +1 -1
  19. package/dist/internal/arbitrary/schema.js.map +1 -1
  20. package/dist/internal/effect.js +16 -8
  21. package/dist/internal/effect.js.map +1 -1
  22. package/dist/internal/schema/fromJsonSchemaDocument.js +221 -230
  23. package/dist/internal/schema/fromJsonSchemaDocument.js.map +1 -1
  24. package/dist/internal/schema/toCodeDocument.js +4 -4
  25. package/dist/internal/schema/toCodeDocument.js.map +1 -1
  26. package/dist/unstable/cli/Prompt.d.ts +6 -2
  27. package/dist/unstable/cli/Prompt.d.ts.map +1 -1
  28. package/dist/unstable/cli/Prompt.js +8 -7
  29. package/dist/unstable/cli/Prompt.js.map +1 -1
  30. package/dist/unstable/cli/internal/command.d.ts +1 -6
  31. package/dist/unstable/cli/internal/command.d.ts.map +1 -1
  32. package/dist/unstable/cli/internal/command.js +2 -0
  33. package/dist/unstable/cli/internal/command.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/Effect.ts +271 -7
  36. package/src/Effectable.ts +2 -2
  37. package/src/FileSystem.ts +3 -4
  38. package/src/Match.ts +1 -1
  39. package/src/Schema.ts +4 -7
  40. package/src/SchemaRepresentation.ts +39 -24
  41. package/src/internal/arbitrary/schema.ts +16 -16
  42. package/src/internal/effect.ts +34 -11
  43. package/src/internal/schema/fromJsonSchemaDocument.ts +294 -268
  44. package/src/internal/schema/toCodeDocument.ts +4 -4
  45. package/src/unstable/cli/Prompt.ts +22 -11
  46. package/src/unstable/cli/internal/command.ts +2 -0
@@ -11,6 +11,11 @@ type Path = ReadonlyArray<string | number>
11
11
  type Representation = SchemaRepresentation.Representation
12
12
  type Check = SchemaRepresentation.Check
13
13
 
14
+ interface Checked {
15
+ readonly checks: ReadonlyArray<Check>
16
+ readonly annotations?: Schema.Annotations.Annotations | undefined
17
+ }
18
+
14
19
  type ImportedJsonSchemaRepresentation = Extract<Representation, {
15
20
  readonly _tag:
16
21
  | "Reference"
@@ -48,12 +53,18 @@ interface ImportedObjectScope {
48
53
  const never: ImportedJsonSchemaRepresentation = { _tag: "Never", checks: [] }
49
54
  const unknown: ImportedJsonSchemaRepresentation = { _tag: "Unknown", checks: [] }
50
55
  const string: ImportedJsonSchemaRepresentation = { _tag: "String", checks: [] }
56
+ const unsupportedStructuredValue = "Only primitive values are supported in \"const\" and \"enum\"."
57
+
58
+ function isObject(input: unknown): input is Record<string, unknown> {
59
+ return typeof input === "object" && input !== null && !Array.isArray(input)
60
+ }
51
61
 
52
- function makeJsonLiteral(input: unknown): ImportedJsonSchemaRepresentation | undefined {
62
+ function makeJsonLiteral(input: unknown, path: Path): ImportedJsonSchemaRepresentation | undefined {
53
63
  if (input === null) return { _tag: "Null", checks: [] }
54
- return typeof input === "string" || typeof input === "number" || typeof input === "boolean"
55
- ? { _tag: "Literal", literal: input, checks: [] }
56
- : undefined
64
+ if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") {
65
+ return { _tag: "Literal", literal: input, checks: [] }
66
+ }
67
+ if (typeof input === "object") throw errorWithPath(unsupportedStructuredValue, path)
57
68
  }
58
69
 
59
70
  const jsonSchemaValueTypes = ["null", "string", "number", "boolean", "object", "array"] as const
@@ -150,56 +161,6 @@ function jsonDeclaration(
150
161
  }
151
162
  }
152
163
 
153
- function unknownJsonSchemas(representation: Representation): Representation {
154
- switch (representation._tag) {
155
- case "Unknown":
156
- return jsonDeclaration(representation.annotations)
157
- case "Suspend":
158
- return { ...representation, thunk: unknownJsonSchemas(representation.thunk) }
159
- case "Arrays":
160
- return {
161
- ...representation,
162
- elements: representation.elements.map((element) => ({
163
- ...element,
164
- type: unknownJsonSchemas(element.type)
165
- })),
166
- rest: representation.rest.map(unknownJsonSchemas)
167
- }
168
- case "Objects":
169
- return {
170
- ...representation,
171
- propertySignatures: representation.propertySignatures.map((property) => ({
172
- ...property,
173
- type: unknownJsonSchemas(property.type)
174
- })),
175
- indexSignatures: representation.indexSignatures.map((indexSignature) => ({
176
- parameter: unknownJsonSchemas(indexSignature.parameter),
177
- type: unknownJsonSchemas(indexSignature.type)
178
- })),
179
- checks: representation.checks.map(unknownJsonSchemaCheck)
180
- }
181
- case "Union":
182
- return { ...representation, types: representation.types.map(unknownJsonSchemas) }
183
- default:
184
- return representation
185
- }
186
- }
187
-
188
- function unknownJsonSchemaCheck(check: Check): Check {
189
- const representation = check.representation
190
- const schemas = representation?.schemas
191
- if (representation === undefined || schemas === undefined) {
192
- return check
193
- }
194
- return {
195
- ...check,
196
- representation: {
197
- ...representation,
198
- schemas: schemas.map(unknownJsonSchemas)
199
- }
200
- }
201
- }
202
-
203
164
  function translateJsonSchemaMultiDocument(
204
165
  document: JsonSchema.MultiDocument<"draft-2020-12">,
205
166
  options?: SchemaRepresentation.FromJsonSchemaOptions,
@@ -209,13 +170,80 @@ function translateJsonSchemaMultiDocument(
209
170
  const reachableDefinitions = new Map<string, Path>()
210
171
  const objectScopesByProperties = new WeakMap<
211
172
  SchemaRepresentation.Objects["propertySignatures"],
212
- ReadonlyArray<ImportedObjectScope>
173
+ readonly [ReadonlyArray<ImportedObjectScope>, Path]
213
174
  >()
214
175
  const annotatedReferences: Array<{
215
176
  readonly reference: SchemaRepresentation.Reference
216
177
  readonly path: Path
217
178
  }> = []
218
179
  const resolvingChoices = new Set<string>()
180
+ const finalReferences = new Set<string>()
181
+ let inNestedResource = false
182
+
183
+ function finalize(representation: Representation): Representation {
184
+ switch (representation._tag) {
185
+ case "Unknown":
186
+ return jsonDeclaration(representation.annotations)
187
+ case "Reference":
188
+ finalReferences.add(representation.$ref)
189
+ return representation
190
+ case "Suspend":
191
+ return { ...representation, thunk: finalize(representation.thunk) }
192
+ case "Arrays":
193
+ return {
194
+ ...representation,
195
+ elements: representation.elements.map((element) => ({
196
+ ...element,
197
+ type: finalize(element.type)
198
+ })),
199
+ rest: representation.rest.map(finalize)
200
+ }
201
+ case "Objects":
202
+ // Intersections may reduce open patterns to fixed properties, so reject only
203
+ // if an open pattern remains in the final representation.
204
+ const [scopes, path] = objectScopesByProperties.get(representation.propertySignatures)!
205
+ if (
206
+ representation.indexSignatures[0]?.parameter === string &&
207
+ scopes.some((scope) => scope.patterns.length > 0)
208
+ ) {
209
+ throw errorWithPath(
210
+ "Cannot import open \"patternProperties\": unmatched keys cannot be typed correctly.",
211
+ path
212
+ )
213
+ }
214
+ return {
215
+ ...representation,
216
+ propertySignatures: representation.propertySignatures.map((property) => ({
217
+ ...property,
218
+ type: finalize(property.type)
219
+ })),
220
+ indexSignatures: representation.indexSignatures.map((indexSignature) => ({
221
+ parameter: finalize(indexSignature.parameter),
222
+ type: finalize(indexSignature.type)
223
+ })),
224
+ checks: representation.checks.map(finalizeCheck)
225
+ }
226
+ case "Union":
227
+ return { ...representation, types: representation.types.map(finalize) }
228
+ default:
229
+ return representation
230
+ }
231
+ }
232
+
233
+ function finalizeCheck(check: Check): Check {
234
+ const representation = check.representation
235
+ const schemas = representation?.schemas
236
+ if (representation === undefined || schemas === undefined) {
237
+ return check
238
+ }
239
+ return {
240
+ ...check,
241
+ representation: {
242
+ ...representation,
243
+ schemas: schemas.map(finalize)
244
+ }
245
+ }
246
+ }
219
247
 
220
248
  function annotate(
221
249
  representation: ImportedJsonSchemaRepresentation,
@@ -239,15 +267,16 @@ function translateJsonSchemaMultiDocument(
239
267
  const cached = definitionCache.get(key)
240
268
  if (cached !== undefined) {
241
269
  if (cached === null) {
242
- throw errorWithPath(recursiveReferenceError ?? `Invalid reference ${key}`, [...path, "$ref"])
270
+ throw errorWithPath(
271
+ recursiveReferenceError ??
272
+ `Definition ${JSON.stringify(key)} is a circular alias.`,
273
+ [...path, "$ref"]
274
+ )
243
275
  }
244
276
  return cached
245
277
  }
246
- if (!Object.hasOwn(document.definitions, key)) {
247
- throw errorWithPath(`Invalid reference ${key}`, [...path, "$ref"])
248
- }
249
278
  definitionCache.set(key, null)
250
- const representation = recur(document.definitions[key], ["definitions", key])
279
+ const representation = translateSchema(document.definitions[key], ["definitions", key])
251
280
  definitionCache.set(key, representation)
252
281
  return representation
253
282
  }
@@ -255,20 +284,23 @@ function translateJsonSchemaMultiDocument(
255
284
  function resolveReference(
256
285
  reference: SchemaRepresentation.Reference,
257
286
  path: Path,
258
- options?: { readonly recursiveReferenceError?: string },
287
+ recursiveReferenceError?: string,
259
288
  seen: Set<string> = new Set()
260
289
  ): ImportedJsonSchemaRepresentation {
261
290
  if (seen.has(reference.$ref)) {
262
- throw errorWithPath(`Invalid reference ${reference.$ref}`, [...path, "$ref"])
291
+ throw errorWithPath(
292
+ `Definition ${JSON.stringify(reference.$ref)} is a circular alias.`,
293
+ [...path, "$ref"]
294
+ )
263
295
  }
264
296
  seen.add(reference.$ref)
265
- const representation = translateDefinition(reference.$ref, path, options?.recursiveReferenceError)
297
+ const representation = translateDefinition(reference.$ref, path, recursiveReferenceError)
266
298
  if (representation._tag === "Reference") {
267
- return resolveReference(representation, path, options, seen)
299
+ return resolveReference(representation, path, recursiveReferenceError, seen)
268
300
  }
269
301
  if (representation._tag === "Suspend" && representation.thunk._tag === "Reference") {
270
302
  return annotate(
271
- resolveReference(representation.thunk, path, options, seen),
303
+ resolveReference(representation.thunk, path, recursiveReferenceError, seen),
272
304
  representation.annotations
273
305
  )
274
306
  }
@@ -284,7 +316,7 @@ function translateJsonSchemaMultiDocument(
284
316
  return { ...left, ...right }
285
317
  }
286
318
 
287
- function combinedAnnotations(
319
+ function annotateIntersection(
288
320
  representation: ImportedJsonSchemaRepresentation,
289
321
  left: ImportedJsonSchemaRepresentation,
290
322
  right: ImportedJsonSchemaRepresentation
@@ -298,42 +330,31 @@ function translateJsonSchemaMultiDocument(
298
330
  )
299
331
  }
300
332
 
301
- function asChecks(
302
- checks: ReadonlyArray<Check>,
303
- annotations: Schema.Annotations.Annotations | undefined
304
- ): ReadonlyArray<Check> | undefined {
305
- if (checks.length === 0) return undefined
306
- if (annotations === undefined) return checks
307
- if (checks.length === 1 && checks[0].annotations === undefined) {
308
- return [{
309
- ...checks[0],
310
- annotations
311
- }]
312
- }
313
- return [{
314
- _tag: "FilterGroup",
315
- checks: checks as [Check, ...Array<Check>],
316
- annotations
317
- }]
318
- }
319
-
320
- function combineChecks(
321
- left: ReadonlyArray<Check>,
322
- right: ReadonlyArray<Check>,
323
- annotations: Schema.Annotations.Annotations | undefined,
333
+ function intersectChecks(
334
+ left: Checked,
335
+ right: Checked,
324
336
  deduplicate: ReadonlyArray<string> = []
325
- ): ReadonlyArray<Check> | undefined {
337
+ ): readonly [ReadonlyArray<Check>, Schema.Annotations.Annotations | undefined] {
338
+ let checks = right.checks
326
339
  for (const id of deduplicate) {
327
- if (left.some((check) => checkId(check) === id)) {
328
- right = right.filter((check) => checkId(check) !== id)
340
+ if (left.checks.some((check) => check._tag === "Filter" && check.representation?.id === id)) {
341
+ checks = checks.filter((check) => check._tag !== "Filter" || check.representation?.id !== id)
329
342
  }
330
343
  }
331
- const checks = asChecks(right, annotations)
332
- return checks === undefined ? undefined : [...left, ...checks]
333
- }
334
-
335
- function checkId(check: Check): string | undefined {
336
- return check._tag === "Filter" ? check.representation?.id : undefined
344
+ if (checks.length === 0) {
345
+ const annotations = mergeAnnotations(left.annotations, right.annotations)
346
+ return [left.checks, annotations]
347
+ }
348
+ if (right.annotations !== undefined) {
349
+ checks = checks.length === 1 && checks[0].annotations === undefined
350
+ ? [{ ...checks[0], annotations: right.annotations }]
351
+ : [{
352
+ _tag: "FilterGroup",
353
+ checks: checks as [Check, ...Array<Check>],
354
+ annotations: right.annotations
355
+ }]
356
+ }
357
+ return [[...left.checks, ...checks], left.annotations]
337
358
  }
338
359
 
339
360
  function satisfiesPrimitiveCheck(check: Check, value: string | number): boolean | undefined {
@@ -379,7 +400,7 @@ function translateJsonSchemaMultiDocument(
379
400
  return representation.checks.every((check) => satisfiesPrimitiveCheck(check, value as string | number))
380
401
  }
381
402
 
382
- function combinePrimitiveWithLiteral(
403
+ function intersectPrimitiveWithLiteral(
383
404
  primitive:
384
405
  | SchemaRepresentation.String
385
406
  | SchemaRepresentation.Number
@@ -389,10 +410,10 @@ function translateJsonSchemaMultiDocument(
389
410
  const satisfies = primitive._tag === "Boolean"
390
411
  ? typeof literal.literal === "boolean"
391
412
  : satisfiesLiteral(primitive, literal)
392
- return satisfies ? combinedAnnotations(literal, primitive, literal) : never
413
+ return satisfies ? annotateIntersection(literal, primitive, literal) : never
393
414
  }
394
415
 
395
- function combineArrays(
416
+ function intersectArrays(
396
417
  left: SchemaRepresentation.Arrays,
397
418
  right: SchemaRepresentation.Arrays,
398
419
  path: Path
@@ -408,7 +429,7 @@ function translateJsonSchemaMultiDocument(
408
429
  if (leftType === undefined || rightType === undefined) {
409
430
  return isOptional ? { elements, rest: [] } : undefined
410
431
  }
411
- const type = combine(
432
+ const type = intersect(
412
433
  leftType as ImportedJsonSchemaRepresentation,
413
434
  rightType as ImportedJsonSchemaRepresentation,
414
435
  [...path, "elements", index, "type"]
@@ -427,7 +448,7 @@ function translateJsonSchemaMultiDocument(
427
448
  if (leftRest === undefined || rightRest === undefined) {
428
449
  return { elements, rest: [] }
429
450
  }
430
- const rest = combine(
451
+ const rest = intersect(
431
452
  leftRest as ImportedJsonSchemaRepresentation,
432
453
  rightRest as ImportedJsonSchemaRepresentation,
433
454
  [...path, "rest", 0]
@@ -435,13 +456,13 @@ function translateJsonSchemaMultiDocument(
435
456
  return { elements, rest: rest._tag === "Never" ? [] : [rest] }
436
457
  }
437
458
 
438
- function combineTypes(
459
+ function intersectAll(
439
460
  types: ReadonlyArray<ImportedJsonSchemaRepresentation>,
440
461
  path: Path
441
462
  ): ImportedJsonSchemaRepresentation {
442
463
  let out = types[0] ?? unknown
443
464
  for (let index = 1; index < types.length; index++) {
444
- out = combine(out, types[index], [...path, index])
465
+ out = intersect(out, types[index], [...path, index])
445
466
  }
446
467
  return out
447
468
  }
@@ -461,8 +482,17 @@ function translateJsonSchemaMultiDocument(
461
482
  requiresFiniteKeyDomain ||= scope.additionalProperties._tag === "Never" ||
462
483
  scope.additionalProperties._tag !== "Unknown" && (scope.hasProperties || scope.patterns.length > 0)
463
484
  }
464
- if (!hasFiniteKeyDomain && requiresFiniteKeyDomain) {
465
- throw errorWithPath("Unsupported object keyword scopes", path)
485
+ const closedPattern = scopes.length === 1 && names.size === 0 &&
486
+ scopes[0].additionalProperties._tag === "Never" && scopes[0].patterns.length === 1
487
+ ? scopes[0].patterns[0]
488
+ : undefined
489
+ if (!hasFiniteKeyDomain && requiresFiniteKeyDomain && closedPattern === undefined) {
490
+ throw errorWithPath(
491
+ scopes.some((scope) => scope.additionalProperties._tag === "Never")
492
+ ? "Cannot import this closed patterned object: only one pattern without properties is supported."
493
+ : "Cannot combine typed \"additionalProperties\" with other property schemas: Effect index signatures also check excluded keys.",
494
+ path
495
+ )
466
496
  }
467
497
 
468
498
  const properties: Array<SchemaRepresentation.PropertySignature> = []
@@ -484,55 +514,33 @@ function translateJsonSchemaMultiDocument(
484
514
  }
485
515
  if (property?.type === undefined && !matches) types.push(scope.additionalProperties)
486
516
  }
487
- const type = combineTypes(types, [...path, "properties", name])
517
+ const type = intersectAll(types, [...path, "properties", name])
488
518
  if (!isOptional && type._tag === "Never") return never
489
519
  properties.push({ name, type, isOptional, isMutable: false })
490
520
  }
491
521
 
492
- const indexSignatures: Array<SchemaRepresentation.IndexSignature> = []
493
- if (!hasFiniteKeyDomain) {
494
- const additionalProperties = combineTypes(
495
- scopes.map((scope) => scope.additionalProperties),
496
- [...path, "additionalProperties"]
497
- )
498
- const patterns = new Map<string, ImportedObjectPattern>()
499
- for (const scope of scopes) {
500
- for (const pattern of scope.patterns) {
501
- const previous = patterns.get(pattern.source)
502
- patterns.set(
503
- pattern.source,
504
- previous === undefined
505
- ? pattern
506
- : {
507
- ...pattern,
508
- type: combine(
509
- previous.type,
510
- pattern.type,
511
- [...path, "patternProperties", pattern.source]
512
- )
513
- }
514
- )
515
- }
516
- }
517
- for (const { parameter, type } of patterns.values()) {
518
- indexSignatures.push({
519
- parameter,
520
- type: combine(type, additionalProperties, [...path, "indexSignatures"])
521
- })
522
- }
523
- indexSignatures.push({ parameter: string, type: additionalProperties })
524
- } else if (properties.length === 0) {
525
- indexSignatures.push({ parameter: string, type: never })
526
- }
527
-
528
- objectScopesByProperties.set(properties, scopes)
529
- return {
522
+ const indexSignatures: ReadonlyArray<SchemaRepresentation.IndexSignature> = closedPattern !== undefined
523
+ ? [{ parameter: closedPattern.parameter, type: closedPattern.type }]
524
+ : !hasFiniteKeyDomain
525
+ ? [{
526
+ parameter: string,
527
+ type: intersectAll(
528
+ scopes.map((scope) => scope.additionalProperties),
529
+ [...path, "additionalProperties"]
530
+ )
531
+ }]
532
+ : properties.length === 0
533
+ ? [{ parameter: string, type: never }]
534
+ : []
535
+ const representation: SchemaRepresentation.Objects = {
530
536
  _tag: "Objects",
531
537
  propertySignatures: properties,
532
538
  indexSignatures,
533
539
  checks,
534
540
  annotations
535
541
  }
542
+ objectScopesByProperties.set(properties, [scopes, path])
543
+ return representation
536
544
  }
537
545
 
538
546
  const jsonRootTags = ["Null", "String", "Number", "Boolean", "Objects", "Arrays"] as const
@@ -597,7 +605,11 @@ function translateJsonSchemaMultiDocument(
597
605
  representation.indexSignatures.some((indexSignature) =>
598
606
  hasChoices(indexSignature.parameter as ImportedJsonSchemaRepresentation) ||
599
607
  hasChoices(indexSignature.type as ImportedJsonSchemaRepresentation)
600
- ) || representation.checks.some(checkHasChoices)
608
+ ) ||
609
+ objectScopesByProperties.get(representation.propertySignatures)![0].some((scope) =>
610
+ scope.patterns.some((pattern) => hasChoices(pattern.type))
611
+ ) ||
612
+ representation.checks.some(checkHasChoices)
601
613
  default:
602
614
  return false
603
615
  }
@@ -629,7 +641,7 @@ function translateJsonSchemaMultiDocument(
629
641
  : undefined
630
642
  }
631
643
 
632
- function combineLiteralUnions(
644
+ function intersectLiteralUnions(
633
645
  left: SchemaRepresentation.Union,
634
646
  right: SchemaRepresentation.Union,
635
647
  path: Path
@@ -650,12 +662,12 @@ function translateJsonSchemaMultiDocument(
650
662
  const value = literalValue(representation)
651
663
  if (value === undefined) return undefined
652
664
  const match = rightByValue.get(value)
653
- if (match !== undefined) types.push(combine(representation, annotate(match, right.annotations), path))
665
+ if (match !== undefined) types.push(intersect(representation, annotate(match, right.annotations), path))
654
666
  }
655
667
  return makeUnion(left, types)
656
668
  }
657
669
 
658
- function combineUnionWithType(
670
+ function intersectUnionWithType(
659
671
  union: SchemaRepresentation.Union,
660
672
  type: ImportedJsonSchemaRepresentation,
661
673
  path: Path,
@@ -674,16 +686,16 @@ function translateJsonSchemaMultiDocument(
674
686
  if (members.length === 1 && union.checks.length === 0) {
675
687
  const member = annotate(members[0].type, union.annotations)
676
688
  return unionOnLeft
677
- ? combine(member, type, [...path, "types", members[0].index])
678
- : combine(type, member, [...path, "types", members[0].index])
689
+ ? intersect(member, type, [...path, "types", members[0].index])
690
+ : intersect(type, member, [...path, "types", members[0].index])
679
691
  }
680
692
  return makeUnion(
681
693
  union,
682
694
  members
683
695
  .map((member) =>
684
696
  unionOnLeft
685
- ? combine(member.type, type, [...path, "types", member.index])
686
- : combine(type, member.type, [...path, "types", member.index])
697
+ ? intersect(member.type, type, [...path, "types", member.index])
698
+ : intersect(type, member.type, [...path, "types", member.index])
687
699
  )
688
700
  .filter((type) => type._tag !== "Never")
689
701
  )
@@ -700,10 +712,13 @@ function translateJsonSchemaMultiDocument(
700
712
  }
701
713
 
702
714
  function unsupportedIntersection(path: Path): never {
703
- throw errorWithPath("Unsupported intersection of overlapping unions", path)
715
+ throw errorWithPath(
716
+ "Cannot intersect these \"anyOf\" or \"oneOf\" alternatives without expanding their branches.",
717
+ path
718
+ )
704
719
  }
705
720
 
706
- function combinePartition(
721
+ function intersectPartition(
707
722
  partition: SchemaRepresentation.Union,
708
723
  other: SchemaRepresentation.Union,
709
724
  path: Path,
@@ -720,91 +735,79 @@ function translateJsonSchemaMultiDocument(
720
735
  const types: Array<ImportedJsonSchemaRepresentation> = []
721
736
  for (let index = 0; index < partitionTypes.length; index++) {
722
737
  const member = partitionTypes[index]
723
- const type = combineUnionWithType(other, member, [...path, "types", index], !partitionOnLeft)
738
+ const type = intersectUnionWithType(other, member, [...path, "types", index], !partitionOnLeft)
724
739
  if (type._tag !== "Never") types.push(type)
725
740
  }
726
741
  return makeUnion(partition, types)
727
742
  }
728
743
 
729
- function combineUnions(
744
+ function intersectUnions(
730
745
  left: SchemaRepresentation.Union,
731
746
  right: SchemaRepresentation.Union,
732
747
  path: Path
733
748
  ): ImportedJsonSchemaRepresentation {
734
- const literals = combineLiteralUnions(left, right, path)
749
+ const literals = intersectLiteralUnions(left, right, path)
735
750
  if (literals !== undefined) return literals
736
- if (isTypePartition(left)) return combinePartition(left, right, path, true)
737
- if (isTypePartition(right)) return combinePartition(right, left, path, false)
751
+ if (isTypePartition(left)) return intersectPartition(left, right, path, true)
752
+ if (isTypePartition(right)) return intersectPartition(right, left, path, false)
738
753
  return unsupportedIntersection(path)
739
754
  }
740
755
 
741
- function combine(
756
+ function intersect(
742
757
  left: ImportedJsonSchemaRepresentation,
743
758
  right: ImportedJsonSchemaRepresentation,
744
759
  path: Path
745
760
  ): ImportedJsonSchemaRepresentation {
746
761
  if (left._tag === "Never") return left
747
762
  if (right._tag === "Never") return right
748
- if (left._tag === "Unknown") return combinedAnnotations(right, left, right)
749
- if (right._tag === "Unknown") return combinedAnnotations(left, left, right)
750
- if (left._tag === "Reference") return combine(resolveReference(left, path), right, path)
751
- if (right._tag === "Reference") return combine(left, resolveReference(right, path), path)
763
+ if (left._tag === "Unknown") return annotateIntersection(right, left, right)
764
+ if (right._tag === "Unknown") return annotateIntersection(left, left, right)
765
+ if (left._tag === "Reference") return intersect(resolveReference(left, path), right, path)
766
+ if (right._tag === "Reference") return intersect(left, resolveReference(right, path), path)
752
767
  if (left._tag === "Suspend") {
753
768
  return annotate(
754
- combine(left.thunk as ImportedJsonSchemaRepresentation, right, path),
769
+ intersect(left.thunk as ImportedJsonSchemaRepresentation, right, path),
755
770
  left.annotations
756
771
  )
757
772
  }
758
773
  if (right._tag === "Suspend") {
759
774
  return annotate(
760
- combine(left, right.thunk as ImportedJsonSchemaRepresentation, path),
775
+ intersect(left, right.thunk as ImportedJsonSchemaRepresentation, path),
761
776
  right.annotations
762
777
  )
763
778
  }
764
- if (left._tag === "Union" && right._tag === "Union") return combineUnions(left, right, path)
765
- if (left._tag === "Union") return combineUnionWithType(left, right, path, true)
766
- if (right._tag === "Union") return combineUnionWithType(right, left, path, false)
779
+ if (left._tag === "Union" && right._tag === "Union") return intersectUnions(left, right, path)
780
+ if (left._tag === "Union") return intersectUnionWithType(left, right, path, true)
781
+ if (right._tag === "Union") return intersectUnionWithType(right, left, path, false)
767
782
 
768
783
  switch (left._tag) {
769
784
  case "Null":
770
785
  return right._tag === "Null"
771
- ? combinedAnnotations({ _tag: "Null", checks: [...left.checks, ...right.checks] }, left, right)
786
+ ? annotateIntersection({ _tag: "Null", checks: [...left.checks, ...right.checks] }, left, right)
772
787
  : never
773
788
  case "String":
774
789
  if (right._tag === "Literal") {
775
- return combinePrimitiveWithLiteral(left, right)
790
+ return intersectPrimitiveWithLiteral(left, right)
776
791
  }
777
792
  if (right._tag !== "String") return never
778
- const stringChecks = combineChecks(left.checks, right.checks, right.annotations)
779
- return annotate(
780
- {
781
- _tag: "String",
782
- checks: stringChecks ?? left.checks
783
- },
784
- mergeAnnotations(left.annotations, stringChecks === undefined ? right.annotations : undefined)
785
- )
793
+ const [stringChecks, stringAnnotations] = intersectChecks(left, right)
794
+ return annotate({ _tag: "String", checks: stringChecks }, stringAnnotations)
786
795
  case "Number":
787
796
  if (right._tag === "Literal") {
788
- return combinePrimitiveWithLiteral(left, right)
797
+ return intersectPrimitiveWithLiteral(left, right)
789
798
  }
790
799
  if (right._tag !== "Number") return never
791
- const numberChecks = combineChecks(left.checks, right.checks, right.annotations, [
800
+ const [numberChecks, numberAnnotations] = intersectChecks(left, right, [
792
801
  "effect/schema/isFinite",
793
802
  "effect/schema/isInt"
794
803
  ])
795
- return annotate(
796
- {
797
- _tag: "Number",
798
- checks: numberChecks ?? left.checks
799
- },
800
- mergeAnnotations(left.annotations, numberChecks === undefined ? right.annotations : undefined)
801
- )
804
+ return annotate({ _tag: "Number", checks: numberChecks }, numberAnnotations)
802
805
  case "Boolean":
803
806
  if (right._tag === "Literal") {
804
- return combinePrimitiveWithLiteral(left, right)
807
+ return intersectPrimitiveWithLiteral(left, right)
805
808
  }
806
809
  return right._tag === "Boolean"
807
- ? combinedAnnotations(
810
+ ? annotateIntersection(
808
811
  {
809
812
  _tag: "Boolean",
810
813
  checks: [...left.checks, ...right.checks]
@@ -816,7 +819,7 @@ function translateJsonSchemaMultiDocument(
816
819
  case "Literal":
817
820
  if (right._tag === "Literal") {
818
821
  return left.literal === right.literal
819
- ? combinedAnnotations(
822
+ ? annotateIntersection(
820
823
  {
821
824
  ...left,
822
825
  checks: [...left.checks, ...right.checks]
@@ -830,58 +833,54 @@ function translateJsonSchemaMultiDocument(
830
833
  (right._tag === "String" || right._tag === "Number") && satisfiesLiteral(right, left) ||
831
834
  right._tag === "Boolean" && typeof left.literal === "boolean"
832
835
  ) {
833
- return combinedAnnotations(left, left, right)
836
+ return annotateIntersection(left, left, right)
834
837
  }
835
838
  return never
836
839
  case "Arrays": {
837
840
  if (right._tag !== "Arrays") return never
838
- const arrays = combineArrays(left, right, path)
841
+ const arrays = intersectArrays(left, right, path)
839
842
  if (arrays === undefined) return never
840
- const arrayChecks = combineChecks(left.checks, right.checks, right.annotations, ["effect/schema/isUnique"])
841
- return annotate(
842
- {
843
- _tag: "Arrays",
844
- elements: arrays.elements,
845
- rest: arrays.rest,
846
- checks: arrayChecks ?? left.checks
847
- },
848
- mergeAnnotations(left.annotations, arrayChecks === undefined ? right.annotations : undefined)
849
- )
843
+ const [checks, annotations] = intersectChecks(left, right, ["effect/schema/isUnique"])
844
+ return annotate({ _tag: "Arrays", ...arrays, checks }, annotations)
850
845
  }
851
846
  case "Objects": {
852
847
  if (right._tag !== "Objects") return never
853
- const objectChecks = combineChecks(left.checks, right.checks, right.annotations)
848
+ const [checks, annotations] = intersectChecks(left, right)
854
849
  const scopes = [
855
- ...objectScopesByProperties.get(left.propertySignatures)!,
856
- ...objectScopesByProperties.get(right.propertySignatures)!
850
+ ...objectScopesByProperties.get(left.propertySignatures)![0],
851
+ ...objectScopesByProperties.get(right.propertySignatures)![0]
857
852
  ]
858
853
  return lowerObject(
859
854
  scopes,
860
- objectChecks ?? left.checks,
861
- mergeAnnotations(left.annotations, objectChecks === undefined ? right.annotations : undefined),
855
+ checks,
856
+ annotations,
862
857
  path
863
858
  )
864
859
  }
865
860
  }
866
861
  }
867
862
 
868
- function recur(input: unknown, path: Path): ImportedJsonSchemaRepresentation {
863
+ function translateSchema(
864
+ input: unknown,
865
+ path: Path,
866
+ isRoot = false
867
+ ): ImportedJsonSchemaRepresentation {
869
868
  if (input === false) {
870
869
  return never
871
870
  }
872
- if (typeof input !== "object" || input === null || Array.isArray(input)) return unknown
871
+ if (!isObject(input)) return unknown
873
872
  const schema = options?.onEnter === undefined
874
873
  ? input as JsonSchema.JsonSchema
875
874
  : options.onEnter(input as JsonSchema.JsonSchema)
876
875
  if (schema === undefined) {
877
876
  return unknown
878
877
  }
879
- const enumIndex = Array.isArray(schema.enum)
880
- ? schema.enum.findIndex((value) => typeof value === "object" && value !== null)
881
- : -1
882
- if (enumIndex !== -1) {
883
- throw errorWithPath(`Unsupported structured JSON Schema value for "enum"`, [...path, "enum", enumIndex])
884
- }
878
+ if (
879
+ isObject(schema.not) &&
880
+ Object.keys(schema.not).length === 0
881
+ ) return annotate(never, jsonSchemaAnnotations(schema))
882
+ const wasInNestedResource = inNestedResource
883
+ inNestedResource ||= !isRoot && typeof schema.$id === "string"
885
884
  for (const keyword of Object.keys(schema)) {
886
885
  if (keyword === "if" && !Object.hasOwn(schema, "then") && !Object.hasOwn(schema, "else")) continue
887
886
  switch (keyword) {
@@ -893,21 +892,22 @@ function translateJsonSchemaMultiDocument(
893
892
  case "not":
894
893
  case "unevaluatedItems":
895
894
  case "unevaluatedProperties":
896
- throw errorWithPath(`Unsupported JSON Schema keyword "${keyword}"`, [...path, keyword])
895
+ throw errorWithPath(
896
+ `Cannot import JSON Schema keyword "${keyword}": Effect has no equivalent constraint.`,
897
+ [...path, keyword]
898
+ )
897
899
  }
898
900
  }
899
901
 
900
- let representation = on(schema, path)
902
+ let representation = translateType(schema, path)
901
903
  if (Object.hasOwn(schema, "const")) {
902
- const literal = makeJsonLiteral(schema.const)
903
- if (literal === undefined && typeof schema.const === "object") {
904
- throw errorWithPath(`Unsupported structured JSON Schema value for "const"`, [...path, "const"])
905
- }
906
- if (literal !== undefined) representation = combine(representation, literal, [...path, "const"])
904
+ const constPath = [...path, "const"]
905
+ const literal = makeJsonLiteral(schema.const, constPath)
906
+ if (literal !== undefined) representation = intersect(representation, literal, constPath)
907
907
  }
908
908
  if (Array.isArray(schema.enum)) {
909
- const types = schema.enum.map((value) => makeJsonLiteral(value) ?? unknown)
910
- representation = combine(
909
+ const types = schema.enum.map((value, index) => makeJsonLiteral(value, [...path, "enum", index]) ?? unknown)
910
+ representation = intersect(
911
911
  representation,
912
912
  types.length === 1
913
913
  ? types[0]
@@ -916,12 +916,24 @@ function translateJsonSchemaMultiDocument(
916
916
  )
917
917
  }
918
918
  if (typeof schema.$ref === "string") {
919
+ if (inNestedResource) {
920
+ throw errorWithPath(
921
+ "Cannot resolve $ref under a nested \"$id\". Resolve or flatten it first.",
922
+ [...path, "$ref"]
923
+ )
924
+ }
919
925
  const $ref = JsonSchema.getReferenceKey(schema.$ref)
920
926
  if ($ref === undefined) {
921
- throw errorWithPath(`Unsupported reference ${JSON.stringify(schema.$ref)}`, [...path, "$ref"])
927
+ throw errorWithPath(
928
+ `Unsupported $ref ${JSON.stringify(schema.$ref)}. Use "#/$defs/Name".`,
929
+ [...path, "$ref"]
930
+ )
922
931
  }
923
932
  if (!Object.hasOwn(document.definitions, $ref)) {
924
- throw errorWithPath(`Invalid reference ${JSON.stringify(schema.$ref)}`, [...path, "$ref"])
933
+ throw errorWithPath(
934
+ `Missing definition ${JSON.stringify($ref)} for $ref ${JSON.stringify(schema.$ref)}.`,
935
+ [...path, "$ref"]
936
+ )
925
937
  }
926
938
  if (!reachableDefinitions.has($ref)) reachableDefinitions.set($ref, path)
927
939
  const reference: SchemaRepresentation.Reference = { _tag: "Reference", $ref }
@@ -929,10 +941,12 @@ function translateJsonSchemaMultiDocument(
929
941
  ? reference
930
942
  : representation._tag === "Never"
931
943
  ? never
932
- : combine(
933
- resolveReference(reference, path, {
934
- recursiveReferenceError: `Unsupported assertion siblings on recursive reference ${reference.$ref}`
935
- }),
944
+ : intersect(
945
+ resolveReference(
946
+ reference,
947
+ path,
948
+ `Recursive $ref ${JSON.stringify(reference.$ref)} cannot have sibling constraints.`
949
+ ),
936
950
  representation,
937
951
  path
938
952
  )
@@ -945,9 +959,9 @@ function translateJsonSchemaMultiDocument(
945
959
 
946
960
  if (Array.isArray(schema.allOf)) {
947
961
  for (let index = 0; index < schema.allOf.length; index++) {
948
- representation = combine(
962
+ representation = intersect(
949
963
  representation,
950
- recur(schema.allOf[index], [...path, "allOf", index]),
964
+ translateSchema(schema.allOf[index], [...path, "allOf", index]),
951
965
  [...path, "allOf", index]
952
966
  )
953
967
  }
@@ -958,17 +972,21 @@ function translateJsonSchemaMultiDocument(
958
972
  if (Array.isArray(members)) {
959
973
  const union: ImportedJsonSchemaRepresentation = {
960
974
  _tag: "Union",
961
- types: members.map((member, index) => recur(member, [...path, mode, index])),
975
+ types: members.map((member, index) => translateSchema(member, [...path, mode, index])),
962
976
  ...(mode === "oneOf" ? { options: { mode } } : {}),
963
977
  checks: []
964
978
  }
965
- representation = combine(union, representation, [...path, mode])
979
+ representation = intersect(union, representation, [...path, mode])
966
980
  }
967
981
  }
982
+ inNestedResource = wasInNestedResource
968
983
  return representation
969
984
  }
970
985
 
971
- function on(schema: JsonSchema.JsonSchema, path: Path): ImportedJsonSchemaRepresentation {
986
+ function translateType(
987
+ schema: JsonSchema.JsonSchema,
988
+ path: Path
989
+ ): ImportedJsonSchemaRepresentation {
972
990
  const types = Array.isArray(schema.type) && schema.type.every(isImportedJsonSchemaType)
973
991
  ? schema.type
974
992
  : !isImportedJsonSchemaType(schema.type) && hasTypeSpecificKeywords(schema)
@@ -977,7 +995,7 @@ function translateJsonSchemaMultiDocument(
977
995
  if (types !== undefined) {
978
996
  return {
979
997
  _tag: "Union",
980
- types: types.map((type) => on({ ...schema, type }, path)),
998
+ types: types.map((type) => translateType({ ...schema, type }, path)),
981
999
  checks: []
982
1000
  }
983
1001
  }
@@ -1007,7 +1025,7 @@ function translateJsonSchemaMultiDocument(
1007
1025
  const minItems = typeof schema.minItems === "number" ? schema.minItems : 0
1008
1026
  const elements = prefixItems?.map((element, index) => ({
1009
1027
  isOptional: index + 1 > minItems,
1010
- type: recur(element, [...path, "prefixItems", index])
1028
+ type: translateSchema(element, [...path, "prefixItems", index])
1011
1029
  })) ?? []
1012
1030
  const isTupleClosed = schema.items === false ||
1013
1031
  (schema.items === undefined &&
@@ -1021,7 +1039,9 @@ function translateJsonSchemaMultiDocument(
1021
1039
  elements,
1022
1040
  rest: isTupleClosed
1023
1041
  ? []
1024
- : [schema.items === undefined ? unknown : recur(schema.items, [...path, "items"])],
1042
+ : [
1043
+ schema.items === undefined ? unknown : translateSchema(schema.items, [...path, "items"])
1044
+ ],
1025
1045
  checks: collectArrayChecks(schema, isMaxItemsRedundant)
1026
1046
  }
1027
1047
  }
@@ -1034,14 +1054,17 @@ function translateJsonSchemaMultiDocument(
1034
1054
  }
1035
1055
  }
1036
1056
 
1037
- function importPatternChecks(pattern: string, path: Path): Array<Check> {
1057
+ function importPatternCheck(pattern: string, path: Path): Check | undefined {
1038
1058
  switch (options?.patterns ?? "error") {
1039
1059
  case "error":
1040
- throw errorWithPath(`Pattern encountered while patterns is set to "error"`, path)
1060
+ throw errorWithPath(
1061
+ "Patterns may block validation and are disabled. Use patterns: \"apply\" for trusted schemas or \"ignore\" to discard them.",
1062
+ path
1063
+ )
1041
1064
  case "ignore":
1042
- return []
1065
+ return undefined
1043
1066
  case "apply":
1044
- return [jsonSchemaFilter("effect/schema/isPattern", { source: pattern, flags: "" })]
1067
+ return jsonSchemaFilter("effect/schema/isPattern", { source: pattern, flags: "" })
1045
1068
  }
1046
1069
  }
1047
1070
 
@@ -1050,7 +1073,8 @@ function translateJsonSchemaMultiDocument(
1050
1073
  addNumberCheck(checks, schema.minLength, "effect/schema/isMinLength", "minLength")
1051
1074
  addNumberCheck(checks, schema.maxLength, "effect/schema/isMaxLength", "maxLength")
1052
1075
  if (typeof schema.pattern === "string") {
1053
- checks.push(...importPatternChecks(schema.pattern, [...path, "pattern"]))
1076
+ const check = importPatternCheck(schema.pattern, [...path, "pattern"])
1077
+ if (check !== undefined) checks.push(check)
1054
1078
  }
1055
1079
  return checks
1056
1080
  }
@@ -1086,45 +1110,42 @@ function translateJsonSchemaMultiDocument(
1086
1110
  schema: JsonSchema.JsonSchema,
1087
1111
  path: Path
1088
1112
  ): ImportedObjectScope {
1089
- const sourceProperties =
1090
- typeof schema.properties === "object" && schema.properties !== null && !Array.isArray(schema.properties)
1091
- ? schema.properties as Record<string, unknown>
1092
- : {}
1093
- const required = Array.isArray(schema.required)
1094
- ? schema.required.filter((key): key is string => typeof key === "string")
1095
- : []
1113
+ const sourceProperties = isObject(schema.properties) ? schema.properties : {}
1114
+ const required = new Set(
1115
+ Array.isArray(schema.required)
1116
+ ? schema.required.filter((key): key is string => typeof key === "string")
1117
+ : []
1118
+ )
1096
1119
  const propertyNames = Object.keys(sourceProperties)
1097
1120
  const keys = new Set([...propertyNames, ...required])
1098
1121
  const properties = new Map(Array.from(keys, (name) => [name, {
1099
1122
  type: Object.hasOwn(sourceProperties, name)
1100
- ? recur(sourceProperties[name], [...path, "properties", name])
1123
+ ? translateSchema(sourceProperties[name], [...path, "properties", name])
1101
1124
  : undefined,
1102
- isOptional: !required.includes(name)
1125
+ isOptional: !required.has(name)
1103
1126
  }]))
1104
1127
  const hasProperties = propertyNames.length > 0
1105
1128
  const patterns: Array<ImportedObjectPattern> = []
1106
1129
  if (
1107
- typeof schema.patternProperties === "object" &&
1108
- schema.patternProperties !== null &&
1109
- !Array.isArray(schema.patternProperties)
1130
+ isObject(schema.patternProperties)
1110
1131
  ) {
1111
1132
  for (const [pattern, value] of Object.entries(schema.patternProperties)) {
1112
- const checks = importPatternChecks(pattern, [...path, "patternProperties", pattern])
1113
- if (checks.length === 0) return { properties, hasProperties, patterns: [], additionalProperties: unknown }
1133
+ const check = importPatternCheck(pattern, [...path, "patternProperties", pattern])
1134
+ if (check === undefined) return { properties, hasProperties, patterns: [], additionalProperties: unknown }
1114
1135
  patterns.push({
1115
1136
  source: pattern,
1116
1137
  parameter: {
1117
1138
  _tag: "String",
1118
- checks
1139
+ checks: [check]
1119
1140
  },
1120
- type: recur(value, [...path, "patternProperties", pattern])
1141
+ type: translateSchema(value, [...path, "patternProperties", pattern])
1121
1142
  })
1122
1143
  }
1123
1144
  }
1124
1145
  const additionalProperties = schema.additionalProperties === false
1125
1146
  ? never
1126
- : typeof schema.additionalProperties === "object" && schema.additionalProperties !== null
1127
- ? recur(schema.additionalProperties, [...path, "additionalProperties"])
1147
+ : isObject(schema.additionalProperties)
1148
+ ? translateSchema(schema.additionalProperties, [...path, "additionalProperties"])
1128
1149
  : unknown
1129
1150
  return { properties, hasProperties, patterns, additionalProperties }
1130
1151
  }
@@ -1141,7 +1162,11 @@ function translateJsonSchemaMultiDocument(
1141
1162
  checks.push(jsonSchemaFilter(
1142
1163
  "effect/schema/isPropertyNames",
1143
1164
  null,
1144
- [combine(string, recur(schema.propertyNames, propertyNamesPath), propertyNamesPath)]
1165
+ [intersect(
1166
+ string,
1167
+ translateSchema(schema.propertyNames, propertyNamesPath),
1168
+ propertyNamesPath
1169
+ )]
1145
1170
  ))
1146
1171
  }
1147
1172
  return checks
@@ -1149,10 +1174,11 @@ function translateJsonSchemaMultiDocument(
1149
1174
 
1150
1175
  const references: Record<string, Representation> = {}
1151
1176
  const representations = document.schemas.map((schema, index) =>
1152
- unknownJsonSchemas(recur(schema, singleRoot ? ["schema"] : ["schemas", index]))
1177
+ finalize(translateSchema(schema, singleRoot ? ["schema"] : ["schemas", index], true))
1153
1178
  ) as [Representation, ...Array<Representation>]
1154
- for (const [key, path] of reachableDefinitions) {
1155
- InternalRecord.assignProperty(references, key, unknownJsonSchemas(translateDefinition(key, path)))
1179
+ // Definitions expanded away by intersections do not need a standalone output schema.
1180
+ for (const key of finalReferences) {
1181
+ InternalRecord.assignProperty(references, key, finalize(translateDefinition(key, reachableDefinitions.get(key)!)))
1156
1182
  }
1157
1183
  for (const { reference, path } of annotatedReferences) {
1158
1184
  resolveReference(reference, path)