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.1
*/
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.1
*/
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.1
*/
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,6 +21,7 @@
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;
Expand Down Expand Up @@ -69,6 +70,37 @@ 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 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 compositeConstraint() {
List<Constraint> constraints = this.resolver.resolveForProperty("composite", ConstrainedFields.class);
Expand Down Expand Up @@ -96,6 +128,19 @@ 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 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,20 @@

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 first parameter's constraints of the `user` method.
This list contains two descriptions: one for the `NotNull` constraint and one for the `Min` 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 +25,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2014-present 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.docs.documentingyourapi.constraints;

import java.util.List;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

import org.springframework.restdocs.constraints.ConstraintDescriptions;

class MethodParameterConstraints {

List<String> describeMethodParameterConstraints() {
ConstraintDescriptions controllerConstraints = new ConstraintDescriptions(UserController.class); // <1>
return controllerConstraints.descriptionsForMethodParameter("user", 0, Long.class); // <2>
}

static class UserController {

void user(@NotNull @Min(1) Long id) {
}

}

}