diff --git a/.gitignore b/.gitignore index 75c9cc260..68abf8c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules dev coverage declaration/out/src +/.idea diff --git a/Decoder.md b/Decoder.md index c4ef3acb5..f4fbdc031 100644 --- a/Decoder.md +++ b/Decoder.md @@ -377,6 +377,38 @@ Note that you can define an `interface` instead of a type alias export interface Person extends D.TypeOf {} ``` +# 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 diff --git a/package-lock.json b/package-lock.json index e9c61cacb..6b51a9bef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "io-ts", - "version": "2.2.12", + "version": "2.2.13", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/DecodeError.ts b/src/DecodeError.ts index dac974485..a3af694a4 100644 --- a/src/DecodeError.ts +++ b/src/DecodeError.ts @@ -97,6 +97,18 @@ export interface Wrap { */ export type DecodeError = Leaf | Key | Index | Member | Lazy | Wrap +/** + * @category model + * @since 2.2.14 + */ +export type DecodeErrorLeaf = + | Leaf + | Omit, 'errors'> + | Omit, 'errors'> + | Omit, 'errors'> + | Omit, 'errors'> + | Omit, 'errors'> + /** * @category constructors * @since 2.2.7 diff --git a/src/Decoder.ts b/src/Decoder.ts index 1b5102282..81bac75e8 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -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' @@ -513,24 +515,12 @@ export type InputOf = K.InputOf */ export type TypeOf = K.TypeOf -interface Tree { - readonly value: A - readonly forest: ReadonlyArray> -} - -const empty: Array = [] - -const make = (value: A, forest: ReadonlyArray> = empty): Tree => ({ - value, - forest -}) +const drawTree = (tree: T.Tree): string => tree.value + drawForest('\n', tree.forest) -const drawTree = (tree: Tree): string => tree.value + drawForest('\n', tree.forest) - -const drawForest = (indentation: string, forest: ReadonlyArray>): string => { +const drawForest = (indentation: string, forest: ReadonlyArray>): string => { let r: string = '' const len = forest.length - let tree: Tree + let tree: T.Tree for (let i = 0; i < len; i++) { tree = forest[i] const isLast = i === len - 1 @@ -540,23 +530,37 @@ const drawForest = (indentation: string, forest: ReadonlyArray>): s return r } -const toTree: (e: DE.DecodeError) => Tree = 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) => T.Tree = 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> => { +const toForestS = (e: DecodeError): Array> => { + const forestE = toForestE(e) + return pipe(forestE, A.map(T.map((de) => toTreeS(de).value))) +} + +const toTreeE = (e: DE.DecodeError): T.Tree> => { + switch (e._tag) { + case 'Leaf': + return T.make(e) + default: + return T.make(e, toForestE(e.errors)) + } +} + +const toForestE = (e: DecodeError): Array>> => { 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 { @@ -571,10 +575,27 @@ const toForest = (e: DecodeError): ReadonlyArray> => { } } +/** + * @category model + * @since 2.2.14 + */ +export const getErrorForest = (e: DecodeError): Array>> => { + 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>> +} + /** * @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 diff --git a/test/Decoder.ts b/test/Decoder.ts index 55b34e650..ec5fffe3d 100644 --- a/test/Decoder.ts +++ b/test/Decoder.ts @@ -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(() => {