UNPKG

@qavajs/validation

Version:

Lib that transform plain english definition to validation functions

204 lines (186 loc) 7.12 kB
import { expect, Assertion, AssertionError} from 'chai'; import Ajv from 'ajv' export class SoftAssertionError extends AssertionError { name = 'SoftAssertionError'; } Assertion.addMethod('notStrictEqual', function (ER) { const obj = this._obj; this.assert( obj == ER, 'expected #{this} to equal #{exp}', 'expected #{this} to not equal #{exp}', ER, obj ); }); Assertion.addMethod('caseInsensitiveEqual', function (ER) { const obj = this._obj; this.assert( obj.toLowerCase() == ER.toLowerCase(), 'expected #{this} to equal #{exp}', 'expected #{this} to not equal #{exp}', ER, obj ); }); Assertion.addMethod('matchSchema', function (schema) { const obj = this._obj; const ajv = new Ajv(); const validate = ajv.compile(schema); const isValid = validate(obj); const messages = validate.errors ? validate.errors?.map(err => `${err.instancePath} ${err.message} (${err.schemaPath})`) : []; const errors = [ 'object does not match schema', ...messages ].join('\n'); this.assert( isValid, errors, 'expected #{this} to not match schema #{exp}', '', '' ); }); 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', }; 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 = { AR: any; ER: any; validation: string; reverse: boolean; soft: boolean; }; const aboveFn = (expectClause: any, ER: any) => expectClause.above(toNumber(ER)); const belowFn = (expectClause: any, ER: any) => expectClause.below(toNumber(ER)); const validationFns = { [validations.EQUAL]: (expectClause: any, ER: any) => expectClause.notStrictEqual(ER), [validations.STRICTLY_EQUAL]: (expectClause: any, ER: any) => expectClause.equal(ER), [validations.DEEPLY_EQUAL]: (expectClause: any, ER: any) => expectClause.eql(ER), [validations.HAVE_MEMBERS]: (expectClause: any, ER: any) => expectClause.have.members(ER), [validations.MATCH]: (expectClause: any, ER: any) => expectClause.match(toRegexp(ER)), [validations.CONTAIN]: (expectClause: any, ER: any) => expectClause.contain(ER), [validations.ABOVE]: aboveFn, [validations.BELOW]: belowFn, [validations.GREATER]: aboveFn, [validations.LESS]: belowFn, [validations.HAVE_TYPE]: (expectClause: any, ER: string) => expectClause.a(ER), [validations.INCLUDE_MEMBERS]: (expectClause: any, ER: string) => expectClause.include.members(ER), [validations.HAVE_PROPERTY]: (expectClause: any, ER: string) => expectClause.have.property(ER), [validations.MATCH_SCHEMA]: (expectClause: any, ER: string) => expectClause.matchSchema(ER), [validations.CASE_INSENSITIVE_EQUAL]: (expectClause: any, ER: any) => expectClause.caseInsensitiveEqual(ER), }; /** * Basic verification function * @param {VerifyInput} object with all needed data for validation */ export function verify({ AR, ER, validation, reverse, soft }: VerifyInput): void { const prefix = 'Fail'; const expectClause = reverse ? expect(AR, prefix).to.not : expect(AR, prefix).to; const validate = validationFns[validation]; try { validate(expectClause, ER); } catch (err) { if (soft && err instanceof Error) throw new SoftAssertionError(err.message, { cause: err }); throw err; } } export function getValidation(validationType: string, options?: { soft: boolean }): (AR: any, ER: 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 (AR: any, ER: any) { verify({ AR, ER, validation, reverse: Boolean(reverse), soft: softProp }); }; } export function getPollValidation(validationType: string, options?: { soft: boolean }): (AR: any, ER: 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 (AR: any, ER: 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 AR(); verify({ AR: actualValue, ER, 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]); } function toNumber(n: any): number { const parsedNumber = parseFloat(n); if (Number.isNaN(parsedNumber)) { throw new Error(`${n} is not a number`); } return parsedNumber; } function toRegexp(r: string | RegExp): RegExp { return r instanceof RegExp ? r : new RegExp(r); }