Skip to content

Commit 846124a

Browse files
committed
Breaking change: when and unless conditions only apply to rules defined since last when/unless call
1 parent ec9702d commit 846124a

3 files changed

Lines changed: 230 additions & 6 deletions

File tree

src/valueValidator/CoreValueValidatorBuilder.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export abstract class CoreValueValidatorBuilder<
4040
rule: Rule<TModel, TTransformedValue> | AsyncRule<TModel, TTransformedValue>;
4141
}> = [];
4242

43+
protected numberOfRulesInCurrentConditionChain: number = 0;
44+
4345
private readonly rebuildValidate: () => void;
4446

4547
protected transformValue: ValueTransformer<TValue, TTransformedValue>;
@@ -54,14 +56,26 @@ export abstract class CoreValueValidatorBuilder<
5456

5557
protected pushRule = (rule: Rule<TModel, TTransformedValue>) => {
5658
this.rules.push({ isAsync: false, rule });
59+
this.numberOfRulesInCurrentConditionChain += 1;
5760
this.rebuildValidate();
5861
};
5962

6063
protected pushAsyncRule = (rule: AsyncRule<TModel, TTransformedValue>) => {
6164
this.rules.push({ isAsync: true, rule });
65+
this.numberOfRulesInCurrentConditionChain += 1;
6266
this.rebuildValidate();
6367
};
6468

69+
protected getRulesInCurrentConditionChain = (): Array<{
70+
isAsync: boolean;
71+
rule: Rule<TModel, TTransformedValue> | AsyncRule<TModel, TTransformedValue>;
72+
}> =>
73+
this.numberOfRulesInCurrentConditionChain === 0
74+
? []
75+
: this.rules.slice(-this.numberOfRulesInCurrentConditionChain);
76+
77+
protected breakOffCurrentConditionChain = () => (this.numberOfRulesInCurrentConditionChain = 0);
78+
6579
public withMessage: WithMessage<TModel, TTransformedValue> = (message: string) => {
6680
const latestRule = this.getLatestRule();
6781
latestRule.rule.setCustomErrorMessage(message);
@@ -74,39 +88,37 @@ export abstract class CoreValueValidatorBuilder<
7488
>;
7589
};
7690

77-
// TODO: Make the behaviour here consistent with FluentValidation
78-
// https://github.com/AlexJPotter/fluentvalidation-ts/issues/45
7991
public when = (
8092
condition: (model: TModel) => boolean,
8193
appliesTo: AppliesTo = 'AppliesToAllValidators',
8294
) => {
8395
if (appliesTo === 'AppliesToAllValidators') {
84-
for (const rule of this.rules) {
96+
for (const rule of this.getRulesInCurrentConditionChain()) {
8597
rule.rule.setWhenCondition(condition);
8698
}
8799
} else {
88100
const latestRule = this.getLatestRule();
89101
latestRule.rule.setWhenCondition(condition);
90102
}
91103
this.rebuildValidate();
104+
this.breakOffCurrentConditionChain();
92105
return this.getAllRules();
93106
};
94107

95-
// TODO: Make the behaviour here consistent with FluentValidation
96-
// https://github.com/AlexJPotter/fluentvalidation-ts/issues/45
97108
public unless = (
98109
condition: (model: TModel) => boolean,
99110
appliesTo: AppliesTo = 'AppliesToAllValidators',
100111
) => {
101112
if (appliesTo === 'AppliesToAllValidators') {
102-
for (const rule of this.rules) {
113+
for (const rule of this.getRulesInCurrentConditionChain()) {
103114
rule.rule.setUnlessCondition(condition);
104115
}
105116
} else {
106117
const latestRule = this.getLatestRule();
107118
latestRule.rule.setUnlessCondition(condition);
108119
}
109120
this.rebuildValidate();
121+
this.breakOffCurrentConditionChain();
110122
return this.getAllRules();
111123
};
112124

test/unless.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,59 @@ describe('unless', () => {
5555
});
5656
});
5757

