/**
* @since 0.67.0
*/
import * as Arr from "effect/Array"
import * as Option from "effect/Option"
import * as Predicate from "effect/Predicate"
import * as AST from "./AST.js"
import * as FastCheck from "./FastCheck.js"
import * as errors_ from "./internal/errors.js"
import * as filters_ from "./internal/filters.js"
import * as util_ from "./internal/util.js"
import type * as Schema from "./Schema.js"
/**
* @category model
* @since 0.67.0
*/
export interface LazyArbitrary {
(fc: typeof FastCheck): FastCheck.Arbitrary
}
/**
* @category hooks
* @since 0.67.0
*/
export const ArbitraryHookId: unique symbol = Symbol.for("@effect/schema/ArbitraryHookId")
/**
* @category hooks
* @since 0.67.0
*/
export type ArbitraryHookId = typeof ArbitraryHookId
/**
* @category hooks
* @since 0.72.3
*/
export interface GenerationContext {
readonly depthIdentifier?: string
readonly maxDepth: number
}
/**
* @category hooks
* @since 0.72.3
*/
export type ArbitraryAnnotation = (
...args: [...ReadonlyArray>, GenerationContext]
) => LazyArbitrary
/**
* @category annotations
* @since 0.67.0
*/
export const arbitrary =
(annotation: ArbitraryAnnotation) => (self: Schema.Schema): Schema.Schema =>
self.annotations({ [ArbitraryHookId]: annotation })
/**
* Returns a LazyArbitrary for the `A` type of the provided schema.
*
* @category arbitrary
* @since 0.67.0
*/
export const makeLazy = (schema: Schema.Schema): LazyArbitrary =>
go(schema.ast, { maxDepth: 2 }, [])
/**
* Returns a fast-check Arbitrary for the `A` type of the provided schema.
*
* @category arbitrary
* @since 0.67.0
*/
export const make = (schema: Schema.Schema): FastCheck.Arbitrary => makeLazy(schema)(FastCheck)
const getHook = AST.getAnnotation>(ArbitraryHookId)
const getRefinementFromArbitrary = (
ast: AST.Refinement,
ctx: Context,
path: ReadonlyArray
) => {
const constraints = combineConstraints(ctx.constraints, getConstraints(ast))
return go(ast.from, constraints ? { ...ctx, constraints } : ctx, path)
}
const getSuspendedContext = (
ctx: Context,
ast: AST.Suspend
): Context => {
if (ctx.depthIdentifier !== undefined) {
return ctx
}
const depthIdentifier = AST.getIdentifierAnnotation(ast).pipe(
Option.orElse(() => AST.getIdentifierAnnotation(ast.f())),
Option.getOrElse(() => "SuspendDefaultDepthIdentifier")
)
return { ...ctx, depthIdentifier }
}
const getSuspendedArray = (
fc: typeof FastCheck,
depthIdentifier: string,
maxDepth: number,
item: FastCheck.Arbitrary,
constraints?: FastCheck.ArrayConstraints
) => {
let minLength = 1
let maxLength = 2
if (constraints && constraints.minLength !== undefined && constraints.minLength > minLength) {
minLength = constraints.minLength
if (minLength > maxLength) {
maxLength = minLength
}
}
return fc.oneof(
{ maxDepth, depthIdentifier },
fc.constant([]),
fc.array(item, { minLength, maxLength })
)
}
interface Context extends GenerationContext {
readonly constraints?: Constraints
}
const go = (
ast: AST.AST,
ctx: Context,
path: ReadonlyArray
): LazyArbitrary => {
const hook = getHook(ast)
if (Option.isSome(hook)) {
switch (ast._tag) {
case "Declaration":
return hook.value(...ast.typeParameters.map((p) => go(p, ctx, path)), ctx)
case "Refinement":
return hook.value(getRefinementFromArbitrary(ast, ctx, path), ctx)
default:
return hook.value(ctx)
}
}
switch (ast._tag) {
case "Declaration": {
throw new Error(errors_.getArbitraryMissingAnnotationErrorMessage(path, ast))
}
case "Literal":
return (fc) => fc.constant(ast.literal)
case "UniqueSymbol":
return (fc) => fc.constant(ast.symbol)
case "UndefinedKeyword":
return (fc) => fc.constant(undefined)
case "NeverKeyword":
return () => {
throw new Error(errors_.getArbitraryUnsupportedErrorMessage(path, ast))
}
case "UnknownKeyword":
case "AnyKeyword":
case "VoidKeyword":
return (fc) => fc.anything()
case "StringKeyword":
return (fc) => {
if (ctx.constraints) {
switch (ctx.constraints._tag) {
case "StringConstraints":
return fc.string(ctx.constraints.constraints)
}
}
return fc.string()
}
case "NumberKeyword":
return (fc) => {
if (ctx.constraints) {
switch (ctx.constraints._tag) {
case "NumberConstraints":
return fc.float(ctx.constraints.constraints)
case "IntegerConstraints":
return fc.integer(ctx.constraints.constraints)
}
}
return fc.float()
}
case "BooleanKeyword":
return (fc) => fc.boolean()
case "BigIntKeyword":
return (fc) => {
if (ctx.constraints) {
switch (ctx.constraints._tag) {
case "BigIntConstraints":
return fc.bigInt(ctx.constraints.constraints)
}
}
return fc.bigInt()
}
case "SymbolKeyword":
return (fc) => fc.string().map((s) => Symbol.for(s))
case "ObjectKeyword":
return (fc) => fc.oneof(fc.object(), fc.array(fc.anything()))
case "TemplateLiteral": {
return (fc) => {
const string = fc.string({ maxLength: 5 })
const number = fc.float({ noDefaultInfinity: true }).filter((n) => !Number.isNaN(n))
const components: Array> = [fc.constant(ast.head)]
for (const span of ast.spans) {
if (AST.isStringKeyword(span.type)) {
components.push(string)
} else {
components.push(number)
}
components.push(fc.constant(span.literal))
}
return fc.tuple(...components).map((spans) => spans.join(""))
}
}
case "TupleType": {
const elements: Array> = []
let hasOptionals = false
let i = 0
for (const element of ast.elements) {
elements.push(go(element.type, ctx, path.concat(i++)))
if (element.isOptional) {
hasOptionals = true
}
}
const rest = ast.rest.map((annotatedAST) => go(annotatedAST.type, ctx, path))
return (fc) => {
// ---------------------------------------------
// handle elements
// ---------------------------------------------
let output = fc.tuple(...elements.map((arb) => arb(fc)))
if (hasOptionals) {
const indexes = fc.tuple(
...ast.elements.map((element) => element.isOptional ? fc.boolean() : fc.constant(true))
)
output = output.chain((tuple) =>
indexes.map((booleans) => {
for (const [i, b] of booleans.reverse().entries()) {
if (!b) {
tuple.splice(booleans.length - i, 1)
}
}
return tuple
})
)
}
// ---------------------------------------------
// handle rest element
// ---------------------------------------------
if (Arr.isNonEmptyReadonlyArray(rest)) {
const [head, ...tail] = rest
const item = head(fc)
const constraints: FastCheck.ArrayConstraints | undefined =
ctx.constraints && ctx.constraints._tag === "ArrayConstraints"
? ctx.constraints.constraints
: undefined
output = output.chain((as) => {
return (ctx.depthIdentifier !== undefined
? getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item, constraints)
: fc.array(item, constraints)).map((rest) => [...as, ...rest])
})
// ---------------------------------------------
// handle post rest elements
// ---------------------------------------------
for (let j = 0; j < tail.length; j++) {
output = output.chain((as) => tail[j](fc).map((a) => [...as, a]))
}
}
return output
}
}
case "TypeLiteral": {
const propertySignaturesTypes = ast.propertySignatures.map((ps) => go(ps.type, ctx, path.concat(ps.name)))
const indexSignatures = ast.indexSignatures.map((is) =>
[go(is.parameter, ctx, path), go(is.type, ctx, path)] as const
)
return (fc) => {
const arbs: any = {}
const requiredKeys: Array = []
// ---------------------------------------------
// handle property signatures
// ---------------------------------------------
for (let i = 0; i < propertySignaturesTypes.length; i++) {
const ps = ast.propertySignatures[i]
const name = ps.name
if (!ps.isOptional) {
requiredKeys.push(name)
}
arbs[name] = propertySignaturesTypes[i](fc)
}
let output = fc.record(arbs, { requiredKeys })
// ---------------------------------------------
// handle index signatures
// ---------------------------------------------
for (let i = 0; i < indexSignatures.length; i++) {
const key = indexSignatures[i][0](fc)
const value = indexSignatures[i][1](fc)
output = output.chain((o) => {
const item = fc.tuple(key, value)
const arr = ctx.depthIdentifier !== undefined ?
getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item) :
fc.array(item)
return arr.map((tuples) => ({ ...Object.fromEntries(tuples), ...o }))
})
}
return output
}
}
case "Union": {
const types = ast.types.map((member) => go(member, ctx, path))
return (fc) => fc.oneof(...types.map((arb) => arb(fc)))
}
case "Enums": {
if (ast.enums.length === 0) {
throw new Error(errors_.getArbitraryEmptyEnumErrorMessage(path))
}
return (fc) => fc.oneof(...ast.enums.map(([_, value]) => fc.constant(value)))
}
case "Refinement": {
const from = getRefinementFromArbitrary(ast, ctx, path)
return (fc) => from(fc).filter((a) => Option.isNone(ast.filter(a, AST.defaultParseOption, ast)))
}
case "Suspend": {
const get = util_.memoizeThunk(() => {
return go(ast.f(), getSuspendedContext(ctx, ast), path)
})
return (fc) => fc.constant(null).chain(() => get()(fc))
}
case "Transformation":
return go(ast.to, ctx, path)
}
}
/** @internal */
export class NumberConstraints {
readonly _tag = "NumberConstraints"
readonly constraints: FastCheck.FloatConstraints
constructor(options: {
readonly min?: number | undefined
readonly max?: number | undefined
readonly noNaN?: boolean | undefined
readonly noDefaultInfinity?: boolean | undefined
}) {
this.constraints = {}
if (Predicate.isNumber(options.min)) {
this.constraints.min = Math.fround(options.min)
}
if (Predicate.isNumber(options.max)) {
this.constraints.max = Math.fround(options.max)
}
if (Predicate.isBoolean(options.noNaN)) {
this.constraints.noNaN = options.noNaN
}
if (Predicate.isBoolean(options.noDefaultInfinity)) {
this.constraints.noDefaultInfinity = options.noDefaultInfinity
}
}
}
/** @internal */
export class StringConstraints {
readonly _tag = "StringConstraints"
readonly constraints: FastCheck.StringSharedConstraints
constructor(options: {
readonly minLength?: number | undefined
readonly maxLength?: number | undefined
}) {
this.constraints = {}
if (Predicate.isNumber(options.minLength)) {
this.constraints.minLength = options.minLength
}
if (Predicate.isNumber(options.maxLength)) {
this.constraints.maxLength = options.maxLength
}
}
}
/** @internal */
export class IntegerConstraints {
readonly _tag = "IntegerConstraints"
readonly constraints: FastCheck.IntegerConstraints
constructor(options: {
readonly min?: number | undefined
readonly max?: number | undefined
}) {
this.constraints = {}
if (Predicate.isNumber(options.min)) {
this.constraints.min = options.min
}
if (Predicate.isNumber(options.max)) {
this.constraints.max = options.max
}
}
}
/** @internal */
export class ArrayConstraints {
readonly _tag = "ArrayConstraints"
readonly constraints: FastCheck.ArrayConstraints
constructor(options: {
readonly minLength?: number | undefined
readonly maxLength?: number | undefined
}) {
this.constraints = {}
if (Predicate.isNumber(options.minLength)) {
this.constraints.minLength = options.minLength
}
if (Predicate.isNumber(options.maxLength)) {
this.constraints.maxLength = options.maxLength
}
}
}
/** @internal */
export class BigIntConstraints {
readonly _tag = "BigIntConstraints"
readonly constraints: FastCheck.BigIntConstraints
constructor(options: {
readonly min?: bigint | undefined
readonly max?: bigint | undefined
}) {
this.constraints = {}
if (Predicate.isBigInt(options.min)) {
this.constraints.min = options.min
}
if (Predicate.isBigInt(options.max)) {
this.constraints.max = options.max
}
}
}
/** @internal */
export type Constraints =
| NumberConstraints
| StringConstraints
| IntegerConstraints
| ArrayConstraints
| BigIntConstraints
/** @internal */
export const getConstraints = (ast: AST.Refinement): Constraints | undefined => {
const TypeAnnotationId = ast.annotations[AST.TypeAnnotationId]
const jsonSchema: any = ast.annotations[AST.JSONSchemaAnnotationId]
switch (TypeAnnotationId) {
// int
case filters_.IntTypeId:
return new IntegerConstraints({})
// number
case filters_.GreaterThanTypeId:
case filters_.GreaterThanOrEqualToTypeId:
case filters_.LessThanTypeId:
case filters_.LessThanOrEqualToTypeId:
case filters_.BetweenTypeId:
return new NumberConstraints({
min: jsonSchema.exclusiveMinimum ?? jsonSchema.minimum,
max: jsonSchema.exclusiveMaximum ?? jsonSchema.maximum
})
// bigint
case filters_.GreaterThanBigintTypeId:
case filters_.GreaterThanOrEqualToBigIntTypeId:
case filters_.LessThanBigIntTypeId:
case filters_.LessThanOrEqualToBigIntTypeId:
case filters_.BetweenBigintTypeId: {
const constraints: any = ast.annotations[TypeAnnotationId]
return new BigIntConstraints(constraints)
}
// string
case filters_.MinLengthTypeId:
case filters_.MaxLengthTypeId:
case filters_.LengthTypeId:
return new StringConstraints(jsonSchema)
// array
case filters_.MinItemsTypeId:
case filters_.MaxItemsTypeId:
case filters_.ItemsCountTypeId:
return new ArrayConstraints({
minLength: jsonSchema.minItems,
maxLength: jsonSchema.maxItems
})
}
}
/** @internal */
export const combineConstraints = (
c1: Constraints | undefined,
c2: Constraints | undefined
): Constraints | undefined => {
if (c1 === undefined) {
return c2
}
if (c2 === undefined) {
return c1
}
switch (c1._tag) {
case "ArrayConstraints": {
switch (c2._tag) {
case "ArrayConstraints":
return new ArrayConstraints({
minLength: getMax(c1.constraints.minLength, c2.constraints.minLength),
maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength)
})
}
break
}
case "NumberConstraints": {
switch (c2._tag) {
case "NumberConstraints":
return new NumberConstraints({
min: getMax(c1.constraints.min, c2.constraints.min),
max: getMin(c1.constraints.max, c2.constraints.max),
noNaN: getOr(c1.constraints.noNaN, c2.constraints.noNaN),
noDefaultInfinity: getOr(c1.constraints.noDefaultInfinity, c2.constraints.noDefaultInfinity)
})
case "IntegerConstraints":
return new IntegerConstraints({
min: getMax(c1.constraints.min, c2.constraints.min),
max: getMin(c1.constraints.max, c2.constraints.max)
})
}
break
}
case "BigIntConstraints": {
switch (c2._tag) {
case "BigIntConstraints":
return new BigIntConstraints({
min: getMax(c1.constraints.min, c2.constraints.min),
max: getMin(c1.constraints.max, c2.constraints.max)
})
}
break
}
case "StringConstraints": {
switch (c2._tag) {
case "StringConstraints":
return new StringConstraints({
minLength: getMax(c1.constraints.minLength, c2.constraints.minLength),
maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength)
})
}
break
}
case "IntegerConstraints": {
switch (c2._tag) {
case "NumberConstraints":
case "IntegerConstraints": {
return new IntegerConstraints({
min: getMax(c1.constraints.min, c2.constraints.min),
max: getMin(c1.constraints.max, c2.constraints.max)
})
}
}
break
}
}
}
const getOr = (a: boolean | undefined, b: boolean | undefined): boolean | undefined => {
return a === undefined ? b : b === undefined ? a : a || b
}
function getMax(n1: bigint | undefined, n2: bigint | undefined): bigint | undefined
function getMax(n1: number | undefined, n2: number | undefined): number | undefined
function getMax(
n1: bigint | number | undefined,
n2: bigint | number | undefined
): bigint | number | undefined {
return n1 === undefined ? n2 : n2 === undefined ? n1 : n1 <= n2 ? n2 : n1
}
function getMin(n1: bigint | undefined, n2: bigint | undefined): bigint | undefined
function getMin(n1: number | undefined, n2: number | undefined): number | undefined
function getMin(
n1: bigint | number | undefined,
n2: bigint | number | undefined
): bigint | number | undefined {
return n1 === undefined ? n2 : n2 === undefined ? n1 : n1 <= n2 ? n1 : n2
}