1import { describe, it } from "@effect/vitest"
2import {
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"
12import { Brand, Result, Schema } from "effect"
13
14function 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
18function 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
26describe("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
112Expected 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
157Expected a value greater than 0`
158 )
159 })
160})