Skip to content

Commit

Permalink
Add support for mulitple validators (#15)
Browse files Browse the repository at this point in the history
* Add support for mulitple validators

* Add postpublish step
  • Loading branch information
poteto authored Dec 22, 2017
1 parent 614871c commit 7620e6c
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 204 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ interface IValidatorOptions {
}

const validateTypeof = ({ type }: IValidatorOptions): IValidatorFunc => {
return (key, value) =>
new ValidationResult(key, {
message: `${value} is not of type '${type}'`,
validation: typeof value === type,
value
});
return (key, newValue, oldValue) => {
return {
message: `${newValue} is not of type '${type}'`,
validation: typeof newValue === type
};
};
};

export default validateTypeof;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"lint": "tslint -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
"prebuild": "rimraf dist",
"prepublishOnly": "yarn run build",
"postpublish": "yarn run deploy:docs",
"release": "release-it",
"report-coverage": "cat ./coverage/lcov.info | coveralls",
"start": "tsc -w & rollup -c rollup.config.ts -w",
Expand Down
74 changes: 40 additions & 34 deletions src/buffered-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import hasOwnProperty from './utils/has-own-property';
import ValidationResult from './validation-result';

export type BufferErrorHandler = (message: string) => void;
export type BufferErrorHandler = (messages: string[]) => void;
export type BufferExecutionHandler = (
target: object,
changes: object
Expand All @@ -10,7 +10,7 @@ export type BufferExecutionHandler = (
export interface IBufferError {
key: PropertyKey;
value: any;
message: string;
messages: string[];
}

export interface IBufferChange {
Expand All @@ -27,7 +27,18 @@ export interface IBufferCache {
[key: string]: ValidationResult;
}

/**
* If no execution handler is defined, this is the default.
*
* @internal
*/
const defaultExecutionHandler = Object.assign;

/**
* If no error handler is defined, this is the default.
*
* @internal
*/
const defaultErrorHandler = () => {}; // tslint:disable-line no-empty

/**
Expand Down Expand Up @@ -91,12 +102,18 @@ export default class BufferedProxy {
* Returns cached errors as an object.
*
* ```ts
* bufferedProxy.errored; // { name: { value: 'Lauren Elizabeth', message: 'Name is too long' } };
* bufferedProxy.errored;
* {
* name: {
* value: 'Lauren Elizabeth',
* messages: ['Name is too long']
* }
* };
* ```
*/
public get errored(): object {
return this.invalidResults.reduce((acc, { key, message, value }) => {
acc[key] = { message, value };
return this.invalidResults.reduce((acc, { key, messages, value }) => {
acc[key] = { messages, value };
return acc;
}, Object.create(null));
}
Expand All @@ -118,12 +135,15 @@ export default class BufferedProxy {
* Returns cached errors as an array.
*
* ```ts
* bufferedProxy.errors; // [{ key: 'name', message: 'must be letters', value: 123 }]
* bufferedProxy.errors;
* [
* { key: 'name', messages: ['must be letters'], value: 123 }
* ]
* ```
*/
public get errors(): IBufferError[] {
return this.invalidResults.map(({ key, message, value }) => {
return { key, message, value };
return this.invalidResults.map(({ key, messages, value }) => {
return { key, messages, value };
});
}

Expand All @@ -136,11 +156,12 @@ export default class BufferedProxy {
* const bufferedProxy = new BufferedProxy(user);
* bufferedProxy.set(
* 'name',
* new ValidationResult('name', {
* message: '',
* validation: true,
* value: 'Lauren Elizabeth
* })
* new ValidationResult('name', 'Lauren Elizabeth', [
* {
* message: ['name must be greater than 3 characters'],
* validation: true
* }
* ])
* );
* bufferedProxy.get('name'); // 'Lauren Elizabeth'
* ```
Expand All @@ -150,28 +171,20 @@ export default class BufferedProxy {
*/
public set(key: PropertyKey, result: ValidationResult): ValidationResult {
if (result.isInvalid) {
this.errorHandler(result.message);
this.errorHandler(result.messages);
}
return this.updateCache(result);
}

/**
* Applies all the changes to the target object with the `executionHanlder`,
* then resets the cached values and errors to an empty state. The default
* `executionHandler` is `Object.assign`, which mutates the target object
* directly.
* then resets the cache to an empty state. The default `executionHandler`
* is `Object.assign`, which mutates the target object directly.
*
* ```ts
* const user = { name: 'Lauren' };
* const bufferedProxy = new BufferedProxy(user);
* bufferedProxy.set(
* 'name',
* new ValidationResult('name', {
* message: '',
* validation: true,
* value: 'Lauren Elizabeth'
* })
* );
* bufferedProxy.set(\/* ... *\/);
* bufferedProxy.flush();
* user.name; // 'Lauren Elizabeth'
* ```
Expand Down Expand Up @@ -203,17 +216,10 @@ export default class BufferedProxy {
}

/**
* Resets all cached values and errors.
* Resets the cache.
*
* ```ts
* bufferedProxy.set(
* 'name',
* new ValidationResult('name', {
* message: '',
* validation: true,
* value: 'Lauren Elizabeth'
* })
* );
* bufferedProxy.get('name'); // 'Lauren Elizabeth'
* bufferedProxy.reset();
* bufferedProxy.get('name'); // 'Lauren'
* ```
Expand Down
64 changes: 45 additions & 19 deletions src/utils/validator-lookup.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,64 @@
import ValidationResult from '../validation-result';
import ValidationResult, { IValidationMeta } from '../validation-result';

/**
* Function signature for validator functions.
*/
export type IValidatorFunc = (
key: PropertyKey,
newValue: any,
oldValue: any
) => ValidationResult;
) => IValidationMeta;

/**
* A validation map is an object containing the mapping between the target
* schema and validator functions.
*/
export interface IValidationMap {
[key: string]: IValidatorFunc;
}

export interface IValidatorFactoryOptions {
[key: string]: any;
[key: string]: IValidatorFunc | IValidatorFunc[];
}

/**
* If no validator is found, this is the default message returned by the
* validator function.
*
* @internal
*/
export const defaultValidatorMessage = 'No validator found';

/**
* If no validator is found, this is the default validation returned by the
* validator function.
*
* @internal
*/
export const defaultValidatorValidation = true;
export const defaultValidator: IValidatorFunc = (key, value, _) => {
return new ValidationResult(key, {

/**
* If no validator is found, this is the default validator function.
*
* @internal
*/
export const defaultValidator: IValidatorFunc = (key, value, oldValue) => {
return {
message: defaultValidatorMessage,
validation: defaultValidatorValidation,
value
});
validation: defaultValidatorValidation
};
};

/**
* Looks up a validator function from a validator map. If none are found, fall
* Looks up validator function(s) from a validator map. If none are found, fall
* back to the `defaultValidator`.
*
* ```ts
* const original = { foo: null };
* const validationMap = { foo: validatePresence() };
* validatorLookup(validationMap, 'foo'); // IValidatorFunc
* const validationMap = {
* foo: validatePresence(),
* bar: [
* validatePresence(),
* validateLength({ gt: 2 })
* ]
* };
* validatorLookup(validationMap, 'foo'); // IValidatorFunc[]
* ```
*
* @param validations
Expand All @@ -40,8 +67,7 @@ export const defaultValidator: IValidatorFunc = (key, value, _) => {
export default function validatorLookup(
validations: IValidationMap,
key: PropertyKey
): IValidatorFunc {
return typeof validations[key] === 'function'
? validations[key]
: defaultValidator;
): IValidatorFunc[] {
const validator = validations[key] || defaultValidator;
return Array.isArray(validator) ? validator : [validator];
}
9 changes: 7 additions & 2 deletions src/validated-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import BufferedProxy, {
BufferExecutionHandler
} from './buffered-proxy';
import validatorLookup, { IValidationMap } from './utils/validator-lookup';
import ValidationResult from './validation-result';

export interface IValidatedProxyOptions {
executionHandler?: BufferExecutionHandler;
Expand Down Expand Up @@ -62,8 +63,12 @@ export default function validatedProxy(
return targetBuffer.get(key);
},
set(targetBuffer, key, value, receiver) {
const validate = validatorLookup(validations, key);
const result = validate(key, value, target[key]);
const validators = validatorLookup(validations, key);
const result = new ValidationResult(
key,
value,
validators.map(validate => validate(key, value, target[key]))
);
targetBuffer.set(key, result);
return true;
}
Expand Down
53 changes: 27 additions & 26 deletions src/validation-result.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { IBufferChange, IBufferError } from './buffered-proxy';

export interface IValidationMeta {
value: any;
validation: boolean;
message?: string;
message: string;
}

/**
Expand All @@ -21,43 +20,45 @@ export default class ValidationResult {
*/
public key: PropertyKey;

private meta: IValidationMeta;

/**
* Creates a new instance of `ValidationResult`.
* The value being validated.
*
* ```ts
* new ValidationResult('name', { message: 'must be a string', validation: false, value: 123 });
* validatedResult.value; // 'Lauren'
* ```
*
* @param value
* @param meta
*/
constructor(key: PropertyKey, meta: IValidationMeta) {
this.key = key;
this.meta = meta;
}
public value: any;

/**
* The value being validated.
* Result of the validations.
*
* ```ts
* validatedResult.value; // 'Lauren'
* validationResult.validations;
* [
* { message: 'must be a string', validation: false },
* { message: 'must be at least 2 characters', validation: true }
* ];
* ```
*/
public get value(): any {
return this.meta.value;
}
public validations: IValidationMeta[];

/**
* Result of the validation.
* Creates a new instance of `ValidationResult`.
*
* ```ts
* validationResult.validation; // true
* new ValidationResult('name', 123, [
* { message: 'must be a string', validation: false },
* { message: 'must be at least 2 characters', validation: true }
* ]);
* ```
*
* @param value
* @param meta
*/
public get validation(): boolean {
return this.meta.validation;
constructor(key: PropertyKey, value: any, validations: IValidationMeta[]) {
this.key = key;
this.value = value;
this.validations = validations;
}

/**
Expand All @@ -67,8 +68,8 @@ export default class ValidationResult {
* validationResult.message; // 'key cannot be blank'
* ```
*/
public get message(): string {
return this.meta.message;
public get messages(): string[] {
return this.validations.map(v => v.message);
}

/**
Expand All @@ -79,7 +80,7 @@ export default class ValidationResult {
* ```
*/
public get isValid(): boolean {
return this.validation === true;
return this.validations.every(v => v.validation === true);
}

/**
Expand All @@ -90,6 +91,6 @@ export default class ValidationResult {
* ```
*/
public get isInvalid(): boolean {
return this.validation === false;
return !this.isValid;
}
}
Loading

0 comments on commit 7620e6c

Please sign in to comment.