Skip to content

Commit

Permalink
Add support for Bean Validation's constraint groups
Browse files Browse the repository at this point in the history
  • Loading branch information
nosan committed Sep 17, 2024
1 parent 0ba538c commit bf01847
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package org.springframework.restdocs.constraints;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

/**
* A constraint.
Expand All @@ -29,6 +31,8 @@ public class Constraint {

private final Map<String, Object> configuration;

private final Set<Class<?>> groups;

/**
* Creates a new {@code Constraint} with the given {@code name} and
* {@code configuration}.
Expand All @@ -38,6 +42,20 @@ public class Constraint {
public Constraint(String name, Map<String, Object> configuration) {
this.name = name;
this.configuration = configuration;
this.groups = Collections.emptySet();
}

/**
* Creates a new {@code Constraint} with the given {@code name} and
* {@code configuration}.
* @param name the name
* @param configuration the configuration
* @param groups the groups
*/
public Constraint(String name, Map<String, Object> configuration, Set<Class<?>> groups) {
this.name = name;
this.configuration = configuration;
this.groups = groups;
}

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

/**
* Returns the groups of the constraint.
* @return the groups
*/
public Set<Class<?>> getGroups() {
return this.groups;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2014-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.restdocs.constraints;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

/**
* Provides access to descriptions of a class's constraints.
*
* @author Dmytro Nosan
*/
public class GroupConstraintDescriptions {

private final Class<?> clazz;

private final ConstraintResolver constraintResolver;

private final ConstraintDescriptionResolver descriptionResolver;

/**
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using a {@link ValidatorConstraintResolver} and
* descriptions will be resolved using a
* {@link ResourceBundleConstraintDescriptionResolver}.
* @param clazz the class
*/
public GroupConstraintDescriptions(Class<?> clazz) {
this(clazz, new ValidatorConstraintResolver(), new ResourceBundleConstraintDescriptionResolver());
}

/**
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using the given {@code constraintResolver} and
* descriptions will be resolved using a
* {@link ResourceBundleConstraintDescriptionResolver}.
* @param clazz the class
* @param constraintResolver the constraint resolver
*/
public GroupConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResolver) {
this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver());
}

/**
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using a {@link ValidatorConstraintResolver} and
* descriptions will be resolved using the given {@code descriptionResolver}.
* @param clazz the class
* @param descriptionResolver the description resolver
*/
public GroupConstraintDescriptions(Class<?> clazz, ConstraintDescriptionResolver descriptionResolver) {
this(clazz, new ValidatorConstraintResolver(), descriptionResolver);
}

/**
* Create a new {@code GroupConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using the given {@code constraintResolver} and
* descriptions will be resolved using the given {@code descriptionResolver}.
* @param clazz the class
* @param constraintResolver the constraint resolver
* @param descriptionResolver the description resolver
*/
public GroupConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResolver,
ConstraintDescriptionResolver descriptionResolver) {
this.clazz = clazz;
this.constraintResolver = constraintResolver;
this.descriptionResolver = descriptionResolver;
}

/**
* Returns a list of the descriptions for the constraints on the given property.
* @param property the property
* @param groups list of groups targeted for constraints
* @return the list of constraint descriptions
*/
public List<String> descriptionsForProperty(String property, Class<?>... groups) {
List<Constraint> constraints = this.constraintResolver.resolveForProperty(property, this.clazz);
List<String> descriptions = new ArrayList<>();
for (Constraint constraint : constraints) {
if (includes(constraint, groups)) {
descriptions.add(this.descriptionResolver.resolveDescription(constraint));
}
}
Collections.sort(descriptions);
return descriptions;
}

private boolean includes(Constraint constraint, Class<?>[] groups) {
if (groups.length == 0 && constraint.getGroups().isEmpty()) {
return true;
}
return Stream.of(groups).anyMatch((clazz) -> constraint.getGroups().contains(clazz));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public List<Constraint> resolveForProperty(String property, Class<?> clazz) {
if (propertyDescriptor != null) {
for (ConstraintDescriptor<?> constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) {
constraints.add(new Constraint(constraintDescriptor.getAnnotation().annotationType().getName(),
constraintDescriptor.getAttributes()));
constraintDescriptor.getAttributes(), constraintDescriptor.getGroups()));
}
}
return constraints;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2014-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.restdocs.constraints;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;

