diff --git a/README.md b/README.md index e642331..dc1c84a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,22 @@ -[![npm](https://img.shields.io/npm/v/validate-interface.svg)](https://www.npmjs.com/package/validate-interface) -[![npm](https://img.shields.io/npm/dt/validate-interface.svg)](https://www.npmjs.com/package/validate-interface) -[![npm](https://img.shields.io/npm/l/validate-interface.svg)](https://www.npmjs.com/package/validate-interface) +[![npm](https://img.shields.io/npm/v/typed-validation.svg)](https://www.npmjs.com/package/typed-validation) +[![npm](https://img.shields.io/npm/dt/typed-validation.svg)](https://www.npmjs.com/package/typed-validation) +[![npm](https://img.shields.io/npm/l/typed-validation.svg)](https://www.npmjs.com/package/typed-validation) -# Validate Objects Against TypeScript Interfaces # +# Strongly-Typed Validators for TypeScript +*(Formerly `validate-interface`)* -Builds strongly-typed validators that can prove to the TypeScript compiler that a given object conforms to a TypeScript interface. +Build strongly-typed validators that TypeScript can understand, so that TypeScript can validate that your validator is correct. ## Installation ## -`$ npm install --save validate-interface` +`$ npm install --save typed-validation` ## Basic Usage ## +**Example:** check that a value of type `any` (perhaps from an untrusted source, such as a file) is an object that conforms to an interface called `Employee`: + ```ts -// 1) Define an interface +// 1) Define the interface interface Employee { name: string; roleCode: number; @@ -21,7 +24,7 @@ interface Employee { addressPostcode: string; } -// 2) Define a schema +// 2) Define the validator const employeeValidator: Validator = { name: isString(minLength(1)), roleCode: isNumber(min(1, max(10))), @@ -30,16 +33,19 @@ const employeeValidator: Validator = { }; // 3) Validate -let bob: Employee = validate({ + +const unsafeObject: any = { name: 'Bob Smith', roleCode: 7, completedTraining: true, addressPostcode: 'AB1 2CD' -}, employeeValidator); +}; + +const bob: Employee = validate(unsafeObject, employeeValidator); // Catch and log error messages try { - let wrong: Employee = validate({ + const wrong: Employee = validate({ name: 'Name', roleCode: 4, completedTraining: 'false', @@ -57,9 +63,9 @@ try { ``` ## Documentation ## -This library provides a number of strongly-typed assertions which can be combined to validate the type of each property. +Validators are built by combining simple assertions using function composition and higher-order functions. For example, the `isString()` assertion returns a function which accepts a single argument of type `any` and returns a `string`. If the argument is a string, it returns the string. If the argument is not a string, it throws an error. This module provides a number of assertions, described below. -An assertion may take another assertion as its last argument; if assertion check passes, it calls the next assertion. For example, `isString(minLength(1, maxLength(10)))` first checks if the value is a string, then checks if its length is at least 1, and then checks that its length is no more than 10. If `isString` fails, `minLength` isn't run. Chaining assertions in this way allows for complex validation. +An assertion may take another assertion as its last argument; if assertion check passes, it calls the next assertion. For example, `isString(minLength(1, maxLength(10)))` first checks if the value is a string, then checks if its length is at least 1, and then checks that its length is no more than 10. If `isString` fails, `minLength` isn't run. Chaining assertions in this way allows for complex validation of types and values. Some assertions require other assertions to come before it. For example, `minLength` can't be used by itself because it needs another assertion to check that the value has the `length` property - so something like `isString(minLength(1))` or `isArray(minLength(1))`. diff --git a/lib/index.ts b/lib/index.ts index 8245d24..869045f 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,5 +1,20 @@ +import { keysOf, tryCatch } from './utils'; +import { + ArrayIndexPathNode, + error, + errorFromException, + ErrorResult, + SuccessResult, + KeyPathNode, + success, + ValidationError, + ValidationResult, +} from './validation-result'; +export * from './validation-result'; + + export type Validator = { - [key in keyof T]: (arg: any) => T[key] + [key in keyof T]: (arg: any) => ValidationResult }; @@ -13,354 +28,264 @@ export interface ILength { } -export abstract class PathNode { } - - -export class KeyPathNode extends PathNode { - constructor(public readonly key: string) { - super(); - } - - public toString(): string { - if (/^[$a-z_][$a-z0-9_]*$/i.test(this.key)) { - return `.${this.key}`; - } else { - return `['${this.key.replace('\\', '\\\\').replace("'", "\\'")}']`; - } - } -} - - -export class ArrayIndexPathNode extends PathNode { - constructor(public readonly index: number) { - super(); - } - - public toString(): string { - return `[${this.index}]`; - } +export interface IValidationOptions { + allowAdditionalProperties?: boolean; } -export class ValidationError { - public readonly path: PathNode[] = []; - - constructor( - public readonly errorCode: string, - public readonly message: string - ) { } - - public toString(root: string = '$root'): string { - return `${this.pathString(root)}: ${this.message}`; - } - - public pathString(root: string = '$root'): string { - return root + this.path.map(node => node.toString()).join(''); - } +export function validate(arg: any, assertion: (arg: any) => ValidationResult): ValidationResult { + return tryCatch( + () => assertion(arg), + (err) => errorFromException(err) + ); } -export class ValidationErrorCollection { - public readonly errors: ValidationError[] = []; - - constructor(error?: ValidationError) { - if (error) { - this.errors.push(error); - } - } - - public insertError(node: PathNode, error: ValidationError) { - error.path.unshift(node); - this.errors.push(error); - } +export function extendValidator(validator1: Validator, validator2: Validator): Validator { + const result: any = {}; - public handleError(node: PathNode, err: any) { - if (err instanceof ValidationErrorCollection) { - for (const error of err.errors) { - this.insertError(node, error); - } - } else if (err instanceof ValidationError) { - this.insertError(node, err); - } else { - this.insertError( - node, - new ValidationError('UNHANDLED_ERROR', `${typeof err === 'object' && err.message || 'Unknown error'}`) - ); - } + for (const key in validator1) { + result[key] = validator1[key]; } - public toString(root: string = '$root'): string { - return `${this.errors.length} validation error${this.errors.length === 1 ? '' : 's'}:\n ${this.errors.map(error => error.toString(root)).join('\n ')}`; + for (const key in validator2) { + result[key] = validator2[key]; } -} - -export interface IValidationOptions { - allowAdditionalProperties?: boolean; + return result as Validator; } -export function validate(arg: any, validator: Validator, options: IValidationOptions = {}): Validated { - const { - allowAdditionalProperties = true - } = options; - - if (typeof arg !== 'object') throw new ValidationErrorCollection(new ValidationError('NOT_OBJECT', `Expected object, got ${typeof arg}`)); - - const result: {[key in keyof T]?: T[key]} = {}; - - let validationErrorCollection: ValidationErrorCollection | null = null; +// ASSERTIONS // - const validatedProperties = Object.keys(arg).reduce((validatedProperties, key) => { - validatedProperties[key] = false; - return validatedProperties; - }, {} as {[key: string]: boolean}); - for (const key in validator) { - validatedProperties[key] = true; +export function conformsTo(validator: Validator): (arg: any) => ValidationResult>; +export function conformsTo(validator: Validator, options: IValidationOptions): (arg: any) => ValidationResult>; +export function conformsTo(validator: Validator, next: (arg: Validated) => ValidationResult): (arg: any) => ValidationResult; +export function conformsTo(validator: Validator, options: IValidationOptions, next: (arg: Validated) => ValidationResult): (arg: any) => ValidationResult; +export function conformsTo(validator: Validator, optionsOrNext?: IValidationOptions | ((arg: Validated) => ValidationResult), next?: (arg: Validated) => ValidationResult): (arg: any) => ValidationResult { + return isObject((arg: any) => { + const options: IValidationOptions = typeof optionsOrNext === 'object' ? optionsOrNext : {}; + const nextAssertion: ((arg: Validated) => any) | undefined = typeof optionsOrNext === 'function' ? optionsOrNext : next; + const { + allowAdditionalProperties = true + } = options; + + const partiallyValidated: {[key in keyof T]?: T[key]} = {}; + + const errors: ValidationError[] = keysOf(validator).reduce((errors, key) => { + const result = tryCatch( + () => validator[key](arg[key]), + (err) => errorFromException(err) + ); - try { - result[key] = validator[key](arg[key]); - } catch (err) { - if (validationErrorCollection === null) { - validationErrorCollection = new ValidationErrorCollection(); + if (!result.success) { + return errors.concat(result.addPathNode(new KeyPathNode(key)).errors); } - validationErrorCollection.handleError(new KeyPathNode(key), err); - } - } + partiallyValidated[key] = result.result; + return errors; + }, [] as ValidationError[]); - if (!allowAdditionalProperties && !Object.keys(validatedProperties).every(key => validatedProperties[key])) { - if (validationErrorCollection === null) { - validationErrorCollection = new ValidationErrorCollection(); + if (!allowAdditionalProperties && keysOf(arg).some(key => !validator.hasOwnProperty(key))) { + errors.push( + new ValidationError('UNEXPECTED_ADDITIONAL_PROPERTIES', `Unexpected additional propertie(s): ${keysOf(arg).filter(key => !validator.hasOwnProperty(key)).join(', ')}`) + ); } - validationErrorCollection.errors.push( - new ValidationError('UNEXPECTED_ADDITIONAL_PROPERTIES', `Unexpected additional properties: ${Object.keys(validatedProperties).filter(key => !validatedProperties[key]).join(', ')}`) - ); - } + if (errors.length > 0) return new ErrorResult(errors); - if (validationErrorCollection !== null) throw validationErrorCollection; + const validated = partiallyValidated as Validated; - return result as Validated; + return next ? next(validated) : success(validated); + }); } -export function extendValidator(validator1: Validator, validator2: Validator): Validator { - const result: any = {}; - - for (const key in validator1) { - result[key] = validator1[key]; - } - - for (const key in validator2) { - result[key] = validator2[key]; - } - - return result as Validator; -} - - -// ASSERTIONS // - - -export function optional(next: (arg: any) => T): (arg: any) => T | undefined { +export function optional(next: (arg: any) => ValidationResult): (arg: any) => ValidationResult { return (arg: any) => { - if (arg === undefined) return undefined; + if (arg === undefined) return success(undefined); return next(arg); }; } -export function nullable(next: (arg: any) => T): (arg: any) => T | null { +export function nullable(next: (arg: any) => ValidationResult): (arg: any) => ValidationResult { return (arg: any) => { - if (arg === null) return null; + if (arg === null) return success(null); return next(arg); }; } -export function defaultsTo(def: any): (arg: any) => any; -export function defaultsTo(def: T, next: (arg: any) => T): (arg: any) => T; -export function defaultsTo(def: any, next?: (arg: any) => any): (arg: any) => any { +export function defaultsTo(def: any): (arg: any) => ValidationResult; +export function defaultsTo(def: T, next: (arg: any) => ValidationResult): (arg: any) => ValidationResult; +export function defaultsTo(def: any, next?: (arg: any) => ValidationResult): (arg: any) => ValidationResult { return (arg: any) => { if (arg === undefined) arg = def; - return next ? next(arg) : arg; + return next ? next(arg) : success(arg); }; } -export function onErrorDefaultsTo(def: U, next: (arg: T) => U): (arg: T) => U { +export function onErrorDefaultsTo(def: U, next: (arg: T) => ValidationResult): (arg: T) => ValidationResult { return (arg: T) => { try { return next(arg); } catch (_) { // Ignore error - resort to default - return def; + return success(def); } }; } -export function isBoolean(): (arg: any) => boolean; -export function isBoolean(next: (arg: boolean) => T): (arg: any) => T; -export function isBoolean(next?: (arg: boolean) => any): (arg: any) => any { +export function isBoolean(): (arg: any) => ValidationResult; +export function isBoolean(next: (arg: boolean) => ValidationResult): (arg: any) => ValidationResult; +export function isBoolean(next?: (arg: boolean) => ValidationResult): (arg: any) => ValidationResult { return (arg: any) => { - if (typeof arg !== 'boolean') throw new ValidationError('NOT_BOOLEAN', `Expected boolean, got ${typeof arg}`); - return next ? next(arg) : arg; + if (typeof arg !== 'boolean') return error('NOT_BOOLEAN', `Expected boolean, got ${typeof arg}`); + return next ? next(arg) : success(arg); }; } -export function isNumber(): (arg: any) => number; -export function isNumber(next: (arg: number) => T): (arg: any) => T; -export function isNumber(next?: (arg: number) => any): (arg: any) => any { +export function isNumber(): (arg: any) => ValidationResult; +export function isNumber(next: (arg: number) => ValidationResult): (arg: any) => ValidationResult; +export function isNumber(next?: (arg: number) => any): (arg: any) => ValidationResult { return (arg: any) => { - if (typeof arg !== 'number') throw new ValidationError('NOT_NUMBER', `Expected number, got ${typeof arg}`); - return next ? next(arg) : arg; + if (typeof arg !== 'number') return error('NOT_NUMBER', `Expected number, got ${typeof arg}`); + return next ? next(arg) : success(arg); }; } -export function min(min: number): (arg: number) => number; -export function min(min: number, next: (arg: number) => T): (arg: number) => T; -export function min(min: number, next?: (arg: number) => any): (arg: number) => any { +export function min(min: number): (arg: number) => ValidationResult; +export function min(min: number, next: (arg: number) => ValidationResult): (arg: number) => ValidationResult; +export function min(min: number, next?: (arg: number) => ValidationResult): (arg: number) => ValidationResult { return (arg: number) => { - if (arg < min) throw new ValidationError('LESS_THAN_MIN', `${arg} is less than ${min}`); - return next ? next(arg) : arg; + if (arg < min) return error('LESS_THAN_MIN', `${arg} is less than ${min}`); + return next ? next(arg) : success(arg); }; } -export function max(max: number): (arg: number) => number; -export function max(max: number, next: (arg: number) => T): (arg: number) => T; -export function max(max: number, next?: (arg: number) => any): (arg: number) => any { +export function max(max: number): (arg: number) => ValidationResult; +export function max(max: number, next: (arg: number) => ValidationResult): (arg: number) => ValidationResult; +export function max(max: number, next?: (arg: number) => ValidationResult): (arg: number) => ValidationResult { return (arg: number) => { - if (arg > max) throw new ValidationError('GREATER_THAN_MAX', `${arg} is greater than ${max}`); - return next ? next(arg) : arg; + if (arg > max) return error('GREATER_THAN_MAX', `${arg} is greater than ${max}`); + return next ? next(arg) : success(arg); }; } -export function isString(): (arg: any) => string; -export function isString(next: (arg: string) => T): (arg: any) => T; -export function isString(next?: (arg: any) => any): (arg: any) => any { +export function isString(): (arg: any) => ValidationResult; +export function isString(next: (arg: string) => ValidationResult): (arg: any) => ValidationResult; +export function isString(next?: (arg: any) => ValidationResult): (arg: any) => ValidationResult { return (arg: any) => { - if (typeof arg !== 'string') throw new ValidationError('NOT_STRING', `Expected string, got ${typeof arg}`); - return next ? next(arg) : arg; + if (typeof arg !== 'string') return error('NOT_STRING', `Expected string, got ${typeof arg}`); + return next ? next(arg) : success(arg); }; } -export function matches(regex: RegExp): (arg: string) => string; -export function matches(regex: RegExp, next: (arg: string) => T): (arg: string) => T; -export function matches(regex: RegExp, next?: (arg: string) => any): (arg: string) => any { +export function matches(regex: RegExp): (arg: string) => ValidationResult; +export function matches(regex: RegExp, next: (arg: string) => ValidationResult): (arg: string) => ValidationResult; +export function matches(regex: RegExp, next?: (arg: string) => ValidationResult): (arg: string) => ValidationResult { return (arg: string) => { - if (!regex.test(arg)) throw new ValidationError('FAILED_REGEXP', `Failed regular expression ${regex.toString()}`); - return next ? next(arg) : arg; + if (!regex.test(arg)) return error('FAILED_REGEXP', `Failed regular expression ${regex.toString()}`); + return next ? next(arg) : success(arg); }; } -export function minLength(min: number): (arg: T) => T; -export function minLength(min: number, next: (arg: T) => U): (arg: T) => U; -export function minLength(min: number, next?: (arg: T) => any): (arg: T) => any { +export function minLength(min: number): (arg: T) => ValidationResult; +export function minLength(min: number, next: (arg: T) => ValidationResult): (arg: T) => ValidationResult; +export function minLength(min: number, next?: (arg: T) => ValidationResult): (arg: T) => ValidationResult { return (arg: T) => { - if (arg.length < min) throw new ValidationError('LESS_THAN_MIN_LENGTH', `Length ${arg.length} is less than ${min}`); - return next ? next(arg) : arg; + if (arg.length < min) return error('LESS_THAN_MIN_LENGTH', `Length ${arg.length} is less than ${min}`); + return next ? next(arg) : success(arg); }; } -export function maxLength(max: number): (arg: T) => T; -export function maxLength(max: number, next: (arg: T) => U): (arg: T) => U; -export function maxLength(max: number, next?: (arg: T) => any): (arg: T) => any { +export function maxLength(max: number): (arg: T) => ValidationResult; +export function maxLength(max: number, next: (arg: T) => ValidationResult): (arg: T) => ValidationResult; +export function maxLength(max: number, next?: (arg: T) => ValidationResult): (arg: T) => ValidationResult { return (arg: T) => { - if (arg.length > max) throw new ValidationError('GREATER_THAN_MAX_LENGTH', `Length ${arg.length} is greater than ${max}`); - return next ? next(arg) : arg; + if (arg.length > max) return error('GREATER_THAN_MAX_LENGTH', `Length ${arg.length} is greater than ${max}`); + return next ? next(arg) : success(arg); }; } -export function lengthIs(length: number): (arg: T) => T; -export function lengthIs(length: number, next: (arg: T) => U): (arg: T) => U; -export function lengthIs(length: number, next?: (arg: T) => any): (arg: T) => any { +export function lengthIs(length: number): (arg: T) => ValidationResult; +export function lengthIs(length: number, next: (arg: T) => ValidationResult): (arg: T) => ValidationResult; +export function lengthIs(length: number, next?: (arg: T) => ValidationResult): (arg: T) => ValidationResult { return (arg: T) => { - if (arg.length !== length) throw new ValidationError('LENGTH_NOT_EQUAL', `Length ${arg.length} is not equal to ${length}`); - return next ? next(arg) : arg; + if (arg.length !== length) return error('LENGTH_NOT_EQUAL', `Length ${arg.length} is not equal to ${length}`); + return next ? next(arg) : success(arg); }; } -export function isArray(): (arg: any) => any[]; -export function isArray(next: (arg: any[]) => T): (arg: any) => T; -export function isArray(next?: (arg: any[]) => any): (arg: any) => any { +export function isArray(): (arg: any) => ValidationResult; +export function isArray(next: (arg: any[]) => ValidationResult): (arg: any) => ValidationResult; +export function isArray(next?: (arg: any[]) => ValidationResult): (arg: any) => ValidationResult { return (arg: any) => { - if (!(arg instanceof Array)) throw new ValidationError('NOT_ARRAY', `Expected array, got ${typeof arg}`); - return next ? next(arg) : arg; + if (!(arg instanceof Array)) return error('NOT_ARRAY', `Expected array, got ${typeof arg}`); + return next ? next(arg) : success(arg); }; } -export function eachItem(assertion: (arg: any) => T): (arg: any[]) => T[]; -export function eachItem(assertion: (arg: any) => T, next: (arg: T[]) => U): (arg: any[]) => U; -export function eachItem(assertion: (arg: any) => T, next?: (arg: any[]) => any): (arg: any[]) => any { +export function eachItem(assertion: (arg: any) => ValidationResult): (arg: any[]) => ValidationResult; +export function eachItem(assertion: (arg: any) => ValidationResult, next: (arg: T[]) => ValidationResult): (arg: any[]) => ValidationResult; +export function eachItem(assertion: (arg: any) => ValidationResult, next?: (arg: any[]) => ValidationResult): (arg: any[]) => ValidationResult { return (arg: any[]) => { - let validationErrorCollection: ValidationErrorCollection | null = null; - - const mapped = arg.map((item, index) => { - try { - return assertion(item); - } catch (err) { - if (validationErrorCollection === null) { - validationErrorCollection = new ValidationErrorCollection(); - } - - validationErrorCollection.handleError(new ArrayIndexPathNode(index), err); - } - }); - - if (validationErrorCollection !== null) throw validationErrorCollection; - - return next ? next(mapped) : mapped; - } -} + const results = arg.map((item, index) => tryCatch( + () => assertion(item), + (err) => error('UNHANDLED_ERROR', `Unhandled error: ${typeof err === 'object' && err.message || 'Unknown error'}`) + )); + + if (results.some(ErrorResult.isErrorResult)) { + return new ErrorResult( + results + .map((item, index) => { + if (!item.success) item.addPathNode(new ArrayIndexPathNode(index)); + return item; + }) + .filter(ErrorResult.isErrorResult) + .reduce((errors, result) => errors.concat(result.errors), [] as ValidationError[]) + ); + } + const mapped = (results as SuccessResult[]).map(result => result.result); -export function isObject(): (arg: any) => any; -export function isObject(next: (arg: any) => T): (arg: any) => T; -export function isObject(next?: (arg: any) => any): (arg: any) => any { - return (arg: any) => { - if (typeof arg !== 'object') throw new ValidationError('NOT_OBJECT', `Expected object, got ${typeof arg}`); - return next ? next(arg) : arg; + return next ? next(mapped) : success(mapped); }; } -export function conformsTo(validator: Validator): (arg: any) => Validated; -export function conformsTo(validator: Validator, options: IValidationOptions): (arg: any) => Validated; -export function conformsTo(validator: Validator, next: (arg: Validated) => U): (arg: any) => U; -export function conformsTo(validator: Validator, options: IValidationOptions, next: (arg: Validated) => U): (arg: any) => U; -export function conformsTo(validator: Validator, optionsOrNext?: IValidationOptions | ((arg: Validated) => any), next?: (arg: Validated) => any): (arg: any) => any { +export function isObject(): (arg: any) => ValidationResult; +export function isObject(next: (arg: any) => ValidationResult): (arg: any) => ValidationResult; +export function isObject(next?: (arg: any) => ValidationResult): (arg: any) => ValidationResult { return (arg: any) => { - if (typeof optionsOrNext === 'function') { - next = optionsOrNext; - } - - const validated = validate(arg, validator, typeof optionsOrNext === 'object' ? optionsOrNext : undefined); - - return next ? next(validated) : validated; + if (typeof arg !== 'object') return error('NOT_OBJECT', `Expected object, got ${typeof arg}`); + return next ? next(arg) : success(arg); }; } -export function equals(value: T, ...values: T[]): (arg: any) => T { - let vals = [value, ...values]; +export function equals(value: T, ...values: T[]): (arg: any) => ValidationResult { + const vals = [value, ...values]; + return (arg: any) => { - for (let val of vals) { - if (val === arg) return arg; + for (const val of vals) { + if (val === arg) return success(arg); } - throw new ValidationError('NOT_EQUAL', vals.length === 1 ? `'${arg}' does not equal '${vals[0]}'` : `'${arg}' not one of: ${vals.join(', ')}`); + return error('NOT_EQUAL', vals.length === 1 ? `'${arg}' does not equal '${vals[0]}'` : `'${arg}' not one of: ${vals.join(', ')}`); }; } diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..e95ddb7 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,22 @@ +export function tryCatch(func: () => T, catchFunc: (err: any) => U): T | U { + try { + return func(); + } catch (err) { + return catchFunc(err); + } +} + + +export function keysOf(arg: T): Array { + if (typeof arg !== 'object') return []; + + const keys: Array = []; + + for (const key in arg) { + keys.push(key); + } + + return keys; +} + + diff --git a/lib/validation-result.ts b/lib/validation-result.ts new file mode 100644 index 0000000..8147a27 --- /dev/null +++ b/lib/validation-result.ts @@ -0,0 +1,105 @@ +export type ValidationResult = SuccessResult | ErrorResult; + + +export abstract class PathNode { } + + +export class KeyPathNode extends PathNode { + constructor(public readonly key: string) { + super(); + } + + public toString(): string { + if (/^[$a-z_][$a-z0-9_]*$/i.test(this.key)) { + return `.${this.key}`; + } else { + return `['${this.key.replace('\\', '\\\\').replace("'", "\\'")}']`; + } + } +} + + +export class ArrayIndexPathNode extends PathNode { + constructor(public readonly index: number) { + super(); + } + + public toString(): string { + return `[${this.index}]`; + } +} + + +export class ValidationError { + public readonly path: PathNode[] = []; + + constructor( + public readonly errorCode: string, + public readonly message: string, + ) { } + + public addPathNode(node: PathNode): ValidationError { + this.path.unshift(node); + return this; + } + + public toString(root: string = '$root'): string { + return `${this.pathString(root)}: ${this.message}`; + } + + public pathString(root: string = '$root'): string { + return root + this.path.map(node => node.toString()).join(''); + } +} + + +export class ErrorResult { + public readonly success: false = false; + + public readonly errors: ValidationError[]; + + constructor(errors: ValidationError | ValidationError[]) { + if (errors instanceof ValidationError) { + this.errors = [errors]; + } else { + this.errors = errors; + } + } + + public addPathNode(node: PathNode): ErrorResult { + for (const error of this.errors) { + error.addPathNode(node); + } + + return this; + } + + public toString(root: string = '$root'): string { + return `${this.errors.length} validation error${this.errors.length === 1 ? '' : 's'}:\n ${this.errors.map(error => error.toString(root)).join('\n ')}`; + } + + public static isErrorResult(arg: any): arg is ErrorResult { + return arg instanceof ErrorResult; + } +} + + +export interface SuccessResult { + readonly success: true; + readonly result: T; +} + + +export function error(errorCode: string, message: string): ErrorResult { + return new ErrorResult(new ValidationError(errorCode, message)); +} + + +export function errorFromException(err: any): ErrorResult { + return new ErrorResult(new ValidationError('UNHANDLED_ERROR', `Unhandled error: ${typeof err === 'object' && err.message || 'Unknown error'}`)); +} + + +export function success(result: T): SuccessResult { + return {success: true, result}; +} diff --git a/package-lock.json b/package-lock.json index 24b68a9..26ab1b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "validate-interface", - "version": "0.5.2", + "name": "typed-validation", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 576082a..edb9efb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "validate-interface", - "version": "0.5.2", + "name": "typed-validation", + "version": "0.6.0", "description": "Validate Objects Against TypeScript Interfaces", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -12,7 +12,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/cpascoe95/validate-interface.git" + "url": "git+https://github.com/cpascoe95/typed-validation.git" }, "keywords": [ "typed", @@ -24,9 +24,9 @@ "author": "Charles Pascoe ", "license": "LGPL-3.0", "bugs": { - "url": "https://github.com/cpascoe95/validate-interface/issues" + "url": "https://github.com/cpascoe95/typed-validation/issues" }, - "homepage": "https://github.com/cpascoe95/validate-interface#readme", + "homepage": "https://github.com/cpascoe95/typed-validation#readme", "devDependencies": { "typescript": "2.5.2" } diff --git a/tsconfig.json b/tsconfig.json index ebcbe3a..aa590ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "declaration": true, "baseUrl": "./lib/", "paths": { - "validate-interface/*": ["*"] + "typed-validation/*": ["*"] }, "target": "ES5", "strict": true