58+
describe('if applied to all validators multiple times in the same chain', () => {
59+
it('applies each condition separately to the rules between the .unless calls', () => {
60+
class TestValidator extends Validator<TestType> {
61+
constructor() {
62+
super();
63+
64+
this.ruleFor('nullableNumberProperty')
65+
.greaterThan(0)
66+
.unless((model) => model.stringProperty === 'Negative', 'AppliesToAllValidators')
67+
.lessThan(0)
68+
.unless((model) => model.stringProperty === 'Positive', 'AppliesToAllValidators');
69+
}
70+
}
71+
const validator = new TestValidator();
72+
73+
expect(
74+
validator.validate({
75+
...testInstance,
76+
stringProperty: 'Positive',
77+
nullableNumberProperty: 1,
78+
}),
79+
).toEqual({});
80+
81+
expect(
82+
validator.validate({
83+
...testInstance,
84+
stringProperty: 'Positive',
85+
nullableNumberProperty: -1,
86+
}),
87+
).toEqual({
88+
nullableNumberProperty: 'Value must be greater than 0',
89+
});
90+
91+
expect(
92+
validator.validate({
93+
...testInstance,
94+
stringProperty: 'Negative',
95+
nullableNumberProperty: -1,
96+
}),
97+
).toEqual({});
98+
99+
expect(
100+
validator.validate({
101+
...testInstance,
102+
stringProperty: 'Negative',
103+
nullableNumberProperty: 1,
104+
}),
105+
).toEqual({
106+
nullableNumberProperty: 'Value must be less than 0',
107+
});
108+
});
109+
});
110+
58111
describe('if applied to only the current validator', () => {
59112
class TestValidator extends Validator<TestType> {
60113
constructor() {
@@ -192,6 +245,59 @@ describe('unless', () => {
192245
});
193246
});
194247

248+
describe('if applied to all validators multiple times in the same chain', () => {
249+
it('applies each condition separately to the rules between the .unless calls', async () => {
250+
class TestValidator extends AsyncValidator<TestType> {
251+
constructor() {
252+
super();
253+
254+
this.ruleFor('nullableNumberProperty')
255+
.greaterThan(0)
256+
.unless((model) => model.stringProperty === 'Negative', 'AppliesToAllValidators')
257+
.lessThan(0)
258+
.unless((model) => model.stringProperty === 'Positive', 'AppliesToAllValidators');
259+
}
260+
}
261+
const validator = new TestValidator();
262+
263+
expect(
264+
await validator.validateAsync({
265+
...testInstance,
266+
stringProperty: 'Positive',
267+
nullableNumberProperty: 1,
268+
}),
269+
).toEqual({});
270+
271+
expect(
272+
await validator.validateAsync({
273+
...testInstance,
274+
stringProperty: 'Positive',
275+
nullableNumberProperty: -1,
276+
}),
277+
).toEqual({
278+
nullableNumberProperty: 'Value must be greater than 0',
279+
});
280+
281+
expect(
282+
await validator.validateAsync({
283+
...testInstance,
284+
stringProperty: 'Negative',
285+
nullableNumberProperty: -1,
286+
}),
287+
).toEqual({});
288+
289+
expect(
290+
await validator.validateAsync({
291+
...testInstance,
292+
stringProperty: 'Negative',
293+
nullableNumberProperty: 1,
294+
}),
295+
).toEqual({
296+
nullableNumberProperty: 'Value must be less than 0',
297+
});
298+
});
299+
});
300+
195301
describe('if applied to only the current validator', () => {
196302
class TestValidator extends AsyncValidator<TestType> {
197303
constructor() {

test/when.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,59 @@ describe('when', () => {
5555
});
5656
});
5757

58+
describe('if applied to all validators multiple times in the same chain', () => {
59+
it('applies each condition separately to the rules between the .when calls', () => {
60+
class TestValidator extends Validator<TestType> {
61+
constructor() {
62+
super();
63+
64+
this.ruleFor('nullableNumberProperty')
65+
.greaterThan(0)
66+
.when((model) => model.stringProperty === 'Positive', 'AppliesToAllValidators')
67+
.lessThan(0)
68+
.when((model) => model.stringProperty === 'Negative', 'AppliesToAllValidators');
69+
}
70+
}
71+
const validator = new TestValidator();
72+
73+
expect(
74+
validator.validate({
75+
...testInstance,
76+
stringProperty: 'Positive',
77+
nullableNumberProperty: 1,
78+
}),
79+
).toEqual({});
80+
81+
expect(
82+
validator.validate({
83+
...testInstance,
84+
stringProperty: 'Positive',
85+
nullableNumberProperty: -1,
86+
}),
87+
).toEqual({
88+
nullableNumberProperty: 'Value must be greater than 0',
89+
});
90+
91+
expect(
92+
validator.validate({
93+
...testInstance,
94+
stringProperty: 'Negative',
95+
nullableNumberProperty: -1,
96+
}),
97+
).toEqual({});
98+
99+
expect(
100+
validator.validate({
101+
...testInstance,
102+
stringProperty: 'Negative',
103+
nullableNumberProperty: 1,
104+
}),
105+
).toEqual({
106+
nullableNumberProperty: 'Value must be less than 0',
107+
});
108+
});
109+
});
110+
58111
describe('if applied to only the current validator', () => {
59112
class TestValidator extends Validator<TestType> {
60113
constructor() {
@@ -192,6 +245,59 @@ describe('when', () => {
192245
});
193246
});
194247

248+
describe('if applied to all validators multiple times in the same chain', () => {
249+
it('applies each condition separately to the rules between the .when calls', async () => {
250+
class TestValidator extends AsyncValidator<TestType> {
251+
constructor() {
252+
super();
253+
254+
this.ruleFor('nullableNumberProperty')
255+
.greaterThan(0)
256+
.when((model) => model.stringProperty === 'Positive', 'AppliesToAllValidators')
257+
.lessThan(0)
258+
.when((model) => model.stringProperty === 'Negative', 'AppliesToAllValidators');
259+
}
260+
}
261+
const validator = new TestValidator();
262+
263+
expect(
264+
await validator.validateAsync({
265+
...testInstance,
266+
stringProperty: 'Positive',
267+
nullableNumberProperty: 1,
268+
}),
269+
).toEqual({});
270+
271+
expect(
272+
await validator.validateAsync({
273+
...testInstance,
274+
stringProperty: 'Positive',
275+
nullableNumberProperty: -1,
276+
}),
277+
).toEqual({
278+
nullableNumberProperty: 'Value must be greater than 0',
279+
});
280+
281+
expect(
282+
await validator.validateAsync({
283+
...testInstance,
284+
stringProperty: 'Negative',
285+
nullableNumberProperty: -1,
286+
}),
287+
).toEqual({});
288+
289+
expect(
290+
await validator.validateAsync({
291+
...testInstance,
292+
stringProperty: 'Negative',
293+
nullableNumberProperty: 1,
294+
}),
295+
).toEqual({
296+
nullableNumberProperty: 'Value must be less than 0',
297+
});
298+
});
299+
});
300+
195301
describe('if applied to only the current validator', () => {
196302
class TestValidator extends AsyncValidator<TestType> {
197303
constructor() {

0 commit comments

Comments
 (0)