Skip to content

Commit bf01847

Browse files
committed
Add support for Bean Validation's constraint groups
gh-887
1 parent 0ba538c commit bf01847

File tree

5 files changed

+243
-2
lines changed

5 files changed

+243
-2
lines changed

spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java

+26
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.restdocs.constraints;
1818

19+
import java.util.Collections;
1920
import java.util.Map;
21+
import java.util.Set;
2022

2123
/**
2224
* A constraint.
@@ -29,6 +31,8 @@ public class Constraint {
2931

3032
private final Map<String, Object> configuration;
3133

34+
private final Set<Class<?>> groups;
35+
3236
/**
3337
* Creates a new {@code Constraint} with the given {@code name} and
3438
* {@code configuration}.
@@ -38,6 +42,20 @@ public class Constraint {
3842
public Constraint(String name, Map<String, Object> configuration) {
3943
this.name = name;
4044
this.configuration = configuration;
45+
this.groups = Collections.emptySet();
46+
}
47+
48+
/**
49+
* Creates a new {@code Constraint} with the given {@code name} and
50+
* {@code configuration}.
51+
* @param name the name
52+
* @param configuration the configuration
53+
* @param groups the groups
54+
*/
55+
public Constraint(String name, Map<String, Object> configuration, Set<Class<?>> groups) {
56+
this.name = name;
57+
this.configuration = configuration;
58+
this.groups = groups;
4159
}
4260

4361
/**
@@ -56,4 +74,12 @@ public Map<String, Object> getConfiguration() {
5674
return this.configuration;
5775
}
5876

77+
/**
78+
* Returns the groups of the constraint.
79+
* @return the groups
80+
*/
81+
public Set<Class<?>> getGroups() {
82+
return this.groups;
83+
}
84+
5985
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2014-2024 the original author or authors.
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+
* https://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+
17+
package org.springframework.restdocs.constraints;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.stream.Stream;
23+
24+
/**
25+
* Provides access to descriptions of a class's constraints.
26+
*
27+
* @author Dmytro Nosan
28+
*/
29+
public class GroupConstraintDescriptions {
30+
31+
private final Class<?> clazz;
32+
33+
private final ConstraintResolver constraintResolver;
34+
35+
private final ConstraintDescriptionResolver descriptionResolver;
36+
37+
/**
38+
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
39+
* Constraints will be resolved using a {@link ValidatorConstraintResolver} and
40+
* descriptions will be resolved using a
41+
* {@link ResourceBundleConstraintDescriptionResolver}.
42+
* @param clazz the class
43+
*/
44+
public GroupConstraintDescriptions(Class<?> clazz) {
45+
this(clazz, new ValidatorConstraintResolver(), new ResourceBundleConstraintDescriptionResolver());
46+
}
47+
48+
/**
49+
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
50+
* Constraints will be resolved using the given {@code constraintResolver} and
51+
* descriptions will be resolved using a
52+
* {@link ResourceBundleConstraintDescriptionResolver}.
53+
* @param clazz the class
54+
* @param constraintResolver the constraint resolver
55+
*/
56+
public GroupConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResolver) {
57+
this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver());
58+
}
59+
60+
/**
61+
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
62+
* Constraints will be resolved using a {@link ValidatorConstraintResolver} and
63+
* descriptions will be resolved using the given {@code descriptionResolver}.
64+
* @param clazz the class
65+
* @param descriptionResolver the description resolver
66+
*/
67+
public GroupConstraintDescriptions(Class<?> clazz, ConstraintDescriptionResolver descriptionResolver) {
68+
this(clazz, new ValidatorConstraintResolver(), descriptionResolver);
69+
}
70+
71+
/**
72+
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
73+
* Constraints will be resolved using the given {@code constraintResolver} and
74+
* descriptions will be resolved using the given {@code descriptionResolver}.
75+
* @param clazz the class
76+
* @param constraintResolver the constraint resolver
77+
* @param descriptionResolver the description resolver
78+
*/
79+
public GroupConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResolver,
80+
ConstraintDescriptionResolver descriptionResolver) {
81+
this.clazz = clazz;
82+
this.constraintResolver = constraintResolver;
83+
this.descriptionResolver = descriptionResolver;
84+
}
85+
86+
/**
87+
* Returns a list of the descriptions for the constraints on the given property.
88+
* @param property the property
89+
* @param groups list of groups targeted for constraints
90+
* @return the list of constraint descriptions
91+
*/
92+
public List<String> descriptionsForProperty(String property, Class<?>... groups) {
93+
List<Constraint> constraints = this.constraintResolver.resolveForProperty(property, this.clazz);
94+
List<String> descriptions = new ArrayList<>();
95+
for (Constraint constraint : constraints) {
96+
if (includes(constraint, groups)) {
97+
descriptions.add(this.descriptionResolver.resolveDescription(constraint));
98+
}
99+
}
100+
Collections.sort(descriptions);
101+
return descriptions;
102+
}
103+
104+
private boolean includes(Constraint constraint, Class<?>[] groups) {
105+
if (groups.length == 0 && constraint.getGroups().isEmpty()) {
106+
return true;
107+
}
108+
return Stream.of(groups).anyMatch((clazz) -> constraint.getGroups().contains(clazz));
109+
}
110+
111+
}

spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public List<Constraint> resolveForProperty(String property, Class<?> clazz) {
6868
if (propertyDescriptor != null) {
6969
for (ConstraintDescriptor<?> constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) {
7070
constraints.add(new Constraint(constraintDescriptor.getAnnotation().annotationType().getName(),
71-
constraintDescriptor.getAttributes()));
71+
constraintDescriptor.getAttributes(), constraintDescriptor.getGroups()));
7272
}
7373
}
7474
return constraints;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2014-2024 the original author or authors.
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+
* https://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+
17+
package org.springframework.restdocs.constraints;
18+
19+
import java.io.Serializable;
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.Set;
23+
24+
import org.junit.Test;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.mockito.BDDMockito.given;
28+
import static org.mockito.Mockito.mock;
29+
30+
/**
31+
* Tests for {@link GroupConstraintDescriptions}.
32+
*
33+
* @author Dmytro Nosan
34+
*
35+
*/
36+
public class GroupConstraintDescriptionsTests {
37+
38+
private final ConstraintResolver constraintResolver = mock(ConstraintResolver.class);
39+
40+
private final ConstraintDescriptionResolver constraintDescriptionResolver = mock(
41+
ConstraintDescriptionResolver.class);
42+
43+
private final GroupConstraintDescriptions constraintDescriptions = new GroupConstraintDescriptions(
44+
Constrained.class, this.constraintResolver, this.constraintDescriptionResolver);
45+
46+
@Test
47+
public void descriptionsForConstraints() {
48+
Constraint constraint1 = new Constraint("constraint1", Collections.emptyMap(), Set.of(Cloneable.class));
49+
Constraint constraint2 = new Constraint("constraint2", Collections.emptyMap());
50+
Constraint constraint3 = new Constraint("constraint3", Collections.emptyMap(),
51+
Set.of(Cloneable.class, Serializable.class));
52+
53+
given(this.constraintResolver.resolveForProperty("foo", Constrained.class))
54+
.willReturn(Arrays.asList(constraint1, constraint2, constraint3));
55+
given(this.constraintDescriptionResolver.resolveDescription(constraint1)).willReturn("Bravo");
56+
given(this.constraintDescriptionResolver.resolveDescription(constraint2)).willReturn("Alpha");
57+
given(this.constraintDescriptionResolver.resolveDescription(constraint3)).willReturn("Delta");
58+
59+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Cloneable.class)).containsExactly("Bravo",
60+
"Delta");
61+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Serializable.class, Cloneable.class))
62+
.containsExactly("Bravo", "Delta");
63+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Serializable.class))
64+
.containsExactly("Delta");
65+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo")).containsExactly("Alpha");
66+
}
67+
68+
@Test
69+
public void emptyListOfDescriptionsWhenThereAreNoConstraints() {
70+
given(this.constraintResolver.resolveForProperty("foo", Constrained.class)).willReturn(Collections.emptyList());
71+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size()).isEqualTo(0);
72+
}
73+
74+
private static final class Constrained {
75+
76+
}
77+
78+
}

spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@
1616

1717
package org.springframework.restdocs.constraints;
1818

19+
import java.io.Serializable;
1920
import java.lang.annotation.Annotation;
2021
import java.lang.annotation.ElementType;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.lang.annotation.Target;
2425
import java.util.HashMap;
26+
import java.util.HashSet;
2527
import java.util.List;
2628
import java.util.Map;
2729
import java.util.Map.Entry;
30+
import java.util.Set;
2831

2932
import jakarta.validation.Payload;
3033
import jakarta.validation.constraints.NotBlank;
@@ -55,6 +58,13 @@ public void singleFieldConstraint() {
5558
assertThat(constraints.get(0).getName()).isEqualTo(NotNull.class.getName());
5659
}
5760

61+
@Test
62+
public void singleGroupedFieldConstraint() {
63+
List<Constraint> constraints = this.resolver.resolveForProperty("singleGrouped", ConstrainedFields.class);
64+
assertThat(constraints).hasSize(1);
65+
assertThat(constraints.get(0)).is(constraint(NotNull.class).groups(Serializable.class));
66+
}
67+
5868
@Test
5969
public void multipleFieldConstraints() {
6070
List<Constraint> constraints = this.resolver.resolveForProperty("multiple", ConstrainedFields.class);
@@ -84,6 +94,9 @@ private static final class ConstrainedFields {
8494
@NotNull
8595
private String single;
8696

97+
@NotNull(groups = Serializable.class)
98+
private String singleGrouped;
99+
87100
@NotNull
88101
@Size(min = 8, max = 16)
89102
private String multiple;
@@ -118,16 +131,24 @@ private static final class ConstraintCondition extends Condition<Constraint> {
118131

119132
private final Map<String, Object> configuration = new HashMap<>();
120133

134+
private final Set<Class<?>> groups = new HashSet<>();
135+
121136
private ConstraintCondition(Class<?> annotation) {
122137
this.annotation = annotation;
123-
as(new TextDescription("Constraint named %s with configuration %s", this.annotation, this.configuration));
138+
as(new TextDescription("Constraint named %s with configuration %s and groups %s", this.annotation,
139+
this.configuration, this.groups));
124140
}
125141

126142
private ConstraintCondition config(String key, Object value) {
127143
this.configuration.put(key, value);
128144
return this;
129145
}
130146

147+
private ConstraintCondition groups(Class<?>... groups) {
148+
this.groups.addAll(List.of(groups));
149+
return this;
150+
}
151+
131152
@Override
132153
public boolean matches(Constraint constraint) {
133154
if (!constraint.getName().equals(this.annotation.getName())) {
@@ -138,6 +159,11 @@ public boolean matches(Constraint constraint) {
138159
return false;
139160
}
140161
}
162+
for (Class<?> group : this.groups) {
163+
if (!constraint.getGroups().contains(group)) {
164+
return false;
165+
}
166+
}
141167
return true;
142168
}
143169

0 commit comments

Comments
 (0)