161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
import { describe, it } from "@effect/vitest"
|
|
import {
|
|
assertFailure as vassertFailure,
|
|
assertFalse,
|
|
assertNone,
|
|
assertSome,
|
|
assertSuccess as vassertSuccess,
|
|
assertTrue,
|
|
strictEqual,
|
|
throws
|
|
} from "@effect/vitest/utils"
|
|
import { Brand, Result, Schema } from "effect"
|
|
|
|
function assertSuccess<T extends Brand.Brand<any>>(ctor: Brand.Constructor<T>, value: Brand.Brand.Unbranded<T>) {
|
|
vassertSuccess(ctor.result(value), value as T)
|
|
}
|
|
|
|
function assertFailure<T extends Brand.Brand<any>>(
|
|
ctor: Brand.Constructor<T>,
|
|
value: Brand.Brand.Unbranded<T>,
|
|
message: string
|
|
) {
|
|
vassertFailure(ctor.result(value).pipe(Result.mapError((e) => e.message)), message)
|
|
}
|
|
|
|
describe("Brand", () => {
|
|
it("formats validation failures as BrandError", () => {
|
|
type Int = number & Brand.Brand<"Int">
|
|
const Int = Brand.check<Int>(Schema.isInt())
|
|
const result = Int.result(1.1)
|
|
assertTrue(Result.isFailure(result))
|
|
strictEqual(String(result.failure), "BrandError(Expected an integer, got 1.1)")
|
|
})
|
|
|
|
it("creates nominal brands without runtime validation", () => {
|
|
type MyNumber = number & Brand.Brand<"MyNumber">
|
|
const MyNumber = Brand.nominal<MyNumber>()
|
|
|
|
strictEqual(MyNumber(1), 1)
|
|
strictEqual(MyNumber(1.1), 1.1)
|
|
strictEqual(MyNumber(-1), -1)
|
|
|
|
assertTrue(MyNumber.is(1))
|
|
assertTrue(MyNumber.is(1.1))
|
|
assertTrue(MyNumber.is(-1))
|
|
|
|
assertSome(MyNumber.option(1), 1 as MyNumber)
|
|
assertSome(MyNumber.option(1.1), 1.1 as MyNumber)
|
|
assertSome(MyNumber.option(-1), -1 as MyNumber)
|
|
|
|
assertSuccess(MyNumber, 1)
|
|
assertSuccess(MyNumber, 1.1)
|
|
assertSuccess(MyNumber, -1)
|
|
})
|
|
|
|
it("validates custom predicates created with make", () => {
|
|
type Int = number & Brand.Brand<"Int">
|
|
const Int = Brand.make<Int>(
|
|
(n) => Number.isInteger(n) || `Expected ${n} to be an integer`
|
|
)
|
|
|
|
strictEqual(Int(1), 1)
|
|
throws(() => Int(1.1))
|
|
strictEqual(Int(-1), -1)
|
|
|
|
assertTrue(Int.is(1))
|
|
assertFalse(Int.is(1.1))
|
|
assertTrue(Int.is(-1))
|
|
|
|
assertSome(Int.option(1), 1 as Int)
|
|
assertNone(Int.option(1.1))
|
|
assertSome(Int.option(-1), -1 as Int)
|
|
|
|
assertSuccess(Int, 1)
|
|
assertFailure(Int, 1.1, "Expected 1.1 to be an integer")
|
|
assertSuccess(Int, -1)
|
|
})
|
|
|
|
describe("check", () => {
|
|
it("validates a single schema check", () => {
|
|
type Int = number & Brand.Brand<"Int">
|
|
const Int = Brand.check<Int>(Schema.isInt())
|
|
|
|
strictEqual(Int(1), 1)
|
|
throws(() => Int(1.1))
|
|
strictEqual(Int(-1), -1)
|
|
|
|
assertTrue(Int.is(1))
|
|
assertFalse(Int.is(1.1))
|
|
assertTrue(Int.is(-1))
|
|
|
|
assertSome(Int.option(1), 1 as Int)
|
|
assertNone(Int.option(1.1))
|
|
assertSome(Int.option(-1), -1 as Int)
|
|
|
|
assertSuccess(Int, 1)
|
|
assertFailure(Int, 1.1, "Expected an integer, got 1.1")
|
|
assertSuccess(Int, -1)
|
|
})
|
|
|
|
it("accumulates failures from multiple schema checks", () => {
|
|
type PositiveInt = number & Brand.Brand<"PositiveInt">
|
|
const PositiveInt = Brand.check<PositiveInt>(Schema.isInt(), Schema.isGreaterThan(0))
|
|
|
|
assertSuccess(PositiveInt, 1)
|
|
assertFailure(PositiveInt, 1.1, "Expected an integer, got 1.1")
|
|
assertFailure(PositiveInt, -1, "Expected a value greater than 0, got -1")
|
|
assertFailure(
|
|
PositiveInt,
|
|
-1.1,
|
|
`Expected an integer, got -1.1
|
|
Expected a value greater than 0, got -1.1`
|
|
)
|
|
})
|
|
|
|
it("stops after an aborted failed schema check", () => {
|
|
type PositiveInt = number & Brand.Brand<"PositiveInt">
|
|
const PositiveInt = Brand.check<PositiveInt>(
|
|
Schema.isInt().abort(), // abort the first check
|
|
Schema.isGreaterThan(0)
|
|
)
|
|
|
|
assertSuccess(PositiveInt, 1)
|
|
assertFailure(PositiveInt, 1.1, "Expected an integer, got 1.1")
|
|
assertFailure(PositiveInt, -1, "Expected a value greater than 0, got -1")
|
|
assertFailure(PositiveInt, -1.1, `Expected an integer, got -1.1`)
|
|
})
|
|
})
|
|
|
|
it("combines existing brand constructors with all", () => {
|
|
type Int = number & Brand.Brand<"Int">
|
|
const Int = Brand.check<Int>(Schema.isInt())
|
|
|
|
type Positive = number & Brand.Brand<"Positive">
|
|
const Positive = Brand.check<Positive>(Schema.isGreaterThan(0))
|
|
|
|
const PositiveInt = Brand.all(Int, Positive)
|
|
|
|
strictEqual(PositiveInt(1), 1)
|
|
throws(() => PositiveInt(1.1))
|
|
throws(() => PositiveInt(-1))
|
|
|
|
assertTrue(PositiveInt.is(1))
|
|
assertFalse(PositiveInt.is(1.1))
|
|
assertFalse(PositiveInt.is(-1))
|
|
|
|
assertSome(PositiveInt.option(1), 1 as any)
|
|
assertNone(PositiveInt.option(1.1))
|
|
assertNone(PositiveInt.option(-1))
|
|
|
|
assertSuccess(PositiveInt, 1)
|
|
assertFailure(PositiveInt, 1.1, "Expected an integer, got 1.1")
|
|
assertFailure(
|
|
PositiveInt,
|
|
-1.1,
|
|
`Expected an integer, got -1.1
|
|
Expected a value greater than 0, got -1.1`
|
|
)
|
|
})
|
|
})
|