Skip to content

Commit 678fada

Browse files
author
Elias Mulhall
committed
Add intersection decoder
1 parent f21e684 commit 678fada

File tree

5 files changed

+89
-0
lines changed

5 files changed

+89
-0
lines changed

src/combinators.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export function union(ad: Decoder<any>, bd: Decoder<any>, ...ds: Decoder<any>[])
5555
return Decoder.oneOf(ad, bd, ...ds);
5656
}
5757

58+
export const intersection = Decoder.intersection;
59+
5860
/** See `Decoder.withDefault` */
5961
export const withDefault: <A>(defaultValue: A, decoder: Decoder<A>) => Decoder<A> =
6062
Decoder.withDefault;

src/decoder.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,41 @@ export class Decoder<A> {
439439
return Decoder.oneOf(ad, bd, ...decoders);
440440
}
441441

442+
/**
443+
* Combines 2-8 object decoders into a decoder for the intersection of all the objects.
444+
*
445+
* Example:
446+
* ```
447+
* interface Pet {
448+
* name: string;
449+
* maxLegs: number;
450+
* }
451+
*
452+
* interface Cat extends Pet {
453+
* evil: boolean;
454+
* }
455+
*
456+
* const petDecoder: Decoder<Pet> = object({name: string(), maxLegs: number()});
457+
* const catDecoder: Decoder<Cat> = intersection(petDecoder, object({evil: boolean()}));
458+
* ```
459+
*/
460+
static intersection <A, B>(ad: Decoder<A>, bd: Decoder<B>): Decoder<A & B>; // prettier-ignore
461+
static intersection <A, B, C>(ad: Decoder<A>, bd: Decoder<B>, cd: Decoder<C>): Decoder<A & B & C>; // prettier-ignore
462+
static intersection <A, B, C, D>(ad: Decoder<A>, bd: Decoder<B>, cd: Decoder<C>, dd: Decoder<D>): Decoder<A & B & C & D>; // prettier-ignore
463+
static intersection <A, B, C, D, E>(ad: Decoder<A>, bd: Decoder<B>, cd: Decoder<C>, dd: Decoder<D>, ed: Decoder<E>): Decoder<A & B & C & D & E>; // prettier-ignore
464+
static intersection <A, B, C, D, E, F>(ad: Decoder<A>, bd: Decoder<B>, cd: Decoder<C>, dd: Decoder<D>, ed: Decoder<E>, fd: Decoder<F>): Decoder<A & B & C & D & E & F>; // prettier-ignore
465+
static intersection <A, B, C, D, E, F, G>(ad: Decoder<A>, bd: Decoder<B>, cd: Decoder<C>, dd: Decoder<D>, ed: Decoder<E>, fd: Decoder<F>, gd: Decoder<G>): Decoder<A & B & C & D & E & F & G>; // prettier-ignore
466+
static intersection <A, B, C, D, E, F, G, H>(ad: Decoder<A>, bd: Decoder<B>, cd: Decoder<C>, dd: Decoder<D>, ed: Decoder<E>, fd: Decoder<F>, gd: Decoder<G>, hd: Decoder<H>): Decoder<A & B & C & D & E & F & G & H>; // prettier-ignore
467+
static intersection(ad: Decoder<any>, bd: Decoder<any>, ...ds: Decoder<any>[]): Decoder<any> {
468+
return new Decoder((json: any) =>
469+
[ad, bd, ...ds].reduce(
470+
(acc: Result.Result<any, Partial<DecoderError>>, decoder) =>
471+
Result.map2(Object.assign, acc, decoder.decode(json)),
472+
Result.ok({})
473+
)
474+
);
475+
}
476+
442477
/**
443478
* Decoder that always succeeds with either the decoded value, or a fallback
444479
* default value.

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export {
1515
optional,
1616
oneOf,
1717
union,
18+
intersection,
1819
withDefault,
1920
valueAt,
2021
succeed,

src/result.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ export const successes = <A>(results: Result<A, any>[]): A[] =>
105105
export const map = <A, B, E>(f: (value: A) => B, r: Result<A, E>): Result<B, E> =>
106106
r.ok === true ? ok<B>(f(r.result)) : r;
107107

108+
/**
109+
* Apply `f` to the result of two `Ok`s, or pass an error through. If both
110+
* `Result`s are errors then the first one is returned.
111+
*/
112+
export const map2 = <A, B, C, E>(f: (av: A, bv: B) => C, ar: Result<A, E>, br: Result<B, E>): Result<C, E> =>
113+
ar.ok === false ? ar :
114+
br.ok === false ? br :
115+
ok<C>(f(ar.result, br.result));
116+
108117
/**
109118
* Apply `f` to the error of an `Err`, or pass the success through.
110119
*/

test/json-decode.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
optional,
1414
oneOf,
1515
union,
16+
intersection,
1617
withDefault,
1718
valueAt,
1819
succeed,
@@ -472,6 +473,47 @@ describe('union', () => {
472473
});
473474
});
474475

476+
describe('intersection', () => {
477+
it('uses two decoders to decode an extended interface', () => {
478+
interface A {
479+
a: number;
480+
}
481+
482+
interface AB extends A {
483+
b: string;
484+
}
485+
486+
const aDecoder: Decoder<A> = object({a: number()});
487+
const abDecoder: Decoder<AB> = intersection(aDecoder, object({b: string()}));
488+
489+
expect(abDecoder.run({a: 12, b: '!!!'})).toEqual({ok: true, result: {a: 12, b: '!!!'}});
490+
});
491+
492+
it('can combine many decoders', () => {
493+
interface UVWXYZ {
494+
u: true;
495+
v: string[];
496+
w: boolean | null;
497+
x: number;
498+
y: string;
499+
z: boolean;
500+
}
501+
502+
const uvwxyzDecoder: Decoder<UVWXYZ> = intersection(
503+
object({u: constant(true)}),
504+
object({v: array(string())}),
505+
object({w: union(boolean(), constant(null))}),
506+
object({x: number()}),
507+
object({y: string(), z: boolean()})
508+
);
509+
510+
expect(uvwxyzDecoder.run({u: true, v: [], w: null, x: 4, y: 'y', z: false})).toEqual({
511+
ok: true,
512+
result: {u: true, v: [], w: null, x: 4, y: 'y', z: false}
513+
});
514+
});
515+
});
516+
475517
describe('withDefault', () => {
476518
const decoder = withDefault('puppies', string());
477519

0 commit comments

Comments
 (0)