Skip to content

Commit 0dcb2bc

Browse files
feat: ParseResult natural tranformations
1 parent 5eba600 commit 0dcb2bc

4 files changed

Lines changed: 150 additions & 3 deletions

File tree

packages/pure-parse/docs/guide/transformations.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,14 @@ const parseUuid = recover(parseString, () => failure('A UUID must be a string'))
8282
formatResult(parseUuid('123e4567-e89b-12d3-a456-426614174000')) // -> ParseSuccess: 123e4567-e89b-12d3-a456-426614174000
8383
formatResult(parseUuid(123)) // -> ParseFailure: A UUID must be a string at $
8484
```
85+
86+
## Natural Transformations of `ParseResult`
87+
88+
It's also possible to apply transformations on the `ParseResult` data type:
89+
90+
| Function Name | Use case | Type Signature |
91+
| ---------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------- |
92+
| [mapResult](/api/utils/ParseResult.md#mapSuccess) | Transform successful values | `(result: ParseResult<A>, fn: (A) => B) => ParseResult<B>` |
93+
| [mapFailure](/api/utils/ParseResult.md#mapFailure) | Improve failure messages | `(result: ParseResult<A>, fn: (ParseFailure) => ParseFailure) => ParseResult<A>` |
94+
| [flatMapResult](/api/utils/ParseResult.md#flatMapSuccess) | Chain together a sequence of computations that may fail | `(result: ParseResult<A>, fn: (A) => ParseResult<B>) => ParseResult<B>` |
95+
| [flatMapFailure](/api/utils/ParseResult.md#flatMapFailure) | Recover from failures | `(result: ParseResult<A>, fn: (ParseFailure) => ParseResult<A>) => ParseResult<A>` |

packages/pure-parse/src/parsers/ParseResult.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {
88
ParseFailure,
99
PathSegment,
1010
propagateFailure,
11+
mapSuccess,
12+
mapFailure,
13+
flatMapSuccess,
14+
flatMapFailure,
1115
} from './ParseResult'
1216
import { Equals } from '../internals'
1317
import { parseNumber } from './primitives'
@@ -144,4 +148,64 @@ describe('ParseResult', () => {
144148
})
145149
})
146150
})
151+
describe('natural transformations', () => {
152+
describe(mapSuccess, () => {
153+
it('maps success values', () => {
154+
const res = success(2)
155+
const mapped = mapSuccess(res, (n) => n * 3)
156+
expect(mapped).toEqual(success(6))
157+
})
158+
it('does not map failure values', () => {
159+
const res = failure('Error occurred')
160+
const mapped = mapSuccess(res, (n: number) => n * 3)
161+
expect(mapped).toEqual(res)
162+
})
163+
})
164+
describe(flatMapSuccess, () => {
165+
it('allows chaining to another success', () => {
166+
const res = parseNumber(5)
167+
const flatMapped = flatMapSuccess(res, (n) => success(n * 2))
168+
expect(flatMapped).toEqual(success(10))
169+
})
170+
it('allows chaining to failure', () => {
171+
const res = parseNumber(5)
172+
const flatMapped = flatMapSuccess(res, (n) =>
173+
n > 10 ? success(n) : failure('Number is too small'),
174+
)
175+
expect(flatMapped).toEqual(failure('Number is too small'))
176+
})
177+
})
178+
describe(mapFailure, () => {
179+
it('maps failure values', () => {
180+
const res = failure('Original error')
181+
const mapped = mapFailure(res, (err) => ({
182+
...err,
183+
message: 'Mapped error',
184+
}))
185+
expect(mapped).toEqual(failure('Mapped error'))
186+
})
187+
it('does not map success values', () => {
188+
const res = success(42)
189+
const mapped = mapFailure(res, (err) => ({
190+
...err,
191+
message: 'Mapped error',
192+
}))
193+
expect(mapped).toEqual(res)
194+
})
195+
})
196+
describe(flatMapFailure, () => {
197+
it('allows recovery to success', () => {
198+
const res = failure('Initial error')
199+
const flatMapped = flatMapFailure(res, (err) => success(100))
200+
expect(flatMapped).toEqual(success(100))
201+
})
202+
it('allows recovery to another failure', () => {
203+
const res = failure('Initial error')
204+
const flatMapped = flatMapFailure(res, (err) =>
205+
failure(`Still failed: ${err.message}`),
206+
)
207+
expect(flatMapped).toEqual(failure('Still failed: Initial error'))
208+
})
209+
})
210+
})
147211
})

packages/pure-parse/src/parsers/ParseResult.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,75 @@ export const propagateFailure = (
141141
path: [pathSegment, ...failureRes.error.path],
142142
},
143143
})
144+
145+
/**
146+
* Transform a successful result to a new value.
147+
* @example
148+
* Transform a successful parse result to a different type:
149+
* ```ts
150+
* const idResult = parseNumber(123)
151+
* mapParseResult(idResult, (id) => id.toString()) // -> ParseSuccess<'123'>
152+
* ```
153+
* @see `map`
154+
* @param result
155+
* @param fn
156+
*/
157+
export const mapSuccess = <A, B>(
158+
result: ParseResult<A>,
159+
fn: (value: A) => B,
160+
): ParseResult<B> => (isSuccess(result) ? success(fn(result.value)) : result)
161+
162+
/**
163+
* Chain together a sequence of computations that may fail.
164+
* @example
165+
* ```
166+
* const result = parseNumber(5)
167+
* flatMapSuccess(result, (value) => success(value * 2)) // -> ParseSuccess<10>
168+
* ```
169+
* @see `chain`
170+
* @param result
171+
* @param fn
172+
*/
173+
export const flatMapSuccess = <T, U>(
174+
result: ParseResult<T>,
175+
fn: (value: T) => ParseResult<U>,
176+
): ParseResult<U> => (isSuccess(result) ? fn(result.value) : result)
177+
178+
/**
179+
* Transform a failure.
180+
* @example
181+
* An error contains too ambiguous information:
182+
* ```ts
183+
* const idResult = parseNumberFromString('abc')
184+
* mapFailure(idResult, (error) => ({ message: 'An ID must be a number', path: error.path }))
185+
* ```
186+
* Map a failure result to a new value.
187+
* @param result
188+
* @param fn
189+
*/
190+
export const mapFailure = <T>(
191+
result: ParseResult<T>,
192+
fn: (failure: Failure) => Failure,
193+
): ParseResult<T> =>
194+
isFailure(result)
195+
? {
196+
tag: 'failure',
197+
error: fn(result.error),
198+
}
199+
: result
200+
201+
/**
202+
* Recover from a failure by providing an alternative parsing result.
203+
* @example
204+
* ```
205+
* const result = parseNumberFromString('abc')
206+
* flatMapFailure(result, (error) => success(0)) // -> ParseSuccess<0>
207+
* ```
208+
* @see `withDefault`
209+
* @param result
210+
* @param fn
211+
*/
212+
export const flatMapFailure = <T>(
213+
result: ParseResult<T>,
214+
fn: (result: Failure) => ParseResult<T>,
215+
): ParseResult<T> => (isSuccess(result) ? result : fn(result.error))

packages/pure-parse/src/parsers/Parser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ export type UnsuccessfulParser = (data: unknown) => ParseFailure
5252
* parseToUpperCase(123) // -> ParseFailure
5353
* ```
5454
* @param parser
55-
* @param mapSuccess
55+
* @param transformSuccess
5656
*/
5757
export const map =
58-
<A, B>(parser: Parser<A>, mapSuccess: (value: A) => B): Parser<B> =>
58+
<A, B>(parser: Parser<A>, transformSuccess: (value: A) => B): Parser<B> =>
5959
(value) => {
6060
const result = parser(value)
61-
return isSuccess(result) ? success(mapSuccess(result.value)) : result
61+
return isSuccess(result) ? success(transformSuccess(result.value)) : result
6262
}
6363

6464
/**

0 commit comments

Comments
 (0)