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 =