effect 3.14.0 → 3.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Arbitrary.ts CHANGED
@@ -11,7 +11,7 @@ import * as util_ from "./internal/schema/util.js"
11
11
  import * as Option from "./Option.js"
12
12
  import * as Predicate from "./Predicate.js"
13
13
  import type * as Schema from "./Schema.js"
14
- import * as AST from "./SchemaAST.js"
14
+ import * as SchemaAST from "./SchemaAST.js"
15
15
  import type * as Types from "./Types.js"
16
16
 
17
17
  /**
@@ -49,8 +49,10 @@ export type ArbitraryAnnotation<A, TypeParameters extends ReadonlyArray<any> = r
49
49
  * @category arbitrary
50
50
  * @since 3.10.0
51
51
  */
52
- export const makeLazy = <A, I, R>(schema: Schema.Schema<A, I, R>): LazyArbitrary<A> =>
53
- go(schema.ast, { maxDepth: 2 }, [])
52
+ export const makeLazy = <A, I, R>(schema: Schema.Schema<A, I, R>): LazyArbitrary<A> => {
53
+ const description = getDescription(schema.ast, [])
54
+ return go(description, { maxDepth: 2 })
55
+ }
54
56
 
55
57
  /**
56
58
  * Returns a fast-check Arbitrary for the `A` type of the provided schema.
@@ -60,65 +62,6 @@ export const makeLazy = <A, I, R>(schema: Schema.Schema<A, I, R>): LazyArbitrary
60
62
  */
61
63
  export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): FastCheck.Arbitrary<A> => makeLazy(schema)(FastCheck)
62
64
 
