Skip to content

Commit b111a59

Browse files
author
Elias Mulhall
committed
Add result decoder
Decoder to prevent error propagation. Always succeeds, but instead of returning the decoded value, return a `Result` object containing either the successfully decoded value, or the decoder failure error.
1 parent b3a658a commit b111a59

File tree

4 files changed

+91
-21
lines changed

4 files changed

+91
-21
lines changed

src/combinators.ts

+3
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,8 @@ export const succeed = Decoder.succeed;
5656
/** See `Decoder.fail` */
5757
export const fail = Decoder.fail;
5858

59+
/** See `Decoder.result` */
60+
export const result = Decoder.result;
61+
5962
/** See `Decoder.lazy` */
6063
export const lazy = Decoder.lazy;

src/decoder.ts

+28-13
Original file line numberDiff line numberDiff line change
@@ -338,19 +338,8 @@ export class Decoder<A> {
338338
* array(array(boolean())).run([[true], [], [true, false, false]])
339339
* // => {ok: true, result: [[true], [], [true, false, false]]}
340340
*
341-
*
342-
* const validNumbersDecoder = array()
343-
* .map((arr: unknown[]) => arr.map(number().run))
344-
* .map(Result.successes)
345-
*
346-
* validNumbersDecoder.run([1, true, 2, 3, 'five', 4, []])
347-
* // {ok: true, result: [1, 2, 3, 4]}
348-
*
349-
* validNumbersDecoder.run([false, 'hi', {}])
350-
* // {ok: true, result: []}
351-
*
352-
* validNumbersDecoder.run(false)
353-
* // {ok: false, error: {..., message: "expected an array, got a boolean"}}
341+
* array().map(a => a.length).run(['a', true, 15, 'z'])
342+
* // => {ok: true, result: 4}
354343
* ```
355344
*/
356345
static array(): Decoder<unknown[]>;
@@ -642,6 +631,32 @@ export class Decoder<A> {
642631
static fail = <A>(errorMessage: string): Decoder<A> =>
643632
new Decoder((_, context) => decoderError(context, errorMessage));
644633

634+
/**
635+
* Decoder to prevent error propagation. Always succeeds, but instead of
636+
* returning the decoded value, return a `Result` object containing either
637+
* the successfully decoded value, or the decoder failure error.
638+
*
639+
* Example:
640+
* ```
641+
* array(result(string())).run(['a', 1])
642+
* // => {
643+
* // ok: true,
644+
* // result: [
645+
* // {ok: true, result: 'a'},
646+
* // {ok: false, error: {..., message: 'expected a string, got a number'}}
647+
* // ]
648+
* // }
649+
*
650+
* array(result(number())).map(Result.successes).run([1, true, 2, 3, [], 4])
651+
* // => {ok: true, result: [1, 2, 3, 4]}
652+
*
653+
* array(result(boolean())).run(false)
654+
* // => {ok: false, error: {..., message: "expected an array, got a boolean"}}
655+
* ```
656+
*/
657+
static result = <A>(decoder: Decoder<A>): Decoder<DecoderResult<A>> =>
658+
new Decoder((json: unknown) => Result.ok(decoder.run(json)));
659+
645660
/**
646661
* Decoder that allows for validating recursive data structures. Unlike with
647662
* functions, decoders assigned to variables can't reference themselves

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ export {
2222
valueAt,
2323
succeed,
2424
fail,
25+
result,
2526
lazy
2627
} from './combinators';

test/json-decode.test.ts

+59-8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
valueAt,
2121
succeed,
2222
tuple,
23+
result,
2324
fail,
2425
lazy
2526
} from '../src/index';
@@ -329,18 +330,18 @@ describe('array', () => {
329330
});
330331

331332
it('decodes any array when the array members decoder is not specified', () => {
332-
const validNumbersDecoder = array()
333-
.map((arr: unknown[]) => arr.map(number().run))
334-
.map(Result.successes);
335-
336-
expect(validNumbersDecoder.run([1, true, 2, 3, 'five', 4, []])).toEqual({
333+
expect(array().run([1, true, 2, 3, 'five', 4, []])).toEqual({
337334
ok: true,
338-
result: [1, 2, 3, 4]
335+
result: [1, true, 2, 3, 'five', 4, []]
339336
});
340337

341-
expect(validNumbersDecoder.run([false, 'hi', {}])).toEqual({ok: true, result: []});
338+
expect(
339+
array()
340+
.map(a => a.length)
341+
.run(['a', true, 15, 'z'])
342+
).toEqual({ok: true, result: 4});
342343

343-
expect(validNumbersDecoder.run(false)).toMatchObject({
344+
expect(array().run(false)).toMatchObject({
344345
ok: false,
345346
error: {message: 'expected an array, got a boolean'}
346347
});
@@ -778,6 +779,56 @@ describe('fail', () => {
778779
});
779780
});
780781

782+
describe('result', () => {
783+
describe('can decode properties of an object separately', () => {
784+
type PropResults<T> = {[K in keyof T]: DecoderResult<T[K]>};
785+
interface Book {
786+
title: string;
787+
author: string;
788+
pageCount?: number;
789+
}
790+
791+
const decoder: Decoder<PropResults<Book>> = object({
792+
title: result(string()),
793+
author: result(string()),
794+
pageCount: result(optional(number()))
795+
});
796+
797+
it('succeeds when given an object', () => {
798+
const book = {title: 'The Only Harmless Great Thing', author: 'Brooke Bolander'};
799+
expect(decoder.run(book)).toEqual({
800+
ok: true,
801+
result: {
802+
author: {ok: true, result: 'Brooke Bolander'},
803+
pageCount: {ok: true, result: undefined},
804+
title: {ok: true, result: 'The Only Harmless Great Thing'}
805+
}
806+
});
807+
});
808+
});
809+
810+
describe('can decode items of an array separately', () => {
811+
it('succeeds even when some array items fail to decode', () => {
812+
const decoder = array(result(string()));
813+
expect(decoder.run(['a', 1])).toMatchObject({
814+
ok: true,
815+
result: [
816+
{ok: true, result: 'a'},
817+
{ok: false, error: {message: 'expected a string, got a number'}}
818+
]
819+
});
820+
});
821+
822+
it('fails when the array decoder fails', () => {
823+
const decoder = array(result(boolean()));
824+
expect(decoder.run(false)).toMatchObject({
825+
ok: false,
826+
error: {message: 'expected an array, got a boolean'}
827+
});
828+
});
829+
});
830+
});
831+
781832
describe('lazy', () => {
782833
describe('decoding a primitive data type', () => {
783834
const decoder = lazy(() => string());

0 commit comments

Comments
 (0)