/**
* Tests for {@link GroupConstraintDescriptions}.
*
* @author Dmytro Nosan
*
*/
public class GroupConstraintDescriptionsTests {

private final ConstraintResolver constraintResolver = mock(ConstraintResolver.class);

private final ConstraintDescriptionResolver constraintDescriptionResolver = mock(
ConstraintDescriptionResolver.class);

private final GroupConstraintDescriptions constraintDescriptions = new GroupConstraintDescriptions(
Constrained.class, this.constraintResolver, this.constraintDescriptionResolver);

@Test
public void descriptionsForConstraints() {
Constraint constraint1 = new Constraint("constraint1", Collections.emptyMap(), Set.of(Cloneable.class));
Constraint constraint2 = new Constraint("constraint2", Collections.emptyMap());
Constraint constraint3 = new Constraint("constraint3", Collections.emptyMap(),
Set.of(Cloneable.class, Serializable.class));

given(this.constraintResolver.resolveForProperty("foo", Constrained.class))
.willReturn(Arrays.asList(constraint1, constraint2, constraint3));
given(this.constraintDescriptionResolver.resolveDescription(constraint1)).willReturn("Bravo");
given(this.constraintDescriptionResolver.resolveDescription(constraint2)).willReturn("Alpha");
given(this.constraintDescriptionResolver.resolveDescription(constraint3)).willReturn("Delta");

assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Cloneable.class)).containsExactly("Bravo",
"Delta");
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Serializable.class, Cloneable.class))
.containsExactly("Bravo", "Delta");
assertThat(this.constraintDescriptions.descriptionsForProperty("foo", Serializable.class))
.containsExactly("Delta");
assertThat(this.constraintDescriptions.descriptionsForProperty("foo")).containsExactly("Alpha");
}

@Test
public void emptyListOfDescriptionsWhenThereAreNoConstraints() {
given(this.constraintResolver.resolveForProperty("foo", Constrained.class)).willReturn(Collections.emptyList());
assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size()).isEqualTo(0);
}

private static final class Constrained {

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@

package org.springframework.restdocs.constraints;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import jakarta.validation.Payload;
import jakarta.validation.constraints.NotBlank;
Expand Down Expand Up @@ -55,6 +58,13 @@ public void singleFieldConstraint() {
assertThat(constraints.get(0).getName()).isEqualTo(NotNull.class.getName());
}

@Test
public void singleGroupedFieldConstraint() {
List<Constraint> constraints = this.resolver.resolveForProperty("singleGrouped", ConstrainedFields.class);
assertThat(constraints).hasSize(1);
assertThat(constraints.get(0)).is(constraint(NotNull.class).groups(Serializable.class));
}

@Test
public void multipleFieldConstraints() {
List<Constraint> constraints = this.resolver.resolveForProperty("multiple", ConstrainedFields.class);
Expand Down Expand Up @@ -84,6 +94,9 @@ private static final class ConstrainedFields {
@NotNull
private String single;

@NotNull(groups = Serializable.class)
private String singleGrouped;

@NotNull
@Size(min = 8, max = 16)
private String multiple;
Expand Down Expand Up @@ -118,16 +131,24 @@ private static final class ConstraintCondition extends Condition<Constraint> {

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

private final Set<Class<?>> groups = new HashSet<>();

private ConstraintCondition(Class<?> annotation) {
this.annotation = annotation;
as(new TextDescription("Constraint named %s with configuration %s", this.annotation, this.configuration));
as(new TextDescription("Constraint named %s with configuration %s and groups %s", this.annotation,
this.configuration, this.groups));
}

private ConstraintCondition config(String key, Object value) {
this.configuration.put(key, value);
return this;
}

private ConstraintCondition groups(Class<?>... groups) {
this.groups.addAll(List.of(groups));
return this;
}

@Override
public boolean matches(Constraint constraint) {
if (!constraint.getName().equals(this.annotation.getName())) {
Expand All @@ -138,6 +159,11 @@ public boolean matches(Constraint constraint) {
return false;
}
}
for (Class<?> group : this.groups) {
if (!constraint.getGroups().contains(group)) {
return false;
}
}
return true;
}

Expand Down

0 comments on commit bf01847

Please sign in to comment.