From 6d87b2d217b97a1437c6b99022a52188e7597984 Mon Sep 17 00:00:00 2001 From: Doug Kent Date: Tue, 23 May 2017 16:31:23 -0400 Subject: [PATCH 1/4] Add FluentRulesGenerator --- .gitignore | 1 + src/implementation/standard-validator.ts | 2 +- src/implementation/validation-rules.ts | 376 +++++++++++++++++++---- test/resources/registration-form.ts | 5 +- test/validation-message-parser.ts | 2 + test/validator.ts | 161 ++++++++++ 6 files changed, 488 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index 1c710195..505da925 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ npm-debug.log* .test dist/doc-temp dist/test +*.bak diff --git a/src/implementation/standard-validator.ts b/src/implementation/standard-validator.ts index 3ac093a9..52027289 100644 --- a/src/implementation/standard-validator.ts +++ b/src/implementation/standard-validator.ts @@ -146,7 +146,7 @@ export class StandardValidator extends Validator { } // any rules? - if (!rules) { + if (!rules || (rules.length === 0)) { return Promise.resolve([]); } diff --git a/src/implementation/validation-rules.ts b/src/implementation/validation-rules.ts index fc487db3..a7173747 100644 --- a/src/implementation/validation-rules.ts +++ b/src/implementation/validation-rules.ts @@ -6,6 +6,209 @@ import { ValidationDisplayNameAccessor } from './rule'; import { PropertyAccessorParser, PropertyAccessor } from '../property-accessor-parser'; import { isString } from '../util'; +export class FluentRulesGenerator { + + private fluentCustomizer: FluentRuleCustomizer | null = null; + private fluentRules: FluentRules | null = null; + + constructor(private fluentEnsure: FluentEnsure) { + if (!fluentEnsure) { + throw new Error(`FluentRuleGenerator requires an instance of FluentEnsure`); + } + } + + /** + * Target a property with validation rules. + * @param property The property to target. Can be the property name or a property accessor function. + */ + public ensure(property: string | PropertyAccessor): FluentRulesGenerator { + this.fluentRules = this.fluentEnsure!.ensure(property); + this.fluentCustomizer = null; + return this; + } + /** + * Targets an object with validation rules. + */ + public ensureObject(): FluentRulesGenerator { + this.fluentRules = this.fluentEnsure!.ensureObject(); + this.fluentCustomizer = null; + return this; + } + /** + * Sets the display name of the ensured property. + */ + public displayName(name: string): FluentRulesGenerator { + this.fluentRules!.displayName(name); + return this; + } + /** + * Applies an ad-hoc rule function to the ensured property or object. + * @param condition The function to validate the rule. + * Will be called with two arguments, the property value and the object. + * Should return a boolean or a Promise that resolves to a boolean. + */ + // tslint:disable-next-line:max-line-length + public satisfies(condition: (value: TValue, object?: TObject) => boolean | Promise, config?: object): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.satisfies(condition, config); + return this; + } + /** + * Applies a rule by name. + * @param name The name of the custom or standard rule. + * @param args The rule's arguments. + */ + public satisfiesRule(name: string, ...args: any[]): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.satisfiesRule(name, ...args); + return this; + } + /** + * Applies the "required" rule to the property. + * The value cannot be null, undefined or whitespace. + */ + public required(): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.required(); + return this; + } + /** + * Applies the "matches" rule to the property. + * Value must match the specified regular expression. + * null, undefined and empty-string values are considered valid. + */ + public matches(regex: RegExp): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.matches(regex); + return this; + } + /** + * Applies the "email" rule to the property. + * null, undefined and empty-string values are considered valid. + */ + public email(): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.email(); + return this; + } + /** + * Applies the "minLength" STRING validation rule to the property. + * null, undefined and empty-string values are considered valid. + */ + public minLength(length: number): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.minLength(length); + return this; + } + /** + * Applies the "maxLength" STRING validation rule to the property. + * null, undefined and empty-string values are considered valid. + */ + public maxLength(length: number): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.maxLength(length); + return this; + } + /** + * Applies the "minItems" ARRAY validation rule to the property. + * null and undefined values are considered valid. + */ + public minItems(count: number): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.minItems(count); + return this; + } + /** + * Applies the "maxItems" ARRAY validation rule to the property. + * null and undefined values are considered valid. + */ + public maxItems(count: number): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.maxItems(count); + return this; + } + /** + * Applies the "equals" validation rule to the property. + * null, undefined and empty-string values are considered valid. + */ + public equals(expectedValue: TValue): FluentRulesGenerator { + this.fluentCustomizer = this.fluentRules!.equals(expectedValue); + return this; + } + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + public then(): FluentRulesGenerator { + this.fluentRules!.sequence++; + return this; + } + /** + * Specifies the key to use when looking up the rule's validation message. + */ + public withMessageKey(key: string): FluentRulesGenerator { + this.assertFluentCustomizer(); + this.fluentCustomizer!.withMessageKey(key); + return this; + } + /** + * Specifies rule's validation message. + */ + public withMessage(message: string): FluentRulesGenerator { + this.assertFluentCustomizer(); + this.fluentCustomizer!.withMessage(message); + return this; + } + /** + * Specifies a condition that must be met before attempting to validate the rule. + * @param condition A function that accepts the object as a parameter and returns true + * or false whether the rule should be evaluated. + */ + public when(condition: (object: TObject) => boolean): FluentRulesGenerator { + this.assertFluentCustomizer(); + this.fluentCustomizer!.when(condition); + return this; + } + /** + * Tags the rule instance, enabling the rule to be found easily + * using ValidationRules.taggedRules(rules, tag) + */ + public tag(tag: string): FluentRulesGenerator { + this.assertFluentCustomizer(); + this.fluentCustomizer!.tag(tag); + return this; + } + /** + * Applies the rules to a class or object, making them discoverable by the StandardValidator. + * @param target A class or object. + */ + public on(target: any): FluentRulesGenerator { + this.fluentEnsure!.on(target); + return this; + } + /** + * Rules that have been defined using the fluent API. + */ + public get rules(): Array>> { + return this.fluentEnsure!.rules; + } + /** + * Current rule sequence number. Used to postpone evaluation of rules until rules + * with lower sequence number have successfully validated. The "then" fluent API method + * manages this property, there's usually no need to set it directly. + */ + public get sequence(): number { + return this.fluentRules!.sequence; + } + + public get customRules(): { + [name: string]: { + condition: (value: any, object?: any, ...fluentArgs: any[]) => boolean | Promise; + argsToConfig?: (...args: any[]) => any; + } + } { + return FluentRules.customRules; + } + + private assertFluentCustomizer(): void | never { + if (!this.fluentCustomizer) { + throw new Error(`Invalid call sequence. Are you trying to modify a rule that has not be defined?`); + } + } +} + /** * Part of the fluent rule API. Enables customizing property rules. */ @@ -32,20 +235,10 @@ export class FluentRuleCustomizer { this.fluentEnsure._addRule(this.rule); } - /** - * Validate subsequent rules after previously declared rules have - * been validated successfully. Use to postpone validation of costly - * rules until less expensive rules pass validation. - */ - public then() { - this.fluentRules.sequence++; - return this; - } - /** * Specifies the key to use when looking up the rule's validation message. */ - public withMessageKey(key: string) { + public withMessageKey(key: string): FluentRuleCustomizer { this.rule.messageKey = key; this.rule.message = null; return this; @@ -54,7 +247,7 @@ export class FluentRuleCustomizer { /** * Specifies rule's validation message. */ - public withMessage(message: string) { + public withMessage(message: string): FluentRuleCustomizer { this.rule.messageKey = 'custom'; this.rule.message = this.parsers.message.parse(message); return this; @@ -65,7 +258,7 @@ export class FluentRuleCustomizer { * @param condition A function that accepts the object as a parameter and returns true * or false whether the rule should be evaluated. */ - public when(condition: (object: TObject) => boolean) { + public when(condition: (object: TObject) => boolean): FluentRuleCustomizer { this.rule.when = condition; return this; } @@ -74,7 +267,7 @@ export class FluentRuleCustomizer { * Tags the rule instance, enabling the rule to be found easily * using ValidationRules.taggedRules(rules, tag) */ - public tag(tag: string) { + public tag(tag: string): FluentRuleCustomizer { this.rule.tag = tag; return this; } @@ -85,21 +278,21 @@ export class FluentRuleCustomizer { * Target a property with validation rules. * @param property The property to target. Can be the property name or a property accessor function. */ - public ensure(subject: string | ((model: TObject) => TValue2)) { + public ensure(subject: string | ((model: TObject) => TValue2)): FluentRules { return this.fluentEnsure.ensure(subject); } /** * Targets an object with validation rules. */ - public ensureObject() { + public ensureObject(): FluentRules { return this.fluentEnsure.ensureObject(); } /** * Rules that have been defined using the fluent API. */ - public get rules() { + public get rules(): Array>> { return this.fluentEnsure.rules; } @@ -107,19 +300,29 @@ export class FluentRuleCustomizer { * Applies the rules to a class or object, making them discoverable by the StandardValidator. * @param target A class or object. */ - public on(target: any) { + public on(target: any): FluentEnsure { return this.fluentEnsure.on(target); } ///////// FluentRules APIs ///////// + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + public then(): FluentRuleCustomizer { + this.fluentRules.sequence++; + return this; + } /** * Applies an ad-hoc rule function to the ensured property or object. * @param condition The function to validate the rule. * Will be called with two arguments, the property value and the object. * Should return a boolean or a Promise that resolves to a boolean. */ - public satisfies(condition: (value: TValue, object: TObject) => boolean | Promise, config?: object) { + // tslint:disable-next-line:max-line-length + public satisfies(condition: (value: TValue, object?: TObject) => boolean | Promise, config?: object): FluentRuleCustomizer { return this.fluentRules.satisfies(condition, config); } @@ -128,7 +331,7 @@ export class FluentRuleCustomizer { * @param name The name of the custom or standard rule. * @param args The rule's arguments. */ - public satisfiesRule(name: string, ...args: any[]) { + public satisfiesRule(name: string, ...args: any[]): FluentRuleCustomizer { return this.fluentRules.satisfiesRule(name, ...args); } @@ -136,7 +339,7 @@ export class FluentRuleCustomizer { * Applies the "required" rule to the property. * The value cannot be null, undefined or whitespace. */ - public required() { + public required(): FluentRuleCustomizer { return this.fluentRules.required(); } @@ -145,7 +348,7 @@ export class FluentRuleCustomizer { * Value must match the specified regular expression. * null, undefined and empty-string values are considered valid. */ - public matches(regex: RegExp) { + public matches(regex: RegExp): FluentRuleCustomizer { return this.fluentRules.matches(regex); } @@ -153,7 +356,7 @@ export class FluentRuleCustomizer { * Applies the "email" rule to the property. * null, undefined and empty-string values are considered valid. */ - public email() { + public email(): FluentRuleCustomizer { return this.fluentRules.email(); } @@ -161,7 +364,7 @@ export class FluentRuleCustomizer { * Applies the "minLength" STRING validation rule to the property. * null, undefined and empty-string values are considered valid. */ - public minLength(length: number) { + public minLength(length: number): FluentRuleCustomizer { return this.fluentRules.minLength(length); } @@ -169,7 +372,7 @@ export class FluentRuleCustomizer { * Applies the "maxLength" STRING validation rule to the property. * null, undefined and empty-string values are considered valid. */ - public maxLength(length: number) { + public maxLength(length: number): FluentRuleCustomizer { return this.fluentRules.maxLength(length); } @@ -177,7 +380,7 @@ export class FluentRuleCustomizer { * Applies the "minItems" ARRAY validation rule to the property. * null and undefined values are considered valid. */ - public minItems(count: number) { + public minItems(count: number): FluentRuleCustomizer { return this.fluentRules.minItems(count); } @@ -185,7 +388,7 @@ export class FluentRuleCustomizer { * Applies the "maxItems" ARRAY validation rule to the property. * null and undefined values are considered valid. */ - public maxItems(count: number) { + public maxItems(count: number): FluentRuleCustomizer { return this.fluentRules.maxItems(count); } @@ -193,7 +396,7 @@ export class FluentRuleCustomizer { * Applies the "equals" validation rule to the property. * null, undefined and empty-string values are considered valid. */ - public equals(expectedValue: TValue) { + public equals(expectedValue: TValue): FluentRuleCustomizer { return this.fluentRules.equals(expectedValue); } } @@ -236,7 +439,8 @@ export class FluentRules { * Will be called with two arguments, the property value and the object. * Should return a boolean or a Promise that resolves to a boolean. */ - public satisfies(condition: (value: TValue, object?: TObject) => boolean | Promise, config?: object) { + // tslint:disable-next-line:max-line-length + public satisfies(condition: (value: TValue, object?: TObject) => boolean | Promise, config?: object): FluentRuleCustomizer { return new FluentRuleCustomizer( this.property, condition, config, this.fluentEnsure, this, this.parsers); } @@ -291,9 +495,8 @@ export class FluentRules { */ public email() { // regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address - /* tslint:disable:max-line-length */ + // tslint:disable-next-line:max-line-length return this.matches(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/) - /* tslint:enable:max-line-length */ .withMessageKey('email'); } @@ -356,7 +559,8 @@ export class FluentEnsure { /** * Rules that have been defined using the fluent API. */ - public rules: Rule[][] = []; + public rules: Array>> = []; + private properties: Map = new Map(); constructor(private parsers: Parsers) { } @@ -365,24 +569,53 @@ export class FluentEnsure { * @param property The property to target. Can be the property name or a property accessor * function. */ - public ensure(property: string | PropertyAccessor) { + public ensure(property: string | PropertyAccessor): FluentRules { this.assertInitialized(); + + // const name = this.parsers.property.parse(property); + // const fluentRules = new FluentRules( + // this, + // this.parsers, + // { name, displayName: null }); + // return this.mergeRules(fluentRules, name); + const name = this.parsers.property.parse(property); - const fluentRules = new FluentRules( - this, - this.parsers, - { name, displayName: null }); - return this.mergeRules(fluentRules, name); + + const ruleProperty = { name, displayName: null }; + + // if this property has been previously-ensured then we want to use that RuleProperty object + const preExistingProperty: RuleProperty | undefined = this.properties.get(name); + + if (!preExistingProperty) { + this.properties.set(name, ruleProperty); + } + + return new FluentRules(this, this.parsers, + preExistingProperty ? preExistingProperty : ruleProperty); } /** * Targets an object with validation rules. */ - public ensureObject() { + public ensureObject(): FluentRules { this.assertInitialized(); - const fluentRules = new FluentRules( - this, this.parsers, { name: null, displayName: null }); - return this.mergeRules(fluentRules, null); + + // const fluentRules = new FluentRules( + // this, this.parsers, { name: null, displayName: null }); + // return this.mergeRules(fluentRules, null); + + const objectPropertyKey = '__objectProp__'; + const ruleProperty: RuleProperty = { name: null, displayName: null }; + + // if this property has been previously-ensured then we want to use that RuleProperty object + const preExistingProperty: RuleProperty | undefined = this.properties.get(objectPropertyKey); + + if (!preExistingProperty) { + this.properties.set(objectPropertyKey, ruleProperty); + } + + return new FluentRules(this, this.parsers, + preExistingProperty ? preExistingProperty : ruleProperty); } /** @@ -412,17 +645,17 @@ export class FluentEnsure { throw new Error(`Did you forget to add ".plugin('aurelia-validation')" to your main.js?`); } - private mergeRules(fluentRules: FluentRules, propertyName: string | null) { - const existingRules = this.rules.find(r => r.length > 0 && r[0].property.name === propertyName); - if (existingRules) { - const rule = existingRules[existingRules.length - 1]; - fluentRules.sequence = rule.sequence; - if (rule.property.displayName !== null) { - fluentRules = fluentRules.displayName(rule.property.displayName); - } - } - return fluentRules; - } + // private mergeRules(fluentRules: FluentRules, propertyName: string | null) { + // const existingRules = this.rules.find(r => r.length > 0 && r[0].property.name === propertyName); + // if (existingRules) { + // const rule = existingRules[existingRules.length - 1]; + // fluentRules.sequence = rule.sequence; + // if (rule.property.displayName !== null) { + // fluentRules = fluentRules.displayName(rule.property.displayName); + // } + // } + // return fluentRules; + // } } /** @@ -438,19 +671,48 @@ export class ValidationRules { }; } + public static CreateFluentRulesGenerator(): FluentRulesGenerator { + return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)); + } + + // tslint:disable:max-line-length + + // /** + // * Target a property with validation rules. + // * @param property The property to target. Can be the property name or a property accessor function. + // */ + // // tslint:disable-next-line:max-line-length + // public static ensure(property: string | PropertyAccessor): FluentRules { + // // this works just as well: + // // return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensure(property); + // return new FluentEnsure(ValidationRules.parsers).ensure(property); + // } + + // /** + // * Targets an object with validation rules. + // */ + // public static ensureObject(): FluentRules { + // // this works just as well: + // // return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensureObject(); + // return new FluentEnsure(ValidationRules.parsers).ensureObject(); + // } + + // tslint:enable:max-line-length + /** * Target a property with validation rules. * @param property The property to target. Can be the property name or a property accessor function. */ - public static ensure(property: string | PropertyAccessor) { - return new FluentEnsure(ValidationRules.parsers).ensure(property); + // tslint:disable-next-line:max-line-length + public static ensure(property: string | PropertyAccessor): FluentRulesGenerator { + return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensure(property); } /** * Targets an object with validation rules. */ - public static ensureObject() { - return new FluentEnsure(ValidationRules.parsers).ensureObject(); + public static ensureObject(): FluentRulesGenerator { + return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensureObject(); } /** diff --git a/test/resources/registration-form.ts b/test/resources/registration-form.ts index 19ec1e51..dd8005a9 100644 --- a/test/resources/registration-form.ts +++ b/test/resources/registration-form.ts @@ -45,6 +45,7 @@ ValidationRules.customRule( || obj[otherPropertyName] === undefined || obj[otherPropertyName] === '' || value === obj[otherPropertyName], + // tslint:disable-next-line:no-invalid-template-strings '${$displayName} must match ${$getDisplayName($config.otherPropertyName)}', otherPropertyName => ({ otherPropertyName }) ); @@ -54,7 +55,9 @@ ValidationRules .ensure(f => f.lastName).required() .ensure('email').required().email() .ensure(f => f.number1).satisfies(value => value > 0) - .ensure(f => f.number2).satisfies(value => value > 0).withMessage('${displayName} gots to be greater than zero.') + .ensure(f => f.number2).satisfies(value => value > 0) + // tslint:disable-next-line:no-invalid-template-strings + .withMessage('${displayName} gots to be greater than zero.') .ensure(f => f.password).required() .ensure(f => f.confirmPassword).required().satisfiesRule('matchesProperty', 'password') .on(RegistrationForm); diff --git a/test/validation-message-parser.ts b/test/validation-message-parser.ts index 5d7ba028..33a9699d 100644 --- a/test/validation-message-parser.ts +++ b/test/validation-message-parser.ts @@ -15,8 +15,10 @@ describe('ValidationMessageParser', () => { it('parses', () => { expect(parser.parse('test') instanceof LiteralString).toBe(true); + // tslint:disable-next-line:no-invalid-template-strings expect(parser.parse('${$value} is invalid') instanceof Binary).toBe(true); // tslint:disable-next-line:max-line-length + // tslint:disable-next-line:no-invalid-template-strings expect(parser.parse('${$value} should equal ${$object.foo.bar().baz ? object.something[0] : \'test\'}') instanceof Binary).toBe(true); }); }); diff --git a/test/validator.ts b/test/validator.ts index 8a23bfef..b8fc5182 100644 --- a/test/validator.ts +++ b/test/validator.ts @@ -127,4 +127,165 @@ describe('Validator', () => { }) .then(done); }); + + it('handles multiple ensures on a property', (done: () => void) => { + const obj = { name: 'value' }; + const displayName = 'product name'; + const propertyName = 'name'; + // tslint:disable-next-line:no-invalid-template-strings + const messageMinLength = '\${$displayName} has fewer than 5 characters'; + const messageMinLengthConfirm = `${displayName} has fewer than 5 characters`; + // tslint:disable-next-line:no-invalid-template-strings + const messageRequired = '\${$displayName} is missing'; + const messageRequiredConfirm = `${displayName} is missing`; + + let rules = ValidationRules + .ensure(propertyName) + .displayName(displayName) + .ensure(propertyName) + .required() + .withMessage(messageRequired) + .on(obj) + .rules; + + validator.validateProperty(obj, propertyName, rules) + .then((results: Array) => { + expect(results.length).toEqual(1); + expect(results[0].valid).toEqual(true); + + (obj as any).name = null; + return validator.validateProperty(obj, propertyName, rules); + }) + .then((results: Array) => { + expect(results.length).toEqual(1); + expect(results[0].message).toEqual(messageRequiredConfirm); + + rules = ValidationRules + .ensure(propertyName) + .minLength(5) + .withMessage(messageMinLength) + .ensure(propertyName) + .displayName(displayName) + .required() + .withMessage(messageRequired) + .on(obj) + .rules; + + obj.name = 'abc'; + return validator.validateProperty(obj, propertyName, rules); + }) + .then((results: Array) => { + expect(results.length).toEqual(2); + expect(results[0].message).toEqual(messageMinLengthConfirm); + expect(results[1].valid).toEqual(true); + + rules = ValidationRules + .ensure(propertyName) + .displayName(displayName) + .minLength(5) + .withMessage(messageMinLength) + .ensure(propertyName) + .required() + .withMessage(messageRequired) + .on(obj) + .rules; + + obj.name = 'abc'; + return validator.validateProperty(obj, propertyName, rules); + }) + .then((results: Array) => { + expect(results[0].message).toEqual(messageMinLengthConfirm); + expect(results[1].valid).toEqual(true); + + rules = ValidationRules + .ensure(propertyName) + .displayName(displayName) + .required() + .withMessage(messageRequired) + .ensure(propertyName) + .minLength(5) + .withMessage(messageMinLength) + .on(obj) + .rules; + + obj.name = 'abc'; + return validator.validateProperty(obj, propertyName, rules); + }) + .then((results: Array) => { + expect(results.length).toEqual(2); + expect(results[0].valid).toEqual(true); + expect(results[1].message).toEqual(messageMinLengthConfirm); + + rules = ValidationRules + .ensure(propertyName) + .displayName(displayName) + .required() + .withMessage(messageRequired) + .then() + .ensure(propertyName) + .minLength(5) + .withMessage(messageMinLength) + .on(obj) + .rules; + + obj.name = ''; + return validator.validateProperty(obj, propertyName, rules); + }) + .then((results: Array) => { + expect(results.length).toEqual(2); + expect(results[0].message).toEqual(messageRequiredConfirm); + // a little weird since it actually isn't valid, it just hasn't been tested + expect(results[1].valid).toEqual(true); + + rules = ValidationRules + .ensure(propertyName) + .required() + .displayName(displayName) // <= not the usual position for this call + .withMessage(messageRequired) + .ensure(propertyName) + .minLength(5) + .withMessage(messageMinLength) + .on(obj) + .rules; + + obj.name = 'abc'; + return validator.validateProperty(obj, propertyName, rules); + }) + .then((results: Array) => { + expect(results.length).toEqual(2); + expect(results[0].valid).toEqual(true); + expect(results[1].message).toEqual(messageMinLengthConfirm); + + rules = ValidationRules + .ensure(propertyName) + .required() + .displayName(displayName) // <= not the usual position for this call + .withMessage(messageRequired) + .ensureObject() + .displayName(displayName) + .ensureObject() + .maxLength(5) + .withMessage(messageMinLength) + .on(obj) + .rules; + + obj.name = 'abc'; + return validator.validateObject(obj, rules); + }) + .then((results: Array) => { + expect(results.length).toEqual(2); + expect(results[0].valid).toEqual(true); + expect(results[1].message).toEqual(messageMinLengthConfirm); + rules = ValidationRules + .ensure(propertyName) + .ensure(propertyName) + .on(obj) + .rules; + + obj.name = 'abc'; + // should not crash + return validator.validateProperty(obj, propertyName, rules); + }) + .then(done); + }); }); From 0e0b3ef0d5ea7a888497eac3a8881b8ab5b13502 Mon Sep 17 00:00:00 2001 From: Doug Kent Date: Tue, 23 May 2017 16:39:29 -0400 Subject: [PATCH 2/4] clean up ensure and ensureObject --- src/implementation/validation-rules.ts | 39 +++++++++----------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/implementation/validation-rules.ts b/src/implementation/validation-rules.ts index a7173747..285f1de5 100644 --- a/src/implementation/validation-rules.ts +++ b/src/implementation/validation-rules.ts @@ -676,45 +676,34 @@ export class ValidationRules { } // tslint:disable:max-line-length - - // /** - // * Target a property with validation rules. - // * @param property The property to target. Can be the property name or a property accessor function. - // */ - // // tslint:disable-next-line:max-line-length - // public static ensure(property: string | PropertyAccessor): FluentRules { - // // this works just as well: - // // return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensure(property); - // return new FluentEnsure(ValidationRules.parsers).ensure(property); - // } - - // /** - // * Targets an object with validation rules. - // */ - // public static ensureObject(): FluentRules { - // // this works just as well: - // // return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensureObject(); - // return new FluentEnsure(ValidationRules.parsers).ensureObject(); - // } - - // tslint:enable:max-line-length - /** * Target a property with validation rules. * @param property The property to target. Can be the property name or a property accessor function. */ - // tslint:disable-next-line:max-line-length public static ensure(property: string | PropertyAccessor): FluentRulesGenerator { return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensure(property); } - /** * Targets an object with validation rules. */ public static ensureObject(): FluentRulesGenerator { return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensureObject(); } + // tslint:enable:max-line-length + // /** + // * Target a property with validation rules. + // * @param property The property to target. Can be the property name or a property accessor function. + // */ + // public static ensure(property: string | PropertyAccessor) { + // return new FluentEnsure(ValidationRules.parsers).ensure(property); + // } + // /** + // * Targets an object with validation rules. + // */ + // public static ensureObject() { + // return new FluentEnsure(ValidationRules.parsers).ensureObject(); + // } /** * Defines a custom rule. * @param name The name of the custom rule. Also serves as the message key. From d08c4854968323a9a59651fb2a21c220ac56be87 Mon Sep 17 00:00:00 2001 From: Doug Kent Date: Tue, 23 May 2017 16:49:24 -0400 Subject: [PATCH 3/4] fixed lint complaints --- src/implementation/validation-messages.ts | 6 +----- src/validation-controller.ts | 25 ++++++++--------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/implementation/validation-messages.ts b/src/implementation/validation-messages.ts index 19e36c59..0e88970b 100644 --- a/src/implementation/validation-messages.ts +++ b/src/implementation/validation-messages.ts @@ -37,11 +37,7 @@ export class ValidationMessageProvider { */ public getMessage(key: string): Expression { let message: string; - if (key in validationMessages) { - message = validationMessages[key]; - } else { - message = validationMessages['default']; - } + message = (key in validationMessages) ? validationMessages[key] : validationMessages['default']; return this.parser.parse(message); } diff --git a/src/validation-controller.ts b/src/validation-controller.ts index 07968ec2..4b1db251 100644 --- a/src/validation-controller.ts +++ b/src/validation-controller.ts @@ -104,11 +104,7 @@ export class ValidationController { propertyName: string | PropertyAccessor | null = null ): ValidateResult { let resolvedPropertyName: string | null; - if (propertyName === null) { - resolvedPropertyName = propertyName; - } else { - resolvedPropertyName = this.propertyParser.parse(propertyName); - } + resolvedPropertyName = (propertyName === null) ? propertyName : this.propertyParser.parse(propertyName); const result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); this.processResultDelta('validate', [], [result]); return result; @@ -176,11 +172,8 @@ export class ValidationController { if (instruction) { const { object, propertyName, rules } = instruction; let predicate: (result: ValidateResult) => boolean; - if (instruction.propertyName) { - predicate = x => x.object === object && x.propertyName === propertyName; - } else { - predicate = x => x.object === object; - } + predicate = (instruction.propertyName) ? x => x.object === object && x.propertyName === propertyName : + x => x.object === object; if (rules) { return x => predicate(x) && this.validator.ruleExists(rules, x.rule); } @@ -204,13 +197,11 @@ export class ValidationController { // if rules were not specified, check the object map. rules = rules || this.objects.get(object); // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = () => this.validator.validateObject(object, rules); - } else { - // validate the specified property. - execute = () => this.validator.validateProperty(object, propertyName, rules); - } + execute = (instruction.propertyName === undefined) ? + // validate the specified object + () => this.validator.validateObject(object, rules) : + // validate the specified property + () => this.validator.validateProperty(object, propertyName, rules); } else { // validate all objects and bindings. execute = () => { From 2cd9c8077fed685093681c56feef3c3b99e12281 Mon Sep 17 00:00:00 2001 From: Doug Kent Date: Tue, 23 May 2017 17:26:21 -0400 Subject: [PATCH 4/4] make sure 'then' works --- package.json | 2 +- src/implementation/validation-rules.ts | 21 +++++++-------------- test/validator.ts | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 28bfefc4..b07626cc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "lint": "cross-env tslint --project tsconfig.json", "pretest": "cross-env npm run lint", "test": "cross-env rimraf dist && tsc && karma start --single-run", - "develop": "concurrently \"./node_modules/.bin/tsc --watch\" \"./node_modules/.bin/karma start\"", + "develop": "concurrently \"tsc --watch\" \"karma start\"", "prebuild:amd": "cross-env rimraf dist/amd", "build:amd": "cross-env tsc --project tsconfig.build.json --outDir dist/amd --module amd", "postbuild:amd": "cross-env copyfiles --up 1 src/**/*.html src/**/*.css dist/amd", diff --git a/src/implementation/validation-rules.ts b/src/implementation/validation-rules.ts index 285f1de5..9b88a894 100644 --- a/src/implementation/validation-rules.ts +++ b/src/implementation/validation-rules.ts @@ -680,30 +680,23 @@ export class ValidationRules { * Target a property with validation rules. * @param property The property to target. Can be the property name or a property accessor function. */ + // public static ensure(property: string | PropertyAccessor) { + // return new FluentEnsure(ValidationRules.parsers).ensure(property); + // } public static ensure(property: string | PropertyAccessor): FluentRulesGenerator { return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensure(property); } + /** * Targets an object with validation rules. */ + // public static ensureObject() { + // return new FluentEnsure(ValidationRules.parsers).ensureObject(); + // } public static ensureObject(): FluentRulesGenerator { return new FluentRulesGenerator(new FluentEnsure(ValidationRules.parsers)).ensureObject(); } // tslint:enable:max-line-length - // /** - // * Target a property with validation rules. - // * @param property The property to target. Can be the property name or a property accessor function. - // */ - // public static ensure(property: string | PropertyAccessor) { - // return new FluentEnsure(ValidationRules.parsers).ensure(property); - // } - - // /** - // * Targets an object with validation rules. - // */ - // public static ensureObject() { - // return new FluentEnsure(ValidationRules.parsers).ensureObject(); - // } /** * Defines a custom rule. * @param name The name of the custom rule. Also serves as the message key. diff --git a/test/validator.ts b/test/validator.ts index b8fc5182..00d3e665 100644 --- a/test/validator.ts +++ b/test/validator.ts @@ -233,6 +233,7 @@ describe('Validator', () => { }) .then((results: Array) => { expect(results.length).toEqual(2); + expect(results[0].valid).toEqual(false); expect(results[0].message).toEqual(messageRequiredConfirm); // a little weird since it actually isn't valid, it just hasn't been tested expect(results[1].valid).toEqual(true); @@ -255,6 +256,28 @@ describe('Validator', () => { expect(results.length).toEqual(2); expect(results[0].valid).toEqual(true); expect(results[1].message).toEqual(messageMinLengthConfirm); + rules = ValidationRules + .ensure(propertyName) + .required() + .displayName(displayName) + .withMessage(messageRequired) + .then() + .ensure(propertyName) // <= challenge is putting another ensure after a then + .maxLength(5) + .withMessage(messageMinLength) + .on(obj) + .rules; + + delete obj.name; + return validator.validateObject(obj, rules); + }) + .then((results: Array) => { + // make sure 'then' is working + expect(results.length).toEqual(2); + expect(results[0].valid).toEqual(false); + expect(results[0].message).toEqual(messageRequiredConfirm); + // a little weird since it actually isn't valid, it just hasn't been tested + expect(results[1].valid).toEqual(true); rules = ValidationRules .ensure(propertyName) @@ -272,6 +295,7 @@ describe('Validator', () => { obj.name = 'abc'; return validator.validateObject(obj, rules); }) + .then((results: Array) => { expect(results.length).toEqual(2); expect(results[0].valid).toEqual(true);