From b26d8c085ddcab7af6c62991839e787cc4d4f577 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Tue, 1 Mar 2016 16:35:32 -0500 Subject: [PATCH] Add support for generating an HTTPie snippet This commit adds support for generating a snippet that contains the HTTPie command for the request. As the snippet does not require any additional configuration, it has added to the existing default snippets. Httpie does not currently support setting the content type for each part in a multipart form -- these multipart types are currently ignored. See: https://github.com/jkbrzt/httpie/issues/199 https://github.com/jkbrzt/httpie/issues/271 https://github.com/jkbrzt/httpie/pull/285 https://github.com/jkbrzt/httpie/pull/398 There is an issue with specifying piped input for multipart form data. There is no way currently to specify the data without specifying a filename parameter in the Content-Disposition. For now, this is ignored. See: https://github.com/jkbrzt/httpie/issues/342 See gh-207 --- config/checkstyle/checkstyle.xml | 2 +- .../docs/asciidoc/documenting-your-api.adoc | 4 + docs/src/docs/asciidoc/getting-started.adoc | 3 +- .../mockmvc/CustomDefaultSnippets.java | 3 +- .../restassured/CustomDefaultSnippets.java | 3 +- .../restdocs/cli/AbstractCliSnippet.java | 182 ++++++++++ .../{curl => cli}/QueryStringParser.java | 2 +- .../restdocs/cli/curl/CurlDocumentation.java | 60 ++++ .../restdocs/cli/curl/CurlRequestSnippet.java | 161 +++++++++ .../restdocs/{ => cli}/curl/package-info.java | 2 +- .../cli/httpie/HttpieDocumentation.java | 59 ++++ .../cli/httpie/HttpieRequestSnippet.java | 183 ++++++++++ .../restdocs/cli/httpie/package-info.java | 20 ++ .../restdocs/config/SnippetConfigurer.java | 11 +- .../restdocs/curl/CurlDocumentation.java | 9 +- .../restdocs/curl/CurlRequestSnippet.java | 230 +----------- .../default-httpie-request.snippet | 4 + .../markdown/default-httpie-request.snippet | 3 + .../{curl => cli}/QueryStringParserTests.java | 2 +- .../curl/CurlRequestSnippetTests.java | 2 +- .../cli/httpie/HttpieRequestSnippetTests.java | 330 ++++++++++++++++++ .../RestDocumentationConfigurerTests.java | 6 +- .../restdocs/test/ExpectedSnippet.java | 5 + .../httpie-request-with-title.snippet | 5 + .../httpie-request-with-title.snippet | 4 + ...kMvcRestDocumentationIntegrationTests.java | 2 +- 26 files changed, 1061 insertions(+), 236 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{curl => cli}/QueryStringParser.java (98%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{ => cli}/curl/package-info.java (93%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{curl => cli}/QueryStringParserTests.java (98%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{ => cli}/curl/CurlRequestSnippetTests.java (99%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 36c8db5b..86947f99 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.cli.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index fb9977c8..f036275c 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -542,6 +542,10 @@ A number of snippets are produced automatically when you document a request and | Contains the http://curl.haxx.se[`curl`] command that is equivalent to the `MockMvc` call that is being documented +| `httpie-request.adoc` +| Contains the http://httpie.org[`HTTPie`] command that is equivalent to the `MockMvc` +call that is being documented + | `http-request.adoc` | Contains the HTTP request that is equivalent to the `MockMvc` call that is being documented diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 67d7dc17..2e2cc2e3 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -371,9 +371,10 @@ static `document` method on <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. -By default, three snippets are written: +By default, four snippets are written: * `/index/curl-request.adoc` + * `/index/httpie-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 4be61c37..7164e058 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -20,12 +20,11 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index ae581cd0..632507c6 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -19,12 +19,11 @@ package com.example.restassured; import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java new file mode 100644 index 00000000..ef00f0ba --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java @@ -0,0 +1,182 @@ +/* + * Copyright 2014-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.cli; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Base64Utils; + +/** + * An abstract {@link Snippet} that for CLI requests. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +public abstract class AbstractCliSnippet extends TemplatedSnippet { + + private static final Set HEADER_FILTERS; + + static { + Set headerFilters = new HashSet<>(); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); + headerFilters.add(new BasicAuthHeaderFilter()); + HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); + } + + /** + * Create a new abstract cli snippet with the given name and attributes. + * @param snippetName The snippet name. + * @param attributes The snippet attributes. + */ + protected AbstractCliSnippet(String snippetName, Map attributes) { + super(snippetName, attributes); + } + + /** + * Create the model which will be passed to the template for rendering. + * @param operation The operation + * @return The model. + */ + protected abstract Map createModel(Operation operation); + + /** + * Gets the unique parameters given a request. + * @param request The operation request. + * @return The unique parameters. + */ + protected Parameters getUniqueParameters(OperationRequest request) { + Parameters queryStringParameters = new QueryStringParser() + .parse(request.getUri()); + Parameters uniqueParameters = new Parameters(); + + for (Map.Entry> parameter : request.getParameters().entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Map.Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } + } + } + } + + /** + * Whether the request operation is a PUT or a POST. + * @param request The request. + * @return boolean + */ + protected boolean isPutOrPost(OperationRequest request) { + return HttpMethod.PUT.equals(request.getMethod()) + || HttpMethod.POST.equals(request.getMethod()); + } + + /** + * Whether the passed header is allowed according to the configured + * header filters. + * @param header The header to test. + * @return boolean + */ + protected boolean allowedHeader(Map.Entry> header) { + for (HeaderFilter headerFilter : HEADER_FILTERS) { + if (!headerFilter.allow(header.getKey(), header.getValue())) { + return false; + } + } + return true; + } + + /** + * Determine if the header passed is a basic auth header. + * @param headerValue The header to test. + * @return boolean + */ + protected boolean isBasicAuthHeader(List headerValue) { + return BasicAuthHeaderFilter.isBasicAuthHeader(headerValue); + } + + /** + * Decodes a basic auth header into name:password credentials. + * @param headerValue The encoded header value. + * @return name:password credentials. + */ + protected String decodeBasicAuthHeader(List headerValue) { + return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); + } + + private interface HeaderFilter { + + boolean allow(String name, List value); + } + + private static final class BasicAuthHeaderFilter implements HeaderFilter { + + @Override + public boolean allow(String name, List value) { + return !(HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)); + } + + static boolean isBasicAuthHeader(List value) { + return value != null && (!value.isEmpty()) + && value.get(0).startsWith("Basic "); + } + + static String decodeBasicAuthHeader(List value) { + return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); + } + + } + + private static final class NamedHeaderFilter implements HeaderFilter { + + private final String name; + + NamedHeaderFilter(String name) { + this.name = name; + } + + @Override + public boolean allow(String name, List value) { + return !this.name.equalsIgnoreCase(name); + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java similarity index 98% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java index ea5f3148..4a3ab8ca 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli; import java.io.UnsupportedEncodingException; import java.net.URI; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java new file mode 100644 index 00000000..73ef5fe4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java @@ -0,0 +1,60 @@ +/* + * 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.cli.curl; + +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API as if it were being driven using + * the cURL command-line utility. + * + * @author Andy Wilkinson + * @author Yann Le Guern + * @author Dmitriy Mayboroda + * @author Jonathan Pearlin + */ +public abstract class CurlDocumentation { + + private CurlDocumentation() { + + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. + * + * @return the snippet that will document the curl request + */ + public static Snippet curlRequest() { + return new CurlRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the curl request + */ + public static Snippet curlRequest(Map attributes) { + return new CurlRequestSnippet(attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java new file mode 100644 index 00000000..af4c253c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java @@ -0,0 +1,161 @@ +/* + * Copyright 2014-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.cli.curl; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.cli.AbstractCliSnippet; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the curl command for a request. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @see CurlDocumentation#curlRequest() + * @see CurlDocumentation#curlRequest(Map) + */ +public class CurlRequestSnippet extends AbstractCliSnippet { + + /** + * Creates a new {@code CurlRequestSnippet} with no additional attributes. + */ + protected CurlRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code CurlRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected CurlRequestSnippet(Map attributes) { + super("curl-request", attributes); + } + + @Override + protected Map createModel(Operation operation) { + Map model = new HashMap<>(); + model.put("url", getUrl(operation)); + model.put("options", getOptions(operation)); + return model; + } + + private String getUrl(Operation operation) { + return String.format("'%s'", operation.getRequest().getUri()); + } + + private String getOptions(Operation operation) { + StringWriter command = new StringWriter(); + PrintWriter printer = new PrintWriter(command); + writeIncludeHeadersInOutputOption(printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); + writeHttpMethodIfNecessary(operation.getRequest(), printer); + writeHeaders(operation.getRequest().getHeaders(), printer); + writePartsIfNecessary(operation.getRequest(), printer); + writeContent(operation.getRequest(), printer); + + return command.toString(); + } + + private void writeIncludeHeadersInOutputOption(PrintWriter writer) { + writer.print("-i"); + } + + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (isBasicAuthHeader(headerValue)) { + String credentials = decodeBasicAuthHeader(headerValue); + writer.print(String.format(" -u '%s'", credentials)); + } + } + + private void writeHttpMethodIfNecessary(OperationRequest request, + PrintWriter writer) { + if (!HttpMethod.GET.equals(request.getMethod())) { + writer.print(String.format(" -X %s", request.getMethod())); + } + } + + private void writeHeaders(HttpHeaders headers, PrintWriter writer) { + for (Entry> entry : headers.entrySet()) { + if (allowedHeader(entry)) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + } + } + } + } + + private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" -F '%s=", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + writer.append(part.getContentAsString()); + } + else { + writer.printf("@%s", part.getSubmittedFileName()); + } + if (part.getHeaders().getContentType() != null) { + writer.append(";type=") + .append(part.getHeaders().getContentType().toString()); + } + + writer.append("'"); + } + } + + private void writeContent(OperationRequest request, PrintWriter writer) { + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + writer.print(String.format(" -d '%s'", content)); + } + else if (!request.getParts().isEmpty()) { + for (Entry> entry : request.getParameters().entrySet()) { + for (String value : entry.getValue()) { + writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); + } + } + } + else if (isPutOrPost(request)) { + writeContentUsingParameters(request, writer); + } + } + + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + String queryString = uniqueParameters.toQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java similarity index 93% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java index 7c07b91e..02ebe62c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java @@ -17,4 +17,4 @@ /** * Documenting the curl command required to make a request to a RESTful API. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli.curl; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java new file mode 100644 index 00000000..0313ae02 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java @@ -0,0 +1,59 @@ +/* + * 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.cli.httpie; + +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API as if it were being driven using + * the httpie command-line utility. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +public abstract class HttpieDocumentation { + + private HttpieDocumentation() { + + } + + /** + * Returns a new {@code Snippet} that will document the httpie request for the API + * operation. + * + * @return the snippet that will document the httpie request + */ + public static Snippet httpieRequest() { + return new HttpieRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the httpie request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the httpie request + */ + public static Snippet httpieRequest(Map attributes) { + return new HttpieRequestSnippet(attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java new file mode 100644 index 00000000..7e251a90 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java @@ -0,0 +1,183 @@ +/* + * Copyright 2014-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.cli.httpie; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.cli.AbstractCliSnippet; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the httpie command for a request. + * + * @author Raman Gupta + * @see HttpieDocumentation#httpieRequest() + * @see HttpieDocumentation#httpieRequest(Map) + */ +public class HttpieRequestSnippet extends AbstractCliSnippet { + + /** + * Creates a new {@code CurlRequestSnippet} with no additional attributes. + */ + protected HttpieRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code CurlRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected HttpieRequestSnippet(Map attributes) { + super("httpie-request", attributes); + } + + @Override + protected Map createModel(final Operation operation) { + Map model = new HashMap<>(); + model.put("echo_content", getContentStdIn(operation)); + model.put("options", getOptions(operation)); + model.put("url", getUrl(operation)); + model.put("request_items", getRequestItems(operation)); + return model; + } + private Object getContentStdIn(final Operation operation) { + OperationRequest request = operation.getRequest(); + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + return String.format("echo '%s' | ", content); + } + else { + return ""; + } + } + + private String getOptions(Operation operation) { + StringWriter options = new StringWriter(); + PrintWriter printer = new PrintWriter(options); + writeOptions(operation.getRequest(), printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); + writeMethodIfNecessary(operation.getRequest(), printer); + return options.toString(); + } + + private String getUrl(Operation operation) { + return String.format("'%s'", operation.getRequest().getUri()); + } + + private String getRequestItems(final Operation operation) { + StringWriter requestItems = new StringWriter(); + PrintWriter printer = new PrintWriter(requestItems); + writeFormDataIfNecessary(operation.getRequest(), printer); + writeHeaders(operation.getRequest(), printer); + writeContent(operation.getRequest(), printer); + return requestItems.toString(); + } + private void writeOptions(OperationRequest request, PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + if (request.getParts().size() > 0 || uniqueParameters.size() > 0) { + writer.print("--form "); + } + } + + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (isBasicAuthHeader(headerValue)) { + String credentials = decodeBasicAuthHeader(headerValue); + writer.print(String.format("--auth '%s' ", credentials)); + } + } + + private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer) { + writer.print(String.format("%s", request.getMethod().name())); + } + + private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" \\\n '%s'", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + // httpie https://github.com/jkbrzt/httpie/issues/342 + writer.printf("@<(echo '%s')", part.getContentAsString()); + } + else { + writer.printf("@'%s'", part.getSubmittedFileName()); + } + // httpie does not currently support manually set content type by part + } + } + + private void writeHeaders(OperationRequest request, PrintWriter writer) { + HttpHeaders headers = request.getHeaders(); + for (Entry> entry : headers.entrySet()) { + if (allowedHeader(entry)) { + for (String header : entry.getValue()) { + // form Content-Type not required, added automatically by httpie with --form + if (request.getParts().size() > 0 + && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + && header.startsWith("multipart/form-data")) { + continue; + } + writer.print(String.format(" '%s:%s'", entry.getKey(), header)); + } + } + } + } + + private void writeContent(OperationRequest request, PrintWriter writer) { + String content = request.getContentAsString(); + if (!StringUtils.hasText(content)) { + if (!request.getParts().isEmpty()) { + for (Entry> entry : request.getParameters().entrySet()) { + for (String value : entry.getValue()) { + writer.print(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + else if (isPutOrPost(request)) { + writeContentUsingParameters(request, writer); + } + } + } + + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + for (Map.Entry> entry : uniqueParameters.entrySet()) { + if (entry.getValue().isEmpty()) { + writer.append(String.format(" '%s='", entry.getKey())); + } + else { + for (String value : entry.getValue()) { + writer.append(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java new file mode 100644 index 00000000..2bdf5df6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the httpie command required to make a request to a RESTful API. + */ +package org.springframework.restdocs.cli.httpie; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index e8faa6ec..7c2b459c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -21,7 +21,8 @@ import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.cli.curl.CurlDocumentation; +import org.springframework.restdocs.cli.httpie.HttpieDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; @@ -38,8 +39,12 @@ import org.springframework.restdocs.templates.TemplateFormats; public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList(CurlDocumentation.curlRequest(), - HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse()); + private List defaultSnippets = Arrays.asList( + CurlDocumentation.curlRequest(), + HttpieDocumentation.httpieRequest(), + HttpDocumentation.httpRequest(), + HttpDocumentation.httpResponse() + ); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index a3549443..a9b094f6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -24,11 +24,14 @@ import org.springframework.restdocs.snippet.Snippet; * Static factory methods for documenting a RESTful API as if it were being driven using * the cURL command-line utility. * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. + * * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda * @author Jonathan Pearlin */ +@Deprecated public abstract class CurlDocumentation { private CurlDocumentation() { @@ -40,6 +43,8 @@ public abstract class CurlDocumentation { * operation. * * @return the snippet that will document the curl request + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. */ public static Snippet curlRequest() { return new CurlRequestSnippet(); @@ -52,6 +57,8 @@ public abstract class CurlDocumentation { * * @param attributes the attributes * @return the snippet that will document the curl request + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. */ public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4f4ea07b..29396bac 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -16,243 +16,35 @@ package org.springframework.restdocs.curl; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.util.Base64Utils; -import org.springframework.util.StringUtils; /** * A {@link Snippet} that documents the curl command for a request. * - * @author Andy Wilkinson - * @author Paul-Christian Volkmer - * @see CurlDocumentation#curlRequest() - * @see CurlDocumentation#curlRequest(Map) + * @author Raman Gupta + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ -public class CurlRequestSnippet extends TemplatedSnippet { - - private static final Set HEADER_FILTERS; - - static { - Set headerFilters = new HashSet<>(); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); - headerFilters.add(new BasicAuthHeaderFilter()); - HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); - } +public class CurlRequestSnippet extends org.springframework.restdocs.cli.curl.CurlRequestSnippet { /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ protected CurlRequestSnippet() { - this(null); + super(); } /** - * Creates a new {@code CurlRequestSnippet} with the given additional - * {@code attributes} that will be included in the model during template rendering. + * Creates a new {@code CurlRequestSnippet} with additional attributes. + * @param attributes The additional attributes. * - * @param attributes The additional attributes + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ - protected CurlRequestSnippet(Map attributes) { - super("curl-request", attributes); - } - - @Override - protected Map createModel(Operation operation) { - Map model = new HashMap<>(); - model.put("url", getUrl(operation)); - model.put("options", getOptions(operation)); - return model; - } - - private String getUrl(Operation operation) { - return String.format("'%s'", operation.getRequest().getUri()); - } - - private String getOptions(Operation operation) { - StringWriter command = new StringWriter(); - PrintWriter printer = new PrintWriter(command); - writeIncludeHeadersInOutputOption(printer); - writeUserOptionIfNecessary(operation.getRequest(), printer); - writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest().getHeaders(), printer); - writePartsIfNecessary(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); - - return command.toString(); - } - - private void writeIncludeHeadersInOutputOption(PrintWriter writer) { - writer.print("-i"); - } - - private void writeUserOptionIfNecessary(OperationRequest request, - PrintWriter writer) { - List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); - if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { - String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); - writer.print(String.format(" -u '%s'", credentials)); - } - } - - private void writeHttpMethodIfNecessary(OperationRequest request, - PrintWriter writer) { - if (!HttpMethod.GET.equals(request.getMethod())) { - writer.print(String.format(" -X %s", request.getMethod())); - } - } - - private void writeHeaders(HttpHeaders headers, PrintWriter writer) { - for (Entry> entry : headers.entrySet()) { - if (allowedHeader(entry)) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); - } - } - } - } - - private boolean allowedHeader(Entry> header) { - for (HeaderFilter headerFilter : HEADER_FILTERS) { - if (!headerFilter.allow(header.getKey(), header.getValue())) { - return false; - } - } - return true; - } - - private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { - for (OperationRequestPart part : request.getParts()) { - writer.printf(" -F '%s=", part.getName()); - if (!StringUtils.hasText(part.getSubmittedFileName())) { - writer.append(part.getContentAsString()); - } - else { - writer.printf("@%s", part.getSubmittedFileName()); - } - if (part.getHeaders().getContentType() != null) { - writer.append(";type=") - .append(part.getHeaders().getContentType().toString()); - } - - writer.append("'"); - } - } - - private void writeContent(OperationRequest request, PrintWriter writer) { - String content = request.getContentAsString(); - if (StringUtils.hasText(content)) { - writer.print(String.format(" -d '%s'", content)); - } - else if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getParameters().entrySet()) { - for (String value : entry.getValue()) { - writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); - } - } - } - else if (isPutOrPost(request)) { - writeContentUsingParameters(request, writer); - } - } - - private void writeContentUsingParameters(OperationRequest request, - PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); - String queryString = uniqueParameters.toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); - } - } - - private Parameters getUniqueParameters(OperationRequest request) { - Parameters queryStringParameters = new QueryStringParser() - .parse(request.getUri()); - Parameters uniqueParameters = new Parameters(); - - for (Entry> parameter : request.getParameters().entrySet()) { - addIfUnique(parameter, queryStringParameters, uniqueParameters); - } - return uniqueParameters; - } - - private void addIfUnique(Entry> parameter, - Parameters queryStringParameters, Parameters uniqueParameters) { - if (!queryStringParameters.containsKey(parameter.getKey())) { - uniqueParameters.put(parameter.getKey(), parameter.getValue()); - } - else { - List candidates = parameter.getValue(); - List existing = queryStringParameters.get(parameter.getKey()); - for (String candidate : candidates) { - if (!existing.contains(candidate)) { - uniqueParameters.add(parameter.getKey(), candidate); - } - } - } - } - - private boolean isPutOrPost(OperationRequest request) { - return HttpMethod.PUT.equals(request.getMethod()) - || HttpMethod.POST.equals(request.getMethod()); - } - - private interface HeaderFilter { - - boolean allow(String name, List value); - } - - private static final class BasicAuthHeaderFilter implements HeaderFilter { - - @Override - public boolean allow(String name, List value) { - if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) { - return false; - } - return true; - } - - static boolean isBasicAuthHeader(List value) { - return value != null && (!value.isEmpty()) - && value.get(0).startsWith("Basic "); - } - - static String decodeBasicAuthHeader(List value) { - return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); - } - - } - - private static final class NamedHeaderFilter implements HeaderFilter { - - private final String name; - - private NamedHeaderFilter(String name) { - this.name = name; - } - - @Override - public boolean allow(String name, List value) { - return !this.name.equalsIgnoreCase(name); - } - + protected CurlRequestSnippet(final Map attributes) { + super(attributes); } } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet new file mode 100644 index 00000000..05771238 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet @@ -0,0 +1,4 @@ +[source,bash] +---- +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet new file mode 100644 index 00000000..acddb53f --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet @@ -0,0 +1,3 @@ +```bash +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java similarity index 98% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java index 634fede1..6a953e16 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli; import java.net.URI; import java.util.Arrays; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java similarity index 99% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java index 2b137192..0d8bb632 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli.curl; import java.io.IOException; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java new file mode 100644 index 00000000..1fb2abd7 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java @@ -0,0 +1,330 @@ +/* + * Copyright 2014-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.cli.httpie; + +import java.io.IOException; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.util.Base64Utils; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link HttpieRequestSnippet}. + * + * @author Andy Wilkinson + * @author Yann Le Guern + * @author Dmitriy Mayboroda + * @author Jonathan Pearlin + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +@RunWith(Parameterized.class) +public class HttpieRequestSnippetTests extends AbstractSnippetTests { + + public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void getRequest() throws IOException { + this.snippet.expectHttpieRequest("get-request").withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document( + operationBuilder("get-request").request("http://localhost/foo").build()); + } + + @Test + public void nonGetRequest() throws IOException { + this.snippet.expectHttpieRequest("non-get-request").withContents( + codeBlock("bash").content("$ http POST 'http://localhost/foo'")); + new HttpieRequestSnippet().document(operationBuilder("non-get-request") + .request("http://localhost/foo").method("POST").build()); + } + + @Test + public void requestWithContent() throws IOException { + this.snippet.expectHttpieRequest("request-with-content") + .withContents(codeBlock("bash") + .content("$ echo 'content' | http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document(operationBuilder("request-with-content") + .request("http://localhost/foo").content("content").build()); + } + + @Test + public void getRequestWithQueryString() throws IOException { + this.snippet.expectHttpieRequest("request-with-query-string") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(operationBuilder("request-with-query-string") + .request("http://localhost/foo?param=value").build()); + } + + @Test + public void getRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param'")); + new HttpieRequestSnippet() + .document(operationBuilder("request-with-query-string-with-no-value") + .request("http://localhost/foo?param").build()); + } + + @Test + public void postRequestWithQueryString() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string") + .request("http://localhost/foo?param=value").method("POST") + .build()); + } + + @Test + public void postRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string-with-no-value") + .request("http://localhost/foo?param").method("POST").build()); + } + + @Test + public void postRequestWithOneParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-one-parameter") + .request("http://localhost/foo").method("POST").param("k1", "v1") + .build()); + } + + @Test + public void postRequestWithOneParameterWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-one-parameter-with-no-value") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1='")); + new HttpieRequestSnippet().document( + operationBuilder("post-request-with-one-parameter-with-no-value") + .request("http://localhost/foo").method("POST").param("k1") + .build()); + } + + @Test + public void postRequestWithMultipleParameters() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-multiple-parameters") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo'" + + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-multiple-parameters") + .request("http://localhost/foo").method("POST") + .param("k1", "v1", "v1-bis").param("k2", "v2").build()); + } + + @Test + public void postRequestWithUrlEncodedParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("POST").param("k1", "a&b") + .build()); + } + + @Test + public void postRequestWithQueryStringAndParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string-and-parameter") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string-and-parameter") + .request("http://localhost/foo?a=alpha").method("POST") + .param("b", "bravo").build()); + } + + @Test + public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpieRequest( + "post-request-with-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "post-request-with-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").build()); + } + + @Test + public void putRequestWithOneParameter() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-one-parameter") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .build()); + } + + @Test + public void putRequestWithMultipleParameters() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-multiple-parameters") + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo'" + + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-multiple-parameters") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); + } + + @Test + public void putRequestWithUrlEncodedParameter() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("PUT").param("k1", "a&b") + .build()); + } + + @Test + public void requestWithHeaders() throws IOException { + this.snippet.expectHttpieRequest("request-with-headers") + .withContents(codeBlock("bash").content("$ http GET 'http://localhost/foo'" + + " 'Content-Type:application/json' 'a:alpha'")); + new HttpieRequestSnippet().document( + operationBuilder("request-with-headers").request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); + } + + @Test + public void multipartPostWithNoSubmittedFileName() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'metadata'@<(echo '{\"description\": \"foo\"}')"; + this.snippet.expectHttpieRequest("multipart-post-no-original-filename") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-no-original-filename") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); + } + + @Test + public void multipartPostWithContentType() throws IOException { + // httpie does not yet support manually set content type by part + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png'"; + this.snippet.expectHttpieRequest("multipart-post-with-content-type") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-with-content-type") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); + } + + @Test + public void multipartPost() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png'"; + this.snippet.expectHttpieRequest("multipart-post") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); + } + + @Test + public void multipartPostWithParameters() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado' 'b=banana'"; + this.snippet.expectHttpieRequest("multipart-post-with-parameters") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-with-parameters") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); + } + + @Test + public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { + this.snippet.expectHttpieRequest("basic-auth").withContents(codeBlock("bash") + .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); + new HttpieRequestSnippet() + .document(operationBuilder("basic-auth").request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, + "Basic " + Base64Utils + .encodeToString("user:secret".getBytes())) + .build()); + } + + @Test + public void customAttributes() throws IOException { + this.snippet.expectHttpieRequest("custom-attributes") + .withContents(containsString("httpie request title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("httpie-request")) + .willReturn(snippetResource("httpie-request-with-title")); + new HttpieRequestSnippet( + attributes( + key("title").value("httpie request title"))) + .document( + operationBuilder("custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost/foo").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0462fb30..7a36e320 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -24,8 +24,9 @@ import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; -import org.springframework.restdocs.curl.CurlRequestSnippet; +import org.springframework.restdocs.cli.curl.CurlDocumentation; +import org.springframework.restdocs.cli.curl.CurlRequestSnippet; +import org.springframework.restdocs.cli.httpie.HttpieRequestSnippet; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; @@ -71,6 +72,7 @@ public class RestDocumentationConfigurerTests { .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class), + instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), instanceOf(HttpResponseSnippet.class))); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index d706ddf6..2799f1cc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -76,6 +76,11 @@ public class ExpectedSnippet implements TestRule { return this; } + public ExpectedSnippet expectHttpieRequest(String name) { + expect(name, "httpie-request"); + return this; + } + public ExpectedSnippet expectRequestFields(String name) { expect(name, "request-fields"); return this; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet new file mode 100644 index 00000000..0bff58b1 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet @@ -0,0 +1,5 @@ +[source,bash] +.{{title}} +---- +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet new file mode 100644 index 00000000..29beade1 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet @@ -0,0 +1,4 @@ +{{title}} +```bash +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +``` \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 162db0d1..ad06b187 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -56,7 +56,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;