Provide an API to ease documenting a request field’s constraints
Previously, users that wanted to document a request field’s constraints had to roll their own solution. This commit introduces a new API that makes it easier to document constraints. Support is provided for discovering Bean Validation constraints and resolving descriptions for them. The constraint descriptions can then be used as required. For example, they can included in a field’s description or in an additional constraints attribute that’s included in an additional table column via the use of a custom snippet template. Closes gh-42
This commit is contained in:
16
build.gradle
16
build.gradle
@@ -1,9 +1,11 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven { url 'https://repo.spring.io/plugins-release' }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE'
|
||||
classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,27 +15,27 @@ allprojects {
|
||||
|
||||
apply plugin: 'samples'
|
||||
|
||||
ext {
|
||||
springVersion = '4.1.7.RELEASE'
|
||||
jmustacheVersion = '1.10'
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'eclipse'
|
||||
apply plugin: 'propdeps'
|
||||
apply plugin: 'propdeps-eclipse'
|
||||
apply plugin: 'propdeps-maven'
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework:spring-framework-bom:$springVersion"
|
||||
mavenBom 'org.springframework:spring-framework-bom:4.1.7.RELEASE'
|
||||
}
|
||||
dependencies {
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6'
|
||||
dependency "com.samskivert:jmustache:$jmustacheVersion"
|
||||
dependency 'com.samskivert:jmustache:1.10'
|
||||
dependency 'javax.servlet:javax.servlet-api:3.1.0'
|
||||
dependency 'javax.validation:validation-api:1.1.0.Final'
|
||||
dependency 'junit:junit:4.12'
|
||||
dependency 'org.hamcrest:hamcrest-core:1.3'
|
||||
dependency 'org.hamcrest:hamcrest-library:1.3'
|
||||
dependency 'org.hibernate:hibernate-validator:5.2.1.Final'
|
||||
dependency 'org.mockito:mockito-core:1.10.19'
|
||||
dependency 'org.springframework.hateoas:spring-hateoas:0.17.0.RELEASE'
|
||||
dependency 'org.jacoco:org.jacoco.agent:0.7.2.201409121644'
|
||||
|
||||
@@ -6,6 +6,7 @@ dependencies {
|
||||
compile 'org.springframework:spring-webmvc'
|
||||
|
||||
testCompile project(':spring-restdocs')
|
||||
testCompile 'javax.validation:validation-api'
|
||||
testCompile 'junit:junit'
|
||||
testCompile 'org.springframework:spring-test'
|
||||
}
|
||||
|
||||
@@ -261,6 +261,92 @@ TIP: To make the path parameters available for documentation, the request must b
|
||||
built using one of the methods on `RestDocumentationRequestBuilders` rather than
|
||||
`MockMvcRequestBuilders`.
|
||||
|
||||
|
||||
|
||||
[[documenting-your-api-constraints]]
|
||||
=== Documenting constraints
|
||||
|
||||
Spring REST Docs provides a number of classes that can help you to document constraints.
|
||||
An instance of `ConstraintDescriptions` can be used to access descriptions of a class's
|
||||
constraints:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{examples-dir}/com/example/Constraints.java[tags=constraints]
|
||||
----
|
||||
<1> Create an instance of `ConstraintDescriptions` for the `UserInput` class
|
||||
<2> Get the descriptions of the name property's constraints. This list will contain two
|
||||
descriptions; one for the `NotNull` constraint and one for the `Size` constraint.
|
||||
|
||||
The `ApiDocumentation` class in the Spring HATEOAS sample shows this functionality in
|
||||
action.
|
||||
|
||||
|
||||
|
||||
[[documenting-your-api-constraints-finding]]
|
||||
==== Finding constraints
|
||||
|
||||
By default, constraints are found using a Bean Validation `Validator`. Currently, only
|
||||
property constraints are supported. You can customize the `Validator` that's used by
|
||||
creating `ConstraintDescriptions` with a custom `ValidatorConstraintResolver` instance.
|
||||
To take complete control of constraint resolution, your own implementation of
|
||||
`ConstraintResolver` can be used.
|
||||
|
||||
|
||||
|
||||
[[documenting-your-api-constraints-describing]]
|
||||
==== Describing constraints
|
||||
|
||||
Default descriptions are provided for all of the Bean Validation 1.1's constraints:
|
||||
|
||||
* AssertFalse
|
||||
* AssertTrue
|
||||
* DecimalMax
|
||||
* DecimalMin
|
||||
* Digits
|
||||
* Future
|
||||
* Max
|
||||
* Min
|
||||
* NotNull
|
||||
* Null
|
||||
* Past
|
||||
* Pattern
|
||||
* Size
|
||||
|
||||
To override the default descriptions, or to provide a new description, create a resource
|
||||
bundle with the base name
|
||||
`org.springframework.restdocs.constraints.ConstraintDescriptions`. The Spring
|
||||
HATEOAS-based sample contains
|
||||
{samples}/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties[an
|
||||
example of such a resource bundle].
|
||||
|
||||
Each key in the resource bundle is the fully-qualified name of a constraint plus
|
||||
`.description`. For example, the key for the standard `@NotNull` constraint is
|
||||
`javax.validation.constraints.NotNull.description`.
|
||||
|
||||
Property placeholder's referring to a constraint's attributes can be used in its
|
||||
description. For example, the default description of the `@Min` constraint,
|
||||
`Must be at least ${value}`, refers to the constraint's `value` attribute.
|
||||
|
||||
To take more control of constraint description resolution, create `ConstraintDescriptions`
|
||||
with a custom `ResourceBundleConstraintDescriptionResolver`. To take complete control,
|
||||
create `ConstraintDescriptions` with a custom `ConstraintDescriptionResolver`
|
||||
implementation.
|
||||
|
||||
|
||||
|
||||
==== Using constraint descriptions in generated snippets
|
||||
|
||||
Once you have a constraint's descriptions, you're free to use them however you like in
|
||||
the generated snippets. For example, you may want to include the constraint descriptions
|
||||
as part of a field's description. Alternatively, you could include the constraints as
|
||||
<<documenting-your-api-customizing-including-extra-information, extra information>> in
|
||||
the request fields snippet. The
|
||||
{samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`]
|
||||
class in the Spring HATEOAS-based sample illustrates the latter approach.
|
||||
|
||||
|
||||
|
||||
[[documenting-your-api-default-snippets]]
|
||||
=== Default snippets
|
||||
|
||||
|
||||
31
docs/src/test/java/com/example/Constraints.java
Normal file
31
docs/src/test/java/com/example/Constraints.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.example;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import org.springframework.restdocs.constraints.ConstraintDescriptions;
|
||||
|
||||
public class Constraints {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// tag::constraints[]
|
||||
public void example() {
|
||||
ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); // <1>
|
||||
List<String> descriptions = userConstraints.descriptionsForProperty("name"); // <2>
|
||||
}
|
||||
|
||||
static class UserInput {
|
||||
|
||||
@NotNull
|
||||
@Size(min = 1)
|
||||
String name;
|
||||
|
||||
@NotNull
|
||||
@Size(min = 8)
|
||||
String password;
|
||||
}
|
||||
// end::constraints[]
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014 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
|
||||
*
|
||||
* http://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 com.example.notes;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
abstract class AbstractNoteInput {
|
||||
|
||||
private final String title;
|
||||
|
||||
private final String body;
|
||||
|
||||
private final List<URI> tagUris;
|
||||
|
||||
public AbstractNoteInput(String title, String body, List<URI> tagUris) {
|
||||
this.title = title;
|
||||
this.body = body;
|
||||
this.tagUris = tagUris == null ? Collections.<URI> emptyList() : tagUris;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
@JsonProperty("tags")
|
||||
public List<URI> getTagUris() {
|
||||
return this.tagUris;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.example.notes;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.validator.constraints.NotBlank;
|
||||
@@ -24,11 +25,34 @@ import org.hibernate.validator.constraints.NotBlank;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class NoteInput extends AbstractNoteInput {
|
||||
public class NoteInput {
|
||||
|
||||
@NotBlank
|
||||
private final String title;
|
||||
|
||||
private final String body;
|
||||
|
||||
private final List<URI> tagUris;
|
||||
|
||||
@JsonCreator
|
||||
public NoteInput(@NotBlank @JsonProperty("title") String title,
|
||||
public NoteInput(@JsonProperty("title") String title,
|
||||
@JsonProperty("body") String body, @JsonProperty("tags") List<URI> tagUris) {
|
||||
super(title, body, tagUris);
|
||||
this.title = title;
|
||||
this.body = body;
|
||||
this.tagUris = tagUris == null ? Collections.<URI>emptyList() : tagUris;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
@JsonProperty("tags")
|
||||
public List<URI> getTagUris() {
|
||||
return this.tagUris;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,16 +17,39 @@
|
||||
package com.example.notes;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class NotePatchInput extends AbstractNoteInput {
|
||||
public class NotePatchInput {
|
||||
|
||||
@NullOrNotBlank
|
||||
private final String title;
|
||||
|
||||
private final String body;
|
||||
|
||||
private final List<URI> tagUris;
|
||||
|
||||
@JsonCreator
|
||||
public NotePatchInput(@NullOrNotBlank @JsonProperty("title") String title,
|
||||
public NotePatchInput(@JsonProperty("title") String title,
|
||||
@JsonProperty("body") String body, @JsonProperty("tags") List<URI> tagUris) {
|
||||
super(title, body, tagUris);
|
||||
this.title = title;
|
||||
this.body = body;
|
||||
this.tagUris = tagUris == null ? Collections.<URI>emptyList() : tagUris;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
@JsonProperty("tags")
|
||||
public List<URI> getTagUris() {
|
||||
return this.tagUris;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,18 +17,30 @@
|
||||
package com.example.notes;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.Null;
|
||||
|
||||
import org.hibernate.validator.constraints.CompositionType;
|
||||
import org.hibernate.validator.constraints.ConstraintComposition;
|
||||
import org.hibernate.validator.constraints.NotBlank;
|
||||
|
||||
@ConstraintComposition(CompositionType.OR)
|
||||
@NotNull
|
||||
@Constraint(validatedBy = {})
|
||||
@Null
|
||||
@NotBlank
|
||||
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NullOrNotBlank {
|
||||
|
||||
String message() default "Must be null or not blank";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
}
|
||||
|
||||
@@ -21,10 +21,18 @@ import org.hibernate.validator.constraints.NotBlank;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class TagInput extends AbstractTagInput {
|
||||
public class TagInput {
|
||||
|
||||
@NotBlank
|
||||
private final String name;
|
||||
|
||||
@JsonCreator
|
||||
public TagInput(@NotBlank @JsonProperty("name") String name) {
|
||||
super(name);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,10 +19,18 @@ package com.example.notes;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class TagPatchInput extends AbstractTagInput {
|
||||
public class TagPatchInput {
|
||||
|
||||
@NullOrNotBlank
|
||||
private final String name;
|
||||
|
||||
@JsonCreator
|
||||
public TagPatchInput(@NullOrNotBlank @JsonProperty("name") String name) {
|
||||
super(name);
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,14 +20,15 @@ import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.springframework.restdocs.RestDocumentation.document;
|
||||
import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
|
||||
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
|
||||
import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
|
||||
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
|
||||
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
|
||||
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
|
||||
import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
|
||||
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
|
||||
import static org.springframework.restdocs.snippet.Attributes.key;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -44,11 +45,14 @@ import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.restdocs.constraints.ConstraintDescriptions;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -147,15 +151,17 @@ public class ApiDocumentation {
|
||||
note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
|
||||
note.put("tags", Arrays.asList(tagLocation));
|
||||
|
||||
ConstrainedFields fields = new ConstrainedFields(NoteInput.class);
|
||||
|
||||
this.mockMvc.perform(
|
||||
post("/notes").contentType(MediaTypes.HAL_JSON).content(
|
||||
this.objectMapper.writeValueAsString(note)))
|
||||
.andExpect(status().isCreated())
|
||||
.andDo(document("notes-create-example",
|
||||
requestFields(
|
||||
fieldWithPath("title").description("The title of the note"),
|
||||
fieldWithPath("body").description("The body of the note"),
|
||||
fieldWithPath("tags").description("An array of tag resource URIs"))));
|
||||
fields.withPath("title").description("The title of the note"),
|
||||
fields.withPath("body").description("The body of the note"),
|
||||
fields.withPath("tags").description("An array of tag resource URIs"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -220,13 +226,15 @@ public class ApiDocumentation {
|
||||
Map<String, String> tag = new HashMap<String, String>();
|
||||
tag.put("name", "REST");
|
||||
|
||||
ConstrainedFields fields = new ConstrainedFields(TagInput.class);
|
||||
|
||||
this.mockMvc.perform(
|
||||
post("/tags").contentType(MediaTypes.HAL_JSON).content(
|
||||
this.objectMapper.writeValueAsString(tag)))
|
||||
.andExpect(status().isCreated())
|
||||
.andDo(document("tags-create-example",
|
||||
requestFields(
|
||||
fieldWithPath("name").description("The name of the tag"))));
|
||||
fields.withPath("name").description("The name of the tag"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -261,15 +269,24 @@ public class ApiDocumentation {
|
||||
Map<String, Object> noteUpdate = new HashMap<String, Object>();
|
||||
noteUpdate.put("tags", Arrays.asList(tagLocation));
|
||||
|
||||
ConstrainedFields fields = new ConstrainedFields(NotePatchInput.class);
|
||||
|
||||
this.mockMvc.perform(
|
||||
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
|
||||
this.objectMapper.writeValueAsString(noteUpdate)))
|
||||
.andExpect(status().isNoContent())
|
||||
.andDo(document("note-update-example",
|
||||
requestFields(
|
||||
fieldWithPath("title").description("The title of the note").type(JsonFieldType.STRING).optional(),
|
||||
fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(),
|
||||
fieldWithPath("tags").description("An array of tag resource URIs").optional())));
|
||||
fields.withPath("title")
|
||||
.description("The title of the note")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fields.withPath("body")
|
||||
.description("The body of the note")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fields.withPath("tags")
|
||||
.description("An array of tag resource URIs"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -311,13 +328,16 @@ public class ApiDocumentation {
|
||||
Map<String, Object> tagUpdate = new HashMap<String, Object>();
|
||||
tagUpdate.put("name", "RESTful");
|
||||
|
||||
ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class);
|
||||
|
||||
this.mockMvc.perform(
|
||||
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
|
||||
this.objectMapper.writeValueAsString(tagUpdate)))
|
||||
.andExpect(status().isNoContent())
|
||||
.andDo(document("tag-update-example",
|
||||
requestFields(
|
||||
fieldWithPath("name").description("The name of the tag"))));
|
||||
fields.withPath("name")
|
||||
.description("The name of the tag"))));
|
||||
}
|
||||
|
||||
private void createNote(String title, String body) {
|
||||
@@ -333,4 +353,20 @@ public class ApiDocumentation {
|
||||
tag.setName(name);
|
||||
this.tagRepository.save(tag);
|
||||
}
|
||||
|
||||
private static class ConstrainedFields {
|
||||
|
||||
private final ConstraintDescriptions constraintDescriptions;
|
||||
|
||||
ConstrainedFields(Class<?> input) {
|
||||
this.constraintDescriptions = new ConstraintDescriptions(input);
|
||||
}
|
||||
|
||||
private FieldDescriptor withPath(String path) {
|
||||
return fieldWithPath(path).attributes(key("constraints").value(StringUtils
|
||||
.collectionToDelimitedString(this.constraintDescriptions
|
||||
.descriptionsForProperty(path), ". ")));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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
|
||||
*
|
||||
* http://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 com.example.notes;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
public class NullOrNotBlankTests {
|
||||
|
||||
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
|
||||
@Test
|
||||
public void nullValue() {
|
||||
Set<ConstraintViolation<Constrained>> violations = validator.validate(new Constrained(null));
|
||||
assertThat(violations.size(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroLengthValue() {
|
||||
Set<ConstraintViolation<Constrained>> violations = validator.validate(new Constrained(""));
|
||||
assertThat(violations.size(), is(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankValue() {
|
||||
Set<ConstraintViolation<Constrained>> violations = validator.validate(new Constrained(" "));
|
||||
assertThat(violations.size(), is(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonBlankValue() {
|
||||
Set<ConstraintViolation<Constrained>> violations = validator.validate(new Constrained("test"));
|
||||
assertThat(violations.size(), is(0));
|
||||
}
|
||||
|
||||
static class Constrained {
|
||||
|
||||
@NullOrNotBlank
|
||||
private final String value;
|
||||
|
||||
public Constrained(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
com.example.notes.NullOrNotBlank.description=Must be null or not blank
|
||||
org.hibernate.validator.constraints.NotBlank.description=Must not be blank
|
||||
@@ -0,0 +1,11 @@
|
||||
|===
|
||||
|Path|Type|Description|Constraints
|
||||
|
||||
{{#fields}}
|
||||
|{{path}}
|
||||
|{{type}}
|
||||
|{{description}}
|
||||
|{{constraints}}
|
||||
|
||||
{{/fields}}
|
||||
|===
|
||||
@@ -33,16 +33,9 @@ configurations {
|
||||
jmustache
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocLinks = [
|
||||
'http://docs.oracle.com/javase/8/docs/api/',
|
||||
"http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/"
|
||||
] as String[]
|
||||
}
|
||||
|
||||
task jmustacheRepackJar(type: Jar) { repackJar ->
|
||||
repackJar.baseName = "restdocs-jmustache-repack"
|
||||
repackJar.version = jmustacheVersion
|
||||
repackJar.version = dependencyManagement.managedVersions['com.samskivert:jmustache']
|
||||
|
||||
doLast() {
|
||||
project.ant {
|
||||
@@ -70,10 +63,13 @@ dependencies {
|
||||
jacoco 'org.jacoco:org.jacoco.agent::runtime'
|
||||
jarjar 'com.googlecode.jarjar:jarjar:1.3'
|
||||
jmustache 'com.samskivert:jmustache@jar'
|
||||
optional 'javax.validation:validation-api'
|
||||
testCompile 'org.springframework.hateoas:spring-hateoas'
|
||||
testCompile 'org.mockito:mockito-core'
|
||||
testCompile 'org.hamcrest:hamcrest-core'
|
||||
testCompile 'org.hamcrest:hamcrest-library'
|
||||
testCompile 'org.hibernate:hibernate-validator'
|
||||
testRuntime 'org.glassfish:javax.el:3.0.0'
|
||||
}
|
||||
|
||||
jar {
|
||||
@@ -94,7 +90,11 @@ javadoc {
|
||||
options.author = true
|
||||
options.header = "Spring REST Docs $version"
|
||||
options.docTitle = "${options.header} API"
|
||||
options.links(project.ext.javadocLinks)
|
||||
options.links = [
|
||||
'http://docs.oracle.com/javase/8/docs/api/',
|
||||
"http://docs.spring.io/spring-framework/docs/${dependencyManagement.managedVersions['org.springframework:spring-core']}/javadoc-api/",
|
||||
'https://docs.jboss.org/hibernate/stable/beanvalidation/api/'
|
||||
] as String[]
|
||||
options.addStringOption '-quiet'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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
|
||||
*
|
||||
* http://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.Map;
|
||||
|
||||
/**
|
||||
* A constraint
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class Constraint {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Map<String, Object> configuration;
|
||||
|
||||
/**
|
||||
* Creates a new {@code Constraint} with the given {@code name} and
|
||||
* {@code configuration}.
|
||||
*
|
||||
* @param name the name
|
||||
* @param configuration the configuration
|
||||
*/
|
||||
public Constraint(String name, Map<String, Object> configuration) {
|
||||
this.name = name;
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the constraint
|
||||
*
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration of the constraint
|
||||
*
|
||||
* @return the configuration
|
||||
*/
|
||||
public Map<String, Object> getConfiguration() {
|
||||
return this.configuration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
* Copyright 2014-2015 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.
|
||||
@@ -14,18 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.example.notes;
|
||||
package org.springframework.restdocs.constraints;
|
||||
|
||||
/**
|
||||
* Resolves a description for a {@link Constraint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
public interface ConstraintDescriptionResolver {
|
||||
|
||||
abstract class AbstractTagInput {
|
||||
|
||||
private final String name;
|
||||
|
||||
public AbstractTagInput(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
/**
|
||||
* Resolves the description for the given {@code constraint}.
|
||||
*
|
||||
* @param constraint the constraint
|
||||
* @return the description
|
||||
*/
|
||||
public String resolveDescription(Constraint constraint);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
/**
|
||||
* Provides access to descriptions of a class's constraints
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ConstraintDescriptions {
|
||||
|
||||
private final Class<?> clazz;
|
||||
|
||||
private final ConstraintResolver constraintResolver;
|
||||
|
||||
private final ConstraintDescriptionResolver descriptionResolver;
|
||||
|
||||
/**
|
||||
* Create a new {@code ConstraintDescriptions} 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 ConstraintDescriptions(Class<?> clazz) {
|
||||
this(clazz, new ValidatorConstraintResolver(),
|
||||
new ResourceBundleConstraintDescriptionResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
|
||||
* Constraints will be resolved using the given {@link constraintResolver} and
|
||||
* descriptions will be resolved using a
|
||||
* {@link ResourceBundleConstraintDescriptionResolver}.
|
||||
*
|
||||
* @param clazz the class
|
||||
* @param constraintResolver the constraint resolver
|
||||
*/
|
||||
public ConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResolver) {
|
||||
this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code ConstraintDescriptions} 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 ConstraintDescriptions(Class<?> clazz,
|
||||
ConstraintDescriptionResolver descriptionResolver) {
|
||||
this(clazz, new ValidatorConstraintResolver(), descriptionResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code ConstraintDescriptions} 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 ConstraintDescriptions(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
|
||||
* @return the list of constraint descriptions
|
||||
*/
|
||||
public List<String> descriptionsForProperty(String property) {
|
||||
List<Constraint> constraints = this.constraintResolver.resolveForProperty(
|
||||
property, this.clazz);
|
||||
List<String> descriptions = new ArrayList<>();
|
||||
for (Constraint constraint : constraints) {
|
||||
descriptions.add(this.descriptionResolver.resolveDescription(constraint));
|
||||
}
|
||||
Collections.sort(descriptions);
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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
|
||||
*
|
||||
* http://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.List;
|
||||
|
||||
/**
|
||||
* An abstraction for resolving a class's constraints.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public interface ConstraintResolver {
|
||||
|
||||
/**
|
||||
* Resolves and returns the constraints for the given {@code property} on the given
|
||||
* {@code clazz}. If there are no constraints, an empty list is returned.
|
||||
*
|
||||
* @param property the property
|
||||
* @param clazz the class
|
||||
* @return the list of constraints, never {@code null}
|
||||
*/
|
||||
List<Constraint> resolveForProperty(String property, Class<?> clazz);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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
|
||||
*
|
||||
* http://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.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javax.validation.constraints.AssertFalse;
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Digits;
|
||||
import javax.validation.constraints.Future;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Null;
|
||||
import javax.validation.constraints.Past;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import org.springframework.util.PropertyPlaceholderHelper;
|
||||
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
|
||||
/**
|
||||
* A {@link ConstraintDescriptionResolver} that resolves constraint descriptions from a
|
||||
* {@link ResourceBundle}. The resource bundle's keys are the name of the constraint with
|
||||
* {@code .description} appended. For example, the key for the constraint named
|
||||
* {@code javax.validation.constraints.NotNull} is
|
||||
* {@code javax.validation.constraints.NotNull.description}.
|
||||
* <p>
|
||||
* Default descriptions are provided for Bean Validation 1.1's constraints:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link AssertFalse}
|
||||
* <li>{@link AssertTrue}
|
||||
* <li>{@link DecimalMax}
|
||||
* <li>{@link DecimalMin}
|
||||
* <li>{@link Digits}
|
||||
* <li>{@link Future}
|
||||
* <li>{@link Max}
|
||||
* <li>{@link Min}
|
||||
* <li>{@link NotNull}
|
||||
* <li>{@link Null}
|
||||
* <li>{@link Past}
|
||||
* <li>{@link Pattern}
|
||||
* <li>{@link Size}
|
||||
* </ul>
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ResourceBundleConstraintDescriptionResolver implements
|
||||
ConstraintDescriptionResolver {
|
||||
|
||||
private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper(
|
||||
"${", "}");
|
||||
|
||||
private final ResourceBundle defaultDescriptions;
|
||||
|
||||
private final ResourceBundle userDescriptions;
|
||||
|
||||
/**
|
||||
* Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve
|
||||
* descriptions by looking them up in a resource bundle with the base name
|
||||
* {@code org.springframework.restdocs.constraints.ConstraintDescriptions} in the
|
||||
* default locale loaded using the thread context class loader
|
||||
*/
|
||||
public ResourceBundleConstraintDescriptionResolver() {
|
||||
this(getBundle("ConstraintDescriptions"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve
|
||||
* descriptions by looking them up in the given {@code resourceBundle}.
|
||||
*
|
||||
* @param resourceBundle the resource bundle
|
||||
*/
|
||||
public ResourceBundleConstraintDescriptionResolver(ResourceBundle resourceBundle) {
|
||||
this.defaultDescriptions = getBundle("DefaultConstraintDescriptions");
|
||||
this.userDescriptions = resourceBundle;
|
||||
}
|
||||
|
||||
private static ResourceBundle getBundle(String name) {
|
||||
try {
|
||||
return ResourceBundle.getBundle(
|
||||
ResourceBundleConstraintDescriptionResolver.class.getPackage()
|
||||
.getName() + "." + name, Locale.getDefault(), Thread
|
||||
.currentThread().getContextClassLoader());
|
||||
}
|
||||
catch (MissingResourceException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveDescription(Constraint constraint) {
|
||||
String key = constraint.getName() + ".description";
|
||||
return this.propertyPlaceholderHelper.replacePlaceholders(getDescription(key),
|
||||
new ConstraintPlaceholderResolver(constraint));
|
||||
}
|
||||
|
||||
private String getDescription(String key) {
|
||||
try {
|
||||
if (this.userDescriptions != null) {
|
||||
return this.userDescriptions.getString(key);
|
||||
}
|
||||
}
|
||||
catch (MissingResourceException ex) {
|
||||
// Continue and return default description, if available
|
||||
}
|
||||
return this.defaultDescriptions.getString(key);
|
||||
}
|
||||
|
||||
private static class ConstraintPlaceholderResolver implements PlaceholderResolver {
|
||||
|
||||
private final Constraint constraint;
|
||||
|
||||
private ConstraintPlaceholderResolver(Constraint constraint) {
|
||||
this.constraint = constraint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolvePlaceholder(String placeholderName) {
|
||||
Object replacement = this.constraint.getConfiguration().get(placeholderName);
|
||||
return replacement != null ? replacement.toString() : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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
|
||||
*
|
||||
* http://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.List;
|
||||
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.metadata.BeanDescriptor;
|
||||
import javax.validation.metadata.ConstraintDescriptor;
|
||||
import javax.validation.metadata.PropertyDescriptor;
|
||||
|
||||
/**
|
||||
* A {@link ConstraintResolver} that uses a Bean Validation {@link Validator} to resolve
|
||||
* constraints. The name of the constraint is the fully-qualified class name of the
|
||||
* constraint annotation. For example, a {@link NotNull} constraint will be named
|
||||
* {@code javax.validation.constraints.NotNull}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
public class ValidatorConstraintResolver implements ConstraintResolver {
|
||||
|
||||
private final Validator validator;
|
||||
|
||||
/**
|
||||
* Creates a new {@code ValidatorConstraintResolver} that will use a {@link Validator}
|
||||
* in its default configurationto resolve constraints.
|
||||
*
|
||||
* @see Validation#buildDefaultValidatorFactory()
|
||||
* @see ValidatorFactory#getValidator()
|
||||
*/
|
||||
public ValidatorConstraintResolver() {
|
||||
this(Validation.buildDefaultValidatorFactory().getValidator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code ValidatorConstraintResolver} that will use the given
|
||||
* {@code Validator} to resolve constraints.
|
||||
*
|
||||
* @param validator the validator
|
||||
*/
|
||||
public ValidatorConstraintResolver(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Constraint> resolveForProperty(String property, Class<?> clazz) {
|
||||
List<Constraint> constraints = new ArrayList<>();
|
||||
BeanDescriptor beanDescriptor = this.validator.getConstraintsForClass(clazz);
|
||||
PropertyDescriptor propertyDescriptor = beanDescriptor
|
||||
.getConstraintsForProperty(property);
|
||||
if (propertyDescriptor != null) {
|
||||
for (ConstraintDescriptor<?> constraintDescriptor : propertyDescriptor
|
||||
.getConstraintDescriptors()) {
|
||||
constraints
|
||||
.add(new Constraint(constraintDescriptor.getAnnotation()
|
||||
.annotationType().getName(), constraintDescriptor
|
||||
.getAttributes()));
|
||||
}
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
javax.validation.constraints.AssertFalse.description=Must be false
|
||||
javax.validation.constraints.AssertTrue.description=Must be true
|
||||
javax.validation.constraints.DecimalMax.description=Must be at most ${value}
|
||||
javax.validation.constraints.DecimalMin.description=Must be at least ${value}
|
||||
javax.validation.constraints.Digits.description=Must have at most ${integer} integral digits and ${fraction} fractional digits
|
||||
javax.validation.constraints.Future.description=Must be in the future
|
||||
javax.validation.constraints.Max.description=Must be at most ${value}
|
||||
javax.validation.constraints.Min.description=Must be at least ${value}
|
||||
javax.validation.constraints.NotNull.description=Must not be null
|
||||
javax.validation.constraints.Null.description=Must be null
|
||||
javax.validation.constraints.Past.description=Must be in the past
|
||||
javax.validation.constraints.Pattern.description=Must match the regular expression '${regexp}'
|
||||
javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive
|
||||
@@ -329,6 +329,7 @@ public class RestDocumentationIntegrationTests {
|
||||
return new ResponseEntity<Map<String, Object>>(response, headers,
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package org.springframework.restdocs.constraints;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.validation.constraints.AssertFalse;
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Digits;
|
||||
import javax.validation.constraints.Future;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Null;
|
||||
import javax.validation.constraints.Past;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConstraintDescriptions}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ConstraintDescriptionsTests {
|
||||
|
||||
private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions(
|
||||
Constrained.class);
|
||||
|
||||
@Test
|
||||
public void assertFalse() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("assertFalse"),
|
||||
contains("Must be false"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assertTrue() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("assertTrue"),
|
||||
contains("Must be true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decimalMax() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMax"),
|
||||
contains("Must be at most 9.875"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decimalMin() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMin"),
|
||||
contains("Must be at least 1.5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void digits() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("digits"),
|
||||
contains("Must have at most 2 integral digits and 5 fractional digits"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void future() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("future"),
|
||||
contains("Must be in the future"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void max() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("max"),
|
||||
contains("Must be at most 10"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void min() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("min"),
|
||||
contains("Must be at least 5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notNull() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("notNull"),
|
||||
contains("Must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nul() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("nul"),
|
||||
contains("Must be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void past() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("past"),
|
||||
contains("Must be in the past"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pattern() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"),
|
||||
contains("Must match the regular expression '[A-Z][a-z]+'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void size() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("size"),
|
||||
contains("Size must be between 0 and 10 inclusive"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sizeList() {
|
||||
assertThat(
|
||||
this.constraintDescriptions.descriptionsForProperty("sizeList"),
|
||||
contains("Size must be between 1 and 4 inclusive",
|
||||
"Size must be between 8 and 10 inclusive"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unconstrained() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("unconstrained"),
|
||||
hasSize(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonExistentProperty() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("doesNotExist"),
|
||||
hasSize(0));
|
||||
}
|
||||
|
||||
private static class Constrained {
|
||||
|
||||
@AssertFalse
|
||||
private boolean assertFalse;
|
||||
|
||||
@AssertTrue
|
||||
private boolean assertTrue;
|
||||
|
||||
@DecimalMax("9.875")
|
||||
private BigDecimal decimalMax;
|
||||
|
||||
@DecimalMin("1.5")
|
||||
private BigDecimal decimalMin;
|
||||
|
||||
@Digits(fraction = 5, integer = 2)
|
||||
private BigDecimal digits;
|
||||
|
||||
@Future
|
||||
private Date future;
|
||||
|
||||
@NotNull
|
||||
private String notNull;
|
||||
|
||||
@Max(10)
|
||||
private int max;
|
||||
|
||||
@Min(5)
|
||||
private int min;
|
||||
|
||||
@Null
|
||||
private String nul;
|
||||
|
||||
@Past
|
||||
private Date past;
|
||||
|
||||
@Pattern(regexp = "[A-Z][a-z]+")
|
||||
private String pattern;
|
||||
|
||||
@Size(min = 0, max = 10)
|
||||
private String size;
|
||||
|
||||
@Size.List({ @Size(min = 1, max = 4), @Size(min = 8, max = 10) })
|
||||
private String sizeList;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private String unconstrained;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package org.springframework.restdocs.constraints;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.ListResourceBundle;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javax.validation.constraints.AssertFalse;
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Digits;
|
||||
import javax.validation.constraints.Future;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Null;
|
||||
import javax.validation.constraints.Past;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link ResourceBundleConstraintDescriptionResolver}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ResourceBundleConstraintDescriptionResolverTests {
|
||||
|
||||
private final ResourceBundleConstraintDescriptionResolver resolver = new ResourceBundleConstraintDescriptionResolver();
|
||||
|
||||
@Test
|
||||
public void defaultMessageAssertFalse() {
|
||||
String description = this.resolver.resolveDescription(new Constraint(
|
||||
AssertFalse.class.getName(), Collections.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Must be false")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageAssertTrue() {
|
||||
String description = this.resolver.resolveDescription(new Constraint(
|
||||
AssertTrue.class.getName(), Collections.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Must be true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageDecimalMax() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
configuration.put("value", "9.875");
|
||||
String description = this.resolver.resolveDescription(new Constraint(
|
||||
DecimalMax.class.getName(), configuration));
|
||||
assertThat(description, is(equalTo("Must be at most 9.875")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageDecimalMin() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
configuration.put("value", "1.5");
|
||||
String description = this.resolver.resolveDescription(new Constraint(
|
||||
DecimalMin.class.getName(), configuration));
|
||||
assertThat(description, is(equalTo("Must be at least 1.5")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageDigits() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
configuration.put("integer", "2");
|
||||
configuration.put("fraction", "5");
|
||||
String description = this.resolver.resolveDescription(new Constraint(Digits.class
|
||||
.getName(), configuration));
|
||||
assertThat(description, is(equalTo("Must have at most 2 integral digits and 5 "
|
||||
+ "fractional digits")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageFuture() {
|
||||
String description = this.resolver.resolveDescription(new Constraint(Future.class
|
||||
.getName(), Collections.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Must be in the future")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageMax() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
configuration.put("value", 10);
|
||||
String description = this.resolver.resolveDescription(new Constraint(Max.class
|
||||
.getName(), configuration));
|
||||
assertThat(description, is(equalTo("Must be at most 10")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageMin() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
configuration.put("value", 10);
|
||||
String description = this.resolver.resolveDescription(new Constraint(Min.class
|
||||
.getName(), configuration));
|
||||
assertThat(description, is(equalTo("Must be at least 10")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageNotNull() {
|
||||
String description = this.resolver.resolveDescription(new Constraint(
|
||||
NotNull.class.getName(), Collections.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Must not be null")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageNull() {
|
||||
String description = this.resolver.resolveDescription(new Constraint(Null.class
|
||||
.getName(), Collections.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Must be null")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessagePast() {
|
||||
String description = this.resolver.resolveDescription(new Constraint(Past.class
|
||||
.getName(), Collections.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Must be in the past")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessagePattern() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
configuration.put("regexp", "[A-Z][a-z]+");
|
||||
String description = this.resolver.resolveDescription(new Constraint(
|
||||
Pattern.class.getName(), configuration));
|
||||
assertThat(description,
|
||||
is(equalTo("Must match the regular expression '[A-Z][a-z]+'")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultMessageSize() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
configuration.put("min", 2);
|
||||
configuration.put("max", 10);
|
||||
String description = this.resolver.resolveDescription(new Constraint(Size.class
|
||||
.getName(), configuration));
|
||||
assertThat(description, is(equalTo("Size must be between 2 and 10 inclusive")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customMessage() {
|
||||
Thread.currentThread().setContextClassLoader(new ClassLoader() {
|
||||
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
if (name.startsWith("org/springframework/restdocs/constraints/ConstraintDescriptions")) {
|
||||
return super
|
||||
.getResource("org/springframework/restdocs/constraints/TestConstraintDescriptions.properties");
|
||||
}
|
||||
return super.getResource(name);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
try {
|
||||
String description = new ResourceBundleConstraintDescriptionResolver()
|
||||
.resolveDescription(new Constraint(NotNull.class.getName(),
|
||||
Collections.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Should not be null")));
|
||||
|
||||
}
|
||||
finally {
|
||||
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customResourceBundle() {
|
||||
ResourceBundle bundle = new ListResourceBundle() {
|
||||
|
||||
@Override
|
||||
protected Object[][] getContents() {
|
||||
return new String[][] { { NotNull.class.getName() + ".description",
|
||||
"Not null" } };
|
||||
}
|
||||
|
||||
};
|
||||
String description = new ResourceBundleConstraintDescriptionResolver(bundle)
|
||||
.resolveDescription(new Constraint(NotNull.class.getName(), Collections
|
||||
.<String, Object> emptyMap()));
|
||||
assertThat(description, is(equalTo("Not null")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.springframework.restdocs.constraints;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Null;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import org.hibernate.validator.constraints.CompositionType;
|
||||
import org.hibernate.validator.constraints.ConstraintComposition;
|
||||
import org.hibernate.validator.constraints.NotBlank;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link ValidatorConstraintResolver}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ValidatorConstraintResolverTests {
|
||||
|
||||
private final ValidatorConstraintResolver resolver = new ValidatorConstraintResolver();
|
||||
|
||||
@Test
|
||||
public void singleFieldConstraint() {
|
||||
List<Constraint> constraints = this.resolver.resolveForProperty("single",
|
||||
ConstrainedFields.class);
|
||||
assertThat(constraints, hasSize(1));
|
||||
assertThat(constraints.get(0).getName(), is(NotNull.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldConstraints() {
|
||||
List<Constraint> constraints = this.resolver.resolveForProperty("multiple",
|
||||
ConstrainedFields.class);
|
||||
assertThat(constraints, hasSize(2));
|
||||
assertThat(constraints.get(0).getName(), is(NotNull.class.getName()));
|
||||
assertThat(constraints.get(1).getName(), is(Size.class.getName()));
|
||||
assertThat(constraints.get(1).getConfiguration().get("min"), is((Object) 8));
|
||||
assertThat(constraints.get(1).getConfiguration().get("max"), is((Object) 16));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noFieldConstraints() {
|
||||
List<Constraint> constraints = this.resolver.resolveForProperty("none",
|
||||
ConstrainedFields.class);
|
||||
assertThat(constraints, hasSize(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeConstraint() {
|
||||
List<Constraint> constraints = this.resolver.resolveForProperty("composite",
|
||||
ConstrainedFields.class);
|
||||
assertThat(constraints, hasSize(1));
|
||||
}
|
||||
|
||||
private static class ConstrainedFields {
|
||||
|
||||
@NotNull
|
||||
private String single;
|
||||
|
||||
@NotNull
|
||||
@Size(min = 8, max = 16)
|
||||
private String multiple;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private String none;
|
||||
|
||||
@CompositeConstraint
|
||||
private String composite;
|
||||
}
|
||||
|
||||
@ConstraintComposition(CompositionType.OR)
|
||||
@Null
|
||||
@NotBlank
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@javax.validation.Constraint(validatedBy = {})
|
||||
public @interface CompositeConstraint {
|
||||
|
||||
String message() default "Must be null or not blank";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
javax.validation.constraints.NotNull.description=Should not be null
|
||||
Reference in New Issue
Block a user