validation icon indicating copy to clipboard operation
validation copied to clipboard

Add alternative to fluent rule API that enables creating rules from data

Open dkent600 opened this issue 9 years ago • 9 comments

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

  1. Create an interface IFluentRules (or whatever you want to name it) that includes all of the methods used here, or that might be used, including ensure, displayName, minLength, required, etc.......
  2. Instead of using ValidationRules, use a new factory method to create an empty FluentRules that implements IFluentRules.
  3. redefine all the methods in IFluentRules to return an IFluentRules

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`));
}

dkent600 avatar Oct 14 '16 14:10 dkent600

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;

jdanyow avatar Oct 15 '16 10:10 jdanyow

Sorry, for the delay in responding....I haven't forgotten about this...

dkent600 avatar Oct 22 '16 14:10 dkent600

no worries- let me know if it helps at all

jdanyow avatar Oct 22 '16 18:10 jdanyow

@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.

dkent600 avatar Oct 25 '16 03:10 dkent600

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.

Beijerinc avatar Nov 01 '16 08:11 Beijerinc

I am also interested in a better way to create Validation Rules from schema data.

lstarky avatar Feb 13 '17 14:02 lstarky

@dkent600 what's the status of this- I think you had done some prototyping but I lost track with the move, etc.

jdanyow avatar Mar 25 '17 21:03 jdanyow

My proposal is here: https://github.com/aurelia/validation/compare/master...dkent600:Incrementally-add-rules-for-properties-%23400

It introduces a FluentRulesGenerator class

dkent600 avatar Mar 25 '17 22:03 dkent600

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;

dkent600 avatar Mar 26 '17 21:03 dkent600