| 1 | import { describe, it } from "@effect/vitest" |
| 2 | import { |
| 3 | assertFailure as vassertFailure, |
| 4 | assertFalse, |
| 5 | assertNone, |
| 6 | assertSome, |
| 7 | assertSuccess as vassertSuccess, |
| 8 | assertTrue, |
| 9 | strictEqual, |
| 10 | throws |
| 11 | } from "@effect/vitest/utils" |
| 12 | import { Brand, Result, Schema } from "effect" |
| 13 | |
| 14 | function assertSuccess<T extends Brand.Brand<any>>(ctor: Brand.Constructor<T>, value: Brand.Brand.Unbranded<T>) { |
| 15 | vassertSuccess(ctor.result(value), value as T) |
| 16 | } |
| 17 | |
| 18 | function assertFailure<T extends Brand.Brand<any>>( |
| 19 | ctor: Brand.Constructor<T>, |
| 20 | value: Brand.Brand.Unbranded<T>, |
| 21 | message: string |
| 22 | ) { |
| 23 | vassertFailure(ctor.result(value).pipe(Result.mapError((e) => e.message)), message) |
| 24 | } |
| 25 | |
| 26 | describe("Brand", () => { |
| 27 | it("formats validation failures as BrandError", () => { |
| 28 | type Int = number & Brand.Brand<"Int"> |
| 29 | const Int = Brand.check<Int>(Schema.isInt()) |
| 30 | const result = Int.result(1.1) |
| 31 | assertTrue(Result.isFailure(result)) |
| 32 | strictEqual(String(result.failure), "BrandError(Expected an integer)") |
| 33 | }) |
| 34 | |
| 35 | it("creates nominal brands without runtime validation", () => { |
| 36 | type MyNumber = number & Brand.Brand<"MyNumber"> |
| 37 | const MyNumber = Brand.nominal<MyNumber>() |
| 38 | |
| 39 | strictEqual(MyNumber(1), 1) |
| 40 | strictEqual(MyNumber(1.1), 1.1) |
| 41 | strictEqual(MyNumber(-1), -1) |
| 42 | |
| 43 | assertTrue(MyNumber.is(1)) |
| 44 | assertTrue(MyNumber.is(1.1)) |
| 45 | assertTrue(MyNumber.is(-1)) |
| 46 | |
| 47 | assertSome(MyNumber.option(1), 1 as MyNumber) |
| 48 | assertSome(MyNumber.option(1.1), 1.1 as MyNumber) |
| 49 | assertSome(MyNumber.option(-1), -1 as MyNumber) |
| 50 | |
| 51 | assertSuccess(MyNumber, 1) |
| 52 | assertSuccess(MyNumber, 1.1) |
| 53 | assertSuccess(MyNumber, -1) |
| 54 | }) |
| 55 | |
| 56 | it("validates custom predicates created with make", () => { |
| 57 | type Int = number & Brand.Brand<"Int"> |
| 58 | const Int = Brand.make<Int>( |
| 59 | (n) => Number.isInteger(n) || `Expected ${n} to be an integer` |
| 60 | ) |
| 61 | |
| 62 | strictEqual(Int(1), 1) |
| 63 | throws(() => Int(1.1)) |
| 64 | strictEqual(Int(-1), -1) |
| 65 | |
| 66 | assertTrue(Int.is(1)) |
| 67 | assertFalse(Int.is(1.1)) |
| 68 | assertTrue(Int.is(-1)) |
| 69 | |
| 70 | assertSome(Int.option(1), 1 as Int) |
| 71 | assertNone(Int.option(1.1)) |
| 72 | assertSome(Int.option(-1), -1 as Int) |
| 73 | |
| 74 | assertSuccess(Int, 1) |
| 75 | assertFailure(Int, 1.1, "Expected 1.1 to be an integer") |
| 76 | assertSuccess(Int, -1) |
| 77 | }) |
| 78 | |
| 79 | describe("check", () => { |
| 80 | it("validates a single schema check", () => { |
| 81 | type Int = number & Brand.Brand<"Int"> |
| 82 | const Int = Brand.check<Int>(Schema.isInt()) |
| 83 | |
| 84 | strictEqual(Int(1), 1) |
| 85 | throws(() => Int(1.1)) |
| 86 | strictEqual(Int(-1), -1) |
| 87 | |
| 88 | assertTrue(Int.is(1)) |
| 89 | assertFalse(Int.is(1.1)) |
| 90 | assertTrue(Int.is(-1)) |
| 91 | |
| 92 | assertSome(Int.option(1), 1 as Int) |
| 93 | assertNone(Int.option(1.1)) |
| 94 | assertSome(Int.option(-1), -1 as Int) |
| 95 | |
| 96 | assertSuccess(Int, 1) |
| 97 | assertFailure(Int, 1.1, "Expected an integer") |
| 98 | assertSuccess(Int, -1) |
| 99 | }) |
| 100 | |
| 101 | it("accumulates failures from multiple schema checks", () => { |
| 102 | type PositiveInt = number & Brand.Brand<"PositiveInt"> |
| 103 | const PositiveInt = Brand.check<PositiveInt>(Schema.isInt(), Schema.isGreaterThan(0)) |
| 104 | |
| 105 | assertSuccess(PositiveInt, 1) |
| 106 | assertFailure(PositiveInt, 1.1, "Expected an integer") |
| 107 | assertFailure(PositiveInt, -1, "Expected a value greater than 0") |
| 108 | assertFailure( |
| 109 | PositiveInt, |
| 110 | -1.1, |
| 111 | `Expected an integer |
| 112 | Expected a value greater than 0` |
| 113 | ) |
| 114 | }) |
| 115 | |
| 116 | it("stops after an aborted failed schema check", () => { |
| 117 | type PositiveInt = number & Brand.Brand<"PositiveInt"> |
| 118 | const PositiveInt = Brand.check<PositiveInt>( |
| 119 | Schema.isInt().abort(), // abort the first check |
| 120 | Schema.isGreaterThan(0) |
| 121 | ) |
| 122 | |
| 123 | assertSuccess(PositiveInt, 1) |
| 124 | assertFailure(PositiveInt, 1.1, "Expected an integer") |
| 125 | assertFailure(PositiveInt, -1, "Expected a value greater than 0") |
| 126 | assertFailure(PositiveInt, -1.1, `Expected an integer`) |
| 127 | }) |
| 128 | }) |
| 129 | |
| 130 | it("combines existing brand constructors with all", () => { |
| 131 | type Int = number & Brand.Brand<"Int"> |
| 132 | const Int = Brand.check<Int>(Schema.isInt()) |
| 133 | |
| 134 | type Positive = number & Brand.Brand<"Positive"> |
| 135 | const Positive = Brand.check<Positive>(Schema.isGreaterThan(0)) |
| 136 | |
| 137 | const PositiveInt = Brand.all(Int, Positive) |
| 138 | |
| 139 | strictEqual(PositiveInt(1), 1) |
| 140 | throws(() => PositiveInt(1.1)) |
| 141 | throws(() => PositiveInt(-1)) |
| 142 | |
| 143 | assertTrue(PositiveInt.is(1)) |
| 144 | assertFalse(PositiveInt.is(1.1)) |
| 145 | assertFalse(PositiveInt.is(-1)) |
| 146 | |
| 147 | assertSome(PositiveInt.option(1), 1 as any) |
| 148 | assertNone(PositiveInt.option(1.1)) |
| 149 | assertNone(PositiveInt.option(-1)) |
| 150 | |
| 151 | assertSuccess(PositiveInt, 1) |
| 152 | assertFailure(PositiveInt, 1.1, "Expected an integer") |
| 153 | assertFailure( |
| 154 | PositiveInt, |
| 155 | -1.1, |
| 156 | `Expected an integer |
| 157 | Expected a value greater than 0` |
| 158 | ) |
| 159 | }) |
| 160 | }) |