Skip to content

Commit f6319bf

Browse files
committed
Add support for Bean Validation's constraint groups
Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 8fd536c commit f6319bf

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,113 @@
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 {@link 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 {@link 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 {@link 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 {@link 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 descriptions for the constraints on the specified property that
88+
* are applicable to the given group(s).
89+
* @param property the name of the property whose constraints are to be described
90+
* @param groups the groups to which the constraints should belong, if specified; can
91+
* be empty. If empty, only constraints without groups are matched.
92+
* @return the list of constraint descriptions
93+
*/
94+
public List<String> descriptionsForProperty(String property, Class<?>... groups) {
95+
List<Constraint> constraints = this.constraintResolver.resolveForProperty(property, this.clazz);
96+
List<String> descriptions = new ArrayList<>();
97+
for (Constraint constraint : constraints) {
98+
if (includes(constraint, groups)) {
99+
descriptions.add(this.descriptionResolver.resolveDescription(constraint));
100+
}
101+
}
102+
Collections.sort(descriptions);
103+
return descriptions;
104+
}
105+
106+
private boolean includes(Constraint constraint, Class<?>[] groups) {
107+
if (groups.length == 0 && constraint.getGroups().isEmpty()) {
108+
return true;
109+
}
110+
return Stream.of(groups).anyMatch((clazz) -> constraint.getGroups().contains(clazz));
111+
}
112+
113+
}

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,76 @@
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+
public class GroupConstraintDescriptionsTests {
36+
37+
private final ConstraintResolver constraintResolver = mock(ConstraintResolver.class);
38+
39+
private final ConstraintDescriptionResolver constraintDescriptionResolver = mock(
40+
ConstraintDescriptionResolver.class);
41+
42+
private final GroupConstraintDescriptions constraintDescriptions = new GroupConstraintDescriptions(
43+
Constrained.class, this.constraintResolver, this.constraintDescriptionResolver);
44+
45+
@Test
46+
public void descriptionsForConstraints() {
47+
Constraint alpha = new Constraint("alpha", Collections.emptyMap(), Set.of(Cloneable.class));
48+
Constraint bravo = new Constraint("bravo", Collections.emptyMap());
49+
Constraint delta = new Constraint("delta", Collections.emptyMap(), Set.of(Cloneable.class, Serializable.class));
50+
51+
given(this.constraintResolver.resolveForProperty("foo", Constrained.class))
52+
.willReturn(Arrays.asList(alpha, bravo, delta));
53+
given(this.constraintDescriptionResolver.resolveDescription(alpha)).willReturn("alpha");
54+
given(this.constraintDescriptionResolver.resolveDescription(bravo)).willReturn("bravo");
55+
given(this.constraintDescriptionResolver.resolveDescription(delta)).willReturn("delta");
56+
57+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Cloneable.class)).containsExactly("alpha",
58+
"delta");
59+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Serializable.class, Cloneable.class))
60+
.containsExactly("alpha", "delta");
61+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Serializable.class))
62+
.containsExactly("delta");
63+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo")).containsExactly("bravo");
64+
}
65+
66+
@Test
67+
public void emptyListOfDescriptionsWhenThereAreNoConstraints() {
68+
given(this.constraintResolver.resolveForProperty("foo", Constrained.class)).willReturn(Collections.emptyList());
69+
assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size()).isEqualTo(0);
70+
}
71+
72+
private static final class Constrained {
73+
74+
}
75+
76+
}

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)