diff --git a/src/async-check.ts b/src/async-check.ts new file mode 100644 index 0000000..1be363f --- /dev/null +++ b/src/async-check.ts @@ -0,0 +1,8 @@ +import Result from "./result"; + +export default AsyncCheck; + +type AsyncCheck = ( + value: I, + ...args: A +) => Promise>; diff --git a/src/chain-async.ts b/src/chain-async.ts new file mode 100644 index 0000000..3afc021 --- /dev/null +++ b/src/chain-async.ts @@ -0,0 +1,255 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; + +export default function chainAsync(): AsyncCheck< + V1, + V1, + A +>; +export default function chainAsync( + ...checks: [MaybeAsyncCheck] +): AsyncCheck; +export default function chainAsync( + ...checks: [MaybeAsyncCheck, MaybeAsyncCheck] +): AsyncCheck; +export default function chainAsync( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync< + V1, + V2, + V3, + V4, + V5, + V6, + V7, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync< + V1, + V2, + V3, + V4, + V5, + V6, + V7, + V8, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync< + V1, + V2, + V3, + V4, + V5, + V6, + V7, + V8, + V9, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync< + V1, + V2, + V3, + V4, + V5, + V6, + V7, + V8, + V9, + V10, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync< + V1, + V2, + V3, + V4, + V5, + V6, + V7, + V8, + V9, + V10, + V11, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync< + V1, + V2, + V3, + V4, + V5, + V6, + V7, + V8, + V9, + V10, + V11, + V12, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function chainAsync< + V1, + V2, + V3, + V4, + V5, + V6, + V7, + V8, + V9, + V10, + V11, + V12, + V13, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; + +/** + * Asynchronous version of [[`chain`]]. + * + * ```js + * const check = chainAsync( + * string(), + * trim(), + * testAsync(async value => value === "jerome"), + * ); + * + * await check(" jerome "); + * // => { + * // isOk: true, + * // value: "jerome", + * // } + * + * await check("john"); + * // => { isOk: false, ... } + * ``` + * + * @param checks The async check functions to chain together. + * @returns An async check function. + */ +export default function chainAsync( + ...checks: Array> +): AsyncCheck { + return async (value, ...args) => { + for (const check of checks) { + const result = await check(value, ...args); + + if (result.isOk) { + value = result.value; + } else { + return result; + } + } + + return ok(value); + }; +} diff --git a/src/entries-async.ts b/src/entries-async.ts new file mode 100644 index 0000000..af4bef7 --- /dev/null +++ b/src/entries-async.ts @@ -0,0 +1,73 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; + +/** + * Asynchronous version of [[`entries`]]. + * + * ```js + * const check = entriesAsync( + * pattern(/^[0-9]+$/), + * chainAsync( + * string(), + * transformAsync(async value => value.trim()), + * ), + * ); + * + * await check({ + * 1: " one ", + * 2: " two " + * }); + * // => { + * // isOk: true, + * // value: { + * // 1: "one", + * // 2: "two", + * // }, + * // } + * + * await check({ 1: 1 }); + * // => { isOk: false, ... } + * + * await check({ one: "one" }); + * // => { isOk: false, ... } + * ``` + * + * @param checkKey The async check function to use on the keys. + * @param checkValue The async check function to use on the values. + * @returns An async check function. + */ +export default function entriesAsync( + checkKey: MaybeAsyncCheck, + checkValue: MaybeAsyncCheck, +): AsyncCheck<{ [key: string]: I }, { [key: string]: O }, A> { + return async (input, ...args) => { + const keys = Object.keys(input); + const [keyResults, valueResults] = await Promise.all([ + Promise.all(keys.map((key) => checkKey(key, ...args))), + Promise.all(keys.map((key) => checkValue(input[key], ...args))), + ]); + const output: { [key: string]: O } = {}; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const keyResult = keyResults[i]; + const valueResult = valueResults[i]; + + if (keyResult.isOk) { + if (valueResult.isOk) { + output[keyResult.value] = valueResult.value; + } else { + return { + ...valueResult, + path: [key, ...valueResult.path], + }; + } + } else { + return keyResult; + } + } + + return ok(output); + }; +} diff --git a/src/entries.ts b/src/entries.ts index d812f40..9a38cca 100644 --- a/src/entries.ts +++ b/src/entries.ts @@ -25,6 +25,10 @@ import ok from "./ok"; * check({ one: "one" }); * // => { isOk: false, ... } * ``` + * + * @param checkKey The check function to use on the keys. + * @param checkValue The check function to use on the values. + * @returns A check function. */ export default function entries( checkKey: Check, diff --git a/src/index.ts b/src/index.ts index 69c92cc..f3a30dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,16 +2,20 @@ export { default as Result } from "./result"; export { default as ok, OkResult } from "./ok"; export { default as error, ErrorResult } from "./error"; export { default as Check } from "./check"; +export { default as AsyncCheck } from "./async-check"; +export { default as MaybeAsyncCheck } from "./maybe-async-check"; export { default as pass } from "./pass"; export { default as fail } from "./fail"; export { default as test } from "./test"; +export { default as testAsync } from "./test-async"; export { default as transform } from "./transform"; +export { default as transformAsync } from "./transform-async"; export { default as is } from "./is"; export { default as null } from "./null"; export { default as undefined } from "./undefined"; -export { default as NaN } from "./NaN"; +export { default as NaN } from "./not-a-number"; export { default as boolean } from "./boolean"; export { default as number } from "./number"; export { default as integer } from "./integer"; @@ -21,10 +25,10 @@ export { default as date } from "./date"; export { default as object } from "./object"; export { default as array } from "./array"; -export { default as toBoolean } from "./toBoolean"; -export { default as toNumber } from "./toNumber"; -export { default as toString } from "./toString"; -export { default as toDate } from "./toDate"; +export { default as toBoolean } from "./to-boolean"; +export { default as toNumber } from "./to-number"; +export { default as toString } from "./to-string"; +export { default as toDate } from "./to-date"; export { default as floor } from "./floor"; export { default as ceil } from "./ceil"; @@ -32,25 +36,34 @@ export { default as round } from "./round"; export { default as truncate } from "./truncate"; export { default as min } from "./min"; export { default as max } from "./max"; -export { default as lessThan } from "./lessThan"; -export { default as moreThan } from "./moreThan"; +export { default as lessThan } from "./less-than"; +export { default as moreThan } from "./more-than"; export { default as trim } from "./trim"; -export { default as toLower } from "./toLower"; -export { default as toUpper } from "./toUpper"; +export { default as toLower } from "./to-lower"; +export { default as toUpper } from "./to-upper"; export { default as pattern } from "./pattern"; -export { default as minLength } from "./minLength"; -export { default as maxLength } from "./maxLength"; +export { default as minLength } from "./min-length"; +export { default as maxLength } from "./max-length"; export { default as not } from "./not"; +export { default as notAsync } from "./not-async"; export { default as chain } from "./chain"; -export { default as oneOf } from "./oneOf"; +export { default as chainAsync } from "./chain-async"; +export { default as oneOf } from "./one-of"; +export { default as oneOfAsync } from "./one-of-async"; export { default as optional } from "./optional"; +export { default as optionalAsync } from "./optional-async"; export { default as nullable } from "./nullable"; +export { default as nullableAsync } from "./nullable-async"; export { default as items } from "./items"; +export { default as itemsAsync } from "./items-async"; export { default as shape } from "./shape"; +export { default as shapeAsync } from "./shape-async"; export { default as tuple } from "./tuple"; +export { default as tupleAsync } from "./tuple-async"; export { default as entries } from "./entries"; +export { default as entriesAsync } from "./entries-async"; diff --git a/src/items-async.ts b/src/items-async.ts new file mode 100644 index 0000000..6d7182d --- /dev/null +++ b/src/items-async.ts @@ -0,0 +1,53 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; + +/** + * Asynchronous version of [[`items`]]. + * + * ```js + * const check = itemsAsync( + * chainAsync( + * string(), + * transformAsync(async value => value.trim()), + * ), + * ); + * + * await check([" one ", " two "]); + * // => { + * // isOk: true, + * // value: ["one", "two"], + * // } + * + * await check(["one", 2]); + * // => { isOk: false, ... } + * ``` + * + * @param check The async check function to use on the items. + * @returns An async check function. + */ +export default function itemsAsync( + check: MaybeAsyncCheck, +): AsyncCheck { + return async (input, ...args) => { + const results = await Promise.all( + input.map((value) => check(value, ...args)), + ); + const output = new Array(results.length); + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + + if (result.isOk) { + output[i] = result.value; + } else { + return { + ...result, + path: [i, ...result.path], + }; + } + } + + return ok(output); + }; +} diff --git a/src/items.ts b/src/items.ts index 61cfe67..392bb30 100644 --- a/src/items.ts +++ b/src/items.ts @@ -2,7 +2,7 @@ import Check from "./check"; import ok from "./ok"; /** - * Creates a check function that validates the items of an iterable. + * Creates a check function that validates the items of an array. * * ```js * const check = items(chain(string(), trim())); @@ -22,17 +22,15 @@ import ok from "./ok"; */ export default function items( check: Check, -): Check, O[], A> { +): Check { return (input, ...args) => { - const output = []; + const output = new Array(input.length); - let i = 0; - for (const value of input) { - const result = check(value, ...args); + for (let i = 0; i < input.length; i++) { + const result = check(input[i], ...args); if (result.isOk) { - output.push(result.value); - i++; + output[i] = result.value; } else { return { ...result, diff --git a/src/lessThan.ts b/src/less-than.ts similarity index 100% rename from src/lessThan.ts rename to src/less-than.ts diff --git a/src/maxLength.ts b/src/max-length.ts similarity index 100% rename from src/maxLength.ts rename to src/max-length.ts diff --git a/src/maybe-async-check.ts b/src/maybe-async-check.ts new file mode 100644 index 0000000..10b1a2f --- /dev/null +++ b/src/maybe-async-check.ts @@ -0,0 +1,8 @@ +import Result from "./result"; + +export default MaybeAsyncCheck; + +type MaybeAsyncCheck = ( + value: I, + ...args: A +) => Promise> | Result; diff --git a/src/minLength.ts b/src/min-length.ts similarity index 100% rename from src/minLength.ts rename to src/min-length.ts diff --git a/src/moreThan.ts b/src/more-than.ts similarity index 100% rename from src/moreThan.ts rename to src/more-than.ts diff --git a/src/NaN.ts b/src/not-a-number.ts similarity index 94% rename from src/NaN.ts rename to src/not-a-number.ts index 9df3836..ab44ff8 100644 --- a/src/NaN.ts +++ b/src/not-a-number.ts @@ -21,7 +21,7 @@ import test from "./test"; * @param error The error to given when the value is not NaN. * @returns A check function. */ -export default function NotANumber( +export default function notANumber( error = "is not NaN", ): Check { return test((value): value is number => isNaN(value as any), error); diff --git a/src/not-async.ts b/src/not-async.ts new file mode 100644 index 0000000..872122c --- /dev/null +++ b/src/not-async.ts @@ -0,0 +1,55 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; +import err from "./error"; + +export default function notAsync( + check: MaybeAsyncCheck, + error?: string, +): AsyncCheck; +export default function notAsync( + check: MaybeAsyncCheck, + error: string, + path: unknown[], + getInvalidValue: (value: V, ...args: A) => unknown, +): AsyncCheck; + +/** + * Asynchronous version of [[`not`]]. + * + * ```js + * const check = notAsync(testAsync(async value => value === 42)); + * + * await check(43); + * // => { isOk: true, ... } + * await check(42); + * // => { isOk: false, ... } + * ``` + * + * @param check The async check to negate. + * @param error The error to give if the value is invalid. + * @param path The path to give with the error. + * @param getInvalidValue A function to get the invalid value from the passed + * value. + * @returns An async check function. + */ +export default function notAsync( + check: MaybeAsyncCheck, + error: string = "is invalid", + path: unknown[] = [], + getInvalidValue?: (value: V, ...args: A) => unknown, +): AsyncCheck { + return async (value, ...args) => { + const result = await check(value, ...args); + + if (result.isOk) { + return err( + getInvalidValue ? getInvalidValue(value, ...args) : value, + error, + path, + ); + } else { + return ok(value); + } + }; +} diff --git a/src/nullable-async.ts b/src/nullable-async.ts new file mode 100644 index 0000000..d21046e --- /dev/null +++ b/src/nullable-async.ts @@ -0,0 +1,32 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; + +/** + * Asynchronous version of [[`nullable`]]. + * + * ```js + * const check = nullableAsync(testAsync(async value => value === 42)); + * + * await check(null); + * // => { isOk: true, ... } + * await check(42); + * // => { isOk: true, ... } + * await check("jerome"); + * // => { isOk: false, ... } + * ``` + * + * @param check The async check function to run when the value is present. + * @returns An async check function. + */ +export default function nullableAsync( + check: MaybeAsyncCheck, +): AsyncCheck { + return async (value, ...args) => { + if (value === null) { + return ok(null); + } else { + return await check(value, ...args); + } + }; +} diff --git a/src/ok.ts b/src/ok.ts index d6d4735..b71519d 100644 --- a/src/ok.ts +++ b/src/ok.ts @@ -16,7 +16,7 @@ export interface OkResult { * Creates a valid result. * * ```js - * check.ok("value"); + * ok("value"); * // => { * // isOk: true, * // value: "value", diff --git a/src/one-of-async.ts b/src/one-of-async.ts new file mode 100644 index 0000000..a96fe58 --- /dev/null +++ b/src/one-of-async.ts @@ -0,0 +1,267 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; + +export default function oneOfAsync( + ...checks: [MaybeAsyncCheck] +): AsyncCheck; +export default function oneOfAsync( + ...checks: [MaybeAsyncCheck, MaybeAsyncCheck] +): AsyncCheck; +export default function oneOfAsync( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + O7, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + O7, + O8, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + O7, + O8, + O9, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + O7, + O8, + O9, + O10, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + O7, + O8, + O9, + O10, + O11, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck; +export default function oneOfAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + O7, + O8, + O9, + O10, + O11, + O12, + A extends unknown[] +>( + ...checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ] +): AsyncCheck< + I, + O1 | O2 | O3 | O4 | O5 | O6 | O7 | O8 | O9 | O10 | O11 | O12, + A +>; + +/** + * Asynchronous version of [[`oneOf`]]. + * + * ```js + * const check = oneOfAsync( + * integer(), + * chainAsync(string(), transformAsync(async value => value.trim())), + * fail("is not an integer or a string"), + * ); + * + * await check(42); + * // => { + * // isOk: true, + * // value: 42, + * // } + * + * await check(" jerome "); + * // => { + * // isOk: true, + * // value: "jerome", + * // } + * + * await check(true); + * // => { + * // isOk: false, + * // error: "is not an integer or a string", + * // ... + * // } + * ``` + * + * @param checks The async check functions to try. + * @returns An async check function. + */ +export default function oneOfAsync( + ...checks: Array> +): AsyncCheck { + if (checks.length === 0) { + throw new Error("oneOfAsync: expected at least one check function"); + } + + return async (value, ...args) => { + let result: any; + + for (const check of checks) { + result = await check(value, ...args); + + if (result.isOk) { + return result; + } + } + + return result; + }; +} diff --git a/src/oneOf.ts b/src/one-of.ts similarity index 100% rename from src/oneOf.ts rename to src/one-of.ts diff --git a/src/optional-async.ts b/src/optional-async.ts new file mode 100644 index 0000000..6f409a5 --- /dev/null +++ b/src/optional-async.ts @@ -0,0 +1,32 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; + +/** + * Asynchronous version of [[`optional`]]. + * + * ```js + * const check = optionalAsync(testAsync(async value => value === 42)); + * + * await check(undefined); + * // => { isOk: true, ... } + * await check(42); + * // => { isOk: true, ... } + * await check("jerome"); + * // => { isOk: false, ... } + * ``` + * + * @param check The async check function to run when the value is present. + * @returns An async check function. + */ +export default function optionalAsync( + check: MaybeAsyncCheck, +): AsyncCheck { + return async (value, ...args) => { + if (value === undefined) { + return ok(undefined); + } else { + return await check(value, ...args); + } + }; +} diff --git a/src/shape-async.ts b/src/shape-async.ts new file mode 100644 index 0000000..1832066 --- /dev/null +++ b/src/shape-async.ts @@ -0,0 +1,74 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; + +export default function shapeAsync( + checks: { [P in keyof O]: MaybeAsyncCheck }, +): AsyncCheck; +export default function shapeAsync< + I extends { [P in keyof O]: I[P] }, + O, + A extends unknown[] +>( + checks: { [P in keyof O]: MaybeAsyncCheck }, +): AsyncCheck; + +/** + * Asynchronous version of [[`shape`]]. + * + * ```js + * const check = shapeAsync({ + * name: chainAsync(string(), transformAsync(async value => value.trim())), + * age: integer(), + * }); + * + * await check({ + * name: " Jérôme ", + * age: 30, + * }); + * // => { + * // isOk: true, + * // value: { + * // name: "Jérôme", + * // age: 30, + * // }, + * // } + * + * await check({ + * name: "Jérôme", + * age: "thirty", + * }); + * // => { isOk: false, ... } + * ``` + * + * @param checks An object containing async check functions. + * @returns An async check function. + */ +export default function shapeAsync(checks: { + [key: string]: MaybeAsyncCheck; +}): AsyncCheck { + const keys = Object.keys(checks); + + return async (input, ...args) => { + const output: any = {}; + const results = await Promise.all( + keys.map((key) => checks[key](input[key], ...args)), + ); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const result = results[i]; + + if (result.isOk) { + output[key] = result.value; + } else { + return { + ...result, + path: [key, ...result.path], + }; + } + } + + return ok(output); + }; +} diff --git a/src/test-async.ts b/src/test-async.ts new file mode 100644 index 0000000..c70914e --- /dev/null +++ b/src/test-async.ts @@ -0,0 +1,53 @@ +import AsyncCheck from "./async-check"; +import ok from "./ok"; +import err from "./error"; + +export default function testAsync( + predicate: (value: V, ...args: A) => Promise | boolean, + error?: string, +): AsyncCheck; +export default function testAsync( + predicate: (value: V, ...args: A) => Promise | boolean, + error: string, + path: unknown[], + getInvalidValue: (value: V, ...args: A) => unknown, +): AsyncCheck; + +/** + * Asynchronous version of [[`test`]]. + * + * ```js + * const check = testAsync(async value => value === 42); + * + * await check(42); + * // => { isOk: true, ... } + * + * await check(43); + * // => { isOk: false, ... } + * ``` + * + * @param predicate The async predicate function to test with. + * @param error The error to give when the predicate fails. + * @param path The path to give with the error. + * @param getInvalidValue A function to get the invalid value from the passed + * value. + * @returns An async check function. + */ +export default function testAsync( + predicate: (value: V, ...args: A) => Promise | boolean, + error: string = "is invalid", + path: unknown[] = [], + getInvalidValue?: (value: V, ...args: A) => unknown, +): AsyncCheck { + return async (value: V, ...args: A) => { + if (await predicate(value, ...args)) { + return ok(value); + } else { + return err( + getInvalidValue ? getInvalidValue(value, ...args) : value, + error, + path, + ); + } + }; +} diff --git a/src/toBoolean.ts b/src/to-boolean.ts similarity index 100% rename from src/toBoolean.ts rename to src/to-boolean.ts diff --git a/src/toDate.ts b/src/to-date.ts similarity index 100% rename from src/toDate.ts rename to src/to-date.ts diff --git a/src/toLower.ts b/src/to-lower.ts similarity index 100% rename from src/toLower.ts rename to src/to-lower.ts diff --git a/src/toNumber.ts b/src/to-number.ts similarity index 100% rename from src/toNumber.ts rename to src/to-number.ts diff --git a/src/toString.ts b/src/to-string.ts similarity index 100% rename from src/toString.ts rename to src/to-string.ts diff --git a/src/toUpper.ts b/src/to-upper.ts similarity index 100% rename from src/toUpper.ts rename to src/to-upper.ts diff --git a/src/transform-async.ts b/src/transform-async.ts new file mode 100644 index 0000000..36d42f4 --- /dev/null +++ b/src/transform-async.ts @@ -0,0 +1,28 @@ +import AsyncCheck from "./async-check"; +import ok from "./ok"; + +/** + * Asynchronous version of [[`transform`]]. + * + * ```js + * const check = transformAsync(async value => value / 2); + * + * await check(42); + * // => { + * // isOk: true, + * // value: 21, + * // } + * ``` + * + * @param trans The async function to transform with. + * @returns An async check function. + */ +export default function transformAsync( + trans: (value: I, ...args: A) => Promise | O, +): AsyncCheck { + return async (value: I, ...args: A) => { + const nextValue = await trans(value, ...args); + + return ok(nextValue); + }; +} diff --git a/src/transform.ts b/src/transform.ts index c197a00..24361af 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -14,18 +14,6 @@ import ok from "./ok"; * // } * ``` * - * All the arguments passed to resulting check function are passed to the - * transform function: - * ```js - * const check = transform((value, other) => value / other); - * - * check(42, 2); - * // => { - * // isOk: true, - * // value: 21, - * // } - * ``` - * * @param trans The function to transform with. * @returns A check function. */ diff --git a/src/tuple-async.ts b/src/tuple-async.ts new file mode 100644 index 0000000..58a9d0e --- /dev/null +++ b/src/tuple-async.ts @@ -0,0 +1,124 @@ +import AsyncCheck from "./async-check"; +import MaybeAsyncCheck from "./maybe-async-check"; +import ok from "./ok"; +import err from "./error"; + +export default function tupleAsync( + checks: [], + lengthError?: string, +): AsyncCheck; +export default function tupleAsync( + checks: [MaybeAsyncCheck], + lengthError?: string, +): AsyncCheck; +export default function tupleAsync( + checks: [MaybeAsyncCheck, MaybeAsyncCheck], + lengthError?: string, +): AsyncCheck; +export default function tupleAsync( + checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ], + lengthError?: string, +): AsyncCheck; +export default function tupleAsync( + checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ], + lengthError?: string, +): AsyncCheck; +export default function tupleAsync( + checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ], + lengthError?: string, +): AsyncCheck; +export default function tupleAsync< + I, + O1, + O2, + O3, + O4, + O5, + O6, + A extends unknown[] +>( + checks: [ + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + MaybeAsyncCheck, + ], + lengthError?: string, +): AsyncCheck; + +/** + * Asynchronous version of [[`tuple`]]. + * + * ```js + * const check = tupleAsync([ + * chainAsync(string(), transformAsync(async value => value.trim())), + * number(), + * ]); + * + * await check([" Jérôme ", 30]); + * // => { + * // isOk: true, + * // value: ["Jérôme", 30], + * // } + * + * await check(["Jérôme", "thirty"]); + * // => { isOk: false, ... } + * + * await check(["Jérôme", 30, "Web Developer"]); + * // => { isOk: false, ... } + * ``` + * + * @param checks An array of async check functions. + * @param lengthError The error to give when the value does not have the right + * length. + * @returns An async check function. + */ +export default function tupleAsync( + checks: Array>, + lengthError = checks.length === 1 + ? "does not have 1 item" + : `does not have ${checks.length} items`, +): AsyncCheck { + return async (input, ...args) => { + if (input.length !== checks.length) { + return err(input, lengthError); + } + + const results = await Promise.all( + checks.map((check, i) => check(input[i], ...args)), + ); + const output = new Array(results.length); + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + + if (result.isOk) { + output[i] = result.value; + } else { + return { + ...result, + path: [i, ...result.path], + }; + } + } + + return ok(output); + }; +} diff --git a/src/tuple.ts b/src/tuple.ts index 2718981..2edbdb5 100644 --- a/src/tuple.ts +++ b/src/tuple.ts @@ -2,14 +2,47 @@ import Check from "./check"; import ok from "./ok"; import err from "./error"; -export default function tuple< - I extends unknown[], - O extends unknown[], - A extends unknown[] ->( - checks: { [P in keyof O]: Check }, +export default function tuple( + checks: [], + lengthError?: string, +): Check; +export default function tuple( + checks: [Check], + lengthError?: string, +): Check; +export default function tuple( + checks: [Check, Check], + lengthError?: string, +): Check; +export default function tuple( + checks: [Check, Check, Check], + lengthError?: string, +): Check; +export default function tuple( + checks: [Check, Check, Check, Check], + lengthError?: string, +): Check; +export default function tuple( + checks: [ + Check, + Check, + Check, + Check, + Check, + ], + lengthError?: string, +): Check; +export default function tuple( + checks: [ + Check, + Check, + Check, + Check, + Check, + Check, + ], lengthError?: string, -): Check; +): Check; /** * Creates a check function that validates the items of an array. @@ -35,11 +68,11 @@ export default function tuple< * * @param checks An array of check functions. * @param lengthError The error to give when the value does not have the right - * length + * length. * @returns A check function. */ export default function tuple( - checks: any, + checks: Array>, lengthError = checks.length === 1 ? "does not have 1 item" : `does not have ${checks.length} items`, diff --git a/test/chain-async.test.ts b/test/chain-async.test.ts new file mode 100644 index 0000000..85ed434 --- /dev/null +++ b/test/chain-async.test.ts @@ -0,0 +1,112 @@ +import * as check from "../src"; + +test("check succeeds when all child checks succeed", async () => { + const checkValue = check.chainAsync( + async () => check.ok(1), + async () => check.ok(2), + async () => check.ok(3), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 3, + }); +}); + +test("check succeeds when no checks are given", async () => { + const checkValue = check.chainAsync(); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 42, + }); +}); + +test("check fails with error from first failing child check", async () => { + const checkValue = check.chainAsync( + async () => check.ok(1), + async () => check.ok(2), + async () => check.error(3, "is wrong"), + async () => check.error(4, "is wrong"), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 3, + path: [], + }); +}); + +test("child checks are called with previous valid value", async () => { + const checkOne = jest.fn(async () => check.ok(1)); + const checkTwo = jest.fn(async () => check.ok(2)); + const checkThree = jest.fn(async () => check.ok(3)); + const checkValue = check.chainAsync(checkOne, checkTwo, checkThree); + await checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(42); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(1); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(2); +}); + +test("child checks are called with the additional arguments", async () => { + const checkOne = jest.fn(async () => check.ok(1)); + const checkTwo = jest.fn(async () => check.ok(2)); + const checkThree = jest.fn(async () => check.ok(3)); + const checkValue = check.chainAsync< + unknown, + number, + number, + number, + unknown[] + >(checkOne, checkTwo, checkThree); + await checkValue(42, "one", "two"); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child checks are not called after first failure", async () => { + const checkOne = jest.fn(async () => check.ok(1)); + const checkTwo = jest.fn(async () => check.error(2, "is wrong")); + const checkThree = jest.fn(async () => check.ok(3)); + const checkFour = jest.fn(async () => check.ok(4)); + const checkValue = check.chainAsync( + checkOne, + checkTwo, + checkThree, + checkFour, + ); + await checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkThree).not.toHaveBeenCalled(); + expect(checkFour).not.toHaveBeenCalled(); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.chainAsync( + async () => check.ok(1), + () => check.ok(2), + async () => check.ok(3), + () => check.ok(4), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 4, + }); +}); diff --git a/test/chain.test.ts b/test/chain.test.ts index 5de815d..620acce 100644 --- a/test/chain.test.ts +++ b/test/chain.test.ts @@ -2,57 +2,89 @@ import * as check from "../src"; test("check succeeds when all child checks succeed", () => { const checkValue = check.chain( - check.string(), - check.trim(), - check.maxLength(6), + () => check.ok(1), + () => check.ok(2), + () => check.ok(3), ); - const result = checkValue(" jerome "); + const result = checkValue(42); expect(result).toEqual({ isOk: true, - value: "jerome", + value: 3, }); }); test("check succeeds when no checks are given", () => { const checkValue = check.chain(); - const result = checkValue("jerome"); + const result = checkValue(42); expect(result).toEqual({ isOk: true, - value: "jerome", + value: 42, }); }); test("check fails with error from first failing child check", () => { const checkValue = check.chain( - check.string(), - check.trim(), - check.minLength(8), - check.pattern(/[A-Z]/), + () => check.ok(1), + () => check.ok(2), + () => check.error(3, "is wrong"), + () => check.error(4, "is wrong"), ); - const result = checkValue(" jerome "); + const result = checkValue(42); expect(result).toEqual({ isOk: false, - error: "is too short", - invalidValue: "jerome", + error: "is wrong", + invalidValue: 3, path: [], }); }); -test("additional arguments are passed to the child checks", () => { - const checkValue = check.chain( - (value, ...args) => check.ok(args), - (value, ...args) => check.ok([value, args]), +test("child checks are called with previous valid value", () => { + const checkOne = jest.fn(() => check.ok(1)); + const checkTwo = jest.fn(() => check.ok(2)); + const checkThree = jest.fn(() => check.ok(3)); + const checkValue = check.chain(checkOne, checkTwo, checkThree); + checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(42); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(1); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(2); +}); + +test("child checks are called with the additional arguments", () => { + const checkOne = jest.fn(() => check.ok(1)); + const checkTwo = jest.fn(() => check.ok(2)); + const checkThree = jest.fn(() => check.ok(3)); + const checkValue = check.chain( + checkOne, + checkTwo, + checkThree, ); - const result = checkValue(null, "one", "two"); + checkValue(42, "one", "two"); - expect(result).toEqual({ - isOk: true, - value: [ - ["one", "two"], - ["one", "two"], - ], - }); + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child checks are not called after first failure", () => { + const checkOne = jest.fn(() => check.ok(1)); + const checkTwo = jest.fn(() => check.error(2, "is wrong")); + const checkThree = jest.fn(() => check.ok(3)); + const checkFour = jest.fn(() => check.ok(4)); + const checkValue = check.chain(checkOne, checkTwo, checkThree, checkFour); + checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkThree).not.toHaveBeenCalled(); + expect(checkFour).not.toHaveBeenCalled(); }); diff --git a/test/entries-async.test.ts b/test/entries-async.test.ts new file mode 100644 index 0000000..32b683b --- /dev/null +++ b/test/entries-async.test.ts @@ -0,0 +1,140 @@ +import * as check from "../src"; + +test("check succeeds when child checks succeeds on all key-value pairs", async () => { + const checkValue = check.entriesAsync( + check.chainAsync( + check.transformAsync(async (value) => value.trim()), + check.pattern(/^[0-9]+$/), + ), + check.chainAsync( + check.string(), + check.transformAsync(async (value) => value.trim()), + ), + ); + const result = await checkValue({ + " 1 ": " one ", + " 2": " two ", + }); + + expect(result).toEqual({ + isOk: true, + value: { + 1: "one", + 2: "two", + }, + }); +}); + +test("check fails when child key check fails on at least one key", async () => { + const checkValue = check.entriesAsync( + check.testAsync(async (value) => value === "one", "is invalid"), + async (value) => check.ok(value), + ); + const result = await checkValue({ one: 1, two: 2 }); + + expect(result).toEqual({ + isOk: false, + error: "is invalid", + invalidValue: "two", + path: [], + }); +}); + +test("check fails when child value check fails on at least one value", async () => { + const checkValue = check.entriesAsync( + async (value) => check.ok(value), + check.testAsync(async (value) => value === "one", "is invalid"), + ); + const result = await checkValue({ 1: "one", 2: "two" }); + + expect(result).toEqual({ + isOk: false, + error: "is invalid", + invalidValue: "two", + path: ["2"], + }); +}); + +test("child checks are called with all key-value pairs", async () => { + const checkKey = jest.fn(async () => check.ok("key")); + const checkValue = jest.fn(async () => check.ok("value")); + const checkEntries = check.entriesAsync(checkKey, checkValue); + await checkEntries({ + one: 1, + two: 2, + }); + + expect(checkKey).toHaveBeenCalledTimes(2); + expect(checkKey).toHaveBeenCalledWith("one"); + expect(checkKey).toHaveBeenCalledWith("two"); + expect(checkValue).toHaveBeenCalledTimes(2); + expect(checkValue).toHaveBeenCalledWith(1); + expect(checkValue).toHaveBeenCalledWith(2); +}); + +test("child checks are called with the additional arguments", async () => { + const checkKey = jest.fn(async () => check.ok("key")); + const checkValue = jest.fn(async () => check.ok("value")); + const checkEntries = check.entriesAsync( + checkKey, + checkValue, + ); + await checkEntries( + { + one: 1, + two: 2, + }, + "one", + "two", + ); + + expect(checkKey).toHaveBeenCalledTimes(2); + expect(checkKey).toHaveBeenNthCalledWith(1, expect.anything(), "one", "two"); + expect(checkKey).toHaveBeenNthCalledWith(2, expect.anything(), "one", "two"); + expect(checkValue).toHaveBeenCalledTimes(2); + expect(checkValue).toHaveBeenNthCalledWith( + 1, + expect.anything(), + "one", + "two", + ); + expect(checkValue).toHaveBeenNthCalledWith( + 2, + expect.anything(), + "one", + "two", + ); +}); + +test("correct path is returned with the value error", async () => { + const checkValue = check.entriesAsync( + check.transformAsync(async (value) => value.trim()), + async () => check.error("invalid", "is wrong", ["two"]), + ); + const result = await checkValue({ + " one ": 1, + }); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: "invalid", + path: [" one ", "two"], + }); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.entriesAsync( + () => check.ok("key"), + () => check.ok("value"), + ); + const result = await checkValue({ + one: 1, + two: 2, + }); + + expect(result).toEqual({ + isOk: true, + value: { key: "value" }, + }); +}); diff --git a/test/entries.test.ts b/test/entries.test.ts index 3e4ba33..da8c9b4 100644 --- a/test/entries.test.ts +++ b/test/entries.test.ts @@ -1,6 +1,6 @@ import * as check from "../src"; -test("check succeeds when given value has only valid key-value pairs", () => { +test("check succeeds when child checks succeeds on all key-value pairs", () => { const checkValue = check.entries( check.chain(check.trim(), check.pattern(/^[0-9]+$/)), check.chain(check.string(), check.trim()), @@ -19,7 +19,7 @@ test("check succeeds when given value has only valid key-value pairs", () => { }); }); -test("check fails when given value has invalid keys", () => { +test("check fails when child key check fails on at least one key", () => { const checkValue = check.entries(check.is("one", "is invalid"), check.pass()); const result = checkValue({ one: 1, two: 2 }); @@ -31,7 +31,7 @@ test("check fails when given value has invalid keys", () => { }); }); -test("check fails when given value has invalid values", () => { +test("check fails when child value check fails on at least one value", () => { const checkValue = check.entries(check.pass(), check.is("one", "is invalid")); const result = checkValue({ 1: "one", 2: "two" }); @@ -43,53 +43,69 @@ test("check fails when given value has invalid values", () => { }); }); -test("correct path is returned with the value error", () => { - const checkValue = check.entries( - check.pass(), - check.entries(check.trim(), check.is("one", "is invalid")), - ); - const result = checkValue({ - foo: { - 1: "one", - " 2 ": "two", - }, +test("child checks are called with all key-value pairs", () => { + const checkKey = jest.fn(() => check.ok("key")); + const checkValue = jest.fn(() => check.ok("value")); + const checkEntries = check.entries(checkKey, checkValue); + checkEntries({ + one: 1, + two: 2, }); - expect(result).toEqual({ - isOk: false, - error: "is invalid", - invalidValue: "two", - path: ["foo", " 2 "], - }); + expect(checkKey).toHaveBeenCalledTimes(2); + expect(checkKey).toHaveBeenCalledWith("one"); + expect(checkKey).toHaveBeenCalledWith("two"); + expect(checkValue).toHaveBeenCalledTimes(2); + expect(checkValue).toHaveBeenCalledWith(1); + expect(checkValue).toHaveBeenCalledWith(2); }); -test("additional arguments are passed to the child key check", () => { - const checkValue = check.entries( - (value, ...args) => check.ok(value + JSON.stringify(args)), - check.pass(), +test("child checks are called with the additional arguments", () => { + const checkKey = jest.fn(() => check.ok("key")); + const checkValue = jest.fn(() => check.ok("value")); + const checkEntries = check.entries( + checkKey, + checkValue, ); - const result = checkValue({ 1: 1, 2: 2 }, "one", "two"); - - expect(result).toEqual({ - isOk: true, - value: { - ["1" + JSON.stringify(["one", "two"])]: 1, - ["2" + JSON.stringify(["one", "two"])]: 2, + checkEntries( + { + one: 1, + two: 2, }, - }); + "one", + "two", + ); + + expect(checkKey).toHaveBeenCalledTimes(2); + expect(checkKey).toHaveBeenNthCalledWith(1, expect.anything(), "one", "two"); + expect(checkKey).toHaveBeenNthCalledWith(2, expect.anything(), "one", "two"); + expect(checkValue).toHaveBeenCalledTimes(2); + expect(checkValue).toHaveBeenNthCalledWith( + 1, + expect.anything(), + "one", + "two", + ); + expect(checkValue).toHaveBeenNthCalledWith( + 2, + expect.anything(), + "one", + "two", + ); }); -test("additional arguments are passed to child value check", () => { - const checkValue = check.entries(check.pass(), (value, ...args) => - check.ok(args), +test("correct path is returned with the value error", () => { + const checkValue = check.entries(check.trim(), () => + check.error("invalid", "is wrong", ["two"]), ); - const result = checkValue({ 1: 1, 2: 2 }, "one", "two"); + const result = checkValue({ + " one ": 1, + }); expect(result).toEqual({ - isOk: true, - value: { - 1: ["one", "two"], - 2: ["one", "two"], - }, + isOk: false, + error: "is wrong", + invalidValue: "invalid", + path: [" one ", "two"], }); }); diff --git a/test/items-async.test.ts b/test/items-async.test.ts new file mode 100644 index 0000000..1b2ac8f --- /dev/null +++ b/test/items-async.test.ts @@ -0,0 +1,73 @@ +import * as check from "../src"; + +test("check succeeds when child check succeeds on all items", async () => { + const checkValue = check.itemsAsync( + check.transformAsync(async (value: string) => value.trim()), + ); + const result = await checkValue(["one", " two "]); + + expect(result).toEqual({ + isOk: true, + value: ["one", "two"], + }); +}); + +test("check fails when child check fails on at least one item", async () => { + const checkValue = check.itemsAsync( + check.testAsync(async (value) => value === "valid", "is invalid"), + ); + const result = await checkValue(["valid", "invalid one", "invalid two"]); + + expect(result).toEqual({ + isOk: false, + error: "is invalid", + invalidValue: "invalid one", + path: [1], + }); +}); + +test("child check is called with all items", async () => { + const checkItem = jest.fn(async () => check.ok(42)); + const checkValue = check.itemsAsync(checkItem); + await checkValue([1, 2, 3]); + + expect(checkItem).toHaveBeenCalledTimes(3); + expect(checkItem).toHaveBeenCalledWith(1); + expect(checkItem).toHaveBeenCalledWith(2); + expect(checkItem).toHaveBeenCalledWith(3); +}); + +test("child check is called with the additional arguments", async () => { + const checkItem = jest.fn(async () => check.ok(42)); + const checkValue = check.itemsAsync(checkItem); + await checkValue([1, 2, 3], "one", "two"); + + expect(checkItem).toHaveBeenCalledTimes(3); + expect(checkItem).toHaveBeenNthCalledWith(1, expect.anything(), "one", "two"); + expect(checkItem).toHaveBeenNthCalledWith(2, expect.anything(), "one", "two"); + expect(checkItem).toHaveBeenNthCalledWith(3, expect.anything(), "one", "two"); +}); + +test("correct path is returned with the error", async () => { + const checkValue = check.itemsAsync(async (value) => + value === "valid" ? check.ok(value) : check.error(value, "is wrong", [2]), + ); + const result = await checkValue(["valid", "invalid"]); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: "invalid", + path: [1, 2], + }); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.itemsAsync(check.trim()); + const result = await checkValue([" one ", "two ", " three "]); + + expect(result).toEqual({ + isOk: true, + value: ["one", "two", "three"], + }); +}); diff --git a/test/items.test.ts b/test/items.test.ts index 13edccc..369005b 100644 --- a/test/items.test.ts +++ b/test/items.test.ts @@ -1,7 +1,7 @@ import * as check from "../src"; -test("check succeeds when given value has only valid items", () => { - const checkValue = check.items(check.chain(check.string(), check.trim())); +test("check succeeds when child check succeeds on all items", () => { + const checkValue = check.items(check.trim()); const result = checkValue(["one", " two "]); expect(result).toEqual({ @@ -10,7 +10,7 @@ test("check succeeds when given value has only valid items", () => { }); }); -test("check fails when given value has invalid items", () => { +test("check fails when child check fails on at least one item", () => { const checkValue = check.items(check.is("valid", "is invalid")); const result = checkValue(["valid", "invalid one", "invalid two"]); @@ -22,27 +22,38 @@ test("check fails when given value has invalid items", () => { }); }); -test("correct path is returned with the error", () => { - const checkValue = check.items(check.items(check.is("valid", "is invalid"))); - const result = checkValue([["valid", "invalid"]]); +test("child check is called with all items", () => { + const checkItem = jest.fn(() => check.ok(42)); + const checkValue = check.items(checkItem); + checkValue([1, 2, 3]); - expect(result).toEqual({ - isOk: false, - error: "is invalid", - invalidValue: "invalid", - path: [0, 1], - }); + expect(checkItem).toHaveBeenCalledTimes(3); + expect(checkItem).toHaveBeenCalledWith(1); + expect(checkItem).toHaveBeenCalledWith(2); + expect(checkItem).toHaveBeenCalledWith(3); }); -test("additional arguments are passed to the child check", () => { - const checkValue = check.items((value, ...args) => check.ok(args)); - const result = checkValue([1, 2], "one", "two"); +test("child check is called with the additional arguments", () => { + const checkItem = jest.fn(() => check.ok(42)); + const checkValue = check.items(checkItem); + checkValue([1, 2, 3], "one", "two"); + + expect(checkItem).toHaveBeenCalledTimes(3); + expect(checkItem).toHaveBeenNthCalledWith(1, expect.anything(), "one", "two"); + expect(checkItem).toHaveBeenNthCalledWith(2, expect.anything(), "one", "two"); + expect(checkItem).toHaveBeenNthCalledWith(3, expect.anything(), "one", "two"); +}); + +test("correct path is returned with the error", () => { + const checkValue = check.items((value) => + value === "valid" ? check.ok(value) : check.error(value, "is wrong", [2]), + ); + const result = checkValue(["valid", "invalid"]); expect(result).toEqual({ - isOk: true, - value: [ - ["one", "two"], - ["one", "two"], - ], + isOk: false, + error: "is wrong", + invalidValue: "invalid", + path: [1, 2], }); }); diff --git a/test/lessThan.test.ts b/test/less-than.test.ts similarity index 100% rename from test/lessThan.test.ts rename to test/less-than.test.ts diff --git a/test/maxLength.test.ts b/test/max-length.test.ts similarity index 100% rename from test/maxLength.test.ts rename to test/max-length.test.ts diff --git a/test/minLength.test.ts b/test/min-length.test.ts similarity index 100% rename from test/minLength.test.ts rename to test/min-length.test.ts diff --git a/test/moreThan.test.ts b/test/more-than.test.ts similarity index 100% rename from test/moreThan.test.ts rename to test/more-than.test.ts diff --git a/test/NaN.test.ts b/test/not-a-number.test.ts similarity index 100% rename from test/NaN.test.ts rename to test/not-a-number.test.ts diff --git a/test/not-async.test.ts b/test/not-async.test.ts new file mode 100644 index 0000000..e4ce034 --- /dev/null +++ b/test/not-async.test.ts @@ -0,0 +1,110 @@ +import * as check from "../src"; + +test("check succeeds when child check fails", async () => { + const checkValue = check.notAsync(async () => + check.error("value", "is wrong"), + ); + const result = await checkValue(43); + + expect(result).toEqual({ + isOk: true, + value: 43, + }); +}); + +test("check fails when child check succeeds", async () => { + const checkValue = check.notAsync(async () => check.ok("value")); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is invalid", + invalidValue: 42, + path: [], + }); +}); + +test("child check is called with given value", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.notAsync(checkChild); + await checkValue(42); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(42); +}); + +test("child check is called with the additional arguments", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.notAsync(checkChild); + await checkValue(42, "one", "two"); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("given error is returned with the invalid result", async () => { + const checkValue = check.notAsync(async () => check.ok("value"), "is wrong"); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 42, + path: [], + }); +}); + +test("given path and invalid value are returned with the invalid result", async () => { + const checkValue = check.notAsync( + async () => check.ok("value"), + "is invalid", + ["value"], + () => "invalid value", + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is invalid", + invalidValue: "invalid value", + path: ["value"], + }); +}); + +test("given function to get the invalid value is called with the validated value", async () => { + const getInvalidValue = jest.fn(() => "invalid value"); + const checkValue = check.notAsync( + async () => check.ok("value"), + "is invalid", + ["value"], + getInvalidValue, + ); + await checkValue(42); + + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(42); +}); + +test("given function to get the invalid value is called with the additional arguments", async () => { + const getInvalidValue = jest.fn(() => "invalid value"); + const checkValue = check.notAsync( + async () => check.ok("value"), + "is invalid", + ["value"], + getInvalidValue, + ); + await checkValue(42, "one", "two"); + + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("synchronous child check are handled", async () => { + const checkValue = check.notAsync(() => check.error("jerome", "is wrong")); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 42, + }); +}); diff --git a/test/not.test.ts b/test/not.test.ts index 95ed14c..519253c 100644 --- a/test/not.test.ts +++ b/test/not.test.ts @@ -1,7 +1,7 @@ import * as check from "../src"; test("check succeeds when child check fails", () => { - const checkValue = check.not(check.is(42)); + const checkValue = check.not(() => check.error("value", "is wrong")); const result = checkValue(43); expect(result).toEqual({ @@ -11,7 +11,7 @@ test("check succeeds when child check fails", () => { }); test("check fails when child check succeeds", () => { - const checkValue = check.not(check.is(42)); + const checkValue = check.not(() => check.ok("value")); const result = checkValue(42); expect(result).toEqual({ @@ -22,13 +22,31 @@ test("check fails when child check succeeds", () => { }); }); +test("child check is called with given value", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.not(checkChild); + checkValue(42); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(42); +}); + +test("child check is called with the additional arguments", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.not(checkChild); + checkValue(42, "one", "two"); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + test("given error is returned with the invalid result", () => { - const checkValue = check.not(check.is(42), "is 42"); + const checkValue = check.not(() => check.ok("value"), "is wrong"); const result = checkValue(42); expect(result).toEqual({ isOk: false, - error: "is 42", + error: "is wrong", invalidValue: 42, path: [], }); @@ -36,51 +54,45 @@ test("given error is returned with the invalid result", () => { test("given path and invalid value are returned with the invalid result", () => { const checkValue = check.not( - check.test(({ value }) => value === 42), + () => check.ok("value"), "is invalid", ["value"], - ({ value }) => value, + () => "invalid value", ); - const result = checkValue({ value: 42 }); + const result = checkValue(42); expect(result).toEqual({ isOk: false, error: "is invalid", - invalidValue: 42, + invalidValue: "invalid value", path: ["value"], }); }); -test("additional arguments are passed to the child check", () => { +test("given function to get the invalid value is called with the validated value", () => { + const getInvalidValue = jest.fn(() => "invalid value"); const checkValue = check.not( - check.test( - (value, ...args) => - args.length === 2 && args[0] === "one" && args[1] === "two", - ), + () => check.ok("value"), + "is invalid", + ["value"], + getInvalidValue, ); - const result = checkValue(42, "one", "two"); + checkValue(42); - expect(result).toEqual({ - isOk: false, - error: "is invalid", - invalidValue: 42, - path: [], - }); + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(42); }); -test("additional arguments are passed to the function to get the invalid value", () => { - const checkValue = check.not( - check.is(42), +test("given function to get the invalid value is called with the additional arguments", () => { + const getInvalidValue = jest.fn(() => "invalid value"); + const checkValue = check.not( + () => check.ok("value"), "is invalid", - [], - (value, ...args: unknown[]) => args, + ["value"], + getInvalidValue, ); - const result = checkValue(42, "one", "two"); + checkValue(42, "one", "two"); - expect(result).toEqual({ - isOk: false, - error: "is invalid", - invalidValue: ["one", "two"], - path: [], - }); + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(expect.anything(), "one", "two"); }); diff --git a/test/nullable-async.test.ts b/test/nullable-async.test.ts new file mode 100644 index 0000000..9a59ff2 --- /dev/null +++ b/test/nullable-async.test.ts @@ -0,0 +1,78 @@ +import * as check from "../src"; + +test("check succeeds when given value is not null and child check succeeds", async () => { + const checkValue = check.nullableAsync(async () => check.ok("value")); + + for (const value of ["jerome", "", 42, 0, undefined]) { + const result = await checkValue(value); + + expect(result).toEqual({ + isOk: true, + value: "value", + }); + } +}); + +test("check fails when given value is not null and child check fails", async () => { + const checkValue = check.nullableAsync(async () => + check.error("value", "is wrong"), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: "value", + path: [], + }); +}); + +test("check succeeds when given value is null", async () => { + const checkValue = check.nullableAsync(async () => + check.error("value", "is wrong"), + ); + const result = await checkValue(null); + + expect(result).toEqual({ + isOk: true, + value: null, + }); +}); + +test("child check is called with given value", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.nullableAsync(checkChild); + await checkValue(42); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(42); +}); + +test("child check is called with the additional arguments", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.nullableAsync( + checkChild, + ); + await checkValue(42, "one", "two"); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child check is not called when given value is null", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.nullableAsync(checkChild); + await checkValue(null); + + expect(checkChild).not.toHaveBeenCalled(); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.nullableAsync(() => check.ok("value")); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: "value", + }); +}); diff --git a/test/nullable.test.ts b/test/nullable.test.ts index e9eafc4..84df9a0 100644 --- a/test/nullable.test.ts +++ b/test/nullable.test.ts @@ -1,32 +1,32 @@ import * as check from "../src"; test("check succeeds when given value is not null and child check succeeds", () => { - const checkValue = check.nullable(check.toString()); + const checkValue = check.nullable(() => check.ok("value")); for (const value of ["jerome", "", 42, 0, undefined]) { const result = checkValue(value); expect(result).toEqual({ isOk: true, - value: value === undefined ? "" : String(value), + value: "value", }); } }); test("check fails when given value is not null and child check fails", () => { - const checkValue = check.nullable(check.string()); + const checkValue = check.nullable(() => check.error("value", "is wrong")); const result = checkValue(42); expect(result).toEqual({ isOk: false, - error: "is not a string", - invalidValue: 42, + error: "is wrong", + invalidValue: "value", path: [], }); }); test("check succeeds when given value is null", () => { - const checkValue = check.nullable(check.string()); + const checkValue = check.nullable(() => check.error("value", "is wrong")); const result = checkValue(null); expect(result).toEqual({ @@ -35,12 +35,28 @@ test("check succeeds when given value is null", () => { }); }); -test("additional arguments are passed to the child check", () => { - const checkValue = check.nullable((value, ...args) => check.ok(args)); - const result = checkValue(42, "one", "two"); +test("child check is called with given value", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.nullable(checkChild); + checkValue(42); - expect(result).toEqual({ - isOk: true, - value: ["one", "two"], - }); + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(42); +}); + +test("child check is called with the additional arguments", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.nullable(checkChild); + checkValue(42, "one", "two"); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child check is not called when given value is null", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.nullable(checkChild); + checkValue(null); + + expect(checkChild).not.toHaveBeenCalled(); }); diff --git a/test/one-of-async.test.ts b/test/one-of-async.test.ts new file mode 100644 index 0000000..b557144 --- /dev/null +++ b/test/one-of-async.test.ts @@ -0,0 +1,101 @@ +import * as check from "../src"; + +test("check succeeds when at least one child check succeeds", async () => { + const checkValue = check.oneOfAsync( + async () => check.error(1, "is wrong"), + async () => check.ok(2), + async () => check.ok(3), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 2, + }); +}); + +test("check fails with error from last failing child check", async () => { + const checkValue = check.oneOfAsync( + async () => check.error(1, "is wrong"), + async () => check.error(2, "is wrong"), + async () => check.error(3, "is wrong"), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 3, + path: [], + }); +}); + +test("child checks are called with given value", async () => { + const checkOne = jest.fn(async () => check.error(1, "is wrong")); + const checkTwo = jest.fn(async () => check.error(2, "is wrong")); + const checkThree = jest.fn(async () => check.error(3, "is wrong")); + const checkValue = check.oneOfAsync(checkOne, checkTwo, checkThree); + await checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(42); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(42); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(42); +}); + +test("child checks are called with the additional arguments", async () => { + const checkOne = jest.fn(async () => check.error(1, "is wrong")); + const checkTwo = jest.fn(async () => check.error(2, "is wrong")); + const checkThree = jest.fn(async () => check.error(3, "is wrong")); + const checkValue = check.oneOfAsync< + unknown, + unknown, + unknown, + unknown, + unknown[] + >(checkOne, checkTwo, checkThree); + await checkValue(42, "one", "two"); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child checks are not called after first success", async () => { + const checkOne = jest.fn(async () => check.error(1, "is wrong")); + const checkTwo = jest.fn(async () => check.ok(2)); + const checkThree = jest.fn(async () => check.ok(3)); + const checkFour = jest.fn(async () => check.ok(4)); + const checkValue = check.oneOfAsync( + checkOne, + checkTwo, + checkThree, + checkFour, + ); + await checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkThree).not.toHaveBeenCalled(); + expect(checkFour).not.toHaveBeenCalled(); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.oneOfAsync( + async () => check.error(1, "is wrong"), + () => check.error(2, "is wrong"), + async () => check.error(3, "is wrong"), + () => check.ok(4), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 4, + }); +}); diff --git a/test/one-of.test.ts b/test/one-of.test.ts new file mode 100644 index 0000000..e540bae --- /dev/null +++ b/test/one-of.test.ts @@ -0,0 +1,79 @@ +import * as check from "../src"; + +test("check succeeds when at least one child check succeeds", () => { + const checkValue = check.oneOf( + () => check.error(1, "is wrong"), + () => check.ok(2), + () => check.ok(3), + ); + const result = checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 2, + }); +}); + +test("check fails with error from last failing child check", () => { + const checkValue = check.oneOf( + () => check.error(1, "is wrong"), + () => check.error(2, "is wrong"), + () => check.error(3, "is wrong"), + ); + const result = checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 3, + path: [], + }); +}); + +test("child checks are called with given value", () => { + const checkOne = jest.fn(() => check.error(1, "is wrong")); + const checkTwo = jest.fn(() => check.error(2, "is wrong")); + const checkThree = jest.fn(() => check.error(3, "is wrong")); + const checkValue = check.oneOf(checkOne, checkTwo, checkThree); + checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(42); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(42); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(42); +}); + +test("child checks are called with the additional arguments", () => { + const checkOne = jest.fn(() => check.error(1, "is wrong")); + const checkTwo = jest.fn(() => check.error(2, "is wrong")); + const checkThree = jest.fn(() => check.error(3, "is wrong")); + const checkValue = check.oneOf( + checkOne, + checkTwo, + checkThree, + ); + checkValue(42, "one", "two"); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkThree).toHaveBeenCalledTimes(1); + expect(checkThree).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child checks are not called after first success", () => { + const checkOne = jest.fn(() => check.error(1, "is wrong")); + const checkTwo = jest.fn(() => check.ok(2)); + const checkThree = jest.fn(() => check.ok(3)); + const checkFour = jest.fn(() => check.ok(4)); + const checkValue = check.oneOf(checkOne, checkTwo, checkThree, checkFour); + checkValue(42); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkThree).not.toHaveBeenCalled(); + expect(checkFour).not.toHaveBeenCalled(); +}); diff --git a/test/oneOf.test.ts b/test/oneOf.test.ts deleted file mode 100644 index 298c886..0000000 --- a/test/oneOf.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as check from "../src"; - -test("check succeeds when at least one child check succeeds", () => { - const checkValue = check.oneOf( - check.number(), - check.chain(check.string(), check.toLower()), - check.chain(check.string(), check.toUpper()), - ); - const result = checkValue("Jerome"); - - expect(result).toEqual({ - isOk: true, - value: "jerome", - }); -}); - -test("check fails with error from last failing child check", () => { - const checkValue = check.oneOf( - check.string(), - check.number(), - check.boolean(), - ); - const result = checkValue(null); - - expect(result).toEqual({ - isOk: false, - error: "is not a boolean", - invalidValue: null, - path: [], - }); -}); - -test("additional arguments are passed to the child checks", () => { - const checkValue = check.oneOf(check.fail(), (value, ...args) => - check.ok(args), - ); - const result = checkValue(null, "one", "two"); - - expect(result).toEqual({ - isOk: true, - value: ["one", "two"], - }); -}); diff --git a/test/optional-async.test.ts b/test/optional-async.test.ts new file mode 100644 index 0000000..5fcea25 --- /dev/null +++ b/test/optional-async.test.ts @@ -0,0 +1,78 @@ +import * as check from "../src"; + +test("check succeeds when given value is present and child check succeeds", async () => { + const checkValue = check.optionalAsync(async () => check.ok("value")); + + for (const value of ["jerome", "", 42, 0, null]) { + const result = await checkValue(value); + + expect(result).toEqual({ + isOk: true, + value: "value", + }); + } +}); + +test("check fails when given value is present and child check fails", async () => { + const checkValue = check.optionalAsync(async () => + check.error("value", "is wrong"), + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: "value", + path: [], + }); +}); + +test("check succeeds when given value is not present", async () => { + const checkValue = check.optionalAsync(async () => + check.error("value", "is wrong"), + ); + const result = await checkValue(undefined); + + expect(result).toEqual({ + isOk: true, + value: undefined, + }); +}); + +test("child check is called with given value", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.optionalAsync(checkChild); + await checkValue(42); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(42); +}); + +test("child check is called with the additional arguments", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.optionalAsync( + checkChild, + ); + await checkValue(42, "one", "two"); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child check is not called when given value is not present", async () => { + const checkChild = jest.fn(async () => check.ok("value")); + const checkValue = check.optionalAsync(checkChild); + await checkValue(undefined); + + expect(checkChild).not.toHaveBeenCalled(); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.optionalAsync(() => check.ok("value")); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: "value", + }); +}); diff --git a/test/optional.test.ts b/test/optional.test.ts index 8001b70..06642a4 100644 --- a/test/optional.test.ts +++ b/test/optional.test.ts @@ -1,32 +1,32 @@ import * as check from "../src"; test("check succeeds when given value is present and child check succeeds", () => { - const checkValue = check.optional(check.toString()); + const checkValue = check.optional(() => check.ok("value")); for (const value of ["jerome", "", 42, 0, null]) { const result = checkValue(value); expect(result).toEqual({ isOk: true, - value: value === null ? "" : String(value), + value: "value", }); } }); test("check fails when given value is present and child check fails", () => { - const checkValue = check.optional(check.string()); + const checkValue = check.optional(() => check.error("value", "is wrong")); const result = checkValue(42); expect(result).toEqual({ isOk: false, - error: "is not a string", - invalidValue: 42, + error: "is wrong", + invalidValue: "value", path: [], }); }); test("check succeeds when given value is not present", () => { - const checkValue = check.optional(check.string()); + const checkValue = check.optional(() => check.error("value", "is wrong")); const result = checkValue(undefined); expect(result).toEqual({ @@ -35,12 +35,28 @@ test("check succeeds when given value is not present", () => { }); }); -test("additional arguments are passed to the child check", () => { - const checkValue = check.optional((value, ...args) => check.ok(args)); - const result = checkValue(42, "one", "two"); +test("child check is called with given value", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.optional(checkChild); + checkValue(42); - expect(result).toEqual({ - isOk: true, - value: ["one", "two"], - }); + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(42); +}); + +test("child check is called with the additional arguments", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.optional(checkChild); + checkValue(42, "one", "two"); + + expect(checkChild).toHaveBeenCalledTimes(1); + expect(checkChild).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("child check is not called when given value is not present", () => { + const checkChild = jest.fn(() => check.ok("value")); + const checkValue = check.optional(checkChild); + checkValue(undefined); + + expect(checkChild).not.toHaveBeenCalled(); }); diff --git a/test/shape-async.test.ts b/test/shape-async.test.ts new file mode 100644 index 0000000..fa3e5f8 --- /dev/null +++ b/test/shape-async.test.ts @@ -0,0 +1,106 @@ +import * as check from "../src"; + +test("check succeeds when all child checks succeed", async () => { + const checkValue = check.shapeAsync({ + one: async () => check.ok(1), + two: async () => check.ok(2), + }); + const result = await checkValue({}); + + expect(result).toEqual({ + isOk: true, + value: { + one: 1, + two: 2, + }, + }); +}); + +test("check fails when at least one child check fails", async () => { + const checkValue = check.shapeAsync({ + one: async () => check.ok(1), + two: async () => check.error(2, "is wrong"), + }); + const result = await checkValue({}); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 2, + path: ["two"], + }); +}); + +test("child checks are called with value at corresponding key", async () => { + const checkOne = jest.fn(async () => check.ok(1)); + const checkTwo = jest.fn(async () => check.ok(2)); + const checkValue = check.shapeAsync({ + one: checkOne, + two: checkTwo, + }); + await checkValue({ + one: 1, + two: 2, + }); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(1); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(2); +}); + +test("child checks are called with the additional arguments", async () => { + const checkOne = jest.fn(async () => check.ok(1)); + const checkTwo = jest.fn(async () => check.ok(2)); + const checkValue = check.shapeAsync({ + one: checkOne, + two: checkTwo, + }); + await checkValue( + { + one: 1, + two: 2, + }, + "one", + "two", + ); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("correct path is returned with the error", async () => { + const checkValue = check.shapeAsync({ + one: async () => check.error(1, "is wrong", ["two"]), + }); + const result = await checkValue({}); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 1, + path: ["one", "two"], + }); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.shapeAsync({ + one: async () => check.ok(1), + two: () => check.ok(2), + three: async () => check.ok(3), + four: () => check.ok(4), + }); + const result = await checkValue({}); + + expect(result).toEqual({ + isOk: true, + value: { + one: 1, + two: 2, + three: 3, + four: 4, + }, + }); +}); diff --git a/test/shape.test.ts b/test/shape.test.ts index 2e2dda3..58c5844 100644 --- a/test/shape.test.ts +++ b/test/shape.test.ts @@ -1,97 +1,86 @@ import * as check from "../src"; -test("check succeeds when given value has the right shape", () => { - const checkValue = check.shape< - { one: string; two: string }, - { one: string; two: string }, - [] - >({ - one: check.chain(check.trim(), check.is("value one")), - two: check.chain(check.trim(), check.is("value two")), - }); - const result = checkValue({ - one: " value one ", - two: " value two ", +test("check succeeds when all child checks succeed", () => { + const checkValue = check.shape({ + one: () => check.ok(1), + two: () => check.ok(2), }); + const result = checkValue({}); expect(result).toEqual({ isOk: true, value: { - one: "value one", - two: "value two", + one: 1, + two: 2, }, }); }); -test("check fails when given value has the wrong shape", () => { - const checkValue = check.shape< - { one: string; two: string }, - { one: string; two: string }, - [] - >({ - one: check.is("value one", 'is not equal to "value one"'), - two: check.is("value two", 'is not equal to "value two"'), - }); - const result = checkValue({ - one: "value one", - two: "value asdf", +test("check fails when at least one child check fails", () => { + const checkValue = check.shape({ + one: () => check.ok(1), + two: () => check.error(2, "is wrong"), }); + const result = checkValue({}); expect(result).toEqual({ isOk: false, - error: 'is not equal to "value two"', - invalidValue: "value asdf", + error: "is wrong", + invalidValue: 2, path: ["two"], }); }); -test("correct path is returned with the error", () => { - const checkValue = check.shape< - { one: { two: string } }, - { one: { two: string } }, - [] - >({ - one: check.shape({ - two: check.is("valid", "is invalid"), - }), +test("child checks are called with value at corresponding key", () => { + const checkOne = jest.fn(() => check.ok(1)); + const checkTwo = jest.fn(() => check.ok(2)); + const checkValue = check.shape({ + one: checkOne, + two: checkTwo, }); - const result = checkValue({ - one: { - two: "invalid", - }, + checkValue({ + one: 1, + two: 2, }); - expect(result).toEqual({ - isOk: false, - error: "is invalid", - invalidValue: "invalid", - path: ["one", "two"], - }); + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(1); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(2); }); -test("additional arguments are passed to the child checks", () => { - const checkValue = check.shape< - { one: unknown; two: unknown }, - { one: unknown[]; two: unknown[] }, - unknown[] - >({ - one: (value, ...args) => check.ok(args), - two: (value, ...args) => check.ok(args), +test("child checks are called with the additional arguments", () => { + const checkOne = jest.fn(() => check.ok(1)); + const checkTwo = jest.fn(() => check.ok(2)); + const checkValue = check.shape({ + one: checkOne, + two: checkTwo, }); - const result = checkValue( + checkValue( { - one: "value one", - two: "value two", + one: 1, + two: 2, }, - "arg one", - "arg two", + "one", + "two", ); + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("correct path is returned with the error", () => { + const checkValue = check.shape({ + one: () => check.error(1, "is wrong", ["two"]), + }); + const result = checkValue({}); + expect(result).toEqual({ - isOk: true, - value: { - one: ["arg one", "arg two"], - two: ["arg one", "arg two"], - }, + isOk: false, + error: "is wrong", + invalidValue: 1, + path: ["one", "two"], }); }); diff --git a/test/test-async.test.ts b/test/test-async.test.ts new file mode 100644 index 0000000..7865fa2 --- /dev/null +++ b/test/test-async.test.ts @@ -0,0 +1,108 @@ +import * as check from "../src"; + +test("check succeeds when given predicate succeeds", async () => { + const checkValue = check.testAsync(async () => true); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 42, + }); +}); + +test("check fails when given predicate fails", async () => { + const checkValue = check.testAsync(async () => false); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is invalid", + invalidValue: 42, + path: [], + }); +}); + +test("given predicate is called with the value to validate", async () => { + const predicate = jest.fn(async () => true); + const checkValue = check.testAsync(predicate); + await checkValue(42); + + expect(predicate).toHaveBeenCalledTimes(1); + expect(predicate).toHaveBeenCalledWith(42); +}); + +test("given predicate is called with the additional arguments", async () => { + const predicate = jest.fn(async () => true); + const checkValue = check.testAsync(predicate); + await checkValue(42, "one", "two"); + + expect(predicate).toHaveBeenCalledTimes(1); + expect(predicate).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("given error is returned with the invalid result", async () => { + const checkValue = check.testAsync(async () => false, "is wrong"); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 42, + path: [], + }); +}); + +test("given path and invalid value are returned with the invalid result", async () => { + const checkValue = check.testAsync( + async () => false, + "is invalid", + ["value"], + () => "invalid value", + ); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: false, + error: "is invalid", + invalidValue: "invalid value", + path: ["value"], + }); +}); + +test("given function to get the invalid value is called with the validated value", async () => { + const getInvalidValue = jest.fn(() => "invalid value"); + const checkValue = check.testAsync( + async () => false, + "is invalid", + ["value"], + getInvalidValue, + ); + await checkValue(42); + + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(42); +}); + +test("given function to get the invalid value is called with the additional arguments", async () => { + const getInvalidValue = jest.fn(() => "invalid value"); + const checkValue = check.testAsync( + async () => false, + "is invalid", + ["value"], + getInvalidValue, + ); + await checkValue(42, "one", "two"); + + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("synchronous predicates are handled", async () => { + const checkValue = check.testAsync(() => true); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: 42, + }); +}); diff --git a/test/test.test.ts b/test/test.test.ts index 4923dbd..32ddc30 100644 --- a/test/test.test.ts +++ b/test/test.test.ts @@ -1,7 +1,7 @@ import * as check from "../src"; -test("check succeeds when given test succeeds", () => { - const checkValue = check.test((value) => value === 42); +test("check succeeds when given predicate succeeds", () => { + const checkValue = check.test(() => true); const result = checkValue(42); expect(result).toEqual({ @@ -10,73 +10,89 @@ test("check succeeds when given test succeeds", () => { }); }); -test("check fails when given test fails", () => { - const checkValue = check.test((value) => value === 42); - const result = checkValue(43); +test("check fails when given predicate fails", () => { + const checkValue = check.test(() => false); + const result = checkValue(42); expect(result).toEqual({ isOk: false, error: "is invalid", - invalidValue: 43, + invalidValue: 42, path: [], }); }); +test("given predicate is called with the value to validate", () => { + const predicate = jest.fn(() => true); + const checkValue = check.test(predicate); + checkValue(42); + + expect(predicate).toHaveBeenCalledTimes(1); + expect(predicate).toHaveBeenCalledWith(42); +}); + +test("given predicate is called with the additional arguments", () => { + const predicate = jest.fn(() => true); + const checkValue = check.test(predicate); + checkValue(42, "one", "two"); + + expect(predicate).toHaveBeenCalledTimes(1); + expect(predicate).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + test("given error is returned with the invalid result", () => { - const checkValue = check.test((value) => value === 42, "is not 42"); - const result = checkValue(43); + const checkValue = check.test(() => false, "is wrong"); + const result = checkValue(42); expect(result).toEqual({ isOk: false, - error: "is not 42", - invalidValue: 43, + error: "is wrong", + invalidValue: 42, path: [], }); }); test("given path and invalid value are returned with the invalid result", () => { const checkValue = check.test( - ({ value }) => value === 42, + () => false, "is invalid", ["value"], - ({ value }) => value, + () => "invalid value", ); - const result = checkValue({ value: 43 }); + const result = checkValue(42); expect(result).toEqual({ isOk: false, error: "is invalid", - invalidValue: 43, + invalidValue: "invalid value", path: ["value"], }); }); -test("additional arguments are passed to the test function", () => { +test("given function to get the invalid value is called with the validated value", () => { + const getInvalidValue = jest.fn(() => "invalid value"); const checkValue = check.test( - (value, ...args) => - args.length === 2 && args[0] === "one" && args[1] === "two", + () => false, + "is invalid", + ["value"], + getInvalidValue, ); - const result = checkValue(42, "one", "two"); + checkValue(42); - expect(result).toEqual({ - isOk: true, - value: 42, - }); + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(42); }); -test("additional arguments are passed to the function to get the invalid value", () => { - const checkValue = check.test( - (value, ...args: unknown[]) => value === 42, +test("given function to get the invalid value is called with the additional arguments", () => { + const getInvalidValue = jest.fn(() => "invalid value"); + const checkValue = check.test( + () => false, "is invalid", - [], - (value, ...args: unknown[]) => args, + ["value"], + getInvalidValue, ); - const result = checkValue(43, "one", "two"); + checkValue(42, "one", "two"); - expect(result).toEqual({ - isOk: false, - error: "is invalid", - invalidValue: ["one", "two"], - path: [], - }); + expect(getInvalidValue).toHaveBeenCalledTimes(1); + expect(getInvalidValue).toHaveBeenCalledWith(expect.anything(), "one", "two"); }); diff --git a/test/toBoolean.test.ts b/test/to-boolean.test.ts similarity index 100% rename from test/toBoolean.test.ts rename to test/to-boolean.test.ts diff --git a/test/toDate.test.ts b/test/to-date.test.ts similarity index 100% rename from test/toDate.test.ts rename to test/to-date.test.ts diff --git a/test/toLower.test.ts b/test/to-lower.test.ts similarity index 100% rename from test/toLower.test.ts rename to test/to-lower.test.ts diff --git a/test/toNumber.test.ts b/test/to-number.test.ts similarity index 100% rename from test/toNumber.test.ts rename to test/to-number.test.ts diff --git a/test/toString.test.ts b/test/to-string.test.ts similarity index 100% rename from test/toString.test.ts rename to test/to-string.test.ts diff --git a/test/toUpper.test.ts b/test/to-upper.test.ts similarity index 100% rename from test/toUpper.test.ts rename to test/to-upper.test.ts diff --git a/test/transform-async.test.ts b/test/transform-async.test.ts new file mode 100644 index 0000000..92e5a6e --- /dev/null +++ b/test/transform-async.test.ts @@ -0,0 +1,41 @@ +import * as check from "../src"; + +test("check succeeds with transformed value", async () => { + const checkValue = check.transformAsync(async () => "jerome"); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: "jerome", + }); +}); + +test("given transform function is called with the value to transform", async () => { + const transform = jest.fn(async () => "jerome"); + const checkValue = check.transformAsync(transform); + await checkValue(42); + + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(42); +}); + +test("given transform function is called with the additional arguments", async () => { + const transform = jest.fn(async () => "jerome"); + const checkValue = check.transformAsync( + transform, + ); + await checkValue(42, "one", "two"); + + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("synchronous transforms are handled", async () => { + const checkValue = check.transformAsync(() => "jerome"); + const result = await checkValue(42); + + expect(result).toEqual({ + isOk: true, + value: "jerome", + }); +}); diff --git a/test/transform.test.ts b/test/transform.test.ts index f1bcea1..43ce1cc 100644 --- a/test/transform.test.ts +++ b/test/transform.test.ts @@ -1,21 +1,29 @@ import * as check from "../src"; test("check succeeds with transformed value", () => { - const checkValue = check.transform(String); + const checkValue = check.transform(() => "jerome"); const result = checkValue(42); expect(result).toEqual({ isOk: true, - value: "42", + value: "jerome", }); }); -test("additional arguments are passed to the transformed function", () => { - const checkValue = check.transform((value, ...args) => args); - const result = checkValue(42, "one", "two"); +test("given transform function is called with the value to transform", () => { + const transform = jest.fn(() => "jerome"); + const checkValue = check.transform(transform); + checkValue(42); - expect(result).toEqual({ - isOk: true, - value: ["one", "two"], - }); + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(42); +}); + +test("given transform function is called with the additional arguments", () => { + const transform = jest.fn(() => "jerome"); + const checkValue = check.transform(transform); + checkValue(42, "one", "two"); + + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(expect.anything(), "one", "two"); }); diff --git a/test/tuple-async.test.ts b/test/tuple-async.test.ts new file mode 100644 index 0000000..bab6733 --- /dev/null +++ b/test/tuple-async.test.ts @@ -0,0 +1,117 @@ +import * as check from "../src"; + +test("check succeeds when all child checks succeed", async () => { + const checkValue = check.tupleAsync([ + async () => check.ok(1), + async () => check.ok(2), + ]); + const result = await checkValue(["one", "two"]); + + expect(result).toEqual({ + isOk: true, + value: [1, 2], + }); +}); + +test("check fails when given value does not have the right length", async () => { + const checkValue = check.tupleAsync([ + async () => check.error(1, "is wrong"), + async () => check.error(2, "is wrong"), + ]); + const result = await checkValue(["one"]); + + expect(result).toEqual({ + isOk: false, + error: "does not have 2 items", + invalidValue: ["one"], + path: [], + }); +}); + +test("check fails when at least one child check fails", async () => { + const checkValue = check.tupleAsync([ + async () => check.ok(1), + async () => check.error(2, "is wrong"), + async () => check.error(3, "is wrong"), + ]); + const result = await checkValue(["one", "two", "three"]); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 2, + path: [1], + }); +}); + +test("child checks are called with value at corresponding index", async () => { + const checkOne = jest.fn(async () => check.ok(1)); + const checkTwo = jest.fn(async () => check.ok(2)); + const checkValue = check.tupleAsync([checkOne, checkTwo]); + await checkValue(["one", "two"]); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith("one"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith("two"); +}); + +test("child checks are called with the additional arguments", async () => { + const checkOne = jest.fn(async () => check.ok(1)); + const checkTwo = jest.fn(async () => check.ok(2)); + const checkValue = check.tupleAsync([ + checkOne, + checkTwo, + ]); + await checkValue([1, 2], "one", "two"); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + +test("given length error is returned with the invalid result", async () => { + const checkValue = check.tupleAsync( + [async () => check.ok(1), async () => check.ok(2)], + "does not have two items", + ); + const result = await checkValue(["one"]); + + expect(result).toEqual({ + isOk: false, + error: "does not have two items", + invalidValue: ["one"], + path: [], + }); +}); + +test("correct path is returned with the item error", async () => { + const checkValue = check.tupleAsync([ + async () => check.ok(1), + async () => check.error(2, "is wrong", [2]), + ]); + const result = await checkValue(["one", "two"]); + + expect(result).toEqual({ + isOk: false, + error: "is wrong", + invalidValue: 2, + path: [1, 2], + }); +}); + +test("synchronous child checks are handled", async () => { + const checkValue = check.tupleAsync([ + async () => check.ok(1), + () => check.ok(2), + async () => check.ok(3), + () => check.ok(4), + ]); + const result = await checkValue(["one", "two", "three", "four"]); + + expect(result).toEqual({ + isOk: true, + value: [1, 2, 3, 4], + }); +}); diff --git a/test/tuple.test.ts b/test/tuple.test.ts index d36892d..f3ff731 100644 --- a/test/tuple.test.ts +++ b/test/tuple.test.ts @@ -1,93 +1,99 @@ import * as check from "../src"; -test("check succeeds when given value has the right shape", () => { - const checkValue = check.tuple([ - check.chain(check.trim(), check.is("value one")), - check.chain(check.trim(), check.is("value two")), - ]); - const result = checkValue([" value one ", " value two "]); +test("check succeeds when all child checks succeed", () => { + const checkValue = check.tuple([() => check.ok(1), () => check.ok(2)]); + const result = checkValue(["one", "two"]); expect(result).toEqual({ isOk: true, - value: ["value one", "value two"], + value: [1, 2], }); }); test("check fails when given value does not have the right length", () => { - const checkValue = check.tuple([ - check.is("value one"), - check.is("value two"), + const checkValue = check.tuple([ + () => check.error(1, "is wrong"), + () => check.error(2, "is wrong"), ]); - const result = checkValue(["asdf"]); + const result = checkValue(["one"]); expect(result).toEqual({ isOk: false, error: "does not have 2 items", - invalidValue: ["asdf"], + invalidValue: ["one"], path: [], }); }); -test("check fails when some items are invalid", () => { - const checkValue = check.tuple([ - check.is("value one"), - check.is("value two", "is invalid two"), - check.is("value three", "is invalid three"), +test("check fails when at least one child check fails", () => { + const checkValue = check.tuple([ + () => check.ok(1), + () => check.error(2, "is wrong"), + () => check.error(3, "is wrong"), ]); - const result = checkValue(["value one", "invalid", "invalid"]); + const result = checkValue(["one", "two", "three"]); expect(result).toEqual({ isOk: false, - error: "is invalid two", - invalidValue: "invalid", + error: "is wrong", + invalidValue: 2, path: [1], }); }); +test("child checks are called with value at corresponding index", () => { + const checkOne = jest.fn(() => check.ok(1)); + const checkTwo = jest.fn(() => check.ok(2)); + const checkValue = check.tuple([checkOne, checkTwo]); + checkValue(["one", "two"]); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith("one"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith("two"); +}); + +test("child checks are called with the additional arguments", () => { + const checkOne = jest.fn(() => check.ok(1)); + const checkTwo = jest.fn(() => check.ok(2)); + const checkValue = check.tuple([ + checkOne, + checkTwo, + ]); + checkValue([1, 2], "one", "two"); + + expect(checkOne).toHaveBeenCalledTimes(1); + expect(checkOne).toHaveBeenCalledWith(expect.anything(), "one", "two"); + expect(checkTwo).toHaveBeenCalledTimes(1); + expect(checkTwo).toHaveBeenCalledWith(expect.anything(), "one", "two"); +}); + test("given length error is returned with the invalid result", () => { - const checkValue = check.tuple( - [check.is("value one"), check.is("value two")], + const checkValue = check.tuple( + [() => check.ok(1), () => check.ok(2)], "does not have two items", ); - const result = checkValue(["value one"]); + const result = checkValue(["one"]); expect(result).toEqual({ isOk: false, error: "does not have two items", - invalidValue: ["value one"], + invalidValue: ["one"], path: [], }); }); test("correct path is returned with the item error", () => { - const checkValue = check.tuple([ - check.tuple([ - check.is("value one", "is invalid"), - check.is("value two", "is invalid"), - ]), + const checkValue = check.tuple([ + () => check.ok(1), + () => check.error(2, "is wrong", [2]), ]); - const result = checkValue([["value one", "invalid"]]); + const result = checkValue(["one", "two"]); expect(result).toEqual({ isOk: false, - error: "is invalid", - invalidValue: "invalid", - path: [0, 1], - }); -}); - -test("additional arguments are passed to the child checks", () => { - const checkValue = check.tuple([ - (value, ...args) => check.ok(args), - (value, ...args) => check.ok(args), - ]); - const result = checkValue(["value one", "value two"], "arg one", "arg two"); - - expect(result).toEqual({ - isOk: true, - value: [ - ["arg one", "arg two"], - ["arg one", "arg two"], - ], + error: "is wrong", + invalidValue: 2, + path: [1, 2], }); });