diff --git a/src/combinators.ts b/src/combinators.ts index 101546f..c4bf00e 100644 --- a/src/combinators.ts +++ b/src/combinators.ts @@ -56,5 +56,8 @@ export const succeed = Decoder.succeed; /** See `Decoder.fail` */ export const fail = Decoder.fail; +/** See `Decoder.result` */ +export const result = Decoder.result; + /** See `Decoder.lazy` */ export const lazy = Decoder.lazy; diff --git a/src/decoder.ts b/src/decoder.ts index e8bd482..7a6f795 100644 --- a/src/decoder.ts +++ b/src/decoder.ts @@ -18,14 +18,12 @@ export interface DecoderError { * with the decoded value of type `A`, on failure returns `Err` containing a * `DecoderError`. */ -type RunResult = Result.Result; +export type DecoderResult = Result.Result; -/** - * Alias for the result of the internal `Decoder.decode` method. Since `decode` - * is a private function it returns a partial decoder error on failure, which - * will be completed and polished when handed off to the `run` method. - */ -type DecodeResult = Result.Result>; +interface Context { + input: unknown; + at: string; +} /** * Defines a mapped type over an interface `A`. `DecoderObject` is an @@ -88,16 +86,21 @@ const typeString = (json: unknown): string => { } }; +const decoderError = ({input, at}: Context, message: string): DecoderResult => + Result.err({ + kind: 'DecoderError' as 'DecoderError', + input, + at, + message + }); + const expectedGot = (expected: string, got: unknown) => `expected ${expected}, got ${typeString(got)}`; const printPath = (paths: (string | number)[]): string => paths.map(path => (typeof path === 'string' ? `.${path}` : `[${path}]`)).join(''); -const prependAt = (newAt: string, {at, ...rest}: Partial): Partial => ({ - at: newAt + (at || ''), - ...rest -}); +const appendAt = ({input, at}: Context, newAt: string): Context => ({input, at: at + newAt}); /** * Decoders transform json objects with unknown structure into known and @@ -122,30 +125,19 @@ const prependAt = (newAt: string, {at, ...rest}: Partial): Partial */ export class Decoder { /** - * The Decoder class constructor is kept private to separate the internal - * `decode` function from the external `run` function. The distinction - * between the two functions is that `decode` returns a - * `Partial` on failure, which contains an unfinished error - * report. When `run` is called on a decoder, the relevant series of `decode` - * calls is made, and then on failure the resulting `Partial` - * is turned into a `DecoderError` by filling in the missing information. - * - * While hiding the constructor may seem restrictive, leveraging the - * provided decoder combinators and helper functions such as - * `andThen` and `map` should be enough to build specialized decoders as - * needed. + * @ignore */ - private constructor(private decode: (json: unknown) => DecodeResult) {} + private constructor(private decode: (json: unknown, context: Context) => DecoderResult) {} /** * Decoder primitive that validates strings, and fails on all other input. */ static string(): Decoder { - return new Decoder( - (json: unknown) => + return new Decoder( + (json, context) => typeof json === 'string' ? Result.ok(json) - : Result.err({message: expectedGot('a string', json)}) + : decoderError(context, expectedGot('a string', json)) ); } @@ -153,11 +145,11 @@ export class Decoder { * Decoder primitive that validates numbers, and fails on all other input. */ static number(): Decoder { - return new Decoder( - (json: unknown) => + return new Decoder( + (json, context) => typeof json === 'number' ? Result.ok(json) - : Result.err({message: expectedGot('a number', json)}) + : decoderError(context, expectedGot('a number', json)) ); } @@ -165,11 +157,11 @@ export class Decoder { * Decoder primitive that validates booleans, and fails on all other input. */ static boolean(): Decoder { - return new Decoder( - (json: unknown) => + return new Decoder( + (json, context) => typeof json === 'boolean' ? Result.ok(json) - : Result.err({message: expectedGot('a boolean', json)}) + : decoderError(context, expectedGot('a boolean', json)) ); } @@ -191,14 +183,13 @@ export class Decoder { * }); * ``` */ - static anyJson = (): Decoder => new Decoder((json: any) => Result.ok(json)); + static anyJson = (): Decoder => new Decoder((json, _) => Result.ok(json)); /** * Decoder identity function which always succeeds and types the result as * `unknown`. */ - static unknownJson = (): Decoder => - new Decoder((json: unknown) => Result.ok(json)); + static unknownJson = (): Decoder => new Decoder((json, _) => Result.ok(json)); /** * Decoder primitive that only matches on exact values. @@ -273,10 +264,10 @@ export class Decoder { static constant(value: A): Decoder; static constant(value: any): Decoder { return new Decoder( - (json: unknown) => + (json, context) => isEqual(json, value) ? Result.ok(value) - : Result.err({message: `expected ${JSON.stringify(value)}, got ${JSON.stringify(json)}`}) + : decoderError(context, `expected ${JSON.stringify(value)}, got ${JSON.stringify(json)}`) ); } @@ -304,21 +295,21 @@ export class Decoder { static object(): Decoder>; static object(decoders: DecoderObject): Decoder; static object(decoders?: DecoderObject) { - return new Decoder((json: unknown) => { + return new Decoder((json, context) => { if (isJsonObject(json) && decoders) { let obj: any = {}; for (const key in decoders) { if (decoders.hasOwnProperty(key)) { - const r = decoders[key].decode(json[key]); + const r = decoders[key].decode(json[key], appendAt(context, `.${key}`)); if (r.ok === true) { // tslint:disable-next-line:strict-type-predicates if (r.result !== undefined) { obj[key] = r.result; } } else if (json[key] === undefined) { - return Result.err({message: `the key '${key}' is required but was not present`}); + return decoderError(context, `the key '${key}' is required but was not present`); } else { - return Result.err(prependAt(`.${key}`, r.error)); + return r; } } } @@ -326,7 +317,7 @@ export class Decoder { } else if (isJsonObject(json)) { return Result.ok(json); } else { - return Result.err({message: expectedGot('an object', json)}); + return decoderError(context, expectedGot('an object', json)); } }); } @@ -347,38 +338,28 @@ export class Decoder { * array(array(boolean())).run([[true], [], [true, false, false]]) * // => {ok: true, result: [[true], [], [true, false, false]]} * - * - * const validNumbersDecoder = array() - * .map((arr: unknown[]) => arr.map(number().run)) - * .map(Result.successes) - * - * validNumbersDecoder.run([1, true, 2, 3, 'five', 4, []]) - * // {ok: true, result: [1, 2, 3, 4]} - * - * validNumbersDecoder.run([false, 'hi', {}]) - * // {ok: true, result: []} - * - * validNumbersDecoder.run(false) - * // {ok: false, error: {..., message: "expected an array, got a boolean"}} + * array().map(a => a.length).run(['a', true, 15, 'z']) + * // => {ok: true, result: 4} * ``` */ static array(): Decoder; static array(decoder: Decoder): Decoder; static array(decoder?: Decoder) { - return new Decoder(json => { + return new Decoder((json, context) => { if (isJsonArray(json) && decoder) { - const decodeValue = (v: unknown, i: number): DecodeResult => - Result.mapError(err => prependAt(`[${i}]`, err), decoder.decode(v)); - return json.reduce( - (acc: DecodeResult, v: unknown, i: number) => - Result.map2((arr, result) => [...arr, result], acc, decodeValue(v, i)), + (acc: DecoderResult, v: unknown, i: number) => + Result.map2( + (arr, result) => [...arr, result], + acc, + decoder.decode(v, appendAt(context, `[${i}]`)) + ), Result.ok([]) ); } else if (isJsonArray(json)) { return Result.ok(json); } else { - return Result.err({message: expectedGot('an array', json)}); + return decoderError(context, expectedGot('an array', json)); } }); } @@ -403,27 +384,26 @@ export class Decoder { static tuple(decoder: [Decoder, Decoder, Decoder, Decoder, Decoder, Decoder, Decoder]): Decoder<[A, B, C, D, E, F, G]>; // prettier-ignore static tuple(decoder: [Decoder, Decoder, Decoder, Decoder, Decoder, Decoder, Decoder, Decoder]): Decoder<[A, B, C, D, E, F, G, H]>; // prettier-ignore static tuple(decoders: Decoder[]) { - return new Decoder((json: unknown) => { + return new Decoder((json, context) => { if (isJsonArray(json)) { if (json.length !== decoders.length) { - return Result.err({ - message: `expected a tuple of length ${decoders.length}, got one of length ${ - json.length - }` - }); + return decoderError( + context, + `expected a tuple of length ${decoders.length}, got one of length ${json.length}` + ); } const result = []; for (let i: number = 0; i < decoders.length; i++) { - const nth = decoders[i].decode(json[i]); - if (nth.ok) { - result[i] = nth.result; + const r = decoders[i].decode(json[i], appendAt(context, `[${i}]`)); + if (r.ok) { + result[i] = r.result; } else { - return Result.err(prependAt(`[${i}]`, nth.error)); + return r; } } return Result.ok(result); } else { - return Result.err({message: expectedGot(`a tuple of length ${decoders.length}`, json)}); + return decoderError(context, expectedGot(`a tuple of length ${decoders.length}`, json)); } }); } @@ -439,22 +419,22 @@ export class Decoder { * ``` */ static dict = (decoder: Decoder): Decoder> => - new Decoder(json => { + new Decoder((json, context) => { if (isJsonObject(json)) { - let obj: Record = {}; + const obj: Record = {}; for (const key in json) { if (json.hasOwnProperty(key)) { - const r = decoder.decode(json[key]); + const r = decoder.decode(json[key], appendAt(context, `.${key}`)); if (r.ok === true) { obj[key] = r.result; } else { - return Result.err(prependAt(`.${key}`, r.error)); + return r; } } } return Result.ok(obj); } else { - return Result.err({message: expectedGot('an object', json)}); + return decoderError(context, expectedGot('an object', json)); } }); @@ -476,8 +456,8 @@ export class Decoder { * ``` */ static optional = (decoder: Decoder): Decoder => - new Decoder( - (json: unknown) => (json === undefined ? Result.ok(undefined) : decoder.decode(json)) + new Decoder( + (json, context) => (json === undefined ? Result.ok(undefined) : decoder.decode(json, context)) ); /** @@ -495,22 +475,21 @@ export class Decoder { * ``` */ static oneOf = (...decoders: Decoder[]): Decoder => - new Decoder((json: unknown) => { - const errors: Partial[] = []; + new Decoder((json, context) => { + const errors: DecoderError[] = []; for (let i: number = 0; i < decoders.length; i++) { - const r = decoders[i].decode(json); + const r = decoders[i].decode(json, context); if (r.ok === true) { return r; } else { errors[i] = r.error; } } - const errorsList = errors - .map(error => `at error${error.at || ''}: ${error.message}`) - .join('", "'); - return Result.err({ - message: `expected a value matching one of the decoders, got the errors ["${errorsList}"]` - }); + const errorsList = errors.map(error => `at ${error.at}: ${error.message}`).join('", "'); + return decoderError( + context, + `expected a value matching one of the decoders, got the errors ["${errorsList}"]` + ); }); /** @@ -566,9 +545,10 @@ export class Decoder { static intersection (ad: Decoder, bd: Decoder, cd: Decoder, dd: Decoder, ed: Decoder, fd: Decoder, gd: Decoder): Decoder; // prettier-ignore static intersection (ad: Decoder, bd: Decoder, cd: Decoder, dd: Decoder, ed: Decoder, fd: Decoder, gd: Decoder, hd: Decoder): Decoder; // prettier-ignore static intersection(ad: Decoder, bd: Decoder, ...ds: Decoder[]): Decoder { - return new Decoder((json: unknown) => + return new Decoder((json, context) => [ad, bd, ...ds].reduce( - (acc: DecodeResult, decoder) => Result.map2(Object.assign, acc, decoder.decode(json)), + (acc: DecoderResult, decoder) => + Result.map2(Object.assign, acc, decoder.decode(json, context)), Result.ok({}) ) ); @@ -579,8 +559,8 @@ export class Decoder { * default value. */ static withDefault = (defaultValue: A, decoder: Decoder): Decoder => - new Decoder((json: unknown) => - Result.ok(Result.withDefault(defaultValue, decoder.decode(json))) + new Decoder((json, context) => + Result.ok(Result.withDefault(defaultValue, decoder.decode(json, context))) ); /** @@ -617,48 +597,65 @@ export class Decoder { * ``` */ static valueAt = (paths: (string | number)[], decoder: Decoder): Decoder => - new Decoder((json: unknown) => { + new Decoder((json, context) => { let jsonAtPath: any = json; for (let i: number = 0; i < paths.length; i++) { + const pathContext = appendAt(context, printPath(paths.slice(0, i + 1))); if (jsonAtPath === undefined) { - return Result.err({ - at: printPath(paths.slice(0, i + 1)), - message: 'path does not exist' - }); + return decoderError(pathContext, 'path does not exist'); } else if (typeof paths[i] === 'string' && !isJsonObject(jsonAtPath)) { - return Result.err({ - at: printPath(paths.slice(0, i + 1)), - message: expectedGot('an object', jsonAtPath) - }); + return decoderError(pathContext, expectedGot('an object', jsonAtPath)); } else if (typeof paths[i] === 'number' && !isJsonArray(jsonAtPath)) { - return Result.err({ - at: printPath(paths.slice(0, i + 1)), - message: expectedGot('an array', jsonAtPath) - }); + return decoderError(pathContext, expectedGot('an array', jsonAtPath)); } else { jsonAtPath = jsonAtPath[paths[i]]; } } - return Result.mapError( - error => - jsonAtPath === undefined - ? {at: printPath(paths), message: 'path does not exist'} - : prependAt(printPath(paths), error), - decoder.decode(jsonAtPath) - ); + const decoderContext = appendAt(context, printPath(paths)); + const r = decoder.decode(jsonAtPath, decoderContext); + if (Result.isErr(r) && jsonAtPath === undefined) { + return decoderError(decoderContext, 'path does not exist'); + } else { + return r; + } }); /** * Decoder that ignores the input json and always succeeds with `fixedValue`. */ - static succeed = (fixedValue: A): Decoder => - new Decoder((json: unknown) => Result.ok(fixedValue)); + static succeed = (fixedValue: A): Decoder => new Decoder((_, __) => Result.ok(fixedValue)); /** * Decoder that ignores the input json and always fails with `errorMessage`. */ static fail = (errorMessage: string): Decoder => - new Decoder((json: unknown) => Result.err({message: errorMessage})); + new Decoder((_, context) => decoderError(context, errorMessage)); + + /** + * Decoder to prevent error propagation. Always succeeds, but instead of + * returning the decoded value, return a `Result` object containing either + * the successfully decoded value, or the decoder failure error. + * + * Example: + * ``` + * array(result(string())).run(['a', 1]) + * // => { + * // ok: true, + * // result: [ + * // {ok: true, result: 'a'}, + * // {ok: false, error: {..., message: 'expected a string, got a number'}} + * // ] + * // } + * + * array(result(number())).map(Result.successes).run([1, true, 2, 3, [], 4]) + * // => {ok: true, result: [1, 2, 3, 4]} + * + * array(result(boolean())).run(false) + * // => {ok: false, error: {..., message: "expected an array, got a boolean"}} + * ``` + */ + static result = (decoder: Decoder): Decoder> => + new Decoder((json, _) => Result.ok(decoder.run(json))); /** * Decoder that allows for validating recursive data structures. Unlike with @@ -681,7 +678,7 @@ export class Decoder { * ``` */ static lazy = (mkDecoder: () => Decoder): Decoder => - new Decoder((json: unknown) => mkDecoder().decode(json)); + new Decoder((json, context) => mkDecoder().decode(json, context)); /** * Run the decoder and return a `Result` with either the decoded value or a @@ -706,16 +703,7 @@ export class Decoder { * // } * ``` */ - run = (json: unknown): RunResult => - Result.mapError( - error => ({ - kind: 'DecoderError' as 'DecoderError', - input: json, - at: 'input' + (error.at || ''), - message: error.message || '' - }), - this.decode(json) - ); + run = (json: unknown): DecoderResult => this.decode(json, {input: json, at: 'input'}); /** * Run the decoder as a `Promise`. @@ -740,7 +728,15 @@ export class Decoder { * ``` */ map = (f: (value: A) => B): Decoder => - new Decoder((json: unknown) => Result.map(f, this.decode(json))); + new Decoder((json, context) => Result.map(f, this.decode(json, context))); + + /** + * Construct a new decoder that applies a transformation to an error message + * on failure. If the decoder fails then `f` will be applied to the error. If + * it succeeds the value will propagated through. + */ + mapError = (f: (error: DecoderError) => DecoderError): Decoder => + new Decoder((json, context) => Result.mapError(f, this.decode(json, context))); /** * Chain together a sequence of decoders. The first decoder will run, and @@ -791,8 +787,8 @@ export class Decoder { * ``` */ andThen = (f: (value: A) => Decoder): Decoder => - new Decoder((json: unknown) => - Result.andThen(value => f(value).decode(json), this.decode(json)) + new Decoder((json, context) => + Result.andThen(value => f(value).decode(json, context), this.decode(json, context)) ); /** diff --git a/src/index.ts b/src/index.ts index d9214d0..524074e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import * as Result from './result'; export {Result}; -export {Decoder, DecoderError, isDecoderError, DecoderObject} from './decoder'; +export {Decoder, DecoderResult, DecoderError, isDecoderError, DecoderObject} from './decoder'; export { string, @@ -22,5 +22,6 @@ export { valueAt, succeed, fail, + result, lazy } from './combinators'; diff --git a/src/result.ts b/src/result.ts index 80520d2..9ae7c64 100644 --- a/src/result.ts +++ b/src/result.ts @@ -99,6 +99,12 @@ export const withException = (r: Result): V => { export const successes = (results: Result[]): A[] => results.reduce((acc: A[], r: Result) => (r.ok === true ? acc.concat(r.result) : acc), []); +/** + * Given an array of `Result`s, return the error values. + */ +export const errors = (results: Result[]): E[] => + results.reduce((acc: E[], r: Result) => (r.ok === false ? acc.concat(r.error) : acc), []); + /** * Apply `f` to the result of an `Ok`, or pass the error through. */ @@ -109,10 +115,11 @@ export const map = (f: (value: A) => B, r: Result): Result * Apply `f` to the result of two `Ok`s, or pass an error through. If both * `Result`s are errors then the first one is returned. */ -export const map2 = (f: (av: A, bv: B) => C, ar: Result, br: Result): Result => - ar.ok === false ? ar : - br.ok === false ? br : - ok(f(ar.result, br.result)); +export const map2 = ( + f: (av: A, bv: B) => C, + ar: Result, + br: Result +): Result => (ar.ok === false ? ar : br.ok === false ? br : ok(f(ar.result, br.result))); /** * Apply `f` to the error of an `Err`, or pass the success through. diff --git a/test/json-decode.test.ts b/test/json-decode.test.ts index 27c6679..d8f0da2 100644 --- a/test/json-decode.test.ts +++ b/test/json-decode.test.ts @@ -1,5 +1,6 @@ import { Decoder, + DecoderResult, Result, isDecoderError, string, @@ -19,6 +20,7 @@ import { valueAt, succeed, tuple, + result, fail, lazy } from '../src/index'; @@ -328,18 +330,18 @@ describe('array', () => { }); it('decodes any array when the array members decoder is not specified', () => { - const validNumbersDecoder = array() - .map((arr: unknown[]) => arr.map(number().run)) - .map(Result.successes); - - expect(validNumbersDecoder.run([1, true, 2, 3, 'five', 4, []])).toEqual({ + expect(array().run([1, true, 2, 3, 'five', 4, []])).toEqual({ ok: true, - result: [1, 2, 3, 4] + result: [1, true, 2, 3, 'five', 4, []] }); - expect(validNumbersDecoder.run([false, 'hi', {}])).toEqual({ok: true, result: []}); + expect( + array() + .map(a => a.length) + .run(['a', true, 15, 'z']) + ).toEqual({ok: true, result: 4}); - expect(validNumbersDecoder.run(false)).toMatchObject({ + expect(array().run(false)).toMatchObject({ ok: false, error: {message: 'expected an array, got a boolean'} }); @@ -536,7 +538,7 @@ describe('oneOf', () => { at: 'input', message: 'expected a value matching one of the decoders, got the errors ' + - '["at error: expected a string, got an array", "at error: expected a number, got an array"]' + '["at input: expected a string, got an array", "at input: expected a number, got an array"]' } }); }); @@ -552,8 +554,8 @@ describe('oneOf', () => { at: 'input[0]', message: 'expected a value matching one of the decoders, got the errors ' + - '["at error[1].a.b: expected a number, got a boolean", ' + - '"at error[1].a.x: path does not exist"]' + '["at input[0][1].a.b: expected a number, got a boolean", ' + + '"at input[0][1].a.x: path does not exist"]' } }); }); @@ -596,7 +598,7 @@ describe('union', () => { at: 'input', message: 'expected a value matching one of the decoders, got the errors ' + - '["at error.kind: expected "a", got "b"", "at error.value: expected a boolean, got a number"]' + '["at input.kind: expected "a", got "b"", "at input.value: expected a boolean, got a number"]' } }); }); @@ -777,6 +779,56 @@ describe('fail', () => { }); }); +describe('result', () => { + describe('can decode properties of an object separately', () => { + type PropResults = {[K in keyof T]: DecoderResult}; + interface Book { + title: string; + author: string; + pageCount?: number; + } + + const decoder: Decoder> = object({ + title: result(string()), + author: result(string()), + pageCount: result(optional(number())) + }); + + it('succeeds when given an object', () => { + const book = {title: 'The Only Harmless Great Thing', author: 'Brooke Bolander'}; + expect(decoder.run(book)).toEqual({ + ok: true, + result: { + author: {ok: true, result: 'Brooke Bolander'}, + pageCount: {ok: true, result: undefined}, + title: {ok: true, result: 'The Only Harmless Great Thing'} + } + }); + }); + }); + + describe('can decode items of an array separately', () => { + it('succeeds even when some array items fail to decode', () => { + const decoder = array(result(string())); + expect(decoder.run(['a', 1])).toMatchObject({ + ok: true, + result: [ + {ok: true, result: 'a'}, + {ok: false, error: {input: 1, at: 'input', message: 'expected a string, got a number'}} + ] + }); + }); + + it('fails when the array decoder fails', () => { + const decoder = array(result(boolean())); + expect(decoder.run(false)).toMatchObject({ + ok: false, + error: {message: 'expected an array, got a boolean'} + }); + }); + }); +}); + describe('lazy', () => { describe('decoding a primitive data type', () => { const decoder = lazy(() => string()); @@ -1007,11 +1059,16 @@ describe('Result', () => { }); }); - it('can return successes from an array of decoded values', () => { + describe('can filter and unwrap arrays of Results', () => { const json: unknown = [1, true, 2, 3, 'five', 4, []]; - const jsonArray: unknown[] = Result.withDefault([], array().run(json)); - const numbers: number[] = Result.successes(jsonArray.map(number().run)); + const numberResults = Result.withDefault([], array(result(number())).run(json)); - expect(numbers).toEqual([1, 2, 3, 4]); + it('returns successes from an array of decoded values', () => { + expect(Result.successes(numberResults)).toEqual([1, 2, 3, 4]); + }); + + it('returns failures from an array of decoded values', () => { + expect(Result.errors(numberResults).map(({input}) => input)).toEqual([true, 'five', []]); + }); }); }); diff --git a/test/logged-failures.test.ts b/test/logged-failures.test.ts new file mode 100644 index 0000000..65bee4e --- /dev/null +++ b/test/logged-failures.test.ts @@ -0,0 +1,46 @@ +import {Decoder, DecoderError, Result, number, array, result} from '../src/index'; + +type Logger = (err: E) => void; + +const logError = (logger: Logger, decoder: Decoder): Decoder => + decoder.mapError(e => { + logger(e); + return e; + }); + +const loggedArrayDecoder = (decoder: Decoder, logger: Logger): Decoder => { + const logged = (d: Decoder) => logError(logger, d); + return logged(array(result(logged(decoder)))).map(Result.successes); +}; + +const makeLogger = () => { + const log: string[] = []; + const logger = ({message}: DecoderError) => log.push(message); + return {log, logger}; +}; + +describe('decode valid array members and log invalid members', () => { + it('succeeds on valid input', () => { + const {log, logger} = makeLogger(); + const r = loggedArrayDecoder(number(), logger).run([1, 2, 3, 4]); + expect(r).toEqual({ok: true, result: [1, 2, 3, 4]}); + expect(log).toEqual([]); + }); + + it('succeeds on valid array members while logging and filtering invalid ones', () => { + const {log, logger} = makeLogger(); + const r = loggedArrayDecoder(number(), logger).run([true, [], 999]); + expect(r).toEqual({ok: true, result: [999]}); + expect(log).toEqual(['expected a number, got a boolean', 'expected a number, got an array']); + }); + + it('failes on non-array json and logs the failure', () => { + const {log, logger} = makeLogger(); + const r = loggedArrayDecoder(number(), logger).run(5); + expect(r).toMatchObject({ + ok: false, + error: {at: 'input', message: 'expected an array, got a number'} + }); + expect(log).toEqual(['expected an array, got a number']); + }); +}); diff --git a/test/phone-example.test.ts b/test/phone-example.test.ts index a179ca9..b8aba72 100644 --- a/test/phone-example.test.ts +++ b/test/phone-example.test.ts @@ -112,8 +112,8 @@ describe('decode phone number objects', () => { at: 'input[1]', message: [ 'expected a value matching one of the decoders, got the errors ', - `["at error: the key 'international' is required but was not present", `, - `"at error: the key 'international' is required but was not present"]` + `["at input[1]: the key 'international' is required but was not present", `, + `"at input[1]: the key 'international' is required but was not present"]` ].join('') } }); diff --git a/test/tagged-json-example.test.ts b/test/tagged-json-example.test.ts index 9d414c7..9677ce9 100644 --- a/test/tagged-json-example.test.ts +++ b/test/tagged-json-example.test.ts @@ -1,14 +1,4 @@ -import { - Decoder, - string, - number, - boolean, - constant, - array, - dict, - union, - lazy -} from '../src/index'; +import {Decoder, string, number, boolean, constant, array, dict, union, lazy} from '../src/index'; describe('create tagged json objects', () => { type TaggedJson =