Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-schema-arbitrary-combiners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"effect": patch
---

Fix schema arbitrary constraint combiners for BigInt, Date, and BigDecimal

Fix `Schema.isBetweenBigDecimal` arbitrary constraint derivation
83 changes: 80 additions & 3 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7199,6 +7199,14 @@ export const isBetweenBigInt = makeIsBetween({
*/
export const isGreaterThanBigDecimal = makeIsGreaterThan({
order: BigDecimal_.Order,
annotate: (exclusiveMinimum) => ({
toArbitraryConstraint: {
bigDecimal: {
min: exclusiveMinimum,
minExcluded: true
}
}
}),
formatter: (bd) => BigDecimal_.format(bd)
})

Expand All @@ -7211,6 +7219,13 @@ export const isGreaterThanBigDecimal = makeIsGreaterThan({
*/
export const isGreaterThanOrEqualToBigDecimal = makeIsGreaterThanOrEqualTo({
order: BigDecimal_.Order,
annotate: (minimum) => ({
toArbitraryConstraint: {
bigDecimal: {
min: minimum
}
}
}),
formatter: (bd) => BigDecimal_.format(bd)
})

Expand All @@ -7222,6 +7237,14 @@ export const isGreaterThanOrEqualToBigDecimal = makeIsGreaterThanOrEqualTo({
*/
export const isLessThanBigDecimal = makeIsLessThan({
order: BigDecimal_.Order,
annotate: (exclusiveMaximum) => ({
toArbitraryConstraint: {
bigDecimal: {
max: exclusiveMaximum,
maxExcluded: true
}
}
}),
formatter: (bd) => BigDecimal_.format(bd)
})

Expand All @@ -7234,6 +7257,13 @@ export const isLessThanBigDecimal = makeIsLessThan({
*/
export const isLessThanOrEqualToBigDecimal = makeIsLessThanOrEqualTo({
order: BigDecimal_.Order,
annotate: (maximum) => ({
toArbitraryConstraint: {
bigDecimal: {
max: maximum
}
}
}),
formatter: (bd) => BigDecimal_.format(bd)
})

Expand All @@ -7250,6 +7280,16 @@ export const isLessThanOrEqualToBigDecimal = makeIsLessThanOrEqualTo({
*/
export const isBetweenBigDecimal = makeIsBetween({
order: BigDecimal_.Order,
annotate: (options) => ({
toArbitraryConstraint: {
bigDecimal: {
min: options.minimum,
max: options.maximum,
...(options.exclusiveMinimum && { minExcluded: true }),
...(options.exclusiveMaximum && { maxExcluded: true })
}
}
}),
formatter: (bd) => BigDecimal_.format(bd)
})

Expand Down Expand Up @@ -10061,9 +10101,31 @@ export const BigDecimal: BigDecimal = declare(
BigDecimalString,
Transformation.bigDecimalFromString
),
toArbitrary: () => (fc) =>
fc.tuple(fc.bigInt(), fc.integer({ min: 0, max: 20 }))
.map(([value, scale]) => BigDecimal_.make(value, scale)),
toArbitrary: () => (fc, ctx) => {
const constraints = ctx.constraints?.bigDecimal

return fc.integer({ min: 0, max: 20 }).map(
(scale) => {
const min = Predicate.isNotUndefined(constraints?.min) ?
BigDecimal_.scale(BigDecimal_.ceil(constraints.min, scale), scale).value :
undefined

const max = Predicate.isNotUndefined(constraints?.max) ?
BigDecimal_.scale(BigDecimal_.floor(constraints.max, scale), scale).value :
undefined

return { min, max, scale }
}
)
// Skip scales where the rounded bounds are too narrow and cause min > max.
.filter(({ min, max }) => min === undefined || max === undefined || min <= max)
.chain(({ min, max, scale }) =>
fc.bigInt({
...(Predicate.isNotUndefined(min) ? { min: min + (constraints?.minExcluded ? 1n : 0n) } : undefined),
...(Predicate.isNotUndefined(max) ? { max: max - (constraints?.maxExcluded ? 1n : 0n) } : undefined)
}).map((value) => BigDecimal_.make(value, scale))
)
},
toFormatter: () => (bd) => BigDecimal_.format(bd),
toEquivalence: () => BigDecimal_.Equivalence
}
Expand Down Expand Up @@ -13545,6 +13607,20 @@ export declare namespace Annotations {
*/
export interface BigIntConstraints extends FastCheck.BigIntConstraints {}

/**
* BigDecimal constraints used when deriving arbitraries for `BigDecimal`
* schemas.
*
* @category models
* @since 4.0.0
*/
export interface BigDecimalConstraints {
readonly min?: BigDecimal_.BigDecimal
readonly max?: BigDecimal_.BigDecimal
readonly minExcluded?: boolean
readonly maxExcluded?: boolean
}

/**
* fast-check array constraints plus an optional comparator used when deriving
* unique-array arbitraries.
Expand Down Expand Up @@ -13575,6 +13651,7 @@ export declare namespace Annotations {
export interface Constraint {
readonly string?: StringConstraints | undefined
readonly number?: NumberConstraints | undefined
readonly bigDecimal?: BigDecimalConstraints | undefined
readonly bigint?: BigIntConstraints | undefined
readonly array?: ArrayConstraints | undefined
readonly date?: DateConstraints | undefined
Expand Down
98 changes: 58 additions & 40 deletions packages/effect/src/internal/schema/arbitrary.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as Array from "../../Array.ts"
import * as BigDecimal from "../../BigDecimal.ts"
import * as BigInt_ from "../../BigInt.ts"
import * as Boolean from "../../Boolean.ts"
import type * as Combiner from "../../Combiner.ts"
import * as Combiner from "../../Combiner.ts"
import { memoize } from "../../Function.ts"
import * as Number from "../../Number.ts"
import * as Option from "../../Option.ts"
import * as Order from "../../Order.ts"
import * as Predicate from "../../Predicate.ts"
import type * as Schema from "../../Schema.ts"
import * as AST from "../../SchemaAST.ts"
Expand Down Expand Up @@ -43,58 +46,73 @@ function array(fc: typeof FastCheck, ctx: Schema.Annotations.ToArbitrary.Context
return out
}

const max = UndefinedOr.makeReducer(Number.ReducerMax)
const min = UndefinedOr.makeReducer(Number.ReducerMin)
const numberMax = UndefinedOr.makeReducer(Number.ReducerMax)
const numberMin = UndefinedOr.makeReducer(Number.ReducerMin)
const or = UndefinedOr.makeReducer(Boolean.ReducerOr)
const concat = UndefinedOr.makeReducer(Array.makeReducerConcat())

const combiner: Combiner.Combiner<any> = Struct.makeCombiner({
isInteger: or,
max: min,
maxExcluded: or,
maxLength: min,
min: max,
minExcluded: or,
minLength: max,
noDefaultInfinity: or,
noInteger: or,
noInvalidDate: or,
noNaN: or,
patterns: concat,
comparator: or
}, {
omitKeyWhen: Predicate.isUndefined
})
type ConstraintKey = keyof Schema.Annotations.ToArbitrary.Constraint
type ConstraintFor<K extends ConstraintKey> = NonNullable<Schema.Annotations.ToArbitrary.Constraint[K]>
type CombinerFields<A> = { readonly [K in keyof A]: Combiner.Combiner<A[K]> }

interface AsCombiner extends Struct.Lambda {
<A>(combiners: CombinerFields<A>): Combiner.Combiner<A>
readonly "~lambda.out": this["~lambda.in"] extends CombinerFields<infer A> ? Combiner.Combiner<A> : never
}

type FastCheckConstraint =
| Schema.Annotations.ToArbitrary.StringConstraints
| Schema.Annotations.ToArbitrary.NumberConstraints
| Schema.Annotations.ToArbitrary.BigIntConstraints
| Schema.Annotations.ToArbitrary.ArrayConstraints
| Schema.Annotations.ToArbitrary.DateConstraints
const constraintCombiners = Struct.map({
string: {
maxLength: numberMin,
minLength: numberMax,
patterns: UndefinedOr.makeReducer(Array.getReadonlyReducerConcat()) as Combiner.Combiner<
Schema.Annotations.ToArbitrary.StringConstraints["patterns"]
>
},
number: {
isInteger: or,
max: numberMin,
maxExcluded: or,
min: numberMax,
minExcluded: or,
noDefaultInfinity: or,
noInteger: or,
noNaN: or
},
bigDecimal: {
max: UndefinedOr.makeReducer(Combiner.min(BigDecimal.Order)),
maxExcluded: or,
min: UndefinedOr.makeReducer(Combiner.max(BigDecimal.Order)),
minExcluded: or
},
bigint: {
max: UndefinedOr.makeReducer(BigInt_.CombinerMin),
min: UndefinedOr.makeReducer(BigInt_.CombinerMax)
},
array: {
comparator: UndefinedOr.makeReducer(Combiner.first()),
maxLength: numberMin,
minLength: numberMax
},
date: {
max: UndefinedOr.makeReducer(Combiner.min(Order.Date)),
min: UndefinedOr.makeReducer(Combiner.max(Order.Date)),
noInvalidDate: or
}
}, Struct.lambda<AsCombiner>((combiners) => Struct.makeCombiner(combiners, { omitKeyWhen: Predicate.isUndefined })))

function merge(
_tag: "string" | "number" | "bigint" | "array" | "date",
function merge<K extends ConstraintKey>(
_tag: K,
constraints: Schema.Annotations.ToArbitrary.Constraint,
constraint: FastCheckConstraint
constraint: ConstraintFor<K>
): Schema.Annotations.ToArbitrary.Constraint {
const c = constraints[_tag]
return {
...constraints,
[_tag]: c ? combiner.combine(c, constraint) : constraint
[_tag]: c ? constraintCombiners[_tag].combine(c, constraint) : constraint
}
}

const constraintsKeys = {
string: null,
number: null,
bigint: null,
array: null,
date: null
}

function isConstraintKey(key: string): key is keyof Schema.Annotations.ToArbitrary.Constraint {
return key in constraintsKeys
return key in constraintCombiners
}

/** @internal */
Expand Down
Loading