UNPKG

@qavajs/validation

Version:

Lib that transform plain english definition to validation functions

152 lines (139 loc) 6.29 kB
import { expect } from './matchers'; export const validations = { EQUAL: 'equal', DEEPLY_EQUAL: 'deeply equal', STRICTLY_EQUAL: 'strictly equal', HAVE_MEMBERS: 'have member', MATCH: 'match', CONTAIN: 'contain', ABOVE: 'above', BELOW: 'below', GREATER: 'greater than', LESS: 'less than', HAVE_TYPE: 'have type', INCLUDE_MEMBERS: 'include member', HAVE_PROPERTY: 'have property', MATCH_SCHEMA: 'match schema', CASE_INSENSITIVE_EQUAL: 'case insensitive equal', SATISFY: 'satisfy' }; const isClause = '(?:is |do |does |to )?'; const notClause = '(?<reverse>not |to not )?'; const toBeClause = '(?:to )?(?:be )?'; const softlyClause = '(?<soft>softly )?'; const validationClause = `(?:(?<validation>${Object.values(validations).join('|')})(?:s|es| to)?)`; export const validationExtractRegexp = new RegExp(`^${isClause}${notClause}${toBeClause}${softlyClause}${validationClause}$`); export const validationRegexp = new RegExp(`(${isClause}${notClause}${toBeClause}${softlyClause}${validationClause})`); type VerifyInput = { received: any; expected: any; validation: string; reverse: boolean; soft: boolean; }; const aboveFn = (expectClause: any, expected: any) => expectClause.toBeGreaterThan(toNumber(expected)); const belowFn = (expectClause: any, expected: any) => expectClause.toBeLessThan(toNumber(expected)); const validationFns: Record<string, (expectClause: any, expected: any) => void> = { [validations.EQUAL]: (expectClause, expected: any) => expectClause.toSimpleEqual(expected), [validations.STRICTLY_EQUAL]: (expectClause: any, expected: any) => expectClause.toEqual(expected), [validations.DEEPLY_EQUAL]: (expectClause: any, expected: any) => expectClause.toDeepEqual(expected), [validations.HAVE_MEMBERS]: (expectClause: any, expected: any) => expectClause.toHaveMembers(expected), [validations.MATCH]: (expectClause: any, expected: any) => expectClause.toMatch(toRegexp(expected)), [validations.CONTAIN]: (expectClause: any, expected: any) => expectClause.toContain(expected), [validations.ABOVE]: aboveFn, [validations.BELOW]: belowFn, [validations.GREATER]: aboveFn, [validations.LESS]: belowFn, [validations.HAVE_TYPE]: (expectClause: any, expected: string) => expectClause.toHaveType(expected), [validations.INCLUDE_MEMBERS]: (expectClause: any, expected: string) => expectClause.toIncludeMembers(expected), [validations.HAVE_PROPERTY]: (expectClause: any, expected: string) => expectClause.toHaveProperty(expected), [validations.MATCH_SCHEMA]: (expectClause: any, expected: string) => expectClause.toMatchSchema(expected), [validations.CASE_INSENSITIVE_EQUAL]: (expectClause: any, expected: any) => expectClause.toCaseInsensitiveEqual(expected), [validations.SATISFY]: (expectClause: any, expected: any) => expectClause.toSatisfy(expected), }; /** * Basic verification function * @param {VerifyInput} object with all needed data for validation */ export function verify({ received, expected, validation, reverse, soft }: VerifyInput): void { const expectClause = expect(received).configure({ not: reverse, soft }); const validate = validationFns[validation]; validate(expectClause, expected); } export function getValidation(validationType: string, options?: { soft: boolean }): (AR: any, expected: any) => void { const match = validationExtractRegexp.exec(validationType); if (!match) throw new Error(`Validation '${validationType}' is not supported`); const { reverse, validation, soft } = match.groups as {[p: string]: string}; const softProp = options?.soft || !!soft; return function (received: any, expected: any) { verify({ received, expected, validation, reverse: Boolean(reverse), soft: softProp }); }; } export function getPollValidation(validationType: string, options?: { soft: boolean }): (AR: any, expected: any, options?: { timeout?: number, interval?: number }) => Promise<unknown> { const match = validationExtractRegexp.exec(validationType); if (!match) throw new Error(`Poll validation '${validationType}' is not supported`); const { reverse, validation, soft } = match.groups as {[p: string]: string}; const softProp = options?.soft || !!soft; return async function (received: any, expected: any, options?: { timeout?: number, interval?: number }) { const timeout = options?.timeout ?? 5000; const interval = options?.interval ?? 500; let lastError: Error = new Error(`Promise was not settled before timeout`); let intervalId: NodeJS.Timeout; const evaluatePromise = new Promise<void>(resolve => { intervalId = setInterval(async () => { try { const actualValue = await received(); verify({ received: actualValue, expected, validation, reverse: Boolean(reverse), soft: softProp }); clearInterval(intervalId); resolve(); } catch (err: any) { lastError = err; } }, interval); }); const timeoutPromise = new Promise((_, reject) => setTimeout(() => { clearInterval(intervalId); reject(lastError) }, timeout)); return Promise.race([evaluatePromise, timeoutPromise]); }; } export async function poll(fn: Function, options?: { timeout?: number, interval?: number }) { const timeout = options?.timeout ?? 5000; const interval = options?.interval ?? 500; let lastError: Error = new Error('Unexpected error'); let intervalId: NodeJS.Timeout; const evaluatePromise = new Promise<void>(resolve => { intervalId = setInterval(async () => { try { await fn(); clearInterval(intervalId); resolve(); } catch (err: any) { lastError = err; } }, interval); }); const timeoutPromise = new Promise((_, reject) => setTimeout(() => { clearInterval(intervalId); reject(lastError); }, timeout)); return Promise.race([evaluatePromise, timeoutPromise]); } export { expect }; function toNumber(n: any): number { const parsedNumber = Number.parseFloat(n); if (Number.isNaN(parsedNumber)) { throw new TypeError(`${n} is not a number`); } return parsedNumber; } function toRegexp(r: string | RegExp): RegExp { return r instanceof RegExp ? r : new RegExp(r); }