63
- const getArbitraryAnnotation = AST.getAnnotation<ArbitraryAnnotation<any, any>>(AST.ArbitraryAnnotationId)
64
-
65
- type Op = Succeed | Deferred
66
-
67
- /**
68
- * Represents an arbitrary with optional filters.
69
- */
70
- class Succeed {
71
- readonly _tag = "Succeed"
72
- constructor(
73
- readonly lazyArbitrary: LazyArbitrary<any>,
74
- readonly filters: Array<Predicate.Predicate<any>> = []
75
- ) {}
76
-
77
- toLazyArbitrary(): LazyArbitrary<any> {
78
- return (fc) => {
79
- let out = this.lazyArbitrary(fc)
80
- for (const f of this.filters) {
81
- out = out.filter(f)
82
- }
83
- return out
84
- }
85
- }
86
- }
87
-
88
- /**
89
- * Represents a deferred arbitrary value generator with optional filters.
90
- */
91
- class Deferred {
92
- readonly _tag = "Deferred"
93
- constructor(
94
- readonly config: Config,
95
- readonly filters: Array<Predicate.Predicate<any>> = []
96
- ) {}
97
-
98
- toLazyArbitrary(ctx: ArbitraryGenerationContext, path: ReadonlyArray<PropertyKey>): LazyArbitrary<any> {
99
- const config = this.config
100
- switch (config._tag) {
101
- case "StringConstraints": {
102
- const pattern = config.pattern
103
- return pattern !== undefined ?
104
- (fc) => fc.stringMatching(new RegExp(pattern)) :
105
- (fc) => fc.string(config.constraints)
106
- }
107
- case "NumberConstraints": {
108
- return config.isInteger ?
109
- (fc) => fc.integer(config.constraints) :
110
- (fc) => fc.float(config.constraints)
111
- }
112
- case "BigIntConstraints":
113
- return (fc) => fc.bigInt(config.constraints)
114
- case "DateConstraints":
115
- return (fc) => fc.date(config.constraints)
116
- case "ArrayConstraints":
117
- return goTupleType(config.ast, ctx, path, config.constraints)
118
- }
119
- }
120
- }
121
-
122
65
  interface StringConstraints {
123
66
  readonly _tag: "StringConstraints"
124
67
  readonly constraints: FastCheck.StringSharedConstraints
@@ -156,9 +99,9 @@ interface NumberConstraints {
156
99
  /** @internal */
157
100
  export const makeNumberConstraints = (options: {
158
101
  readonly isInteger?: boolean | undefined
159
- readonly min?: number | undefined
102
+ readonly min?: unknown
160
103
  readonly minExcluded?: boolean | undefined
161
- readonly max?: number | undefined
104
+ readonly max?: unknown
162
105
  readonly maxExcluded?: boolean | undefined
163
106
  readonly noNaN?: boolean | undefined
164
107
  readonly noDefaultInfinity?: boolean | undefined
@@ -219,8 +162,8 @@ interface ArrayConstraints {
219
162
 
220
163
  /** @internal */
221
164
  export const makeArrayConstraints = (options: {
222
- readonly minLength?: number | undefined
223
- readonly maxLength?: number | undefined
165
+ readonly minLength?: unknown
166
+ readonly maxLength?: unknown
224
167
  }): ArrayConstraints => {
225
168
  const out: Types.Mutable<ArrayConstraints> = {
226
169
  _tag: "ArrayConstraints",
@@ -248,9 +191,7 @@ export const makeDateConstraints = (options: {
248
191
  }): DateConstraints => {
249
192
  const out: Types.Mutable<DateConstraints> = {
250
193
  _tag: "DateConstraints",
251
- constraints: {
252
- noInvalidDate: options.noInvalidDate ?? false
253
- }
194
+ constraints: {}
254
195
  }
255
196
  if (Predicate.isDate(options.min)) {
256
197
  out.constraints.min = options.min
@@ -258,69 +199,151 @@ export const makeDateConstraints = (options: {
258
199
  if (Predicate.isDate(options.max)) {
259
200
  out.constraints.max = options.max
260
201
  }
202
+ if (Predicate.isBoolean(options.noInvalidDate)) {
203
+ out.constraints.noInvalidDate = options.noInvalidDate
204
+ }
261
205
  return out
262
206
  }
263
207
 
264
- interface ArrayConfig extends ArrayConstraints {
265
- readonly ast: AST.TupleType
208
+ type Refinements = ReadonlyArray<SchemaAST.Refinement>
209
+
210
+ interface Base {
211
+ readonly path: ReadonlyArray<PropertyKey>
212
+ readonly refinements: Refinements
213
+ readonly annotations: ReadonlyArray<ArbitraryAnnotation<any, any>>
266
214
  }
267
215
 
268
- const makeArrayConfig = (options: {
269
- readonly minLength?: number | undefined
270
- readonly maxLength?: number | undefined
271
- }, ast: AST.TupleType): ArrayConfig => {
272
- return {
273
- ast,
274
- ...makeArrayConstraints(options)
275
- }
216
+ interface StringKeyword extends Base {
217
+ readonly _tag: "StringKeyword"
218
+ readonly constraints: ReadonlyArray<StringConstraints>
276
219
  }
277
220
 
278
- type Config = StringConstraints | NumberConstraints | BigIntConstraints | DateConstraints | ArrayConfig
221
+ interface NumberKeyword extends Base {
222
+ readonly _tag: "NumberKeyword"
223
+ readonly constraints: ReadonlyArray<NumberConstraints>
224
+ }
279
225
 
280
- const arbitraryMemoMap = globalValue(
281
- Symbol.for("effect/Arbitrary/arbitraryMemoMap"),
282
- () => new WeakMap<AST.AST, LazyArbitrary<any>>()
283
- )
226
+ interface BigIntKeyword extends Base {
227
+ readonly _tag: "BigIntKeyword"
228
+ readonly constraints: ReadonlyArray<BigIntConstraints>
229
+ }
284
230
 
285
- const go = (
286
- ast: AST.AST,
287
- ctx: ArbitraryGenerationContext,
288
- path: ReadonlyArray<PropertyKey>
289
- ): LazyArbitrary<any> => {
290
- const hook = getArbitraryAnnotation(ast)
291
- if (Option.isSome(hook)) {
292
- switch (ast._tag) {
293
- case "Declaration":
294
- return hook.value(...ast.typeParameters.map((p) => go(p, ctx, path)), ctx)
295
- case "Refinement": {
296
- const op = toOp(ast, ctx, path)
297
- ctx = op._tag === "Deferred" ? { ...ctx, constraints: op.config } : ctx
298
- const from = go(ast.from, ctx, path)
299
- return new Succeed(hook.value(from, ctx), op.filters).toLazyArbitrary()
300
- }
301
- default:
302
- return hook.value(ctx)
303
- }
304
- }
305
- if (AST.isDeclaration(ast)) {
306
- throw new Error(errors_.getArbitraryMissingAnnotationErrorMessage(path, ast))
307
- }
308
- const op = toOp(ast, ctx, path)
309
- switch (op._tag) {
310
- case "Succeed":
311
- return op.toLazyArbitrary()
312
- case "Deferred":
313
- return new Succeed(op.toLazyArbitrary(ctx, path), op.filters).toLazyArbitrary()
314
- }
231
+ interface DateFromSelf extends Base {
232
+ readonly _tag: "DateFromSelf"
233
+ readonly constraints: ReadonlyArray<DateConstraints>
234
+ }
235
+
236
+ interface Declaration extends Base {
237
+ readonly _tag: "Declaration"
238
+ readonly typeParameters: ReadonlyArray<Description>
239
+ readonly ast: SchemaAST.AST
240
+ }
241
+
242
+ interface TupleType extends Base {
243
+ readonly _tag: "TupleType"
244
+ readonly constraints: ReadonlyArray<ArrayConstraints>
245
+ readonly elements: ReadonlyArray<{
246
+ readonly isOptional: boolean
247
+ readonly description: Description
248
+ }>
249
+ readonly rest: ReadonlyArray<Description>
250
+ }
251
+
252
+ interface TypeLiteral extends Base {
253
+ readonly _tag: "TypeLiteral"
254
+ readonly propertySignatures: ReadonlyArray<{
255
+ readonly isOptional: boolean
256
+ readonly name: PropertyKey
257
+ readonly value: Description
258
+ }>
259
+ readonly indexSignatures: ReadonlyArray<{
260
+ readonly parameter: Description
261
+ readonly value: Description
262
+ }>
263
+ }
264
+
265
+ interface Union extends Base {
266
+ readonly _tag: "Union"
267
+ readonly members: ReadonlyArray<Description>
268
+ }
269
+
270
+ interface Suspend extends Base {
271
+ readonly _tag: "Suspend"
272
+ readonly id: string
273
+ readonly ast: SchemaAST.AST
274
+ readonly description: () => Description
275
+ }
276
+
277
+ interface Ref extends Base {
278
+ readonly _tag: "Ref"
279
+ readonly id: string
280
+ readonly ast: SchemaAST.AST
281
+ }
282
+
283
+ interface NeverKeyword extends Base {
284
+ readonly _tag: "NeverKeyword"
285
+ readonly ast: SchemaAST.AST
286
+ }
287
+
288
+ interface Keyword extends Base {
289
+ readonly _tag: "Keyword"
290
+ readonly value:
291
+ | "UndefinedKeyword"
292
+ | "VoidKeyword"
293
+ | "UnknownKeyword"
294
+ | "AnyKeyword"
295
+ | "BooleanKeyword"
296
+ | "SymbolKeyword"
297
+ | "ObjectKeyword"
298
+ }
299
+
300
+ interface Literal extends Base {
301
+ readonly _tag: "Literal"
302
+ readonly literal: SchemaAST.LiteralValue
303
+ }
304
+
305
+ interface UniqueSymbol extends Base {
306
+ readonly _tag: "UniqueSymbol"
307
+ readonly symbol: symbol
315
308
  }
316
309
 
317
- const constStringConstraints = makeStringConstraints({})
318
- const constNumberConstraints = makeNumberConstraints({})
319
- const constBigIntConstraints = makeBigIntConstraints({})
320
- const defaultSuspendedArrayConstraints: FastCheck.ArrayConstraints = { maxLength: 2 }
310
+ interface Enums extends Base {
311
+ readonly _tag: "Enums"
312
+ readonly enums: ReadonlyArray<readonly [string, string | number]>
313
+ readonly ast: SchemaAST.AST
314
+ }
315
+
316
+ interface TemplateLiteral extends Base {
317
+ readonly _tag: "TemplateLiteral"
318
+ readonly head: string
319
+ readonly spans: ReadonlyArray<{
320
+ readonly description: Description
321
+ readonly literal: string
322
+ }>
323
+ }
324
+
325
+ type Description =
326
+ | Declaration
327
+ | NeverKeyword
328
+ | Keyword
329
+ | Literal
330
+ | UniqueSymbol
331
+ | Enums
332
+ | TemplateLiteral
333
+ | StringKeyword
334
+ | NumberKeyword
335
+ | BigIntKeyword
336
+ | DateFromSelf
337
+ | TupleType
338
+ | TypeLiteral
339
+ | Union
340
+ | Suspend
341
+ | Ref
342
+
343
+ const getArbitraryAnnotation = SchemaAST.getAnnotation<ArbitraryAnnotation<any, any>>(SchemaAST.ArbitraryAnnotationId)
321
344
 
322
- const getASTConstraints = (ast: AST.AST) => {
323
- const TypeAnnotationId = ast.annotations[AST.SchemaIdAnnotationId]
345
+ const getASTConstraints = (ast: SchemaAST.AST) => {
346
+ const TypeAnnotationId = ast.annotations[SchemaAST.SchemaIdAnnotationId]
324
347
  if (Predicate.isPropertyKey(TypeAnnotationId)) {
325
348
  const out = ast.annotations[TypeAnnotationId]
326
349
  if (Predicate.isReadonlyRecord(out)) {
@@ -329,323 +352,277 @@ const getASTConstraints = (ast: AST.AST) => {
329
352
  }
330
353
  }
331
354
 
355
+ const idMemoMap = globalValue(
356
+ Symbol.for("effect/Arbitrary/IdMemoMap"),
357
+ () => new Map<SchemaAST.AST, string>()
358
+ )
359
+
360
+ let counter = 0
361
+
362
+ function wrapGetDescription(
363
+ f: (ast: SchemaAST.AST, description: Description) => Description,
364
+ g: (ast: SchemaAST.AST, path: ReadonlyArray<PropertyKey>) => Description
365
+ ): (ast: SchemaAST.AST, path: ReadonlyArray<PropertyKey>) => Description {
366
+ return (ast, path) => f(ast, g(ast, path))
367
+ }
368
+
369
+ function parseMeta(ast: SchemaAST.AST): [SchemaAST.SchemaIdAnnotation | undefined, Record<string | symbol, unknown>] {
370
+ const jsonSchema = SchemaAST.getJSONSchemaAnnotation(ast).pipe(
371
+ Option.filter(Predicate.isReadonlyRecord),
372
+ Option.getOrUndefined
373
+ )
374
+ const schemaId = Option.getOrElse(SchemaAST.getSchemaIdAnnotation(ast), () => undefined)
375
+ const schemaParams = Option.fromNullable(schemaId).pipe(
376
+ Option.map((id) => ast.annotations[id]),
377
+ Option.filter(Predicate.isReadonlyRecord),
378
+ Option.getOrUndefined
379
+ )
380
+ return [schemaId, { ...schemaParams, ...jsonSchema }]
381
+ }
382
+
332
383
  /** @internal */
333
- export const toOp = (
334
- ast: AST.AST,
335
- ctx: ArbitraryGenerationContext,
336
- path: ReadonlyArray<PropertyKey>
337
- ): Op => {
338
- switch (ast._tag) {
339
- case "Declaration": {
340
- const TypeAnnotationId = ast.annotations[AST.SchemaIdAnnotationId]
341
- if (TypeAnnotationId === schemaId_.DateFromSelfSchemaId) {
342
- const c = getASTConstraints(ast)
343
- if (c !== undefined) {
344
- return new Deferred(makeDateConstraints(c))
345
- }
346
- }
347
- return new Succeed(go(ast, ctx, path))
348
- }
349
- case "Literal":
350
- return new Succeed((fc) => fc.constant(ast.literal))
351
- case "UniqueSymbol":
352
- return new Succeed((fc) => fc.constant(ast.symbol))
353
- case "UndefinedKeyword":
354
- return new Succeed((fc) => fc.constant(undefined))
355
- case "NeverKeyword":
356
- throw new Error(errors_.getArbitraryMissingAnnotationErrorMessage(path, ast))
357
- case "VoidKeyword":
358
- case "UnknownKeyword":
359
- case "AnyKeyword":
360
- return new Succeed((fc) => fc.anything())
361
- case "StringKeyword":
362
- return new Deferred(constStringConstraints)
363
- case "NumberKeyword":
364
- return new Deferred(constNumberConstraints)
365
- case "BooleanKeyword":
366
- return new Succeed((fc) => fc.boolean())
367
- case "BigIntKeyword":
368
- return new Deferred(constBigIntConstraints)
369
- case "SymbolKeyword":
370
- return new Succeed((fc) => fc.string().map((s) => Symbol.for(s)))
371
- case "ObjectKeyword":
372
- return new Succeed((fc) => fc.oneof(fc.object(), fc.array(fc.anything())))
373
- case "Enums": {
374
- if (ast.enums.length === 0) {
375
- throw new Error(errors_.getArbitraryEmptyEnumErrorMessage(path))
384
+ export const getDescription = wrapGetDescription(
385
+ (ast, description) => {
386
+ const annotation = getArbitraryAnnotation(ast)
387
+ if (Option.isSome(annotation)) {
388
+ return {
389
+ ...description,
390
+ annotations: [...description.annotations, annotation.value]
376
391
  }
377
- return new Succeed((fc) => fc.oneof(...ast.enums.map(([_, value]) => fc.constant(value))))
378
392
  }
379
- case "TemplateLiteral":
380
- return new Succeed((fc) => {
381
- const string = fc.string({ maxLength: 5 })
382
- const number = fc.float({ noDefaultInfinity: true, noNaN: true })
383
-
384
- const getTemplateLiteralArb = (ast: AST.TemplateLiteral) => {
385
- const components: Array<FastCheck.Arbitrary<string | number>> = ast.head !== "" ? [fc.constant(ast.head)] : []
386
-
387
- const getTemplateLiteralSpanTypeArb = (
388
- ast: AST.TemplateLiteralSpan["type"]
389
- ): FastCheck.Arbitrary<string | number> => {
390
- switch (ast._tag) {
391
- case "StringKeyword":
392
- return string
393
- case "NumberKeyword":
394
- return number
395
- case "Literal":
396
- return fc.constant(String(ast.literal))
397
- case "Union":
398
- return fc.oneof(...ast.types.map(getTemplateLiteralSpanTypeArb))
399
- case "TemplateLiteral":
400
- return getTemplateLiteralArb(ast)
393
+ return description
394
+ },
395
+ (ast, path) => {
396
+ const [schemaId, meta] = parseMeta(ast)
397
+ switch (ast._tag) {
398
+ case "Refinement": {
399
+ const from = getDescription(ast.from, path)
400
+ switch (from._tag) {
401
+ case "StringKeyword":
402
+ return {
403
+ ...from,
404
+ constraints: [...from.constraints, makeStringConstraints(meta)],
405
+ refinements: [...from.refinements, ast]
406
+ }
407
+ case "NumberKeyword": {
408
+ const c = schemaId === schemaId_.NonNaNSchemaId ?
409
+ makeNumberConstraints({ noNaN: true }) :
410
+ makeNumberConstraints({
411
+ isInteger: "type" in meta && meta.type === "integer",
412
+ noNaN: "type" in meta && meta.type === "number" ? true : undefined,
413
+ noDefaultInfinity: "type" in meta && meta.type === "number" ? true : undefined,
414
+ min: meta.exclusiveMinimum ?? meta.minimum,
415
+ minExcluded: "exclusiveMinimum" in meta ? true : undefined,
416
+ max: meta.exclusiveMaximum ?? meta.maximum,
417
+ maxExcluded: "exclusiveMaximum" in meta ? true : undefined
418
+ })
419
+ return {
420
+ ...from,
421
+ constraints: [...from.constraints, c],
422
+ refinements: [...from.refinements, ast]
401
423
  }
402
424
  }
403
-
404
- ast.spans.forEach((span) => {
405
- components.push(getTemplateLiteralSpanTypeArb(span.type))
406
- if (span.literal !== "") {
407
- components.push(fc.constant(span.literal))
425
+ case "BigIntKeyword": {
426
+ const c = getASTConstraints(ast)
427
+ return {
428
+ ...from,
429
+ constraints: c !== undefined ? [...from.constraints, makeBigIntConstraints(c)] : from.constraints,
430
+ refinements: [...from.refinements, ast]
431
+ }
432
+ }
433
+ case "TupleType":
434
+ return {
435
+ ...from,
436
+ constraints: [
437
+ ...from.constraints,
438
+ makeArrayConstraints({
439
+ minLength: meta.minItems,
440
+ maxLength: meta.maxItems
441
+ })
442
+ ],
443
+ refinements: [...from.refinements, ast]
444
+ }
445
+ case "DateFromSelf":
446
+ return {
447
+ ...from,
448
+ constraints: [...from.constraints, makeDateConstraints(meta)],
449
+ refinements: [...from.refinements, ast]
450
+ }
451
+ default:
452
+ return {
453
+ ...from,
454
+ refinements: [...from.refinements, ast]
408
455
  }
409
- })
410
-
411
- return fc.tuple(...components).map((spans) => spans.join(""))
412
456
  }
413
-
414
- return getTemplateLiteralArb(ast)
415
- })
416
- case "Refinement": {
417
- const from = toOp(ast.from, ctx, path)
418
- const filters: Op["filters"] = [
419
- ...from.filters,
420
- (a) => Option.isNone(ast.filter(a, AST.defaultParseOption, ast))
421
- ]
422
- switch (from._tag) {
423
- case "Succeed": {
424
- return new Succeed(from.lazyArbitrary, filters)
457
+ }
458
+ case "Declaration": {
459
+ if (schemaId === schemaId_.DateFromSelfSchemaId) {
460
+ return {
461
+ _tag: "DateFromSelf",
462
+ constraints: [makeDateConstraints(meta)],
463
+ path,
464
+ refinements: [],
465
+ annotations: []
466
+ }
425
467
  }
426
- case "Deferred": {
427
- return new Deferred(merge(from.config, getRefinementConstraints(from.config._tag, ast)), filters)
468
+ return {
469
+ _tag: "Declaration",
470
+ typeParameters: ast.typeParameters.map((ast) => getDescription(ast, path)),
471
+ path,
472
+ refinements: [],
473
+ annotations: [],
474
+ ast
428
475
  }
429
476
  }
430
- }
431
- case "TupleType":
432
- return new Deferred(makeArrayConfig({}, ast))
433
- case "TypeLiteral": {
434
- const propertySignaturesTypes = ast.propertySignatures.map((ps) => go(ps.type, ctx, path.concat(ps.name)))
435
- const indexSignatures = ast.indexSignatures.map((is) =>
436
- [go(is.parameter, ctx, path), go(is.type, ctx, path)] as const
437
- )
438
- return new Succeed((fc) => {
439
- const arbs: any = {}
440
- const requiredKeys: Array<PropertyKey> = []
441
- // ---------------------------------------------
442
- // handle property signatures
443
- // ---------------------------------------------
444
- for (let i = 0; i < propertySignaturesTypes.length; i++) {
445
- const ps = ast.propertySignatures[i]
446
- const name = ps.name
447
- if (!ps.isOptional) {
448
- requiredKeys.push(name)
449
- }
450
- arbs[name] = propertySignaturesTypes[i](fc)
477
+ case "Literal": {
478
+ return {
479
+ _tag: "Literal",
480
+ literal: ast.literal,
481
+ path,
482
+ refinements: [],
483
+ annotations: []
451
484
  }
452
- let output = fc.record<any, any>(arbs, { requiredKeys })
453
- // ---------------------------------------------
454
- // handle index signatures
455
- // ---------------------------------------------
456
- for (let i = 0; i < indexSignatures.length; i++) {
457
- const key = indexSignatures[i][0](fc)
458
- const value = indexSignatures[i][1](fc)
459
- output = output.chain((o) => {
460
- const item = fc.tuple(key, value)
461
- /*
462
-
463
- `getSuspendedArray` is used to generate less key/value pairs in
464
- the context of a recursive schema. Without it, the following schema
465
- would generate an big amount of values possibly leading to a stack
466
- overflow:
467
-
468
- ```ts
469
- type A = { [_: string]: A }
470
-
471
- const schema = S.Record({ key: S.String, value: S.suspend((): S.Schema<A> => schema) })
472
- ```
473
-
474
- */
475
- const arr = ctx.depthIdentifier !== undefined ?
476
- getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item, defaultSuspendedArrayConstraints) :
477
- fc.array(item)
478
- return arr.map((tuples) => ({ ...Object.fromEntries(tuples), ...o }))
479
- })
485
+ }
486
+ case "UniqueSymbol": {
487
+ return {
488
+ _tag: "UniqueSymbol",
489
+ symbol: ast.symbol,
490
+ path,
491
+ refinements: [],
492
+ annotations: []
480
493
  }
481
-
482
- return output
483
- })
484
- }
485
- case "Union": {
486
- const types = ast.types.map((member) => go(member, ctx, path))
487
- return new Succeed((fc) => fc.oneof(...types.map((arb) => arb(fc))))
488
- }
489
- case "Suspend": {
490
- const memo = arbitraryMemoMap.get(ast)
491
- if (memo) {
492
- return new Succeed(memo)
493
494
  }
494
- const get = util_.memoizeThunk(() => {
495
- return go(ast.f(), getSuspendedContext(ctx, ast), path)
496
- })
497
- const out: LazyArbitrary<any> = (fc) => fc.constant(null).chain(() => get()(fc))
498
- arbitraryMemoMap.set(ast, out)
499
- return new Succeed(out)
500
- }
501
- case "Transformation":
502
- return toOp(ast.to, ctx, path)
503
- }
504
- }
505
-
506
- function subtractElementsLength(
507
- constraints: FastCheck.ArrayConstraints,
508
- elementsLength: number
509
- ): FastCheck.ArrayConstraints {
510
- if (elementsLength === 0 || (constraints.minLength === undefined && constraints.maxLength === undefined)) {
511
- return constraints
512
- }
513
- const out = { ...constraints }
514
- if (out.minLength !== undefined) {
515
- out.minLength = Math.max(out.minLength - elementsLength, 0)
516
- }
517
- if (out.maxLength !== undefined) {
518
- out.maxLength = Math.max(out.maxLength - elementsLength, 0)
519
- }
520
- return out
521
- }
522
-
523
- const goTupleType = (
524
- ast: AST.TupleType,
525
- ctx: ArbitraryGenerationContext,
526
- path: ReadonlyArray<PropertyKey>,
527
- constraints: FastCheck.ArrayConstraints
528
- ): LazyArbitrary<any> => {
529
- const elements: Array<LazyArbitrary<any>> = []
530
- let hasOptionals = false
531
- let i = 0
532
- for (const element of ast.elements) {
533
- elements.push(go(element.type, ctx, path.concat(i++)))
534
- if (element.isOptional) {
535
- hasOptionals = true
536
- }
537
- }
538
- const rest = ast.rest.map((annotatedAST) => go(annotatedAST.type, ctx, path))
539
- return (fc) => {
540
- // ---------------------------------------------
541
- // handle elements
542
- // ---------------------------------------------
543
- let output = fc.tuple(...elements.map((arb) => arb(fc)))
544
- if (hasOptionals) {
545
- const indexes = fc.tuple(
546
- ...ast.elements.map((element) => element.isOptional ? fc.boolean() : fc.constant(true))
547
- )
548
- output = output.chain((tuple) =>
549
- indexes.map((booleans) => {
550
- for (const [i, b] of booleans.reverse().entries()) {
551
- if (!b) {
552
- tuple.splice(booleans.length - i, 1)
553
- }
495
+ case "Enums": {
496
+ return {
497
+ _tag: "Enums",
498
+ enums: ast.enums,
499
+ path,
500
+ refinements: [],
501
+ annotations: [],
502
+ ast
503
+ }
504
+ }
505
+ case "TemplateLiteral": {
506
+ return {
507
+ _tag: "TemplateLiteral",
508
+ head: ast.head,
509
+ spans: ast.spans.map((span) => ({
510
+ description: getDescription(span.type, path),
511
+ literal: span.literal
512
+ })),
513
+ path,
514
+ refinements: [],
515
+ annotations: []
516
+ }
517
+ }
518
+ case "StringKeyword":
519
+ return {
520
+ _tag: "StringKeyword",
521
+ constraints: [],
522
+ path,
523
+ refinements: [],
524
+ annotations: []
525
+ }
526
+ case "NumberKeyword":
527
+ return {
528
+ _tag: "NumberKeyword",
529
+ constraints: [],
530
+ path,
531
+ refinements: [],
532
+ annotations: []
533
+ }
534
+ case "BigIntKeyword":
535
+ return {
536
+ _tag: "BigIntKeyword",
537
+ constraints: [],
538
+ path,
539
+ refinements: [],
540
+ annotations: []
541
+ }
542
+ case "TupleType":
543
+ return {
544
+ _tag: "TupleType",
545
+ constraints: [],
546
+ elements: ast.elements.map((element, i) => ({
547
+ isOptional: element.isOptional,
548
+ description: getDescription(element.type, [...path, i])
549
+ })),
550
+ rest: ast.rest.map((element, i) => getDescription(element.type, [...path, i])),
551
+ path,
552
+ refinements: [],
553
+ annotations: []
554
+ }
555
+ case "TypeLiteral":
556
+ return {
557
+ _tag: "TypeLiteral",
558
+ propertySignatures: ast.propertySignatures.map((ps) => ({
559
+ isOptional: ps.isOptional,
560
+ name: ps.name,
561
+ value: getDescription(ps.type, [...path, ps.name])
562
+ })),
563
+ indexSignatures: ast.indexSignatures.map((is) => ({
564
+ parameter: getDescription(is.parameter, path),
565
+ value: getDescription(is.type, path)
566
+ })),
567
+ path,
568
+ refinements: [],
569
+ annotations: []
570
+ }
571
+ case "Union":
572
+ return {
573
+ _tag: "Union",
574
+ members: ast.types.map((member, i) => getDescription(member, [...path, i])),
575
+ path,
576
+ refinements: [],
577
+ annotations: []
578
+ }
579
+ case "Suspend": {
580
+ const memoId = idMemoMap.get(ast)
581
+ if (memoId !== undefined) {
582
+ return {
583
+ _tag: "Ref",
584
+ id: memoId,
585
+ ast,
586
+ path,
587
+ refinements: [],
588
+ annotations: []
554
589
  }
555
- return tuple
556
- })
557
- )
558
- }
559
-
560
- // ---------------------------------------------
561
- // handle rest element
562
- // ---------------------------------------------
563
- if (Arr.isNonEmptyReadonlyArray(rest)) {
564
- const [head, ...tail] = rest
565
- const item = head(fc)
566
- output = output.chain((as) => {
567
- const len = as.length
568
- // We must adjust the constraints for the rest element
569
- // because the elements might have generated some values
570
- const restArrayConstraints = subtractElementsLength(constraints, len)
571
- if (restArrayConstraints.maxLength === 0) {
572
- return fc.constant(as)
573
590
  }
574
- /*
575
-
576
- `getSuspendedArray` is used to generate less values in
577
- the context of a recursive schema. Without it, the following schema
578
- would generate an big amount of values possibly leading to a stack
579
- overflow:
580
-
581
- ```ts
582
- type A = ReadonlyArray<A | null>
583
-
584
- const schema = S.Array(
585
- S.NullOr(S.suspend((): S.Schema<A> => schema))
586
- )
587
- ```
588
-
589
- */
590
- const arr = ctx.depthIdentifier !== undefined
591
- ? getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item, restArrayConstraints)
592
- : fc.array(item, restArrayConstraints)
593
- if (len === 0) {
594
- return arr
591
+ counter++
592
+ const id = `__id-${counter}__`
593
+ idMemoMap.set(ast, id)
594
+ return {
595
+ _tag: "Suspend",
596
+ id,
597
+ ast,
598
+ description: () => getDescription(ast.f(), path),
599
+ path,
600
+ refinements: [],
601
+ annotations: []
595
602
  }
596
- return arr.map((rest) => [...as, ...rest])
597
- })
598
- // ---------------------------------------------
599
- // handle post rest elements
600
- // ---------------------------------------------
601
- for (let j = 0; j < tail.length; j++) {
602
- output = output.chain((as) => tail[j](fc).map((a) => [...as, a]))
603
603
  }
604
- }
605
-
606
- return output
607
- }
608
- }
609
-
610
- type Constraints = StringConstraints | NumberConstraints | BigIntConstraints | DateConstraints | ArrayConstraints
611
-
612
- const getRefinementConstraints = (_tag: Constraints["_tag"], ast: AST.Refinement): Constraints | undefined => {
613
- const TypeAnnotationId = ast.annotations[AST.SchemaIdAnnotationId]
614
- const jsonSchema: Record<string, any> = Option.getOrElse(AST.getJSONSchemaAnnotation(ast), () => ({}))
615
-
616
- switch (_tag) {
617
- case "StringConstraints":
618
- return makeStringConstraints(jsonSchema)
619
- case "NumberConstraints": {
620
- if (TypeAnnotationId === schemaId_.NonNaNSchemaId) {
621
- return makeNumberConstraints({ noNaN: true })
622
- } else {
623
- return makeNumberConstraints({
624
- isInteger: "type" in jsonSchema && jsonSchema.type === "integer",
625
- noNaN: "type" in jsonSchema && jsonSchema.type === "number" ? true : undefined,
626
- noDefaultInfinity: "type" in jsonSchema && jsonSchema.type === "number" ? true : undefined,
627
- min: jsonSchema.exclusiveMinimum ?? jsonSchema.minimum,
628
- minExcluded: "exclusiveMinimum" in jsonSchema ? true : undefined,
629
- max: jsonSchema.exclusiveMaximum ?? jsonSchema.maximum,
630
- maxExcluded: "exclusiveMaximum" in jsonSchema ? true : undefined
631
- })
604
+ case "Transformation":
605
+ return getDescription(ast.to, path)
606
+ case "NeverKeyword":
607
+ return {
608
+ _tag: "NeverKeyword",
609
+ path,
610
+ refinements: [],
611
+ annotations: [],
612
+ ast
613
+ }
614
+ default: {
615
+ return {
616
+ _tag: "Keyword",
617
+ value: ast._tag,
618
+ path,
619
+ refinements: [],
620
+ annotations: []
621
+ }
632
622
  }
633
623
  }
634
- case "BigIntConstraints": {
635
- const c = getASTConstraints(ast)
636
- return c !== undefined ? makeBigIntConstraints(c) : undefined
637
- }
638
- case "DateConstraints": {
639
- const c = getASTConstraints(ast)
640
- return c !== undefined ? makeDateConstraints(c) : undefined
641
- }
642
- case "ArrayConstraints":
643
- return makeArrayConstraints({
644
- minLength: jsonSchema.minItems,
645
- maxLength: jsonSchema.maxItems
646
- })
647
624
  }
648
- }
625
+ )
649
626
 
650
627
  function getMax(n1: Date | undefined, n2: Date | undefined): Date | undefined
651
628
  function getMax(n1: bigint | undefined, n2: bigint | undefined): bigint | undefined
@@ -681,64 +658,423 @@ function mergePattern(pattern1: string | undefined, pattern2: string | undefined
681
658
  return `(?:${pattern1})|(?:${pattern2})`
682
659
  }
683
660
 
684
- const merge = (c1: Config, c2: Constraints | undefined): Config => {
685
- if (c2) {
686
- switch (c1._tag) {
687
- case "StringConstraints": {
688
- if (c2._tag === "StringConstraints") {
689
- return makeStringConstraints({
690
- minLength: getMax(c1.constraints.minLength, c2.constraints.minLength),
691
- maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength),
692
- pattern: mergePattern(c1.pattern, c2.pattern)
693
- })
661
+ function mergeStringConstraints(c1: StringConstraints, c2: StringConstraints): StringConstraints {
662
+ return makeStringConstraints({
663
+ minLength: getMax(c1.constraints.minLength, c2.constraints.minLength),
664
+ maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength),
665
+ pattern: mergePattern(c1.pattern, c2.pattern)
666
+ })
667
+ }
668
+
669
+ function buildStringConstraints(description: StringKeyword): StringConstraints | undefined {
670
+ return description.constraints.length === 0
671
+ ? undefined
672
+ : description.constraints.reduce(mergeStringConstraints)
673
+ }
674
+
675
+ function mergeNumberConstraints(c1: NumberConstraints, c2: NumberConstraints): NumberConstraints {
676
+ return makeNumberConstraints({
677
+ isInteger: c1.isInteger || c2.isInteger,
678
+ min: getMax(c1.constraints.min, c2.constraints.min),
679
+ minExcluded: getOr(c1.constraints.minExcluded, c2.constraints.minExcluded),
680
+ max: getMin(c1.constraints.max, c2.constraints.max),
681
+ maxExcluded: getOr(c1.constraints.maxExcluded, c2.constraints.maxExcluded),
682
+ noNaN: getOr(c1.constraints.noNaN, c2.constraints.noNaN),
683
+ noDefaultInfinity: getOr(c1.constraints.noDefaultInfinity, c2.constraints.noDefaultInfinity)
684
+ })
685
+ }
686
+
687
+ function buildNumberConstraints(description: NumberKeyword): NumberConstraints | undefined {
688
+ return description.constraints.length === 0
689
+ ? undefined
690
+ : description.constraints.reduce(mergeNumberConstraints)
691
+ }
692
+
693
+ function mergeBigIntConstraints(c1: BigIntConstraints, c2: BigIntConstraints): BigIntConstraints {
694
+ return makeBigIntConstraints({
695
+ min: getMax(c1.constraints.min, c2.constraints.min),
696
+ max: getMin(c1.constraints.max, c2.constraints.max)
697
+ })
698
+ }
699
+
700
+ function buildBigIntConstraints(description: BigIntKeyword): BigIntConstraints | undefined {
701
+ return description.constraints.length === 0
702
+ ? undefined
703
+ : description.constraints.reduce(mergeBigIntConstraints)
704
+ }
705
+
706
+ function mergeDateConstraints(c1: DateConstraints, c2: DateConstraints): DateConstraints {
707
+ return makeDateConstraints({
708
+ min: getMax(c1.constraints.min, c2.constraints.min),
709
+ max: getMin(c1.constraints.max, c2.constraints.max),
710
+ noInvalidDate: getOr(c1.constraints.noInvalidDate, c2.constraints.noInvalidDate)
711
+ })
712
+ }
713
+
714
+ function buildDateConstraints(description: DateFromSelf): DateConstraints | undefined {
715
+ return description.constraints.length === 0
716
+ ? undefined
717
+ : description.constraints.reduce(mergeDateConstraints)
718
+ }
719
+
720
+ const constArrayConstraints = makeArrayConstraints({})
721
+
722
+ function mergeArrayConstraints(c1: ArrayConstraints, c2: ArrayConstraints): ArrayConstraints {
723
+ return makeArrayConstraints({
724
+ minLength: getMax(c1.constraints.minLength, c2.constraints.minLength),
725
+ maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength)
726
+ })
727
+ }
728
+
729
+ function buildArrayConstraints(description: TupleType): ArrayConstraints | undefined {
730
+ return description.constraints.length === 0
731
+ ? undefined
732
+ : description.constraints.reduce(mergeArrayConstraints)
733
+ }
734
+
735
+ const arbitraryMemoMap = globalValue(
736
+ Symbol.for("effect/Arbitrary/arbitraryMemoMap"),
737
+ () => new WeakMap<SchemaAST.AST, LazyArbitrary<any>>()
738
+ )
739
+
740
+ function applyFilters(filters: ReadonlyArray<Predicate.Predicate<any>>, arb: LazyArbitrary<any>): LazyArbitrary<any> {
741
+ return (fc) => filters.reduce((arb, filter) => arb.filter(filter), arb(fc))
742
+ }
743
+
744
+ function absurd(message: string): LazyArbitrary<any> {
745
+ return () => {
746
+ throw new Error(message)
747
+ }
748
+ }
749
+
750
+ function getContextConstraints(description: Description): ArbitraryGenerationContext["constraints"] {
751
+ switch (description._tag) {
752
+ case "StringKeyword":
753
+ return buildStringConstraints(description)
754
+ case "NumberKeyword":
755
+ return buildNumberConstraints(description)
756
+ case "BigIntKeyword":
757
+ return buildBigIntConstraints(description)
758
+ case "DateFromSelf":
759
+ return buildDateConstraints(description)
760
+ case "TupleType":
761
+ return buildArrayConstraints(description)
762
+ }
763
+ }
764
+
765
+ function wrapGo(
766
+ f: (description: Description, ctx: ArbitraryGenerationContext, lazyArb: LazyArbitrary<any>) => LazyArbitrary<any>,
767
+ g: (description: Description, ctx: ArbitraryGenerationContext) => LazyArbitrary<any>
768
+ ): (description: Description, ctx: ArbitraryGenerationContext) => LazyArbitrary<any> {
769
+ return (description, ctx) => f(description, ctx, g(description, ctx))
770
+ }
771
+
772
+ const go = wrapGo(
773
+ (description, ctx, lazyArb) => {
774
+ const annotation: ArbitraryAnnotation<any, any> | undefined =
775
+ description.annotations[description.annotations.length - 1]
776
+
777
+ // error handling
778
+ if (annotation === undefined) {
779
+ switch (description._tag) {
780
+ case "Declaration":
781
+ case "NeverKeyword":
782
+ throw new Error(errors_.getArbitraryMissingAnnotationErrorMessage(description.path, description.ast))
783
+ case "Enums":
784
+ if (description.enums.length === 0) {
785
+ throw new Error(errors_.getArbitraryEmptyEnumErrorMessage(description.path))
786
+ }
787
+ }
788
+ }
789
+
790
+ const filters = description.refinements.map((ast) => (a: any) =>
791
+ Option.isNone(ast.filter(a, SchemaAST.defaultParseOption, ast))
792
+ )
793
+ if (annotation === undefined) {
794
+ return applyFilters(filters, lazyArb)
795
+ }
796
+
797
+ const constraints = getContextConstraints(description)
798
+ if (constraints !== undefined) {
799
+ ctx = { ...ctx, constraints }
800
+ }
801
+
802
+ if (description._tag === "Declaration") {
803
+ return applyFilters(filters, annotation(...description.typeParameters.map((p) => go(p, ctx)), ctx))
804
+ }
805
+ if (description.refinements.length > 0) {
806
+ // TODO(4.0): remove the `lazyArb` parameter
807
+ return applyFilters(filters, annotation(lazyArb, ctx))
808
+ }
809
+ return annotation(ctx)
810
+ },
811
+ (description, ctx) => {
812
+ switch (description._tag) {
813
+ case "DateFromSelf": {
814
+ const constraints = buildDateConstraints(description)
815
+ return (fc) => fc.date(constraints?.constraints)
816
+ }
817
+ case "Declaration":
818
+ case "NeverKeyword":
819
+ return absurd(`BUG: cannot generate an arbitrary for ${description._tag}`)
820
+ case "Literal":
821
+ return (fc) => fc.constant(description.literal)
822
+ case "UniqueSymbol":
823
+ return (fc) => fc.constant(description.symbol)
824
+ case "Keyword": {
825
+ switch (description.value) {
826
+ case "UndefinedKeyword":
827
+ return (fc) => fc.constant(undefined)
828
+ case "VoidKeyword":
829
+ case "UnknownKeyword":
830
+ case "AnyKeyword":
831
+ return (fc) => fc.anything()
832
+ case "BooleanKeyword":
833
+ return (fc) => fc.boolean()
834
+ case "SymbolKeyword":
835
+ return (fc) => fc.string().map((s) => Symbol.for(s))
836
+ case "ObjectKeyword":
837
+ return (fc) => fc.oneof(fc.object(), fc.array(fc.anything()))
838
+ }
839
+ }
840
+ case "Enums":
841
+ return (fc) => fc.oneof(...description.enums.map(([_, value]) => fc.constant(value)))
842
+ case "TemplateLiteral": {
843
+ return (fc) => {
844
+ const string = fc.string({ maxLength: 5 })
845
+ const number = fc.float({ noDefaultInfinity: true, noNaN: true })
846
+
847
+ const getTemplateLiteralArb = (description: TemplateLiteral) => {
848
+ const components: Array<FastCheck.Arbitrary<string | number>> = description.head !== ""
849
+ ? [fc.constant(description.head)]
850
+ : []
851
+
852
+ const getTemplateLiteralSpanTypeArb = (
853
+ description: Description
854
+ ): FastCheck.Arbitrary<string | number> => {
855
+ switch (description._tag) {
856
+ case "StringKeyword":
857
+ return string
858
+ case "NumberKeyword":
859
+ return number
860
+ case "Literal":
861
+ return fc.constant(String(description.literal))
862
+ case "Union":
863
+ return fc.oneof(...description.members.map(getTemplateLiteralSpanTypeArb))
864
+ case "TemplateLiteral":
865
+ return getTemplateLiteralArb(description)
866
+ default:
867
+ return fc.constant("")
868
+ }
869
+ }
870
+
871
+ description.spans.forEach((span) => {
872
+ components.push(getTemplateLiteralSpanTypeArb(span.description))
873
+ if (span.literal !== "") {
874
+ components.push(fc.constant(span.literal))
875
+ }
876
+ })
877
+
878
+ return fc.tuple(...components).map((spans) => spans.join(""))
879
+ }
880
+
881
+ return getTemplateLiteralArb(description)
694
882
  }
695
- break
696
883
  }
697
- case "NumberConstraints": {
698
- if (c2._tag === "NumberConstraints") {
699
- return makeNumberConstraints({
700
- isInteger: c1.isInteger || c2.isInteger,
701
- min: getMax(c1.constraints.min, c2.constraints.min),
702
- minExcluded: getOr(c1.constraints.minExcluded, c2.constraints.minExcluded),
703
- max: getMin(c1.constraints.max, c2.constraints.max),
704
- maxExcluded: getOr(c1.constraints.maxExcluded, c2.constraints.maxExcluded),
705
- noNaN: getOr(c1.constraints.noNaN, c2.constraints.noNaN),
706
- noDefaultInfinity: getOr(c1.constraints.noDefaultInfinity, c2.constraints.noDefaultInfinity)
707
- })
884
+ case "StringKeyword": {
885
+ const constraints = buildStringConstraints(description)
886
+ const pattern = constraints?.pattern
887
+ return pattern !== undefined ?
888
+ (fc) => fc.stringMatching(new RegExp(pattern)) :
889
+ (fc) => fc.string(constraints?.constraints)
890
+ }
891
+ case "NumberKeyword": {
892
+ const constraints = buildNumberConstraints(description)
893
+ return constraints?.isInteger ?
894
+ (fc) => fc.integer(constraints.constraints) :
895
+ (fc) => fc.float(constraints?.constraints)
896
+ }
897
+ case "BigIntKeyword": {
898
+ const constraints = buildBigIntConstraints(description)
899
+ return (fc) => fc.bigInt(constraints?.constraints ?? {})
900
+ }
901
+ case "TupleType": {
902
+ const elements: Array<LazyArbitrary<any>> = []
903
+ let hasOptionals = false
904
+ for (const element of description.elements) {
905
+ elements.push(go(element.description, ctx))
906
+ if (element.isOptional) {
907
+ hasOptionals = true
908
+ }
909
+ }
910
+ const rest = description.rest.map((d) => go(d, ctx))
911
+ return (fc) => {
912
+ // ---------------------------------------------
913
+ // handle elements
914
+ // ---------------------------------------------
915
+ let output = fc.tuple(...elements.map((arb) => arb(fc)))
916
+ if (hasOptionals) {
917
+ const indexes = fc.tuple(
918
+ ...description.elements.map((element) => element.isOptional ? fc.boolean() : fc.constant(true))
919
+ )
920
+ output = output.chain((tuple) =>
921
+ indexes.map((booleans) => {
922
+ for (const [i, b] of booleans.reverse().entries()) {
923
+ if (!b) {
924
+ tuple.splice(booleans.length - i, 1)
925
+ }
926
+ }
927
+ return tuple
928
+ })
929
+ )
930
+ }
931
+
932
+ // ---------------------------------------------
933
+ // handle rest element
934
+ // ---------------------------------------------
935
+ if (Arr.isNonEmptyReadonlyArray(rest)) {
936
+ const constraints = buildArrayConstraints(description) ?? constArrayConstraints
937
+ const [head, ...tail] = rest
938
+ const item = head(fc)
939
+ output = output.chain((as) => {
940
+ const len = as.length
941
+ // We must adjust the constraints for the rest element
942
+ // because the elements might have generated some values
943
+ const restArrayConstraints = subtractElementsLength(constraints.constraints, len)
944
+ if (restArrayConstraints.maxLength === 0) {
945
+ return fc.constant(as)
946
+ }
947
+ /*
948
+
949
+ `getSuspendedArray` is used to generate less values in
950
+ the context of a recursive schema. Without it, the following schema
951
+ would generate an big amount of values possibly leading to a stack
952
+ overflow:
953
+
954
+ ```ts
955
+ type A = ReadonlyArray<A | null>
956
+
957
+ const schema = S.Array(
958
+ S.NullOr(S.suspend((): S.Schema<A> => schema))
959
+ )
960
+ ```
961
+
962
+ */
963
+ const arr = ctx.depthIdentifier !== undefined
964
+ ? getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item, restArrayConstraints)
965
+ : fc.array(item, restArrayConstraints)
966
+ if (len === 0) {
967
+ return arr
968
+ }
969
+ return arr.map((rest) => [...as, ...rest])
970
+ })
971
+ // ---------------------------------------------
972
+ // handle post rest elements
973
+ // ---------------------------------------------
974
+ for (let j = 0; j < tail.length; j++) {
975
+ output = output.chain((as) => tail[j](fc).map((a) => [...as, a]))
976
+ }
977
+ }
978
+
979
+ return output
708
980
  }
709
- break
710
981
  }
711
- case "BigIntConstraints": {
712
- if (c2._tag === "BigIntConstraints") {
713
- return makeBigIntConstraints({
714
- min: getMax(c1.constraints.min, c2.constraints.min),
715
- max: getMin(c1.constraints.max, c2.constraints.max)
716
- })
982
+ case "TypeLiteral": {
983
+ const propertySignatures: Array<LazyArbitrary<any>> = []
984
+ const requiredKeys: Array<PropertyKey> = []
985
+ for (const ps of description.propertySignatures) {
986
+ if (!ps.isOptional) {
987
+ requiredKeys.push(ps.name)
988
+ }
989
+ propertySignatures.push(go(ps.value, ctx))
990
+ }
991
+ const indexSignatures = description.indexSignatures.map((is) =>
992
+ [go(is.parameter, ctx), go(is.value, ctx)] as const
993
+ )
994
+ return (fc) => {
995
+ const pps: any = {}
996
+ for (let i = 0; i < propertySignatures.length; i++) {
997
+ const ps = description.propertySignatures[i]
998
+ pps[ps.name] = propertySignatures[i](fc)
999
+ }
1000
+ let output = fc.record<any, any>(pps, { requiredKeys })
1001
+ // ---------------------------------------------
1002
+ // handle index signatures
1003
+ // ---------------------------------------------
1004
+ for (let i = 0; i < indexSignatures.length; i++) {
1005
+ const key = indexSignatures[i][0](fc)
1006
+ const value = indexSignatures[i][1](fc)
1007
+ output = output.chain((o) => {
1008
+ const item = fc.tuple(key, value)
1009
+ /*
1010
+
1011
+ `getSuspendedArray` is used to generate less key/value pairs in
1012
+ the context of a recursive schema. Without it, the following schema
1013
+ would generate an big amount of values possibly leading to a stack
1014
+ overflow:
1015
+
1016
+ ```ts
1017
+ type A = { [_: string]: A }
1018
+
1019
+ const schema = S.Record({ key: S.String, value: S.suspend((): S.Schema<A> => schema) })
1020
+ ```
1021
+
1022
+ */
1023
+ const arr = ctx.depthIdentifier !== undefined ?
1024
+ getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item, { maxLength: 2 }) :
1025
+ fc.array(item)
1026
+ return arr.map((tuples) => ({ ...Object.fromEntries(tuples), ...o }))
1027
+ })
1028
+ }
1029
+
1030
+ return output
717
1031
  }
718
- break
719
1032
  }
720
- case "DateConstraints": {
721
- if (c2._tag === "DateConstraints") {
722
- return makeDateConstraints({
723
- min: getMax(c1.constraints.min, c2.constraints.min),
724
- max: getMin(c1.constraints.max, c2.constraints.max),
725
- noInvalidDate: getOr(c1.constraints.noInvalidDate, c2.constraints.noInvalidDate)
726
- })
1033
+ case "Union": {
1034
+ const members = description.members.map((member) => go(member, ctx))
1035
+ return (fc) => fc.oneof(...members.map((arb) => arb(fc)))
1036
+ }
1037
+ case "Suspend": {
1038
+ const memo = arbitraryMemoMap.get(description.ast)
1039
+ if (memo) {
1040
+ return memo
1041
+ }
1042
+ if (ctx.depthIdentifier === undefined) {
1043
+ ctx = { ...ctx, depthIdentifier: description.id }
727
1044
  }
728
- break
1045
+ const get = util_.memoizeThunk(() => {
1046
+ return go(description.description(), ctx)
1047
+ })
1048
+ const out: LazyArbitrary<any> = (fc) => fc.constant(null).chain(() => get()(fc))
1049
+ arbitraryMemoMap.set(description.ast, out)
1050
+ return out
729
1051
  }
730
- case "ArrayConstraints": {
731
- if (c2._tag === "ArrayConstraints") {
732
- return makeArrayConfig({
733
- minLength: getMax(c1.constraints.minLength, c2.constraints.minLength),
734
- maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength)
735
- }, c1.ast)
1052
+ case "Ref": {
1053
+ const memo = arbitraryMemoMap.get(description.ast)
1054
+ if (memo) {
1055
+ return memo
736
1056
  }
737
- break
1057
+ throw new Error(`BUG: Ref ${JSON.stringify(description.id)} not found`)
738
1058
  }
739
1059
  }
740
1060
  }
741
- return c1
1061
+ )
1062
+
1063
+ function subtractElementsLength(
1064
+ constraints: FastCheck.ArrayConstraints,
1065
+ len: number
1066
+ ): FastCheck.ArrayConstraints {
1067
+ if (len === 0 || (constraints.minLength === undefined && constraints.maxLength === undefined)) {
1068
+ return constraints
1069
+ }
1070
+ const out = { ...constraints }
1071
+ if (out.minLength !== undefined) {
1072
+ out.minLength = Math.max(out.minLength - len, 0)
1073
+ }
1074
+ if (out.maxLength !== undefined) {
1075
+ out.maxLength = Math.max(out.maxLength - len, 0)
1076
+ }
1077
+ return out
742
1078
  }
743
1079
 
744
1080
  const getSuspendedArray = (
@@ -761,17 +1097,3 @@ const getSuspendedArray = (
761
1097
  fc.array(item, constraints)
762
1098
  )
763
1099
  }
764
-
765
- const getSuspendedContext = (
766
- ctx: ArbitraryGenerationContext,
767
- ast: AST.Suspend
768
- ): ArbitraryGenerationContext => {
769
- if (ctx.depthIdentifier !== undefined) {
770
- return ctx
771
- }
772
- const depthIdentifier = AST.getIdentifierAnnotation(ast).pipe(
773
- Option.orElse(() => AST.getIdentifierAnnotation(ast.f())),
774
- Option.getOrElse(() => "SuspendDefaultDepthIdentifier")
775
- )
776
- return { ...ctx, depthIdentifier }
777
- }