Skip to content

Commit

Permalink
intersection should accumulate all errors
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Jun 16, 2020
1 parent 6526525 commit 8ae6b36
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
70 changes: 36 additions & 34 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -14,6 +14,8 @@ import { Alt1 } from 'fp-ts/lib/Alt'
// model
// -------------------------------------------------------------------------------------

import Either = E.Either

/**
* @category model
* @since 2.2.0
Expand Down Expand Up @@ -59,15 +61,15 @@ export function tree<A>(value: A, forest: Forest<A> = empty): Tree<A> {
* @since 2.2.0
*/
export function success<A>(a: A): Either<DecodeError, A> {
return right(a)
return E.right(a)
}

/**
* @category DecodeError
* @since 2.2.0
*/
export function failure<A = never>(message: string): Either<DecodeError, A> {
return left([tree(message)])
return E.left([tree(message)])
}

/**
Expand Down Expand Up @@ -170,7 +172,7 @@ export function withExpected<A>(
decode: (u) =>
pipe(
decoder.decode(u),
mapLeft((nea) => expected(u, nea))
E.mapLeft((nea) => expected(u, nea))
)
}
}
Expand All @@ -187,7 +189,7 @@ export function refinement<A, B extends A>(
return {
decode: (u) => {
const e = from.decode(u)
if (isLeft(e)) {
if (E.isLeft(e)) {
return e
}
const a = e.right
Expand All @@ -204,11 +206,11 @@ export function parse<A, B>(from: Decoder<A>, parser: (a: A) => Either<string, B
return {
decode: (u) => {
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
Expand All @@ -232,21 +234,21 @@ export function type<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<
return {
decode: (u) => {
const e = UnknownRecord.decode(u)
if (isLeft(e)) {
if (E.isLeft(e)) {
return e
} else {
const r = e.right
const a: Partial<A> = {}
const errors: Array<Tree<string>> = []
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)
}
}
}
Expand All @@ -260,7 +262,7 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
return {
decode: (u) => {
const e = UnknownRecord.decode(u)
if (isLeft(e)) {
if (E.isLeft(e)) {
return e
} else {
const r = e.right
Expand All @@ -275,15 +277,15 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): 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
}
}
}
}
return isNotEmpty(errors) ? left(errors) : success(a)
return isNotEmpty(errors) ? E.left(errors) : success(a)
}
}
}
Expand All @@ -297,21 +299,21 @@ export function record<A>(codomain: Decoder<A>): Decoder<Record<string, A>> {
return {
decode: (u) => {
const e = UnknownRecord.decode(u)
if (isLeft(e)) {
if (E.isLeft(e)) {
return e
} else {
const r = e.right
const a: Record<string, A> = {}
const errors: Array<Tree<string>> = []
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)
}
}
}
Expand All @@ -325,7 +327,7 @@ export function array<A>(items: Decoder<A>): Decoder<Array<A>> {
return {
decode: (u) => {
const e = UnknownArray.decode(u)
if (isLeft(e)) {
if (E.isLeft(e)) {
return e
} else {
const us = e.right
Expand All @@ -334,13 +336,13 @@ export function array<A>(items: Decoder<A>): Decoder<Array<A>> {
const errors: Array<Tree<string>> = []
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)
}
}
}
Expand All @@ -354,21 +356,21 @@ export function tuple<A extends ReadonlyArray<unknown>>(...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
const a: Array<unknown> = []
const errors: Array<Tree<string>> = []
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)
}
}
}
Expand Down Expand Up @@ -399,11 +401,11 @@ export function intersection<A, B>(left: Decoder<A>, right: Decoder<B>): 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))
Expand All @@ -421,7 +423,7 @@ export function lazy<A>(id: string, f: () => Decoder<A>): Decoder<A> {
decode: (u) =>
pipe(
get().decode(u),
mapLeft((nea) => [tree(id, nea)])
E.mapLeft((nea) => [tree(id, nea)])
)
}
}
Expand All @@ -440,14 +442,14 @@ export function sum<T extends string>(tag: T): <A>(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}`)
])
Expand All @@ -471,19 +473,19 @@ export function union<A extends ReadonlyArray<unknown>>(
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)
}
}
}
Expand All @@ -502,7 +504,7 @@ export const map: <A, B>(f: (a: A) => B) => (fa: Decoder<A>) => Decoder<B> = (f)
const map_: <A, B>(fa: Decoder<A>, f: (a: A) => B) => Decoder<B> = (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))
}
})

Expand All @@ -515,7 +517,7 @@ export const alt: <A>(that: () => Decoder<A>) => (fa: Decoder<A>) => Decoder<A>
const alt_: <A>(fx: Decoder<A>, fy: () => Decoder<A>) => Decoder<A> = (fx, fy) => ({
decode: (u) => {
const e = fx.decode(u)
return isLeft(e) ? fy().decode(u) : e
return E.isLeft(e) ? fy().decode(u) : e
}
})

Expand Down
12 changes: 0 additions & 12 deletions test/Codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
21 changes: 21 additions & 0 deletions test/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit 8ae6b36

Please sign in to comment.