validation
validation copied to clipboard
Add alternative to fluent rule API that enables creating rules from data
Use Case Looping over a set of model properties to apply validation rules to each.
Problem Currently, three different object classes are used in this scenario. This adds complexity to coding in TypeScript. Consider the following code:
let ruleSetter: FluentRules<any, any> | FluentRuleCustomizer<any, any> = null;
for (let property of formSchema.properties) {
if (this.requiresValidation(property, formSchema)) {
/**
* ... then first we will call ensure.displayName, returning FluentRules,
* and second will call at least one method that will return FluentRuleCustomizer.
* Thus each time we reach here ruleSetter will be a FluentRuleCustomizer (except the first time, when it is null),
* whereupon we will immediately convert it into a FluentRules and then again to a FluentRuleCustomizer.
*/
if (ruleSetter === null) {
ruleSetter = ValidationRules
.ensure(property.name)
.displayName(property.description);
// at this point, ruleSetter is a FluentRules
} else {
/**
* At this point we know it is a FluentRuleCustomizer because some rule was applied to it,
* but here it will again become a FluentRules.
*/
ruleSetter = (ruleSetter as FluentRuleCustomizer<any, any>)
.ensure(property.name)
.displayName(property.description);
}
// these will set ruleSetter to a FluentRuleCustomizer
ruleSetter = ruleSetter.required());
ruleSetter = ruleSetter
.minLength(property.minLength)
.withMessage(`${property.description} must contain at least ${property.minLength} characters`));
.
.
.
}
Request
- Create an interface
IFluentRules(or whatever you want to name it) that includes all of the methods used here, or that might be used, includingensure,displayName,minLength,required, etc....... - Instead of using
ValidationRules, use a new factory method to create an empty FluentRules that implementsIFluentRules. - redefine all the methods in
IFluentRulesto return anIFluentRules
The resulting code would look like this:
let ruleSetter: IFluentRules= ValidationRules.create();
for (let property of formSchema.properties) {
if (this.requiresValidation(property, formSchema)) {
ruleSetter
.ensure(property.name)
.displayName(property.description);
// these will set ruleSetter to a FluentRuleCustomizer
ruleSetter = ruleSetter.required();
ruleSetter = ruleSetter
.minLength(property.minLength)
.withMessage(`${property.description} must contain at least ${property.minLength} characters`));
}
here's a workaround- let me know if it helps:
const ruleDefinitions = [
{
propertyName: 'firstName',
displayName: 'First Name',
rules: [
{ name: 'required', args: [] },
{ name: 'minLength', args: [3] },
{ name: 'maxLength', args: [30] },
]
},
{
propertyName: 'lastName',
displayName: 'Last Name',
rules: [
{ name: 'required', args: [] },
{ name: 'minLength', args: [3] },
{ name: 'maxLength', args: [30] },
]
},
];
let ensureAPI: { ensure: (propertyName: string) => FluentRules<any, any> } = ValidationRules;
for (let ruleDefinition of ruleDefinitions) {
let ruleAPI: { satisfiesRule: (name: string, ...args: any[]) => FluentRuleCustomizer<any, any> };
ruleAPI = ensureAPI.ensure(ruleDefinition.propertyName)
.displayName(ruleDefinition.displayName);
for (let rule of ruleDefinition.rules) {
ensureAPI = ruleAPI = ruleAPI.satisfiesRule(rule.name, ...rule.args);
}
}
let rules = (<FluentEnsure<any>>ensureAPI).rules;
Sorry, for the delay in responding....I haven't forgotten about this...
no worries- let me know if it helps at all
@jdanyow I've played around with your suggestion, above. I particularly wanted to see if my code could be any better using named rules.
It is basically a minor improvement over the code I posted at the top (it mainly gets rid of the if (ruleSetter === null) / else). But the ensureAPI = ruleAPI = are a bit messy, as are the types for ensureAPI and ruleAPI.
It is an interesting exercise, but I prefer the way I proposed.
I had the same problem with trying to dynamically construct validation rules using the fluent API. I ended up using a 'void' rule to make sure the first call already resulted in a FluentRuleCustomizer instead of a FluentRule.
if (this.dataItem.__validationRules__) {
rules = this.dataItem.__validationRules__.
ensure(this.propertyMetadata.Name).
satisfiesRule("void");
} else {
rules = ValidationRules.
ensure(this.propertyMetadata.Name).
satisfiesRule("void");
}
An interface with the overlap between the FluentRule and FluentRuleCustomizer would be nice to have.
I am also interested in a better way to create Validation Rules from schema data.
@dkent600 what's the status of this- I think you had done some prototyping but I lost track with the move, etc.
My proposal is here: https://github.com/aurelia/validation/compare/master...dkent600:Incrementally-add-rules-for-properties-%23400
It introduces a FluentRulesGenerator class
Regarding the proposed code enhancement I referenced just above ^^^^ , what follows is an example of what the code enables you to do that accomplishes the task of the original use case given at the top of this issue.
I will also note here that this change also, as an effortless side-effect, addresses #400, though I note that #400 was recently addressed separately. That code could be discarded.
Here is an example of how one can currently accomplish the task at hand:
let ensureApi: { ensure: (propertyName: string) => FluentRules<any, any> } = ValidationRules;
for (let property of formSchema.properties) {
if (this.requiresValidation(property, formSchema)) {
let ruleApi: { satisfiesRule: (name: string, ...args: any[]) => FluentRuleCustomizer<any, any> } =
ensureApi.ensure(property.name).displayName(property.description);
if (property.required) {
ruleApi = ruleApi.satisfiesRule("required");
}
if (property.minLength !== undefined) {
ruleApi = ruleApi.satisfiesRule("minLength", property.minLength);
}
ensureApi = ruleApi as FluentRuleCustomizer<any, any>;
}
}
return ensureApi;
Here is how I am able to do it with the proposed code changes:
let ruleGenerator: FluentRulesGenerator<any> = ValidationRules.CreateFluentRulesGenerator();
for (let property of formSchema.properties) {
if (this.requiresValidation(property, formSchema)) {
ruleGenerator.ensure(property.name).displayName(property.description);
if (property.required) {
ruleGenerator.satisfiesRule("required");
}
if (property.minLength !== undefined) {
ruleGenerator.satisfiesRule("minLength", property.minLength);
}
}
}
return ruleGenerator;