From e8a84795653dfa7a719689c1cd90e94970b772c7 Mon Sep 17 00:00:00 2001 From: Adam Chalemian Date: Mon, 16 Aug 2021 14:08:45 -0400 Subject: [PATCH] refactor to allow for chaining --- .vscode/settings.json | 1 + README.md | 77 +++++++------ index.ts | 88 ++++++++------- mutator.ts | 12 +-- package.json | 2 +- primitive.ts | 56 ++++++---- test/index.test.ts | 245 +++++++++++++++++++++++------------------- types.ts | 155 ++++++++++++-------------- validator.ts | 16 +-- 9 files changed, 353 insertions(+), 299 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ab3c9f..1d9bb9b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,6 +41,7 @@ "arrayify", "chainable", "codecov", + "nonstring", "o'donnel" ] } diff --git a/README.md b/README.md index f89fb5e..f0b3983 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Coerce -Coerce inputs to chainable types, formatters, sanitizers, and validators. +Coerce input to types and formats with sanitizers and validators. ## Features @@ -17,50 +17,65 @@ npm i @resolute/coerce ## Usage ```js -import coerce, { string, trim, nonEmpty } from '@resolute/coerce'; -``` -trim a string and confirm it is not empty, `.or()` return undefined -```js -coerce(' foo ').to(string, trim, nonEmpty).or(undefined); // 'foo' -coerce(' ').to(string, trim, nonEmpty).or(undefined); // undefined +import coerce, { string, safe, spaces, trim, nonEmpty } from '@resolute/coerce'; +// sanitize input: removing dangerous characters, normalize double/half/utf +// spaces, trim whitespace, and confirm result is non-empty +const sanitize = coerce(string, safe, spaces, trim, nonEmpty); ``` -`.or()` throw an error -```js -coerce(' ').to(string, trim, nonEmpty).or(Error); -// Uncaught Error: Unable to parse “undefined” as a string. -``` - -`.or()` throw a specific error -```js -coerce(' ').to(string, trim, nonEmpty).or(new Error('Unable to parse input.')); -// Uncaught Error: Unable to parse input. -``` +* failures **throw** a coerce TypeError + ```js + sanitize(' foo '); // 'foo' + sanitize(' '); // Uncaught TypeError + ``` +* failures **return** default value (never throws) + ```js + sanitize(' ', undefined); // undefined + ``` +* failures **throw** error instance + ```js + sanitize(' ', new Error('Oh no!')); // Uncaught Error: Oh no! + ``` +* failures **throw** error factory + ```js + class CustomError extends Error { } + const errorFactory = (error: Error) => new CustomError(error.message); + sanitize(' ', errorFactory); // Uncaught CustomError + ``` ## Examples -Confirm a string value is within a list (enum). +Confirm a string value is within a list (enum) ```js import coerce, { within } from '@resolute/coerce'; -const list = ['foo', 'bar']; // any iterable type ok to use +const inList = coerce(within(['foo', 'bar'])); // any iterable type ok to use try { - const item = coerce(input).to(within(list)).or(Error); + inList(input); // input is either 'foo' or 'bar' } catch (error) { // input was not 'foo' or 'bar' } ``` -Convert any iterable (except strings) to an array. Non-iterables return an array -of length=1 containing the non-iterable. +Convert anything to an array ```js import coerce, { array } from '@resolute/coerce'; -const arrayify = (input) => coerce(input).to(array).or(Error); -arrayify(new Map([[1, 1], [2, 2]]); // [[1, 1], [2, 2]] -arrayify(new Set([1, 2, 3])); // [1, 2, 3] -arrayify([1, 2, 3]); // [1, 2, 3] (no change) -arrayify(1); // [1] -arrayify('123'); // ['123'] (NOT ['1', '2', '3'] even though Strings are iterable) -arrayify(Buffer.from('123')); // [49, 50, 51] // Buffer char codes -arrayify(null); // [null] +const arrayify = coerce(array); ``` +* Iterables (except strings) → `Array` + ```js + arrayify(new Map([[1, 1], [2, 2]]); // [[1, 1], [2, 2]] + arrayify(new Set([1, 2, 3])); // [1, 2, 3] + arrayify(Buffer.from('123')); // [49, 50, 51] (char codes) + // even though Strings are iterable, they are NOT broken apart + arrayify('123'); // ['123'] + ``` +* Non-iterables (including strings) → `[item]` (wrapped in array) + ```js + arrayify(1); // [1] + arrayify(null); // [null] + ``` +* Arrays → `Array` (no change) + ```js + arrayify([1, 2, 3]); // [1, 2, 3] + ``` diff --git a/index.ts b/index.ts index dee6532..159a578 100644 --- a/index.ts +++ b/index.ts @@ -1,57 +1,67 @@ -import type { To } from './types.js'; +import type { Coerce } from './types.js'; /** - * Pipe/flow/chain the input and output of each function + * 1. throw Error; or + * 2. throw error function factory; or + * 3. return the `otherwise` value */ -const pipe = any>( - value: unknown, - functions: S[], - index: number, -): ReturnType => { - if (index === functions.length) { - return value as ReturnType; - } - const newValue = functions[index](value); - return pipe(newValue, functions, index + 1); -}; - -/** - * Throw Error or throw user-passed function or return a default value - */ -const handleFailure = (otherwise: U, error: Error) => { +const failure = (error: Error, otherwise: U) => { if (typeof otherwise === 'function') { throw otherwise(error); } if (typeof otherwise === 'object' && otherwise instanceof Error) { throw otherwise; } - return otherwise as Exclude; + return otherwise; }; /** - * Coerce a value `.to()` specified type `.or()` on failure return a default - * value or throw an `Error`. + * Pipe the input through coerce functions + */ +const pipe = any>(value: V, coercer: C) => + coercer(value); + +/** + * Handles issues where passing otherwise: undefined triggers the default + * TypeError value. This workaround determines if the default otherwise: + * TypeError should be used based on the argument count passed to the function. + * This is instead of simply using a default parameter value, which would not + * work in the case where undefined is passed. */ -export const coerce = (value: I): To => ({ - /** - * `.to()` one or many sanitizer functions - */ - to: (...sanitizers: Array<(value: any) => any>) => ({ - /** - * `.or()` return a default value, throw a specific `Error`, or throws a the - * `Error` returned from a function. - */ - or: (otherwise: U) => { - try { - return pipe(value, sanitizers, 0); - } catch (error) { - return handleFailure(otherwise, error); - } - }, - }), -}); +const params = (args: unknown[]) => { + if (args.length === 1) { + return [args[0] as unknown, TypeError] as const; + } + return args; +}; + +/** +* Coerce a value +* `coerce(...coercers)(value[, default])` +* +* @example +* // trim a string and confirm it is not empty +* const trimCheckEmpty = coerce(string, trim, nonEmpty); +* +* trimCheckEmpty(' foo '); // 'foo' +* trimCheckEmpty(' '); // Error +* +* // alternatively, return undefined instead of throwing error +* trimCheckEmpty(' ', undefined); // undefined +* +*/ +export const coerce: Coerce = (...coercers: any[]) => + (...args: unknown[]) => { + const [value, otherwise] = params(args); + try { + return coercers.reduce(pipe, value); + } catch (error) { + return failure(error, otherwise); + } + }; export default coerce; export * from './primitive.js'; export * from './validator.js'; export * from './mutator.js'; +export type { Coerce, Coercer } from './types.js'; diff --git a/mutator.ts b/mutator.ts index 13740d7..0b0834d 100644 --- a/mutator.ts +++ b/mutator.ts @@ -1,4 +1,4 @@ -import { nonEmpty } from './validator.js'; +import { nonempty } from './validator.js'; /** * Remove dangerous characters from string @@ -129,7 +129,7 @@ export const proper = (value: string) => * Format email addresses */ export const email = (value: string) => - nonEmpty(value.toLowerCase().replace(/\s+/g, '')); + nonempty(value.toLowerCase().replace(/\s+/g, '')); /** * Strip all non-digit characters from string @@ -142,7 +142,7 @@ export const digits = (value: string) => value.replace(/[^\d]/g, ''); export const phone = (value: string) => { const onlyDigits = digits(value).replace(/^[01]+/, ''); if (onlyDigits.length < 10) { - throw new Error('Invalid US phone number.'); + throw new TypeError('Invalid US phone number.'); } return onlyDigits; }; @@ -153,7 +153,7 @@ export const phone = (value: string) => { export const phone10 = (value: string) => { const valid = phone(value); if (valid.length !== 10) { - throw new Error('Invalid US 10-digit phone number.'); + throw new TypeError('Invalid US 10-digit phone number.'); } return valid; }; @@ -175,7 +175,7 @@ export const prettyPhone = (value: string) => { export const postalCodeUs5 = (value: string) => { const code = digits(value).slice(0, 5); if (code.length !== 5) { - throw new Error('Invalid US postal code'); + throw new TypeError('Invalid US postal code'); } return code; }; @@ -204,7 +204,7 @@ export const limit = (max: number) => if (Array.isArray(value)) { return value.slice(0, max) as T; } - throw new Error(`Unable to apply a max of ${max} to ${value}`); + throw new TypeError(`Unable to apply a max of ${max} to ${value}`); }; /** diff --git a/package.json b/package.json index d4e760f..76e969b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@resolute/coerce", "version": "0.0.0", - "description": "Coerce inputs to chainable types, formatters, sanitizers, and validators.", + "description": "Coerce input to types and formats with sanitizers and validators.", "type": "module", "main": "./", "types": "./", diff --git a/primitive.ts b/primitive.ts index 63772a0..30d8836 100644 --- a/primitive.ts +++ b/primitive.ts @@ -1,4 +1,5 @@ -import { nonEmpty, nonZero } from './validator.js'; +import { coerce } from './index.js'; +import { nonempty, nonZero } from './validator.js'; /** * Coerce value to primitive `string` @@ -10,9 +11,17 @@ export const string = (value: string | number | bigint) => { if ((typeof value === 'number' && Number.isFinite(value)) || typeof value === 'bigint') { return value.toString(); } - throw new Error(`Unable to parse “${value}” as a string.`); + throw new TypeError(`Unable to parse “${value}” as a string.`); }; +export const nonstring = (value: T) => { + if (typeof value === 'string') { + throw new TypeError(`${value} is a string.`); + } + return value as Exclude; +}; +export const notString = nonstring; + /** * Coerce value to `number` */ @@ -21,7 +30,7 @@ export const number = (value: string | number | bigint): number => { return value as number; } // remove everything except characters allowed in a number - return number(Number(nonEmpty(string(value).replace(/[^0-9oex.-]/g, '')))); + return number(Number(nonempty(string(value).replace(/[^0-9oex.-]/g, '')))); }; /** @@ -36,14 +45,14 @@ export const date = (value: number | string | Date) => { /** * Boolean */ -interface Boolean { +interface CoerceBoolean { (): (value: unknown) => true | false; (truthy: T): (value: unknown) => T | false; (truthy: T, falsey: F): (value: unknown) => T | F; (truthy: T, falsey: F, nully: N): (value: unknown) => T | F | N; (truthy: T, falsey: F, nully: N, undefy: U): (value: unknown) => T | F | N | U; } -export const boolean: Boolean = +export const boolean: CoerceBoolean = (truthy: any = true, falsy: any = false, nully: any = falsy, undefy: any = falsy) => (value: unknown) => { switch (typeof value) { @@ -73,24 +82,33 @@ export const boolean: Boolean = return value ? truthy : falsy; }; -const isIterable = (value: Iterable | any): value is Iterable => { +/** + * Confirm `value` is Iterable + */ +export const iterable = (value: Iterable | T) => { + // export const iterable:CoerceIterable = + // >>(value: Iterable | U) => { if (typeof value === 'object' && value && typeof value[Symbol.iterator] === 'function') { - return true; + // if (isIterable(value)) { + return value as Iterable; } - return false; + throw new Error(`${value} is not iterable`); }; /** -* `value` as an array if not an array -*/ -export const array = (value: T | T[] | Iterable) => { - if (Array.isArray(value)) { - return value; - } - // a `string` _is_ Iterable, but we do not want to return an array of - // characters - if (typeof value !== 'string' && isIterable(value)) { - return [...value]; + * `value` as an array if not an array + */ +interface CoerceArray { + (input: Iterable): T[]; + (input: T): T[]; +} +export const array: CoerceArray = (value: Iterable | U) => { + try { + // a `string` _is_ Iterable, but we do not want to return an array of + // characters + const iterableValue = coerce(nonstring, iterable)(value); + return [...iterableValue]; + } catch { + return [value] as U[]; } - return [value]; }; diff --git a/test/index.test.ts b/test/index.test.ts index 11f44fd..781de48 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,168 +1,189 @@ /* eslint-disable no-eval */ -import test from 'ava'; +import test, { Macro } from 'ava'; import { coerce, - string, safe, nonEmpty, spaces, trim, quotes, proper, postalCodeUs5, + string, safe, nonempty, spaces, trim, quotes, proper, postalCodeUs5, boolean, array, number, positive, negative, - limit, split, within, email, phone, phone10, prettyPhone, integer, nonZero, date, + limit, split, within, email, phone, phone10, prettyPhone, integer, nonzero, date, } from '../index.js'; -const pass = any>(input: string, expected: S, ...sanitizers: T[]) => { - test(`${input} ✅ ${sanitizers.map(({ name }) => name).join(', ')}`, - (t) => t.deepEqual( - coerce(eval(input)).to(...sanitizers).or(Error), - expected, - )); +const pass: Macro = (t, command, input, expected) => { + t.deepEqual(command(eval(input)), expected); }; +pass.title = (providedTitle = '', _command, input, expected) => + `${providedTitle} ${input} = ${expected} (${typeof expected})`.trim(); -const fail = any>(input: string, ...sanitizers: T[]) => { - const expected = new Error('The expected error that coerce threw.'); - expected.name = 'ExpectedError'; - test(`${input} ❌ ${sanitizers.map(({ name }) => name).join(', ')}`, - (t) => { - t.throws(() => { - coerce(eval(input)).to(...sanitizers).or(expected); - }, { is: expected }); - }); +const fail: Macro = (t, command, input) => { + t.throws(() => { command(eval(input)); }, { instanceOf: Error }); }; +fail.title = (providedTitle = '', _command, input) => + `${providedTitle} ${input} TypeError`.trim(); // string -pass("'1'", '1', string); -pass('1', '1', string); -fail('true', string); -fail('Symbol(1)', string); -fail('new Error("foo")', string); -fail('Buffer.from("foo")', string); -fail('["1"]', string); -fail('NaN', string); -fail('Infinity', string); -fail('null', string); -fail('undefined', string); -fail('{}', string); -fail('{ function toString() { return "1"; } }', string); -fail('{ function noToStringMethod() { return "1"; } }', string); +test(pass, coerce(string), "'1'", '1'); +test(pass, coerce(string), '1', '1'); +test(fail, coerce(string), 'true'); +test(fail, coerce(string), 'Symbol(1)'); +test(fail, coerce(string), 'new Error("foo")'); +test(fail, coerce(string), 'Buffer.from("foo")'); +test(fail, coerce(string), '["1"]'); +test(fail, coerce(string), '-Infinity'); +test(fail, coerce(string), 'null'); +test(fail, coerce(string), 'undefined'); +test(fail, coerce(string), '{}'); +test(fail, coerce(string), '{ function toString() { return "1"; } }'); +test(fail, coerce(string), '{ function noToStringMethod() { return "1"; } }'); // trim -pass("' \t foo \\n \t'", 'foo', trim); +test(pass, coerce(trim), "' \t foo \\n \t'", 'foo'); // spaces // eslint-disable-next-line no-template-curly-in-string -pass('`${String.fromCharCode(0x200A)}foo `', ' foo ', spaces); +test(pass, coerce(spaces), '`${String.fromCharCode(0x200A)}foo `', ' foo '); // nonEmpty -pass('" "', ' ', nonEmpty); +test(pass, coerce(nonempty), '" "', ' '); // safe -pass('\'INSERT INTO `foo` VALUES ("bar")\'', 'INSERT INTO foo VALUES bar', safe); +test(pass, coerce(safe), '\'INSERT INTO `foo` VALUES ("bar")\'', 'INSERT INTO foo VALUES bar'); // proper -pass("'abc company'", 'Abc Company', quotes, proper); -pass("'ABC company'", 'ABC Company', quotes, proper); -pass('"john q. o\'donnel, III"', 'John Q O’Donnel, III', quotes, proper); -pass("'VON Trap'", 'von Trap', quotes, proper); +test(pass, coerce(quotes, proper), "'abc company'", 'Abc Company'); +test(pass, coerce(quotes, proper), "'ABC company'", 'ABC Company'); +test(pass, coerce(quotes, proper), '"john q. o\'donnel, III"', 'John Q O’Donnel, III'); +test(pass, coerce(quotes, proper), "'VON Trap'", 'von Trap'); // Postal Code US -pass("'10001-1234'", '10001', postalCodeUs5); -pass("'07417'", '07417', postalCodeUs5); -pass("'07417-1111'", '07417', postalCodeUs5); -fail("'0741'", postalCodeUs5); -fail('10001', postalCodeUs5); // numbers not allowed because leading 0’s mess things up +test(pass, coerce(postalCodeUs5), "'10001-1234'", '10001'); +test(pass, coerce(postalCodeUs5), "'07417'", '07417'); +test(pass, coerce(postalCodeUs5), "'07417-1111'", '07417'); +test(fail, coerce(postalCodeUs5), "'0741'"); +test(fail, coerce(postalCodeUs5), '10001'); // numbers not allowed because leading 0’s mess things up // boolean const trueOrFalse = boolean(); Object.defineProperty(trueOrFalse, 'name', { value: 'boolean' }); -pass('undefined', false, trueOrFalse); -pass('null', false, trueOrFalse); -pass("''", false, trueOrFalse); -pass('false', false, trueOrFalse); -pass("'false'", false, trueOrFalse); -pass("'0'", false, trueOrFalse); -pass('0', false, trueOrFalse); -pass('({})', true, trueOrFalse); -pass('new Error()', true, trueOrFalse); -pass('1', true, trueOrFalse); -pass("'foo'", true, trueOrFalse); +test(pass, coerce(trueOrFalse), 'undefined', false); +test(pass, coerce(trueOrFalse), '(null)', false); +test(pass, coerce(trueOrFalse), "''", false); +test(pass, coerce(trueOrFalse), 'false', false); +test(pass, coerce(trueOrFalse), "'false'", false); +test(pass, coerce(trueOrFalse), "'0'", false); +test(pass, coerce(trueOrFalse), '0', false); +test(pass, coerce(trueOrFalse), '({})', true); +test(pass, coerce(trueOrFalse), 'new Error()', true); +test(pass, coerce(trueOrFalse), '1', true); +test(pass, coerce(trueOrFalse), "'foo'", true); // array -pass("new Map([[1, '1']])", [[1, '1']], array); -pass("new Set(['1', '2'])", ['1', '2'], array); -pass("['1']", ['1'], array); -pass("'123'", ['123'], array); // not ['1', '2', '3'] even though Strings are iterable -pass("Buffer.from('123')", [49, 50, 51], array); // Buffer will be char codes -pass('null', [null], array); -pass('true', [true], array); -pass('undefined', [undefined], array); -pass('new WeakSet()', [new WeakSet()], array); // WeakSet non-iterable, so it gets wrapped in array +test(pass, coerce(array), "new Map([[1, '1']])", [[1, '1']]); +test(pass, coerce(array), "new Set(['1', '2'])", ['1', '2']); +test(pass, coerce(array), "['1']", ['1']); +test(pass, coerce(array), "'123'", ['123']); // not ['1', '2', '3'] even though Strings are iterable +test(pass, coerce(array), "Buffer.from('123')", [49, 50, 51]); // Buffer will be char codes +test(pass, coerce(array), 'true', [true]); +test(pass, coerce(array), 'undefined', [undefined]); +test(pass, coerce(array), 'new WeakSet()', [new WeakSet()]); // WeakSet non-iterable, so it gets wrapped in array // number -fail('NaN', number); -fail('Infinity', number); -fail("'foo'", number); -fail("''", number); -fail("'-1.234.5'", number); -fail('0', positive); -fail('0', negative); -pass('0o10', 8, number); -pass('0xff', 255, number); -pass('2e3', 2000, number); -pass('1n', 1, number); -pass('1', 1, number); -pass('1', 1, number, positive); -pass("'-1.234'", -1.234, number); -pass("'0'", 0, number); -pass('-0.5', -0.5, number, negative); -pass('-1', -1, number, negative); -pass("'-1.234'", -1.234, number, negative); -pass('1.2', 1, nonZero, integer); -fail('0', nonZero, integer); -fail('', number, nonZero); +test(fail, coerce(number), 'NaN'); +test(fail, coerce(number), 'Infinity'); +test(fail, coerce(number), "'foo'"); +test(fail, coerce(number), "''"); +test(fail, coerce(number), "'-1.234.5'"); +test(fail, coerce(positive), '+0'); +test(fail, coerce(negative), '-0'); +test(pass, coerce(number), '0o10', 8); +test(pass, coerce(number), '0xff', 255); +test(pass, coerce(number), '2e3', 2000); +test(pass, coerce(number), '1n', 1); +test(pass, coerce(number), '1.1', 1.1); +test(pass, coerce(number, positive), '1.2', 1.2); +test(pass, coerce(number), "'-1.234'", -1.234); +test(pass, coerce(number), "'0'", 0); +test(pass, coerce(number, negative), '-0.5', -0.5); +test(pass, coerce(number, negative), '-1', -1); +test(pass, coerce(number, negative), "'-2.345'", -2.345); +test(pass, coerce(nonzero, integer), '1.2', 1); +test(fail, coerce(nonzero, integer), '0'); +test(fail, coerce(number, nonzero), ''); // limit const limit3 = limit(3); Object.defineProperty(limit3, 'name', { value: 'limit' }); -pass('5', 3, limit3); -pass("'foobar'", 'foo', limit3); -pass('[1, 2, 3, 4, 5]', [1, 2, 3], limit3); -fail('({})', limit3); -fail('null', limit3); +test(pass, coerce(limit3), '5', 3); +test(pass, coerce(limit3), "'foobar'", 'foo'); +test(pass, coerce(limit3), '[1, 2, 3, 4, 5]', [1, 2, 3]); +test(fail, coerce(limit3), '({})'); +test(fail, coerce(limit3), '((null))'); // split const splitBasic = split(); Object.defineProperty(splitBasic, 'name', { value: 'split' }); -pass("'a,b,,,c d e foo'", ['a', 'b', 'c', 'd', 'e', 'foo'], splitBasic); -pass("',,,,,, , , '", [], splitBasic); +test(pass, coerce(splitBasic), "'a,b,,,c d e foo'", ['a', 'b', 'c', 'd', 'e', 'foo']); +test(pass, coerce(splitBasic), "',,,,,, , , '", []); // within const withinList = within(['foo', 'bar']); Object.defineProperty(withinList, 'name', { value: 'within' }); -pass("'foo'", 'foo', withinList); -fail("'baz'", withinList); +test(pass, coerce(withinList), "'foo'", 'foo'); +test(fail, coerce(withinList), "'baz'"); // email -pass("' Foo@Bar.com'", 'foo@bar.com', email); -pass("'foo '", 'foo', email); // this will also pass as the @ format is not validated +test(pass, coerce(email), "' Foo@Bar.com'", 'foo@bar.com'); +test(pass, coerce(email), "'foo '", 'foo'); // this will also pass as the @ format is not validated // phone -pass("'+1 (222) 333-4444x555'", '2223334444555', phone); -pass("'+1 (222) 333-4444'", '2223334444', phone10); -pass("'+1 (222) 333-4444'", '(222) 333-4444', prettyPhone); -pass("'+1 (222) 333-4444x555'", '(222) 333-4444 ext 555', prettyPhone); -fail("'+1 (222) 333-444'", phone10); -fail("'+1 (222) 333-44444'", phone10); +test(pass, coerce(phone), "'+1 (222) 333-4444x555'", '2223334444555'); +test(pass, coerce(prettyPhone), "'+1 (333) 444-5555'", '(333) 444-5555'); +test(pass, coerce(prettyPhone), "'+1 (444) 555-6666x777'", '(444) 555-6666 ext 777'); +test(pass, coerce(phone10), "'+1 (222) 333-4444'", '2223334444'); +test(fail, coerce(phone10), "'+1 (222) 333-444'"); +test(fail, coerce(phone10), "'+1 (222) 333-44444'"); // date -pass('1628623372929', new Date(1628623372929), date); +test(pass, coerce(date), '1628623372929', new Date(1628623372929)); -// test coerce .or() with a default value -test('coerce(…).to(…).or(0)', (t) => { - const defaultValue = 1; - t.is(coerce('foo').to(number).or(defaultValue), defaultValue); +// test coerce with a default value +test('coerce(…)(…, 0)', (t) => { + const defaultValue = undefined; + t.is(coerce(number)('foo', defaultValue), defaultValue); }); -// test coerce .or() with an Error returning function -test('coerce(…).to(…).or(()=>Error))', (t) => { - const errorHandler = (error: string) => new Error(error); - t.throws(() => coerce('foo').to(number).or(errorHandler)); +// test coerce throw specific Error instance +test('coerce(…)(…, new Error(…)))', (t) => { + const predefinedError = new Error('this is my error, there are many like it…'); + t.throws(() => coerce(number)('foo', predefinedError), { + is: predefinedError, + }); }); + +// test coerce Error function factory +test('coerce(…)(…, () => CustomError))', (t) => { + class CustomError extends Error { } + const errorHandler = (error: Error) => new CustomError(error.message); + t.throws(() => coerce(number)('foo', errorHandler), { + instanceOf: CustomError, + }); +}); + +// const foo = coerce(number, date, string)([1], new Error('foo')); +// const bar = coerce()([1]); +// const bar2 = coerce()(1); +// const baz = coerce(string, trim)(1); +// const baz = coerce(...[string, number] as const)(1); +// const c1 = coerce(number, string, date); +// type foobar = ReturnType; +// const c2 = coerce(c1)('foo'); +// const c3 = c1('foo'); +// const d1 = coerceAlt('foo').to(string, number).or(undefined); + +// const foo = array(Symbol('foo')); +// const foo = array(new Set(['foo'])); +// const foo = array(true); +// const foo = array(null); +// const foo = array(Buffer.from('foo')); +// const foo = array('foo'); +// const foo = array(['foo', 1]); diff --git a/types.ts b/types.ts index 1d599dc..20696af 100644 --- a/types.ts +++ b/types.ts @@ -1,87 +1,74 @@ -export interface Or { - /** - * `.or` return a default value, throw a specific `Error`, or throws a the - * `Error` returned from a function. - */ - or: { - /** - * `.or` **throw** a function that returns an error. Your function will - * receive the Error from the failing coerce function. - */ - (otherwise: ((error: Error) => Error)): T; - /** - * `.or` **throw** a specific error. - */ - (otherwise: Error): T; - /** - * **WARNING** `.or` function _must_ produce an instance of `Error`, because - * it will be `throw`n. - */ - (otherwise: Function): unknown; - /** - * `.or` **return** a default value. - */ - (otherwise: U): T | Exclude; - } +export interface Coercer { + (value: unknown, otherwise: E): O | Exclude; + (value: I): [O] extends [never] ? I : O; } -export interface To { - to: { - (): Or; - ( - s1: (input: I) => A, - ): Or; - ( - s1: (input: I) => A, - s2: (input: A) => B, - ): Or; - ( - s1: (input: I) => A, - s2: (input: A) => B, - s3: (input: B) => C, - ): Or; - ( - s1: (input: I) => A, - s2: (input: A) => B, - s3: (input: B) => C, - s4: (input: C) => D, - ): Or; - ( - s1: (input: I) => A, - s2: (input: A) => B, - s3: (input: B) => C, - s4: (input: C) => D, - s5: (input: D) => E, - ): Or; - ( - s1: (input: I) => A, - s2: (input: A) => B, - s3: (input: B) => C, - s4: (input: C) => D, - s5: (input: D) => E, - s6: (input: E) => F, - ): Or; - ( - s1: (input: I) => A, - s2: (input: A) => B, - s3: (input: B) => C, - s4: (input: C) => D, - s5: (input: D) => E, - s6: (input: E) => F, - s7: (input: F) => G, - ): Or; - ( - s1: (input: I) => A, - s2: (input: A) => B, - s3: (input: B) => C, - s4: (input: C) => D, - s5: (input: D) => E, - s6: (input: E) => F, - s7: (input: F) => G, - s8: (input: G) => H, - ): Or; - ( - ...sN: ((input: A) => A)[] - ): Or; - } +export interface Coerce { + (): Coercer; + , B>( + ab: (...a: A) => B + ): Coercer; + , B, C>( + ab: (...a: A) => B, + bc: (b: B) => C + ): Coercer; + , B, C, D>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D + ): Coercer; + , B, C, D, E>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E + ): Coercer; + , B, C, D, E, F>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F + ): Coercer; + , B, C, D, E, F, G>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G + ): Coercer; + , B, C, D, E, F, G, H>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H + ): Coercer; + , B, C, D, E, F, G, H, I>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I + ): Coercer; + , B, C, D, E, F, G, H, I, J>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J + ): Coercer; + ( + ...az: ((a: A) => Z)[] + ): Coercer; } diff --git a/validator.ts b/validator.ts index 2acb854..1e29841 100644 --- a/validator.ts +++ b/validator.ts @@ -1,22 +1,24 @@ /** * Confirm string is _not_ empty */ -export const nonEmpty = (value: string) => { +export const nonempty = (value: string) => { if (value === '') { - throw new Error('Empty string'); + throw new TypeError('Empty string'); } return value; }; +export const nonEmpty = nonempty; /** * Confirm input is _not_ 0 */ -export const nonZero = (value: number) => { +export const nonzero = (value: number) => { if (value !== 0) { return value; } - throw new Error(`“${value}” must be a non-zero number.`); + throw new TypeError(`“${value}” must be a non-zero number.`); }; +export const nonZero = nonzero; /** * Confirm that input is greater than 0 @@ -25,7 +27,7 @@ export const positive = (value: number) => { if (value > 0) { return value; } - throw new Error(`“${value}” is not a positive number.`); + throw new TypeError(`“${value}” is not a positive number.`); }; /** @@ -35,7 +37,7 @@ export const negative = (value: number) => { if (value < 0) { return value; } - throw new Error(`“${value}” is not a negative number.`); + throw new TypeError(`“${value}” is not a negative number.`); }; /** @@ -50,5 +52,5 @@ export const within = (list: T[]) if (index >= 0) { return list[index]; } - throw new Error(`“${value}” must be one of ${list}`); + throw new TypeError(`“${value}” must be one of ${list}`); };