Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@

package org.springframework.restdocs.constraints;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.jspecify.annotations.Nullable;

/**
* Provides access to descriptions of a class's constraints.
*
Expand Down Expand Up @@ -89,6 +93,56 @@ public ConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResol
*/
public List<String> descriptionsForProperty(String property) {
List<Constraint> constraints = this.constraintResolver.resolveForProperty(property, this.clazz);
return resolveDescriptions(constraints);
}

/**
* Returns a list of the descriptions for the constraints on the given
* {@code parameterIndex} of the given {@code method}.
* @param method the method
* @param parameterIndex the index of the parameter
* @return the list of constraint descriptions
* @since 4.0.2
*/
public List<String> descriptionsForMethodParameter(Method method, int parameterIndex) {
List<Constraint> constraints = this.constraintResolver.resolveForMethodParameter(method, parameterIndex);
return resolveDescriptions(constraints);
}

/**
* Returns a list of the descriptions for the constraints on the given
* {@code parameterIndex} of the method with the given {@code methodName} and
* {@code parameterTypes}.
* @param methodName the name of the method
* @param parameterIndex the index of the parameter
* @param parameterTypes the types of the parameters of the method
* @return the list of constraint descriptions
* @since 4.0.2
*/
public List<String> descriptionsForMethodParameter(String methodName, int parameterIndex,
Class<?>... parameterTypes) {
Method method = findMethod(this.clazz, methodName, parameterTypes);
if (method == null) {
throw new IllegalArgumentException("No method named '" + methodName + "' with parameter types "
+ Arrays.toString(parameterTypes) + " found on " + this.clazz);
}
return descriptionsForMethodParameter(method, parameterIndex);
}

@Nullable private Method findMethod(Class<?> clazz, String name, Class<?>[] parameterTypes) {
Class<?> currentClass = clazz;
while (currentClass != null) {
try {
return currentClass.getDeclaredMethod(name, parameterTypes);
}
catch (NoSuchMethodException ex) {
currentClass = currentClass.getSuperclass();
}
}
return null;
}

