Skip to content

Commit

Permalink
feat(validation): link two or more properties' validation
Browse files Browse the repository at this point in the history
* possibility to connect properties if they are related to each other
  • Loading branch information
carola-massardi committed Jul 28, 2024
1 parent af7c252 commit 84ebba7
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 0 deletions.
13 changes: 13 additions & 0 deletions docs/user-docs/aurelia-packages/validation/defining-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ validationRules

With TypeScript support, intellisense is available for both the variants.

## Linking a property's validation with others

This functionality can be useful when a change in one property has to trigger additional validations because the values are intrinsically related.
Linked properties must be provided as an array of object keys.

```typescript
validationRules
.on(person)
.ensure('name')
.linkProperties(['age', 'address']);
```


## Associating validation rules with property

After selecting a property with `.ensure` the next step is to associate rules. The rules can be built-in or custom. Irrespective of what kind of rule it is, at the low-level it is nothing but an instance of the rule class. For example, the "required" validation is implemented by the `RequiredRule` class. This will be more clear when you will define custom validation rules. However, let us take a look at the built-in rules first.
Expand Down
21 changes: 21 additions & 0 deletions packages/__tests__/src/validation/rule-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,27 @@ describe('validation/rule-provider.spec.ts', function () {
validationRules.off();
});

it('can be linked to other properties', async function () {
const { validationRules } = setup();
const obj: Person = new Person((void 0)!, (void 0)!, (void 0)!);
const properties: PropertyKey[] = ['age', 'address'];
const rule = validationRules
.on(obj)
.ensure('name')
.required()
.satisfies((value) => value === 'foobar')
.linkProperties(properties)
.ensure('age').min(18)
.ensure('address').required()
.rules[0];

assert.equal(rule.linkedProperties.length, 2);
assert.deepEqual(rule.linkedProperties[0], 'age');
assert.deepEqual(rule.linkedProperties[1], 'address');

validationRules.off();
});

// The state rule is tested here as the individual unit tests are somewhat pointless.
describe('StateRule', function () {
it('stateful message - sync state function', async function () {
Expand Down
31 changes: 31 additions & 0 deletions packages/__tests__/src/validation/validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,37 @@ describe('validation/validator.spec.ts', function () {

validationRules.off();
});

it('if given, validates linked properties', async function () {
const { sut, validationRules } = setup();
const obj: Person = new Person((void 0)!, (void 0)!, (void 0)!);
const linkedProperties = ['age', 'address.line1'];

const rules = validationRules
.on(defineRuleOnClass ? Person : obj)

.ensure(getProperty1() as any)
.required()
.linkProperties(linkedProperties)

.ensure(getProperty2() as any)
.required()

.ensure(getProperty3() as any)
.required()
.withMessage('Address is required.')

.rules;

const result = await sut.validate(new ValidateInstruction(obj, 'name'));
assert.equal(result.length, 3);

assertValidationResult(result[0], false, 'name', obj, RequiredRule, 'Name is required.');
assertValidationResult(result[1], false, 'age', obj, RequiredRule, 'Age is required.');
assertValidationResult(result[2], false, 'address.line1', obj, RequiredRule, 'Address is required.');

validationRules.off();
});
}

const properties3 = [
Expand Down
10 changes: 10 additions & 0 deletions packages/validation/src/rule-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export class PropertyRule<TObject extends IValidateable = IValidateable, TValue
private latestRule?: IValidationRule;
/** @internal */
public readonly l: IServiceLocator;
public linkedProperties: PropertyKey[] = [];

public constructor(
locator: IServiceLocator,
Expand Down Expand Up @@ -266,6 +267,15 @@ export class PropertyRule<TObject extends IValidateable = IValidateable, TValue
throw createMappedError(ErrorNames.rule_provider_no_rule_found);
}
}

/**
* Links the PropertyRule to other key values.
* @param properties - Array of property keys to link.
*/
public linkProperties(properties: PropertyKey[]) {
this.linkedProperties.push(...properties);
return this;
}
// #endregion

// #region rule helper API
Expand Down
5 changes: 5 additions & 0 deletions packages/validation/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export class StandardValidator implements IValidator {
const scope = Scope.create({ [rootObjectSymbol]: object });

if (propertyName !== void 0) {
const propertyRule: PropertyRule | undefined = rules.find((r) => r.property.name === propertyName);
if (propertyRule !== void 0 && propertyRule.linkedProperties.length > 0) {
const additionalRules = propertyRule.linkedProperties.map(lp => rules.find(r => r.property.name === lp)).filter(Boolean) as PropertyRule[];
return (await Promise.all([propertyRule?.validate(object, propertyTag, scope), ...additionalRules.map(async (rule) => rule.validate(object, propertyTag, scope))])).flat();
}
return (await rules.find((r) => r.property.name === propertyName)?.validate(object, propertyTag, scope)) ?? [];
}

Expand Down

0 comments on commit 84ebba7

Please sign in to comment.