Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental: add getErrorForest #544

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
dev
coverage
declaration/out/src
/.idea
32 changes: 32 additions & 0 deletions Decoder.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,38 @@ Note that you can define an `interface` instead of a type alias
export interface Person extends D.TypeOf<typeof Person> {}
```

# Customize errors tree

Errors tree can be got by `getErrorForest` to customize error report

```ts
const decoder = _.type({
a: _.string
})
const tree = getErrorForest(decoder.decode({ c: [1] })
/*
E.left([
{
value: {
_tag: 'Key',
key: 'a',
kind: 'required'
},
forest: [
{
value: {
_tag: 'Leaf',
error: 'string',
actual: undefined
},
forest: []
}
]
}
])
*/
```

# Built-in error reporter

```ts
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/DecodeError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ export interface Wrap<E> {
*/
export type DecodeError<E> = Leaf<E> | Key<E> | Index<E> | Member<E> | Lazy<E> | Wrap<E>

/**
* @category model
* @since 2.2.14
*/
export type DecodeErrorLeaf<E> =
| Leaf<E>
| Omit<Key<E>, 'errors'>
| Omit<Index<E>, 'errors'>
| Omit<Member<E>, 'errors'>
| Omit<Lazy<E>, 'errors'>
| Omit<Wrap<E>, 'errors'>

/**
* @category constructors
* @since 2.2.7
Expand Down
71 changes: 46 additions & 25 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { Refinement } from 'fp-ts/lib/function'
import { Functor2 } from 'fp-ts/lib/Functor'
import { MonadThrow2C } from 'fp-ts/lib/MonadThrow'
import { pipe } from 'fp-ts/lib/pipeable'
import * as T from 'fp-ts/lib/Tree'
import * as A from 'fp-ts/lib/Array'
import * as DE from './DecodeError'
import * as FS from './FreeSemigroup'
import * as G from './Guard'
Expand Down Expand Up @@ -513,24 +515,12 @@ export type InputOf<D> = K.InputOf<E.URI, D>
*/
export type TypeOf<D> = K.TypeOf<E.URI, D>

interface Tree<A> {
readonly value: A
readonly forest: ReadonlyArray<Tree<A>>
}

const empty: Array<never> = []

const make = <A>(value: A, forest: ReadonlyArray<Tree<A>> = empty): Tree<A> => ({
value,
forest
})
const drawTree = (tree: T.Tree<string>): string => tree.value + drawForest('\n', tree.forest)

const drawTree = (tree: Tree<string>): string => tree.value + drawForest('\n', tree.forest)

const drawForest = (indentation: string, forest: ReadonlyArray<Tree<string>>): string => {
const drawForest = (indentation: string, forest: ReadonlyArray<T.Tree<string>>): string => {
let r: string = ''
const len = forest.length
let tree: Tree<string>
let tree: T.Tree<string>
for (let i = 0; i < len; i++) {
tree = forest[i]
const isLast = i === len - 1
Expand All @@ -540,23 +530,37 @@ const drawForest = (indentation: string, forest: ReadonlyArray<Tree<string>>): s
return r
}

const toTree: (e: DE.DecodeError<string>) => Tree<string> = DE.fold({
Leaf: (input, error) => make(`cannot decode ${JSON.stringify(input)}, should be ${error}`),
Key: (key, kind, errors) => make(`${kind} property ${JSON.stringify(key)}`, toForest(errors)),
Index: (index, kind, errors) => make(`${kind} index ${index}`, toForest(errors)),
Member: (index, errors) => make(`member ${index}`, toForest(errors)),
Lazy: (id, errors) => make(`lazy type ${id}`, toForest(errors)),
Wrap: (error, errors) => make(error, toForest(errors))
const toTreeS: (e: DE.DecodeError<string>) => T.Tree<string> = DE.fold({
Leaf: (input, error) => T.make(`cannot decode ${JSON.stringify(input)}, should be ${error}`),
Key: (key, kind, errors) => T.make(`${kind} property ${JSON.stringify(key)}`, toForestS(errors)),
Index: (index, kind, errors) => T.make(`${kind} index ${index}`, toForestS(errors)),
Member: (index, errors) => T.make(`member ${index}`, toForestS(errors)),
Lazy: (id, errors) => T.make(`lazy type ${id}`, toForestS(errors)),
Wrap: (error, errors) => T.make(error, toForestS(errors))
})

const toForest = (e: DecodeError): ReadonlyArray<Tree<string>> => {
const toForestS = (e: DecodeError): Array<T.Tree<string>> => {
const forestE = toForestE(e)
return pipe(forestE, A.map(T.map((de) => toTreeS(de).value)))
}

const toTreeE = (e: DE.DecodeError<string>): T.Tree<DE.DecodeError<string>> => {
switch (e._tag) {
case 'Leaf':
return T.make(e)
default:
return T.make(e, toForestE(e.errors))
}
}

const toForestE = (e: DecodeError): Array<T.Tree<DE.DecodeError<string>>> => {
const stack = []
let focus = e
const res = []
while (true) {
switch (focus._tag) {
case 'Of':
res.push(toTree(focus.value))
res.push(toTreeE(focus.value))
if (stack.length === 0) {
return res
} else {
Expand All @@ -571,10 +575,27 @@ const toForest = (e: DecodeError): ReadonlyArray<Tree<string>> => {
}
}

/**
* @category model
* @since 2.2.14
*/
export const getErrorForest = (e: DecodeError): Array<T.Tree<DE.DecodeErrorLeaf<string>>> => {
const fe = toForestE(e)
const omit = (a: any, omitK: string) => {
return Object.keys(a).reduce((acc: any, k) => {
if (k !== omitK) {
acc[k] = a[k]
}
return acc
}, {})
}
return pipe(fe, A.map(T.map((val) => omit(val, 'errors')))) as Array<T.Tree<DE.DecodeErrorLeaf<string>>>
}

/**
* @since 2.2.7
*/
export const draw = (e: DecodeError): string => toForest(e).map(drawTree).join('\n')
export const draw = (e: DecodeError): string => toForestS(e).map(drawTree).join('\n')

/**
* @internal
Expand Down
30 changes: 30 additions & 0 deletions test/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,36 @@ describe('Decoder', () => {
// utils
// -------------------------------------------------------------------------------------

describe('getErrorForest', () => {
it('getErrorForest', function () {
const decoder = _.type({
a: _.string
})
assert.deepStrictEqual(
pipe(decoder.decode({ c: [1] }), E.mapLeft(_.getErrorForest)),
E.left([
{
value: {
_tag: 'Key',
key: 'a',
kind: 'required'
},
forest: [
{
value: {
_tag: 'Leaf',
error: 'string',
actual: undefined
},
forest: []
}
]
}
])
)
})
})

describe('draw', () => {
it('is stack safe', () => {
expect(() => {
Expand Down