private List<String> resolveDescriptions(List<Constraint> constraints) {
List<String> descriptions = new ArrayList<>();
for (Constraint constraint : constraints) {
descriptions.add(this.descriptionResolver.resolveDescription(constraint));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.restdocs.constraints;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

/**
Expand All @@ -34,4 +36,16 @@ public interface ConstraintResolver {
*/
List<Constraint> resolveForProperty(String property, Class<?> clazz);

/**
* Resolves and returns the constraints for the given {@code parameterIndex} of the
* given {@code method}. If there are no constraints, an empty list is returned.
* @param method the method
* @param parameterIndex the index of the parameter
* @return the list of constraints, never {@code null}
* @since 4.0.2
*/
default List<Constraint> resolveForMethodParameter(Method method, int parameterIndex) {
return Collections.emptyList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.restdocs.constraints;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -25,6 +26,8 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.metadata.BeanDescriptor;
import jakarta.validation.metadata.ConstraintDescriptor;
import jakarta.validation.metadata.MethodDescriptor;
import jakarta.validation.metadata.ParameterDescriptor;
import jakarta.validation.metadata.PropertyDescriptor;

/**
Expand Down Expand Up @@ -74,4 +77,26 @@ public List<Constraint> resolveForProperty(String property, Class<?> clazz) {
return constraints;
}

@Override
public List<Constraint> resolveForMethodParameter(Method method, int parameterIndex) {
List<Constraint> constraints = new ArrayList<>();
if (parameterIndex < 0) {
return constraints;
}
BeanDescriptor beanDescriptor = this.validator.getConstraintsForClass(method.getDeclaringClass());
MethodDescriptor methodDescriptor = beanDescriptor.getConstraintsForMethod(method.getName(),
method.getParameterTypes());
if (methodDescriptor != null) {
List<ParameterDescriptor> parameterDescriptors = methodDescriptor.getParameterDescriptors();
if (parameterIndex < parameterDescriptors.size()) {
ParameterDescriptor parameterDescriptor = parameterDescriptors.get(parameterIndex);
for (ConstraintDescriptor<?> constraintDescriptor : parameterDescriptor.getConstraintDescriptors()) {
constraints.add(new Constraint(constraintDescriptor.getAnnotation().annotationType().getName(),
constraintDescriptor.getAttributes()));
}
}
}
return constraints;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package org.springframework.restdocs.constraints;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;

import org.junit.jupiter.api.Test;

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

Expand Down Expand Up @@ -58,7 +60,71 @@ void emptyListOfDescriptionsWhenThereAreNoConstraints() {
assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size()).isEqualTo(0);
}

private static final class Constrained {
@Test
void descriptionsForMethodParameterConstraints() throws NoSuchMethodException {
Method method = Constrained.class.getDeclaredMethod("foo", String.class);
Constraint constraint1 = new Constraint("constraint1", Collections.<String, Object>emptyMap());
Constraint constraint2 = new Constraint("constraint2", Collections.<String, Object>emptyMap());
given(this.constraintResolver.resolveForMethodParameter(method, 0))
.willReturn(Arrays.asList(constraint1, constraint2));
given(this.constraintDescriptionResolver.resolveDescription(constraint1)).willReturn("Bravo");
given(this.constraintDescriptionResolver.resolveDescription(constraint2)).willReturn("Alpha");
assertThat(this.constraintDescriptions.descriptionsForMethodParameter(method, 0)).containsExactly("Alpha",
"Bravo");
}

@Test
void descriptionsForMethodParameterConstraintsUsingName() throws NoSuchMethodException {
Method method = Constrained.class.getDeclaredMethod("foo", String.class);
Constraint constraint1 = new Constraint("constraint1", Collections.<String, Object>emptyMap());
Constraint constraint2 = new Constraint("constraint2", Collections.<String, Object>emptyMap());
given(this.constraintResolver.resolveForMethodParameter(method, 0))
.willReturn(Arrays.asList(constraint1, constraint2));
given(this.constraintDescriptionResolver.resolveDescription(constraint1)).willReturn("Bravo");
given(this.constraintDescriptionResolver.resolveDescription(constraint2)).willReturn("Alpha");
assertThat(this.constraintDescriptions.descriptionsForMethodParameter("foo", 0, String.class))
.containsExactly("Alpha", "Bravo");
}

@Test
void descriptionsForMethodParameterConstraintsWithNoTypesUsingName() throws NoSuchMethodException {
Method method = Constrained.class.getDeclaredMethod("bar");
Constraint constraint1 = new Constraint("constraint1", Collections.<String, Object>emptyMap());
given(this.constraintResolver.resolveForMethodParameter(method, 0)).willReturn(Arrays.asList(constraint1));
given(this.constraintDescriptionResolver.resolveDescription(constraint1)).willReturn("Alpha");
assertThat(this.constraintDescriptions.descriptionsForMethodParameter("bar", 0)).containsExactly("Alpha");
}

@Test
void descriptionsForInheritedMethodParameterConstraintsUsingName() throws NoSuchMethodException {
Method method = Constrained.class.getDeclaredMethod("foo", String.class);
Constraint constraint1 = new Constraint("constraint1", Collections.<String, Object>emptyMap());
given(this.constraintResolver.resolveForMethodParameter(method, 0)).willReturn(Arrays.asList(constraint1));
given(this.constraintDescriptionResolver.resolveDescription(constraint1)).willReturn("Alpha");

ConstraintDescriptions subclassDescriptions = new ConstraintDescriptions(SubclassConstrained.class,
this.constraintResolver, this.constraintDescriptionResolver);
assertThat(subclassDescriptions.descriptionsForMethodParameter("foo", 0, String.class))
.containsExactly("Alpha");
}

@Test
void descriptionsForNonExistentMethodParameter() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.constraintDescriptions.descriptionsForMethodParameter("baz", 0));
}

private static class Constrained {

void foo(String foo) {
}

void bar() {
}

}

private static final class SubclassConstrained extends Constrained {

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,28 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import jakarta.validation.Payload;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.assertj.core.api.Condition;
import org.assertj.core.description.TextDescription;
import org.hibernate.validator.constraints.CompositionType;
import org.hibernate.validator.constraints.ConstraintComposition;
import org.junit.jupiter.api.Test;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import static org.assertj.core.api.Assertions.assertThat;

/**
Expand Down Expand Up @@ -69,6 +75,55 @@ void noFieldConstraints() {
assertThat(constraints).hasSize(0);
}

@Test
void singleMethodParameterConstraint() throws NoSuchMethodException {
Method method = ConstrainedMethods.class.getDeclaredMethod("single", String.class);
List<Constraint> constraints = this.resolver.resolveForMethodParameter(method, 0);
assertThat(constraints).hasSize(1);
assertThat(constraints.get(0).getName()).isEqualTo(NotNull.class.getName());
}

@Test
void multipleMethodParameterConstraints() throws NoSuchMethodException {
Method method = ConstrainedMethods.class.getDeclaredMethod("multiple", String.class);
List<Constraint> constraints = this.resolver.resolveForMethodParameter(method, 0);
assertThat(constraints).hasSize(2);
assertThat(constraints.get(0)).is(constraint(NotNull.class));
assertThat(constraints.get(1)).is(constraint(Size.class).config("min", 8).config("max", 16));
}

@Test
void springMvcMethodParameterConstraints() throws NoSuchMethodException {
Method method = ConstrainedMethods.class.getDeclaredMethod("springMvc", int.class, String.class);
List<Constraint> requestParamConstraints = this.resolver.resolveForMethodParameter(method, 0);
assertThat(requestParamConstraints).hasSize(1);
assertThat(requestParamConstraints.get(0)).is(constraint(Min.class).config("value", 1L));
List<Constraint> pathVariableConstraints = this.resolver.resolveForMethodParameter(method, 1);
assertThat(pathVariableConstraints).hasSize(1);
assertThat(pathVariableConstraints.get(0)).is(constraint(Pattern.class).config("regexp", "^[A-Z0-9_-]+$"));
}

@Test
void noMethodParameterConstraints() throws NoSuchMethodException {
Method method = ConstrainedMethods.class.getDeclaredMethod("none", String.class);
List<Constraint> constraints = this.resolver.resolveForMethodParameter(method, 0);
assertThat(constraints).hasSize(0);
}

@Test
void negativeMethodParameterIndexReturnsNoConstraints() throws NoSuchMethodException {
Method method = ConstrainedMethods.class.getDeclaredMethod("single", String.class);
List<Constraint> constraints = this.resolver.resolveForMethodParameter(method, -1);
assertThat(constraints).isEmpty();
}

@Test
void outOfRangeMethodParameterIndexReturnsNoConstraints() throws NoSuchMethodException {
Method method = ConstrainedMethods.class.getDeclaredMethod("single", String.class);
List<Constraint> constraints = this.resolver.resolveForMethodParameter(method, 1);
assertThat(constraints).isEmpty();
}

@Test
void compositeConstraint() {
List<Constraint> constraints = this.resolver.resolveForProperty("composite", ConstrainedFields.class);
Expand Down Expand Up @@ -96,6 +151,22 @@ private static final class ConstrainedFields {

}

private static final class ConstrainedMethods {

void single(@NotNull String single) {
}

void multiple(@NotNull @Size(min = 8, max = 16) String multiple) {
}

void springMvc(@RequestParam @Min(1) int limit, @PathVariable @Pattern(regexp = "^[A-Z0-9_-]+$") String id) {
}

void none(String none) {
}

}

@ConstraintComposition(CompositionType.OR)
@Null
@NotBlank
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@

Spring REST Docs provides a number of classes that can help you to document constraints.
You can use an instance of `ConstraintDescriptions` to access descriptions of a class's constraints.
The following example shows how to do so:
The following example shows how to do so for property constraints:

include-code::Constraints[]
<1> Create an instance of `ConstraintDescriptions` for the `UserInput` class.
<2> Get the descriptions of the `name` property's constraints.
This list contains two descriptions: one for the `NotNull` constraint and one for the `Size` constraint.

The following example shows how to do so for method parameter constraints:

include-code::MethodParameterConstraints[]
<1> Create an instance of `ConstraintDescriptions` for the `UserController` class.
<2> Get the descriptions of the request parameter's constraints of the `user` method.
<3> Get the descriptions of the path variable's constraints of the `user` method.
The first list contains two descriptions: one for the `NotNull` constraint and one for the `Min` constraint.
The second list contains one description for the `Pattern` constraint.

The {samples}/restful-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] class in the Spring HATEOAS sample shows this functionality in action.


Expand All @@ -18,7 +27,7 @@ The {samples}/restful-notes-spring-hateoas/src/test/java/com/example/notes/ApiDo
== Finding Constraints

By default, constraints are found by using a Bean Validation `Validator`.
Currently, only property constraints are supported.
Both property and method parameter constraints are supported.
You can customize the `Validator` that is used by creating `ConstraintDescriptions` with a custom `ValidatorConstraintResolver` instance.
To take complete control of constraint resolution, you can use your own implementation of `ConstraintResolver`.

Expand Down
Loading