Skip to content

Commit 4602c98

Browse files
authored
Introduce Conflict Strategy that defines the behavior when a constraint name conflicts (#331)
when adding a constraint. fixes gh-253
1 parent be19c9c commit 4602c98

File tree

6 files changed

+241
-1
lines changed

6 files changed

+241
-1
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2018-2023 Toshiaki Maki <makingx@gmail.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package am.ik.yavi.builder;
17+
18+
import java.util.List;
19+
20+
import am.ik.yavi.core.ConstraintPredicates;
21+
22+
/**
23+
* Conflict Strategy that defines the behavior when a constraint name conflicts when
24+
* adding a constraint.
25+
*
26+
* @since 0.13.0
27+
*/
28+
@FunctionalInterface
29+
public interface ConflictStrategy {
30+
/**
31+
* Define how to resolve conflicts of a constraint name when adding a constraint.
32+
*
33+
* @param predicatesList existing predicates list
34+
* @param predicates new (conflicting) predicate
35+
* @param <T> target type
36+
*/
37+
<T> void resolveConflict(List<ConstraintPredicates<T, ?>> predicatesList,
38+
ConstraintPredicates<T, ?> predicates);
39+
40+
/**
41+
* Do nothing if a conflict occurs. That means simply appending the predicate.
42+
*/
43+
ConflictStrategy NOOP = new ConflictStrategy() {
44+
@Override
45+
public <T> void resolveConflict(
46+
List<ConstraintPredicates<T, ?>> constraintPredicates,
47+
ConstraintPredicates<T, ?> predicates) {
48+
// NOOP
49+
}
50+
};
51+
52+
/**
53+
* If there is a conflict, remove the existing predicates and adopt the new predicate.
54+
*/
55+
ConflictStrategy OVERRIDE = new ConflictStrategy() {
56+
@Override
57+
public <T> void resolveConflict(
58+
List<ConstraintPredicates<T, ?>> constraintPredicates,
59+
ConstraintPredicates<T, ?> predicates) {
60+
constraintPredicates.clear();
61+
}
62+
};
63+
}

src/main/java/am/ik/yavi/builder/ValidatorBuilder.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Arrays;
3030
import java.util.Collection;
3131
import java.util.Deque;
32+
import java.util.LinkedHashMap;
3233
import java.util.LinkedList;
3334
import java.util.List;
3435
import java.util.Map;
@@ -125,6 +126,8 @@ public class ValidatorBuilder<T> implements Cloneable {
125126

126127
boolean failFast = false;
127128

129+
ConflictStrategy conflictStrategy = ConflictStrategy.NOOP;
130+
128131
public ValidatorBuilder() {
129132
this(DEFAULT_SEPARATOR);
130133
}
@@ -188,7 +191,8 @@ public static <X> ValidatorBuilder<X> of() {
188191
}
189192

190193
public Validator<T> build() {
191-
return new Validator<>(messageKeySeparator, this.predicatesList,
194+
return new Validator<>(messageKeySeparator,
195+
new PredicatesList<>(this.conflictStrategy, this.predicatesList).toList(),
192196
this.collectionValidators, this.conditionalValidators,
193197
this.messageFormatter == null ? new SimpleMessageFormatter()
194198
: this.messageFormatter,
@@ -903,6 +907,20 @@ public ValidatorBuilder<T> failFast(boolean failFast) {
903907
return this;
904908
}
905909

910+
/**
911+
* Sets the {@link ConflictStrategy} that defines the behavior when a constraint name
912+
* conflicts when adding a constraint. By default, {@link ConflictStrategy#NOOP} is
913+
* used.
914+
*
915+
* @param conflictStrategy Conflict Strategy
916+
* @return validator builder (self)
917+
* @since 0.13.0
918+
*/
919+
public ValidatorBuilder<T> conflictStrategy(ConflictStrategy conflictStrategy) {
920+
this.conflictStrategy = conflictStrategy;
921+
return this;
922+
}
923+
906924
public <N> ValidatorBuilder<T> nest(Function<T, N> nested, String name,
907925
Validator<N> validator) {
908926
return this.nest(nested, name, validator, NullAs.INVALID);
@@ -1197,4 +1215,46 @@ public interface ToYearMonthConstraint<T> extends Function<T, YearMonth> {
11971215
public interface ValidatorBuilderConverter<T>
11981216
extends Function<ValidatorBuilder<T>, ValidatorBuilder<T>> {
11991217
}
1218+
1219+
/**
1220+
* An internal class that manages predicate conflicts
1221+
*
1222+
* @param <T> target type
1223+
* @since 0.13.0
1224+
*/
1225+
static final class PredicatesList<T> {
1226+
private final ConflictStrategy conflictStrategy;
1227+
1228+
private final Map<String, List<ConstraintPredicates<T, ?>>> predicatesListMap = new LinkedHashMap<>();
1229+
1230+
public PredicatesList(ConflictStrategy conflictStrategy) {
1231+
this.conflictStrategy = conflictStrategy;
1232+
}
1233+
1234+
public PredicatesList(ConflictStrategy conflictStrategy,
1235+
List<ConstraintPredicates<T, ?>> predicatesList) {
1236+
this(conflictStrategy);
1237+
predicatesList.forEach(this::add);
1238+
}
1239+
1240+
public void add(ConstraintPredicates<T, ?> predicates) {
1241+
final List<ConstraintPredicates<T, ?>> predicatesList = this.predicatesListMap
1242+
.computeIfAbsent(predicates.name(), s -> new ArrayList<>());
1243+
if (!predicatesList.isEmpty()) {
1244+
this.conflictStrategy.resolveConflict(predicatesList, predicates);
1245+
}
1246+
predicatesList.add(predicates);
1247+
}
1248+
1249+
public List<ConstraintPredicates<T, ?>> toList() {
1250+
final int length = predicatesListMap.values().stream().mapToInt(List::size)
1251+
.sum();
1252+
final List<ConstraintPredicates<T, ?>> list = new ArrayList<>(length);
1253+
for (List<ConstraintPredicates<T, ?>> predicates : predicatesListMap
1254+
.values()) {
1255+
list.addAll(predicates);
1256+
}
1257+
return list;
1258+
}
1259+
}
12001260
}

src/main/java/am/ik/yavi/core/ErrorHandler.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/*
2+
* Copyright (C) 2018-2023 Toshiaki Maki <makingx@gmail.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package am.ik.yavi.core;
217

318
/**

src/test/java/am/ik/yavi/arguments/Car.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/*
2+
* Copyright (C) 2018-2023 Toshiaki Maki <makingx@gmail.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package am.ik.yavi.arguments;
217

318
import am.ik.yavi.builder.IntegerValidatorBuilder;

src/test/java/am/ik/yavi/core/Gh249Test.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/*
2+
* Copyright (C) 2018-2023 Toshiaki Maki <makingx@gmail.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package am.ik.yavi.core;
217

318
import java.util.Collections;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (C) 2018-2023 Toshiaki Maki <makingx@gmail.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package am.ik.yavi.core;
17+
18+
import am.ik.yavi.builder.ConflictStrategy;
19+
import am.ik.yavi.builder.ValidatorBuilder;
20+
import org.junit.jupiter.api.Test;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
public class Gh253Test {
25+
26+
ValidatorBuilder<Car> baseValidatorBuilder = ValidatorBuilder.<Car> of()
27+
.constraint(Car::getManufacturer, "manufacturer", c -> c.notNull())
28+
.constraint(Car::getSeatCount, "seatCount", c -> c.greaterThanOrEqual(2));
29+
30+
@Test
31+
void overrideConstraint() {
32+
final Car target = new Car("foo", "AAA", 2);
33+
final Validator<Car> baseValidator = baseValidatorBuilder.build();
34+
final ConstraintViolations violations1 = baseValidator.validate(target);
35+
assertThat(violations1.isValid()).isTrue();
36+
37+
final Validator<Car> overwrittenValidator = baseValidatorBuilder
38+
.constraint(Car::getManufacturer, "manufacturer", c -> c.isNull())
39+
.conflictStrategy(ConflictStrategy.OVERRIDE).build();
40+
final ConstraintViolations violations2 = overwrittenValidator.validate(target);
41+
assertThat(violations2.isValid()).isFalse();
42+
assertThat(violations2).hasSize(1);
43+
assertThat(violations2.get(0).message())
44+
.isEqualTo("\"manufacturer\" must be null");
45+
}
46+
47+
public static class Car {
48+
private final String manufacturer;
49+
50+
private final String licensePlate;
51+
52+
private final int seatCount;
53+
54+
public Car(String manufacturer, String licencePlate, int seatCount) {
55+
this.manufacturer = manufacturer;
56+
this.licensePlate = licencePlate;
57+
this.seatCount = seatCount;
58+
}
59+
60+
public String getManufacturer() {
61+
return manufacturer;
62+
}
63+
64+
public String getLicensePlate() {
65+
return licensePlate;
66+
}
67+
68+
public int getSeatCount() {
69+
return seatCount;
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)