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;