Skip to content

Commit

Permalink
collect all errors while decoding, closes #449
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Apr 20, 2020
1 parent f74d235 commit 052d2b6
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 61 deletions.
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
> - [Experimental]
> - [Deprecation]
**Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a
high state of flux, you're at risk of it changing without notice.
**Note**: Gaps between patch versions are faulty/broken releases.
**Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.

# 2.2.1

- **Experimental**
- collect all errors while decoding, closes #449 (@gcanti)

# 2.2.0

Expand Down
2 changes: 2 additions & 0 deletions Decoder.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,5 +341,7 @@ if (isLeft(result)) {
/*
required property "name"
└─ cannot decode undefined, should be string
required property "age"
└─ cannot decode undefined, should be number
*/
```
14 changes: 0 additions & 14 deletions docs/modules/Decoder.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Added in v2.2.0
- [boolean](#boolean)
- [decoder](#decoder)
- [failure](#failure)
- [failures](#failures)
- [fromGuard](#fromguard)
- [intersection](#intersection)
- [lazy](#lazy)
Expand Down Expand Up @@ -195,19 +194,6 @@ export declare function failure<A = never>(message: string): Either<NonEmptyArra

Added in v2.2.0

# failures

**Signature**

```ts
export declare function failures<A = never>(
message: string,
errors: NonEmptyArray<Tree<string>>
): Either<NonEmptyArray<Tree<string>>, A>
```

Added in v2.2.0

# fromGuard

**Signature**
Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "io-ts",
"version": "2.2.0",
"description": "TypeScript compatible runtime type system for IO validation",
"version": "2.2.1",
"description": "TypeScript runtime type system for IO decoding/encoding",
"files": [
"lib",
"es6"
Expand Down Expand Up @@ -63,16 +63,16 @@
},
"tags": [
"typescript",
"validation",
"inference",
"types",
"runtime"
"runtime",
"decoder",
"encoder",
"schema"
],
"keywords": [
"typescript",
"validation",
"inference",
"types",
"runtime"
"runtime",
"decoder",
"encoder",
"schema"
]
}
108 changes: 108 additions & 0 deletions perf/Decoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as Benchmark from 'benchmark'
import * as t from '../src/Decoder'

/*
space-object (good) x 662,359 ops/sec ±0.65% (88 runs sampled)
space-object (bad) x 379,528 ops/sec ±0.56% (89 runs sampled)
*/

const suite = new Benchmark.Suite()

const Vector = t.tuple(t.number, t.number, t.number)

const Asteroid = t.type({
type: t.literal('asteroid'),
location: Vector,
mass: t.number
})

const Planet = t.type({
type: t.literal('planet'),
location: Vector,
mass: t.number,
population: t.number,
habitable: t.boolean
})

const Rank = t.literal('captain', 'first mate', 'officer', 'ensign')

const CrewMember = t.type({
name: t.string,
age: t.number,
rank: Rank,
home: Planet
})

const Ship = t.type({
type: t.literal('ship'),
location: Vector,
mass: t.number,
name: t.string,
crew: t.array(CrewMember)
})

const T = t.sum('type')({
asteroid: Asteroid,
planet: Planet,
ship: Ship
})

const good = {
type: 'ship',
location: [1, 2, 3],
mass: 4,
name: 'foo',
crew: [
{
name: 'bar',
age: 44,
rank: 'captain',
home: {
type: 'planet',
location: [5, 6, 7],
mass: 8,
population: 1000,
habitable: true
}
}
]
}

const bad = {
type: 'ship',
location: [1, 2, 'a'],
mass: 4,
name: 'foo',
crew: [
{
name: 'bar',
age: 44,
rank: 'captain',
home: {
type: 'planet',
location: [5, 6, 7],
mass: 8,
population: 'a',
habitable: true
}
}
]
}

// console.log(T.decode(good))
// console.log(T.decode(bad))

suite
.add('space-object (good)', function () {
T.decode(good)
})
.add('space-object (bad)', function () {
T.decode(bad)
})
.on('cycle', function (event: any) {
console.log(String(event.target))
})
.on('complete', function (this: any) {
console.log('Fastest is ' + this.filter('fastest').map('name'))
})
.run({ async: true })
14 changes: 7 additions & 7 deletions perf/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as Benchmark from 'benchmark'
import * as t from '../src'

/*
space-object (good) x 435,472 ops/sec ±0.61% (83 runs sampled)
space-object (bad) x 392,528 ops/sec ±2.39% (87 runs sampled)
space-object (good) x 476,424 ops/sec ±0.45% (92 runs sampled)
space-object (bad) x 434,563 ops/sec ±0.58% (87 runs sampled)
*/

const suite = new Benchmark.Suite()
Expand Down Expand Up @@ -71,7 +71,7 @@ const good = {

const bad = {
type: 'ship',
location: [1, 2, 3],
location: [1, 2, 'a'],
mass: 4,
name: 'foo',
crew: [
Expand All @@ -94,16 +94,16 @@ const bad = {
// console.log(T.decode(bad))

suite
.add('space-object (good)', function() {
.add('space-object (good)', function () {
T.decode(good)
})
.add('space-object (bad)', function() {
.add('space-object (bad)', function () {
T.decode(bad)
})
.on('cycle', function(event: any) {
.on('cycle', function (event: any) {
console.log(String(event.target))
})
.on('complete', function(this: any) {
.on('complete', function (this: any) {
console.log('Fastest is ' + this.filter('fastest').map('name'))
})
.run({ async: true })
47 changes: 24 additions & 23 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,8 @@ export function failure<A = never>(message: string): Either<NonEmptyArray<Tree<s
return left([tree(message)])
}

/**
* @since 2.2.0
*/
export function failures<A = never>(
message: string,
errors: NonEmptyArray<Tree<string>>
): Either<NonEmptyArray<Tree<string>>, A> {
return left([tree(message, errors)])
function isNotEmpty<A>(as: Array<A>): as is NonEmptyArray<A> {
return as.length > 0
}

/**
Expand Down Expand Up @@ -197,16 +191,17 @@ export function type<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<
return e
} else {
const r = e.right
let a: Partial<A> = {}
const a: Partial<A> = {}
const errors: Array<Tree<string>> = []
for (const k in properties) {
const e = properties[k].decode(r[k])
if (isLeft(e)) {
return failures(`required property ${JSON.stringify(k)}`, e.left)
errors.push(tree(`required property ${JSON.stringify(k)}`, e.left))
} else {
a[k] = e.right
}
}
return success(a as A)
return isNotEmpty(errors) ? left(errors) : success(a as A)
}
}
}
Expand All @@ -223,7 +218,8 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
return e
} else {
const r = e.right
let a: Partial<A> = {}
const a: Partial<A> = {}
const errors: Array<Tree<string>> = []
for (const k in properties) {
// don't add missing properties
if (k in r) {
Expand All @@ -234,14 +230,14 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
} else {
const e = properties[k].decode(rk)
if (isLeft(e)) {
return failures(`optional property ${JSON.stringify(k)}`, e.left)
errors.push(tree(`optional property ${JSON.stringify(k)}`, e.left))
} else {
a[k] = e.right
}
}
}
}
return success(a)
return isNotEmpty(errors) ? left(errors) : success(a)
}
}
}
Expand All @@ -258,16 +254,17 @@ export function record<A>(codomain: Decoder<A>): Decoder<Record<string, A>> {
return e
} else {
const r = e.right
let a: Record<string, A> = {}
const a: Record<string, A> = {}
const errors: Array<Tree<string>> = []
for (const k in r) {
const e = codomain.decode(r[k])
if (isLeft(e)) {
return failures(`key ${JSON.stringify(k)}`, e.left)
errors.push(tree(`key ${JSON.stringify(k)}`, e.left))
} else {
a[k] = e.right
}
}
return success(a)
return isNotEmpty(errors) ? left(errors) : success(a)
}
}
}
Expand All @@ -286,15 +283,16 @@ export function array<A>(items: Decoder<A>): Decoder<Array<A>> {
const us = e.right
const len = us.length
const a: Array<A> = new Array(len)
const errors: Array<Tree<string>> = []
for (let i = 0; i < len; i++) {
const e = items.decode(us[i])
if (isLeft(e)) {
return failures(`item ${i}`, e.left)
errors.push(tree(`item ${i}`, e.left))
} else {
a[i] = e.right
}
}
return success(a)
return isNotEmpty(errors) ? left(errors) : success(a)
}
}
}
Expand All @@ -312,15 +310,16 @@ export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in k
}
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)) {
return failures(`component ${i}`, e.left)
errors.push(tree(`component ${i}`, e.left))
} else {
a.push(e.right)
}
}
return success(a as any)
return isNotEmpty(errors) ? left(errors) : success(a as any)
}
}
}
Expand Down Expand Up @@ -398,8 +397,10 @@ export function sum<T extends string>(
if (G.string.is(v) && v in members) {
return (members as any)[v].decode(u)
}
return failures(`required property ${JSON.stringify(tag)}`, [
tree(`cannot decode ${JSON.stringify(v)}, should be ${expected}`)
return left([
tree(`required property ${JSON.stringify(tag)}`, [
tree(`cannot decode ${JSON.stringify(v)}, should be ${expected}`)
])
])
}
}
Expand Down
Loading

0 comments on commit 052d2b6

Please sign in to comment.