diff --git a/CHANGELOG.md b/CHANGELOG.md index b9bf62958..e8a37c2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - **Experimental** - `Decoder` - add support for non-`string` tag values to `sum`, closes #481 (@gcanti) + - `intersection` should accumulate all errors (@gcanti) # 2.2.5 diff --git a/src/Decoder.ts b/src/Decoder.ts index ed478d7d0..f92eb4fbf 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -1,7 +1,7 @@ /** * @since 2.2.0 */ -import { Either, isLeft, isRight, left, mapLeft, right } from 'fp-ts/lib/Either' +import * as E from 'fp-ts/lib/Either' import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray' import { pipe } from 'fp-ts/lib/pipeable' import { Forest, Tree } from 'fp-ts/lib/Tree' @@ -14,6 +14,8 @@ import { Alt1 } from 'fp-ts/lib/Alt' // model // ------------------------------------------------------------------------------------- +import Either = E.Either + /** * @category model * @since 2.2.0 @@ -59,7 +61,7 @@ export function tree(value: A, forest: Forest = empty): Tree { * @since 2.2.0 */ export function success(a: A): Either { - return right(a) + return E.right(a) } /** @@ -67,7 +69,7 @@ export function success(a: A): Either { * @since 2.2.0 */ export function failure(message: string): Either { - return left([tree(message)]) + return E.left([tree(message)]) } /** @@ -170,7 +172,7 @@ export function withExpected( decode: (u) => pipe( decoder.decode(u), - mapLeft((nea) => expected(u, nea)) + E.mapLeft((nea) => expected(u, nea)) ) } } @@ -187,7 +189,7 @@ export function refinement( return { decode: (u) => { const e = from.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } const a = e.right @@ -204,11 +206,11 @@ export function parse(from: Decoder, parser: (a: A) => Either { const e = from.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } const pe = parser(e.right) - if (isLeft(pe)) { + if (E.isLeft(pe)) { return failure(pe.left) } return pe @@ -232,7 +234,7 @@ export function type(properties: { [K in keyof A]: Decoder }): Decoder< return { decode: (u) => { const e = UnknownRecord.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } else { const r = e.right @@ -240,13 +242,13 @@ export function type(properties: { [K in keyof A]: Decoder }): Decoder< const errors: Array> = [] for (const k in properties) { const e = properties[k].decode(r[k]) - if (isLeft(e)) { + if (E.isLeft(e)) { errors.push(tree(`required property ${JSON.stringify(k)}`, e.left)) } else { a[k] = e.right } } - return isNotEmpty(errors) ? left(errors) : success(a as A) + return isNotEmpty(errors) ? E.left(errors) : success(a as A) } } } @@ -260,7 +262,7 @@ export function partial(properties: { [K in keyof A]: Decoder }): Decod return { decode: (u) => { const e = UnknownRecord.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } else { const r = e.right @@ -275,7 +277,7 @@ export function partial(properties: { [K in keyof A]: Decoder }): Decod a[k] = undefined } else { const e = properties[k].decode(rk) - if (isLeft(e)) { + if (E.isLeft(e)) { errors.push(tree(`optional property ${JSON.stringify(k)}`, e.left)) } else { a[k] = e.right @@ -283,7 +285,7 @@ export function partial(properties: { [K in keyof A]: Decoder }): Decod } } } - return isNotEmpty(errors) ? left(errors) : success(a) + return isNotEmpty(errors) ? E.left(errors) : success(a) } } } @@ -297,7 +299,7 @@ export function record(codomain: Decoder): Decoder> { return { decode: (u) => { const e = UnknownRecord.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } else { const r = e.right @@ -305,13 +307,13 @@ export function record(codomain: Decoder): Decoder> { const errors: Array> = [] for (const k in r) { const e = codomain.decode(r[k]) - if (isLeft(e)) { + if (E.isLeft(e)) { errors.push(tree(`key ${JSON.stringify(k)}`, e.left)) } else { a[k] = e.right } } - return isNotEmpty(errors) ? left(errors) : success(a) + return isNotEmpty(errors) ? E.left(errors) : success(a) } } } @@ -325,7 +327,7 @@ export function array(items: Decoder): Decoder> { return { decode: (u) => { const e = UnknownArray.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } else { const us = e.right @@ -334,13 +336,13 @@ export function array(items: Decoder): Decoder> { const errors: Array> = [] for (let i = 0; i < len; i++) { const e = items.decode(us[i]) - if (isLeft(e)) { + if (E.isLeft(e)) { errors.push(tree(`item ${i}`, e.left)) } else { a[i] = e.right } } - return isNotEmpty(errors) ? left(errors) : success(a) + return isNotEmpty(errors) ? E.left(errors) : success(a) } } } @@ -354,7 +356,7 @@ export function tuple>(...components: { [K in k return { decode: (u) => { const e = UnknownArray.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } const us = e.right @@ -362,13 +364,13 @@ export function tuple>(...components: { [K in k const errors: Array> = [] for (let i = 0; i < components.length; i++) { const e = components[i].decode(us[i]) - if (isLeft(e)) { + if (E.isLeft(e)) { errors.push(tree(`component ${i}`, e.left)) } else { a.push(e.right) } } - return isNotEmpty(errors) ? left(errors) : success(a as any) + return isNotEmpty(errors) ? E.left(errors) : success(a as any) } } } @@ -399,11 +401,11 @@ export function intersection(left: Decoder, right: Decoder): Decoder return { decode: (u) => { const ea = left.decode(u) - if (isLeft(ea)) { - return ea - } const eb = right.decode(u) - if (isLeft(eb)) { + if (E.isLeft(ea)) { + return E.isLeft(eb) ? E.left(ea.left.concat(eb.left) as DecodeError) : ea + } + if (E.isLeft(eb)) { return eb } return success(intersect(ea.right, eb.right)) @@ -421,7 +423,7 @@ export function lazy(id: string, f: () => Decoder): Decoder { decode: (u) => pipe( get().decode(u), - mapLeft((nea) => [tree(id, nea)]) + E.mapLeft((nea) => [tree(id, nea)]) ) } } @@ -440,14 +442,14 @@ export function sum(tag: T): (members: { [K in keyof A]: De return { decode: (u) => { const e = UnknownRecord.decode(u) - if (isLeft(e)) { + if (E.isLeft(e)) { return e } const v = e.right[tag] as keyof A if (v in members) { return members[v].decode(u) } - return left([ + return E.left([ tree(`required property ${JSON.stringify(tag)}`, [ tree(`cannot decode ${JSON.stringify(v)}, should be ${expected}`) ]) @@ -471,19 +473,19 @@ export function union>( return { decode: (u) => { const e = members[0].decode(u) - if (isRight(e)) { + if (E.isRight(e)) { return e } else { const errors: DecodeError = [tree(`member 0`, e.left)] for (let i = 1; i < len; i++) { const e = members[i].decode(u) - if (isRight(e)) { + if (E.isRight(e)) { return e } else { errors.push(tree(`member ${i}`, e.left)) } } - return left(errors) + return E.left(errors) } } } @@ -502,7 +504,7 @@ export const map: (f: (a: A) => B) => (fa: Decoder) => Decoder = (f) const map_: (fa: Decoder, f: (a: A) => B) => Decoder = (fa, f) => ({ decode: (u) => { const e = fa.decode(u) - return isLeft(e) ? e : right(f(e.right)) + return E.isLeft(e) ? e : E.right(f(e.right)) } }) @@ -515,7 +517,7 @@ export const alt: (that: () => Decoder) => (fa: Decoder) => Decoder const alt_: (fx: Decoder, fy: () => Decoder) => Decoder = (fx, fy) => ({ decode: (u) => { const e = fx.decode(u) - return isLeft(e) ? fy().decode(u) : e + return E.isLeft(e) ? fy().decode(u) : e } }) diff --git a/test/Codec.ts b/test/Codec.ts index c9c30e14e..7b3809e56 100644 --- a/test/Codec.ts +++ b/test/Codec.ts @@ -501,18 +501,6 @@ describe('Codec', () => { const codec = C.intersection(Int, Positive) assert.deepStrictEqual(codec.decode(1), right(1)) }) - - it('should reject an invalid input', () => { - const codec = C.intersection(C.type({ a: C.string }), C.type({ b: C.number })) - assert.deepStrictEqual( - codec.decode({ a: 'a' }), - left([D.tree('required property "b"', [D.tree('cannot decode undefined, should be number')])]) - ) - assert.deepStrictEqual( - codec.decode({ b: 1 }), - left([D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')])]) - ) - }) }) describe('encode', () => { diff --git a/test/Decoder.ts b/test/Decoder.ts index a04636b3b..81f762e74 100644 --- a/test/Decoder.ts +++ b/test/Decoder.ts @@ -70,6 +70,27 @@ describe('Decoder', () => { }) }) + describe('intersection', () => { + it('should accumulate all errors', () => { + const decoder = D.intersection(D.type({ a: D.string }), D.type({ b: D.number })) + assert.deepStrictEqual( + decoder.decode({}), + E.left([ + D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')]), + D.tree('required property "b"', [D.tree('cannot decode undefined, should be number')]) + ]) + ) + assert.deepStrictEqual( + decoder.decode({ b: 1 }), + E.left([D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')])]) + ) + assert.deepStrictEqual( + decoder.decode({ a: 'a' }), + E.left([D.tree('required property "b"', [D.tree('cannot decode undefined, should be number')])]) + ) + }) + }) + describe('intersect', () => { it('should concat strings', () => { assert.deepStrictEqual(D.intersect('a', 'b'), 'b')