@typed/guard 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/biome.json +36 -0
- package/dist/ExtensibleFunction.d.ts +1 -0
- package/dist/ExtensibleFunction.js +7 -0
- package/dist/ExtensibleFunction.js.map +1 -0
- package/dist/Guard.d.ts +143 -0
- package/dist/{esm/index.js → Guard.js} +32 -107
- package/dist/Guard.js.map +1 -0
- package/dist/Guardable.d.ts +10 -0
- package/dist/Guardable.js +13 -0
- package/dist/Guardable.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -21
- package/readme.md +197 -0
- package/src/ExtensibleFunction.test.ts +104 -0
- package/src/ExtensibleFunction.ts +8 -0
- package/src/Guard.test.ts +189 -0
- package/src/Guard.ts +536 -0
- package/src/Guardable.test.ts +18 -0
- package/src/Guardable.ts +21 -0
- package/src/index.ts +3 -494
- package/tsconfig.json +27 -0
- package/LICENSE +0 -21
- package/README.md +0 -5
- package/dist/cjs/index.js +0 -199
- package/dist/cjs/index.js.map +0 -1
- package/dist/dts/index.d.ts +0 -254
- package/dist/dts/index.d.ts.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/package.json +0 -4
package/readme.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @typed/guard
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40typed%2Fguard)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A powerful, type-safe runtime validation library built on top of [Effect](https://effect.website/). `@typed/guard` provides a flexible and composable way to validate and transform data with full type inference and runtime safety.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🛡️ **Type-safe Guards**: Runtime validation with complete type inference
|
|
11
|
+
- 🔄 **Effect Integration**: Seamless integration with Effect's ecosystem
|
|
12
|
+
- 🎯 **Composable**: Rich set of combinators for building complex validations
|
|
13
|
+
- 🌳 **Tree-shakeable**: Only import what you need
|
|
14
|
+
- 🔍 **Schema Integration**: First-class support for Effect's Schema
|
|
15
|
+
- ⚡ **Zero Dependencies**: Only requires Effect as a peer dependency
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Using npm
|
|
21
|
+
npm install @typed/guard effect
|
|
22
|
+
|
|
23
|
+
# Using yarn
|
|
24
|
+
yarn add @typed/guard effect
|
|
25
|
+
|
|
26
|
+
# Using pnpm
|
|
27
|
+
pnpm add @typed/guard effect
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import * as Guard from '@typed/guard'
|
|
34
|
+
import { Effect, Option } from 'effect'
|
|
35
|
+
|
|
36
|
+
// Simple number validation
|
|
37
|
+
const isPositive = Guard.liftPredicate((n: number) => n > 0)
|
|
38
|
+
|
|
39
|
+
// Usage
|
|
40
|
+
const result = Effect.runSync(isPositive(5)) // Option.some(5)
|
|
41
|
+
const invalid = Effect.runSync(isPositive(-1)) // Option.none()
|
|
42
|
+
|
|
43
|
+
// Composing guards
|
|
44
|
+
const toString = Guard.map(isPositive, n => n.toString())
|
|
45
|
+
const result2 = Effect.runSync(toString(5)) // Option.some("5")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Core Concepts
|
|
49
|
+
|
|
50
|
+
### Guards
|
|
51
|
+
|
|
52
|
+
A Guard is a function that takes an input and returns an `Effect` containing an `Option` of the output:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
type Guard<I, O = never, E = never, R = never> =
|
|
56
|
+
(input: I) => Effect.Effect<Option.Option<O>, E, R>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `I`: Input type
|
|
60
|
+
- `O`: Output type
|
|
61
|
+
- `E`: Error type
|
|
62
|
+
- `R`: Required context
|
|
63
|
+
|
|
64
|
+
### Key Operations
|
|
65
|
+
|
|
66
|
+
#### Composition
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const numberToString = (n: number) =>
|
|
70
|
+
Effect.succeed(n > 5 ? Option.some(n.toString()) : Option.none())
|
|
71
|
+
|
|
72
|
+
const stringToLength = (s: string) =>
|
|
73
|
+
Effect.succeed(s.length > 1 ? Option.some(s.length) : Option.none())
|
|
74
|
+
|
|
75
|
+
// Compose them together
|
|
76
|
+
const composed: Guard<number, number> = Guard.compose(isPositive, stringLength)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Filtering and Mapping
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Filter values
|
|
83
|
+
const evenNumbers = Guard.filter(isPositive, n => n % 2 === 0)
|
|
84
|
+
|
|
85
|
+
// Map values with effects
|
|
86
|
+
const asyncTransform = Guard.mapEffect(isPositive, n =>
|
|
87
|
+
Effect.promise(() => Promise.resolve(n * 2))
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Error Handling
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const withRecovery = Guard.catchAll(
|
|
95
|
+
failingGuard,
|
|
96
|
+
error => Effect.succeed(`Recovered from: ${error}`)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const withTaggedRecovery = Guard.catchTag(
|
|
100
|
+
failingGuard,
|
|
101
|
+
'ValidationError',
|
|
102
|
+
error => Effect.succeed(`Invalid input: ${error.message}`)
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Schema Integration
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { Schema } from 'effect'
|
|
110
|
+
|
|
111
|
+
const PersonSchema = Schema.struct({
|
|
112
|
+
name: Schema.string,
|
|
113
|
+
age: Schema.number
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const personGuard = Guard.fromSchemaDecode(PersonSchema)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Advanced Usage
|
|
120
|
+
|
|
121
|
+
### Pattern Matching with `any`
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const numberOrString = Guard.any({
|
|
125
|
+
number: Guard.liftPredicate((n): n is number => typeof n === 'number'),
|
|
126
|
+
string: Guard.liftPredicate((s): s is string => typeof s === 'string')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Returns: { _tag: 'number', value: 123 }
|
|
130
|
+
Effect.runSync(numberOrString(123))
|
|
131
|
+
|
|
132
|
+
// Returns: { _tag: 'string', value: 'hello' }
|
|
133
|
+
Effect.runSync(numberOrString('hello'))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Property Binding
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { pipe } from 'effect'
|
|
140
|
+
|
|
141
|
+
const complexGuard = pipe(
|
|
142
|
+
Guard.identity<number>,
|
|
143
|
+
Guard.bindTo('value'),
|
|
144
|
+
Guard.bind('asString', ({ value }) =>
|
|
145
|
+
Effect.succeedSome(value.toString())
|
|
146
|
+
),
|
|
147
|
+
Guard.let('asBigInt', ({ asString }) =>
|
|
148
|
+
BigInt(asString)
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// Returns: { value: 123, asString: '123', asBigInt: 123n }
|
|
153
|
+
Effect.runSync(complexGuard(123))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Creating Reusable Guards with `Guardable`
|
|
157
|
+
|
|
158
|
+
The `Guardable` class allows you to create reusable guards as classes. This is particularly useful when you need to create guards that are also data structures
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { Guardable, GUARDABLE } from '@typed/guard'
|
|
162
|
+
import { Effect } from 'effect'
|
|
163
|
+
|
|
164
|
+
// Create a guard that multiplies numbers by a configurable value
|
|
165
|
+
class MultiplyGuard extends Guardable<number, number> {
|
|
166
|
+
constructor(readonly multiplier: number) {
|
|
167
|
+
super()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Implement the GUARDABLE symbol to define the guard's behavior
|
|
171
|
+
[GUARDABLE] = (input: number) =>
|
|
172
|
+
Effect.succeedSome(input * this.multiplier)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Create an instance with a specific multiplier
|
|
176
|
+
const multiplyByThree = new MultiplyGuard(3)
|
|
177
|
+
|
|
178
|
+
// Use it like a regular guard function
|
|
179
|
+
Effect.runSync(multiplyByThree(4)) // Option.some(12)
|
|
180
|
+
|
|
181
|
+
// It's pipeable too!
|
|
182
|
+
|
|
183
|
+
const guard = multiplyByThree.pipe(
|
|
184
|
+
Guard.map(n => n.toString()),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
Effect.runSync(guard(4)) // Option.some("12")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Contributing
|
|
191
|
+
|
|
192
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT © [Tylor Steinberger](https://github.com/TylorS)
|
|
197
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from '@effect/vitest'
|
|
2
|
+
import { ExtensibleFunction } from './ExtensibleFunction.js'
|
|
3
|
+
|
|
4
|
+
describe('ExtensibleFunction', () => {
|
|
5
|
+
it('should create a function that can be called normally', () => {
|
|
6
|
+
const add = new ExtensibleFunction((x: number) => x + 1)
|
|
7
|
+
expect(add(1)).toBe(2)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should allow extending the function with additional properties', () => {
|
|
11
|
+
class EnhancedFunction extends ExtensibleFunction<(x: number) => number> {
|
|
12
|
+
description: string
|
|
13
|
+
|
|
14
|
+
constructor(fn: (x: number) => number, description: string) {
|
|
15
|
+
super(fn)
|
|
16
|
+
this.description = description
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const add = new EnhancedFunction((x: number) => x + 1, 'adds one')
|
|
21
|
+
expect(add(1)).toBe(2)
|
|
22
|
+
expect(add.description).toBe('adds one')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should maintain function properties like length and name', () => {
|
|
26
|
+
const namedFn = function addOne(x: number) {
|
|
27
|
+
return x + 1
|
|
28
|
+
}
|
|
29
|
+
const add = new ExtensibleFunction(namedFn)
|
|
30
|
+
expect(add.length).toBe(1)
|
|
31
|
+
expect(add.name).toBe('addOne')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should work with async functions', async () => {
|
|
35
|
+
const asyncAdd = new ExtensibleFunction(async (x: number) => {
|
|
36
|
+
return x + 1
|
|
37
|
+
})
|
|
38
|
+
expect(await asyncAdd(1)).toBe(2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should preserve this context in methods', () => {
|
|
42
|
+
class Calculator extends ExtensibleFunction<(x: number) => number> {
|
|
43
|
+
private multiplier: number
|
|
44
|
+
|
|
45
|
+
constructor(multiplier: number) {
|
|
46
|
+
super((x: number) => this.multiply(x))
|
|
47
|
+
this.multiplier = multiplier
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private multiply(x: number): number {
|
|
51
|
+
return x * this.multiplier
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const calc = new Calculator(2)
|
|
56
|
+
expect(calc(3)).toBe(6)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should handle multiple arguments correctly', () => {
|
|
60
|
+
const add = new ExtensibleFunction((a: number, b: number) => a + b)
|
|
61
|
+
expect(add(1, 2)).toBe(3)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should work with generic functions', () => {
|
|
65
|
+
const identity = new ExtensibleFunction(<T>(x: T) => x)
|
|
66
|
+
expect(identity(42)).toBe(42)
|
|
67
|
+
expect(identity('test')).toBe('test')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should throw when constructed without arguments', () => {
|
|
71
|
+
expect(() => new ExtensibleFunction(undefined as any)).toThrow()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should maintain proper error stack traces', () => {
|
|
75
|
+
class Throws<E> extends ExtensibleFunction<(cause: E) => never> {
|
|
76
|
+
constructor() {
|
|
77
|
+
super((cause) => {
|
|
78
|
+
throw cause
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const throws = new Throws<Error>()
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
throws(new Error('test error'))
|
|
87
|
+
} catch (e) {
|
|
88
|
+
expect(e instanceof Error).toBe(true)
|
|
89
|
+
expect(e.stack).toBeDefined()
|
|
90
|
+
expect(e.message).toBe('test error')
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should allow function composition', () => {
|
|
95
|
+
const add1 = new ExtensibleFunction((x: number) => x + 1)
|
|
96
|
+
const multiply2 = new ExtensibleFunction((x: number) => x * 2)
|
|
97
|
+
const composed = new ExtensibleFunction((x: number) => multiply2(add1(x)))
|
|
98
|
+
expect(composed(3)).toBe(8) // (3 + 1) * 2
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should allow class decorator', () => {
|
|
102
|
+
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
function Extensible<I, O>(f: (input: I) => O): (input: I) => O {
|
|
2
|
+
return Object.setPrototypeOf(f, new.target.prototype)
|
|
3
|
+
}
|
|
4
|
+
Extensible.prototype = Function.prototype
|
|
5
|
+
|
|
6
|
+
// Hack to make the type inference work
|
|
7
|
+
export const ExtensibleFunction: new <F extends (...inputs: readonly any[]) => any>(f: F) => F =
|
|
8
|
+
Extensible as any
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Effect, identity, Option, pipe, Predicate, Schema } from 'effect'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import * as Guard from './Guard'
|
|
4
|
+
|
|
5
|
+
describe('Guard', () => {
|
|
6
|
+
// Helper function to run a guard and get the result
|
|
7
|
+
const runGuard = <I, O, E>(guard: Guard.Guard<I, O, E>, input: I) => Effect.runSync(guard(input))
|
|
8
|
+
|
|
9
|
+
describe('basic functionality', () => {
|
|
10
|
+
it('should create a basic guard that succeeds', () => {
|
|
11
|
+
const guard = (input: number): Effect.Effect<Option.Option<string>, never, never> =>
|
|
12
|
+
Effect.succeed(input > 5 ? Option.some(input.toString()) : Option.none())
|
|
13
|
+
|
|
14
|
+
expect(runGuard(guard, 10)).toEqual(Option.some('10'))
|
|
15
|
+
expect(runGuard(guard, 3)).toEqual(Option.none())
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('compose', () => {
|
|
20
|
+
it('should compose two guards', () => {
|
|
21
|
+
const numberToString = (n: number) =>
|
|
22
|
+
Effect.succeed(n > 5 ? Option.some(n.toString()) : Option.none())
|
|
23
|
+
const stringToLength = (s: string) =>
|
|
24
|
+
Effect.succeed(s.length > 1 ? Option.some(s.length) : Option.none())
|
|
25
|
+
|
|
26
|
+
const composed = Guard.compose(numberToString, stringToLength)
|
|
27
|
+
|
|
28
|
+
expect(runGuard(composed, 10)).toEqual(Option.some(2))
|
|
29
|
+
expect(runGuard(composed, 3)).toEqual(Option.none())
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('mapEffect and map', () => {
|
|
34
|
+
it('should map over guard results with Effect', () => {
|
|
35
|
+
const guard = (n: number) => Effect.succeed(n > 5 ? Option.some(n) : Option.none())
|
|
36
|
+
const mapped = Guard.mapEffect(guard, (n) => Effect.succeed(n * 2))
|
|
37
|
+
|
|
38
|
+
expect(runGuard(mapped, 10)).toEqual(Option.some(20))
|
|
39
|
+
expect(runGuard(mapped, 3)).toEqual(Option.none())
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should map over guard results synchronously', () => {
|
|
43
|
+
const guard = (n: number) => Effect.succeed(n > 5 ? Option.some(n) : Option.none())
|
|
44
|
+
const mapped = Guard.map(guard, (n) => n * 2)
|
|
45
|
+
|
|
46
|
+
expect(runGuard(mapped, 10)).toEqual(Option.some(20))
|
|
47
|
+
expect(runGuard(mapped, 3)).toEqual(Option.none())
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('tap', () => {
|
|
52
|
+
it('should perform side effects without modifying the result', () => {
|
|
53
|
+
let sideEffect = 0
|
|
54
|
+
const guard = (n: number) => Effect.succeed(n > 5 ? Option.some(n) : Option.none())
|
|
55
|
+
const tapped = Guard.tap(guard, (n) =>
|
|
56
|
+
Effect.sync(() => {
|
|
57
|
+
sideEffect = n
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(runGuard(tapped, 10)).toEqual(Option.some(10))
|
|
62
|
+
expect(sideEffect).toBe(10)
|
|
63
|
+
|
|
64
|
+
expect(runGuard(tapped, 3)).toEqual(Option.none())
|
|
65
|
+
expect(sideEffect).toBe(10) // Unchanged because guard returned None
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('filterMap and filter', () => {
|
|
70
|
+
it('should filter and map values', () => {
|
|
71
|
+
const filtered = Guard.filterMap(Guard.identity<number>, (n) =>
|
|
72
|
+
n > 5 ? Option.some(n.toString()) : Option.none(),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
expect(runGuard(filtered, 10)).toEqual(Option.some('10'))
|
|
76
|
+
expect(runGuard(filtered, 3)).toEqual(Option.none())
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should filter values', () => {
|
|
80
|
+
const filtered = Guard.filter(Guard.identity<number>, (n) => n > 5)
|
|
81
|
+
|
|
82
|
+
expect(runGuard(filtered, 10)).toEqual(Option.some(10))
|
|
83
|
+
expect(runGuard(filtered, 3)).toEqual(Option.none())
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('any', () => {
|
|
88
|
+
it('should match against multiple guards', () => {
|
|
89
|
+
const anyGuard = Guard.any({
|
|
90
|
+
number: Guard.liftPredicate(Predicate.isNumber),
|
|
91
|
+
string: Guard.liftPredicate(Predicate.isString),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(runGuard(anyGuard, 123)).toEqual(Option.some({ _tag: 'number', value: 123 }))
|
|
95
|
+
expect(runGuard(anyGuard, 'test')).toEqual(Option.some({ _tag: 'string', value: 'test' }))
|
|
96
|
+
expect(runGuard(anyGuard, true)).toEqual(Option.none())
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('liftPredicate', () => {
|
|
101
|
+
it('should lift a predicate into a guard', () => {
|
|
102
|
+
const guard = Guard.liftPredicate(Predicate.isNumber)
|
|
103
|
+
|
|
104
|
+
expect(runGuard(guard, 123)).toEqual(Option.some(123))
|
|
105
|
+
expect(runGuard(guard, 'test')).toEqual(Option.none())
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('error handling', () => {
|
|
110
|
+
describe('catchAll and catchAllCause', () => {
|
|
111
|
+
it('should catch errors', () => {
|
|
112
|
+
const failingGuard = (_: unknown) => Effect.fail('error')
|
|
113
|
+
const recovered = Guard.catchAll(failingGuard, (e) =>
|
|
114
|
+
Effect.succeed(['recovered:', e].join(' ')),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
expect(runGuard(recovered, 123)).toEqual(Option.some('recovered: error'))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should catch error causes', () => {
|
|
121
|
+
const failingGuard = (_: unknown) => Effect.fail('error')
|
|
122
|
+
const recovered = Guard.catchAllCause(failingGuard, () =>
|
|
123
|
+
Effect.succeed('recovered from cause'),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expect(runGuard(recovered, 123)).toEqual(Option.some('recovered from cause'))
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('catchTag', () => {
|
|
131
|
+
it('should catch specific error tags', () => {
|
|
132
|
+
type MyError = { _tag: 'MyError'; message: string }
|
|
133
|
+
const failingGuard = (_: unknown) =>
|
|
134
|
+
Effect.fail({ _tag: 'MyError', message: 'test error' } as MyError)
|
|
135
|
+
const recovered = Guard.catchTag(failingGuard, 'MyError', (e) =>
|
|
136
|
+
Effect.succeed(['recovered:', e.message].join(' ')),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
expect(runGuard(recovered, 123)).toEqual(Option.some('recovered: test error'))
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('schema integration', () => {
|
|
145
|
+
describe('fromSchemaDecode', () => {
|
|
146
|
+
it('should create a guard from a schema decoder', () => {
|
|
147
|
+
const guard = Guard.fromSchemaDecodeUnknown(Schema.Number)
|
|
148
|
+
|
|
149
|
+
expect(runGuard(guard, 123)).toEqual(Option.some(123))
|
|
150
|
+
expect(runGuard(guard, 'test')).toEqual(Option.none())
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('fromSchemaEncode', () => {
|
|
155
|
+
it('should create a guard from a schema encoder', () => {
|
|
156
|
+
const guard = Guard.fromSchemaEncode(Schema.Number)
|
|
157
|
+
|
|
158
|
+
expect(runGuard(guard, 123)).toEqual(Option.some(123))
|
|
159
|
+
expect(runGuard(guard, 'test' as any)).toEqual(Option.none())
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('property attachment', () => {
|
|
165
|
+
describe('attachProperty', () => {
|
|
166
|
+
it('should attach a property to the guarded value', () => {
|
|
167
|
+
const guard = (n: number) => Effect.succeed(Option.some({ value: n }))
|
|
168
|
+
const withProp = Guard.addTag(guard, 'number')
|
|
169
|
+
|
|
170
|
+
expect(runGuard(withProp, 123)).toEqual(Option.some({ _tag: 'number', value: 123 }))
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('bind', () => {
|
|
175
|
+
it('should bind a new property based on the guarded value', () => {
|
|
176
|
+
const guard = pipe(
|
|
177
|
+
Guard.identity<number>,
|
|
178
|
+
Guard.bindTo('value'),
|
|
179
|
+
Guard.bind('asString', ({ value }) => Effect.succeedSome(value.toString())),
|
|
180
|
+
Guard.let('asBigInt', ({ asString }) => BigInt(asString)),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
expect(runGuard(guard, 123)).toEqual(
|
|
184
|
+
Option.some({ value: 123, asString: '123', asBigInt: 123n }),
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
})
|