Reduce duplication by introducing RestDocumentationHandler
Previously, logic for creating an Operation, determining the snippets to call, and calling them was duplicated in both the MockMvc and REST Assured modules. This commit introduces a new core class, RestDocumentationHandler, that now does the bulk of the work in a reusable manner. The MockMvc and REST Assured modules have been updated to delegate to RestDocumentationHandler. Closes gh-194
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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;
|
||||
|
||||
/**
|
||||
* An exception that can be thrown when a failure occurs during REST documentation
|
||||
* generation.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RestDocumentationException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates a new {@code RestDocumentationException} with the given {@code cause}.
|
||||
*
|
||||
* @param cause the cause
|
||||
*/
|
||||
public RestDocumentationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code RestDocumentationException} with the given {@code message} and
|
||||
* {@code cause}.
|
||||
*
|
||||
* @param message the message
|
||||
* @param cause the cause
|
||||
*/
|
||||
public RestDocumentationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.restdocs.config.SnippetConfigurer;
|
||||
import org.springframework.restdocs.operation.Operation;
|
||||
import org.springframework.restdocs.operation.OperationRequest;
|
||||
import org.springframework.restdocs.operation.OperationResponse;
|
||||
import org.springframework.restdocs.operation.RequestConverter;
|
||||
import org.springframework.restdocs.operation.ResponseConverter;
|
||||
import org.springframework.restdocs.operation.StandardOperation;
|
||||
import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
|
||||
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
|
||||
import org.springframework.restdocs.snippet.Snippet;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@code RestDocumentationHandler} is used to produce documentation snippets from the
|
||||
* request and response of an operation performed on a service.
|
||||
*
|
||||
* @param <REQ> the request type that can be handled
|
||||
* @param <RESP> the response type that can be handled
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public final class RestDocumentationHandler<REQ, RESP> {
|
||||
|
||||
private final String identifier;
|
||||
|
||||
private final OperationRequestPreprocessor requestPreprocessor;
|
||||
|
||||
private final OperationResponsePreprocessor responsePreprocessor;
|
||||
|
||||
private final List<Snippet> snippets;
|
||||
|
||||
private final RequestConverter<REQ> requestConverter;
|
||||
|
||||
private final ResponseConverter<RESP> responseConverter;
|
||||
|
||||
/**
|
||||
* Creates a new {@code RestDocumentationHandler} for the operation identified by the
|
||||
* given {@code identifier}. The given {@code requestConverter} and
|
||||
* {@code responseConverter} are used to convert the operation's request and response
|
||||
* into generic {@code OperationRequest} and {@code OperationResponse} instances that
|
||||
* can then be documented. The given documentation {@code snippets} will be produced.
|
||||
*
|
||||
* @param identifier the identifier for the operation
|
||||
* @param requestConverter the request converter
|
||||
* @param responseConverter the response converter
|
||||
* @param snippets the snippets
|
||||
*/
|
||||
public RestDocumentationHandler(String identifier,
|
||||
RequestConverter<REQ> requestConverter,
|
||||
ResponseConverter<RESP> responseConverter, Snippet... snippets) {
|
||||
this(identifier, requestConverter, responseConverter,
|
||||
new IdentityOperationRequestPreprocessor(),
|
||||
new IdentityOperationResponsePreprocessor(), snippets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code RestDocumentationHandler} for the operation identified by the
|
||||
* given {@code identifier}. The given {@code requestConverter} and
|
||||
* {@code responseConverter} are used to convert the operation's request and response
|
||||
* into generic {@code OperationRequest} and {@code OperationResponse} instances that
|
||||
* can then be documented. The given {@code requestPreprocessor} is applied to the
|
||||
* request before it is documented. The given documentation {@code snippets} will be
|
||||
* produced.
|
||||
*
|
||||
* @param identifier the identifier for the operation
|
||||
* @param requestConverter the request converter
|
||||
* @param responseConverter the response converter
|
||||
* @param requestPreprocessor the request preprocessor
|
||||
* @param snippets the snippets
|
||||
*/
|
||||
public RestDocumentationHandler(String identifier,
|
||||
RequestConverter<REQ> requestConverter,
|
||||
ResponseConverter<RESP> responseConverter,
|
||||
OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) {
|
||||
this(identifier, requestConverter, responseConverter, requestPreprocessor,
|
||||
new IdentityOperationResponsePreprocessor(), snippets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code RestDocumentationHandler} for the operation identified by the
|
||||
* given {@code identifier}. The given {@code requestConverter} and
|
||||
* {@code responseConverter} are used to convert the operation's request and response
|
||||
* into generic {@code OperationRequest} and {@code OperationResponse} instances that
|
||||
* can then be documented. The given {@code responsePreprocessor} is applied to the
|
||||
* response before it is documented. The given documentation {@code snippets} will be
|
||||
* produced.
|
||||
*
|
||||
* @param identifier the identifier for the operation
|
||||
* @param requestConverter the request converter
|
||||
* @param responseConverter the response converter
|
||||
* @param responsePreprocessor the response preprocessor
|
||||
* @param snippets the snippets
|
||||
*/
|
||||
public RestDocumentationHandler(String identifier,
|
||||
RequestConverter<REQ> requestConverter,
|
||||
ResponseConverter<RESP> responseConverter,
|
||||
OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) {
|
||||
this(identifier, requestConverter, responseConverter,
|
||||
new IdentityOperationRequestPreprocessor(), responsePreprocessor,
|
||||
snippets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code RestDocumentationHandler} for the operation identified by the
|
||||
* given {@code identifier}. The given {@code requestConverter} and
|
||||
* {@code responseConverter} are used to convert the operation's request and response
|
||||
* into generic {@code OperationRequest} and {@code OperationResponse} instances that
|
||||
* can then be documented. The given {@code requestPreprocessor} and
|
||||
* {@code responsePreprocessor} are applied to the request and response before they
|
||||
* are documented. The given documentation {@code snippets} will be produced.
|
||||
*
|
||||
* @param identifier the identifier for the operation
|
||||
* @param requestConverter the request converter
|
||||
* @param responseConverter the response converter
|
||||
* @param requestPreprocessor the request preprocessor
|
||||
* @param responsePreprocessor the response preprocessor
|
||||
* @param snippets the snippets
|
||||
*/
|
||||
public RestDocumentationHandler(String identifier,
|
||||
RequestConverter<REQ> requestConverter,
|
||||
ResponseConverter<RESP> responseConverter,
|
||||
OperationRequestPreprocessor requestPreprocessor,
|
||||
OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) {
|
||||
Assert.notNull(identifier, "identifier must be non-null");
|
||||
Assert.notNull(requestConverter, "requestConverter must be non-null");
|
||||
Assert.notNull(responseConverter, "responseConverter must be non-null");
|
||||
Assert.notNull(identifier, "identifier must be non-null");
|
||||
Assert.notNull(requestPreprocessor, "requestPreprocessor must be non-null");
|
||||
Assert.notNull(responsePreprocessor, "responsePreprocessor must be non-null");
|
||||
Assert.notNull(snippets, "snippets must be non-null");
|
||||
this.identifier = identifier;
|
||||
this.requestConverter = requestConverter;
|
||||
this.responseConverter = responseConverter;
|
||||
this.requestPreprocessor = requestPreprocessor;
|
||||
this.responsePreprocessor = responsePreprocessor;
|
||||
this.snippets = new ArrayList<>(Arrays.asList(snippets));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given {@code request} and {@code response}, producing documentation
|
||||
* snippets for them using the given {@code configuration}.
|
||||
*
|
||||
* @param request the request
|
||||
* @param response the request
|
||||
* @param configuration the configuration
|
||||
* @throws RestDocumentationException if a failure occurs during handling
|
||||
*/
|
||||
public void handle(REQ request, RESP response, Map<String, Object> configuration) {
|
||||
OperationRequest operationRequest = this.requestPreprocessor
|
||||
.preprocess(this.requestConverter.convert(request));
|
||||
|
||||
OperationResponse operationResponse = this.responsePreprocessor
|
||||
.preprocess(this.responseConverter.convert(response));
|
||||
Map<String, Object> attributes = new HashMap<>(configuration);
|
||||
Operation operation = new StandardOperation(this.identifier, operationRequest,
|
||||
operationResponse, attributes);
|
||||
try {
|
||||
for (Snippet snippet : getSnippets(attributes)) {
|
||||
snippet.document(operation);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RestDocumentationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given {@code snippets} such that they are documented when this handler is
|
||||
* called.
|
||||
*
|
||||
* @param snippets the snippets to add
|
||||
*/
|
||||
public void addSnippets(Snippet... snippets) {
|
||||
this.snippets.addAll(Arrays.asList(snippets));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Snippet> getSnippets(Map<String, Object> configuration) {
|
||||
List<Snippet> combinedSnippets = new ArrayList<>(this.snippets);
|
||||
List<Snippet> defaultSnippets = (List<Snippet>) configuration
|
||||
.get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS);
|
||||
if (defaultSnippets != null) {
|
||||
combinedSnippets.addAll(defaultSnippets);
|
||||
}
|
||||
return combinedSnippets;
|
||||
}
|
||||
|
||||
private static final class IdentityOperationRequestPreprocessor implements
|
||||
OperationRequestPreprocessor {
|
||||
|
||||
@Override
|
||||
public OperationRequest preprocess(OperationRequest request) {
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class IdentityOperationResponsePreprocessor implements
|
||||
OperationResponsePreprocessor {
|
||||
|
||||
@Override
|
||||
public OperationResponse preprocess(OperationResponse response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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.operation;
|
||||
|
||||
/**
|
||||
* An exception that can be thrown by {@link RequestConverter} and
|
||||
* {@link ResponseConverter} implementations to indicate that a failure has occurred
|
||||
* during conversion.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @see RequestConverter#convert(Object)
|
||||
* @see ResponseConverter#convert(Object)
|
||||
*/
|
||||
public class ConversionException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates a new {@code ConversionException} with the given {@code cause}.
|
||||
*
|
||||
* @param cause the cause
|
||||
*/
|
||||
public ConversionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code ConversionException} with the given {@code message} and
|
||||
* {@code cause}.
|
||||
*
|
||||
* @param message the message
|
||||
* @param cause the cause
|
||||
*/
|
||||
public ConversionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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.operation;
|
||||
|
||||
/**
|
||||
* A {@code RequestConverter} is used to convert an implementation-specific request into
|
||||
* an {@link OperationRequest}.
|
||||
*
|
||||
* @param <R> The implementation-specific request type
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public interface RequestConverter<R> {
|
||||
|
||||
/**
|
||||
* Converts the given {@code request} into an {@code OperationRequest}.
|
||||
*
|
||||
* @param request the request
|
||||
* @return the operation request
|
||||
* @throws ConversionException if the conversion fails
|
||||
*/
|
||||
OperationRequest convert(R request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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.operation;
|
||||
|
||||
/**
|
||||
* A {@code ResponseConverter} is used to convert an implementation-specific response into
|
||||
* an {@link OperationResponse}.
|
||||
*
|
||||
* @param <R> The implementation-specific response type
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public interface ResponseConverter<R> {
|
||||
|
||||
/**
|
||||
* Converts the given {@code response} into an {@code OperationResponse}.
|
||||
*
|
||||
* @param response the response
|
||||
* @return the operation response
|
||||
* @throws ConversionException if the conversion fails
|
||||
*/
|
||||
OperationResponse convert(R response);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.restdocs.config.SnippetConfigurer;
|
||||
import org.springframework.restdocs.operation.Operation;
|
||||
import org.springframework.restdocs.operation.OperationRequest;
|
||||
import org.springframework.restdocs.operation.OperationRequestFactory;
|
||||
import org.springframework.restdocs.operation.OperationResponse;
|
||||
import org.springframework.restdocs.operation.OperationResponseFactory;
|
||||
import org.springframework.restdocs.operation.RequestConverter;
|
||||
import org.springframework.restdocs.operation.ResponseConverter;
|
||||
import org.springframework.restdocs.snippet.Snippet;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link RestDocumentationHandler}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RestDocumentationHandlerTests {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private final RequestConverter<Object> requestConverter = mock(RequestConverter.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private final ResponseConverter<Object> responseConverter = mock(ResponseConverter.class);
|
||||
|
||||
private final Object request = new Object();
|
||||
|
||||
private final Object response = new Object();
|
||||
|
||||
private final OperationRequest operationRequest = new OperationRequestFactory()
|
||||
.create(URI.create("http://localhost:8080"), null, null, new HttpHeaders(),
|
||||
null, null);
|
||||
|
||||
private final OperationResponse operationResponse = new OperationResponseFactory()
|
||||
.create(null, null, null);
|
||||
|
||||
private final Snippet snippet = mock(Snippet.class);
|
||||
|
||||
@Test
|
||||
public void basicHandling() throws IOException {
|
||||
given(this.requestConverter.convert(this.request)).willReturn(
|
||||
this.operationRequest);
|
||||
given(this.responseConverter.convert(this.response)).willReturn(
|
||||
this.operationResponse);
|
||||
HashMap<String, Object> configuration = new HashMap<>();
|
||||
new RestDocumentationHandler<>("id", this.requestConverter,
|
||||
this.responseConverter, this.snippet).handle(this.request, this.response,
|
||||
configuration);
|
||||
verifySnippetInvocation(this.snippet, configuration);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultSnippetsAreCalled() throws IOException {
|
||||
given(this.requestConverter.convert(this.request)).willReturn(
|
||||
this.operationRequest);
|
||||
given(this.responseConverter.convert(this.response)).willReturn(
|
||||
this.operationResponse);
|
||||
HashMap<String, Object> configuration = new HashMap<>();
|
||||
Snippet defaultSnippet1 = mock(Snippet.class);
|
||||
Snippet defaultSnippet2 = mock(Snippet.class);
|
||||
configuration.put(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS,
|
||||
Arrays.asList(defaultSnippet1, defaultSnippet2));
|
||||
new RestDocumentationHandler<>("id", this.requestConverter,
|
||||
this.responseConverter, this.snippet).handle(this.request, this.response,
|
||||
configuration);
|
||||
verifySnippetInvocation(this.snippet, configuration);
|
||||
verifySnippetInvocation(defaultSnippet1, configuration);
|
||||
verifySnippetInvocation(defaultSnippet2, configuration);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void additionalSnippetsAreCalled() throws IOException {
|
||||
given(this.requestConverter.convert(this.request)).willReturn(
|
||||
this.operationRequest);
|
||||
given(this.responseConverter.convert(this.response)).willReturn(
|
||||
this.operationResponse);
|
||||
Snippet additionalSnippet1 = mock(Snippet.class);
|
||||
Snippet additionalSnippet2 = mock(Snippet.class);
|
||||
RestDocumentationHandler<Object, Object> handler = new RestDocumentationHandler<>(
|
||||
"id", this.requestConverter, this.responseConverter, this.snippet);
|
||||
handler.addSnippets(additionalSnippet1, additionalSnippet2);
|
||||
HashMap<String, Object> configuration = new HashMap<>();
|
||||
handler.handle(this.request, this.response, configuration);
|
||||
verifySnippetInvocation(this.snippet, configuration);
|
||||
verifySnippetInvocation(additionalSnippet1, configuration);
|
||||
verifySnippetInvocation(additionalSnippet2, configuration);
|
||||
}
|
||||
|
||||
private void verifySnippetInvocation(Snippet snippet, Map<String, Object> attributes)
|
||||
throws IOException {
|
||||
ArgumentCaptor<Operation> operation = ArgumentCaptor.forClass(Operation.class);
|
||||
verify(snippet).document(operation.capture());
|
||||
assertThat(this.operationRequest, is(equalTo(operation.getValue().getRequest())));
|
||||
assertThat(this.operationResponse,
|
||||
is(equalTo(operation.getValue().getResponse())));
|
||||
assertThat(attributes, is(equalTo(operation.getValue().getAttributes())));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user