From 5c3fe23e9f558f3337676125eb5a17532315d397 Mon Sep 17 00:00:00 2001 From: Benjamin Douglas Date: Sat, 1 Apr 2017 00:33:20 -0700 Subject: [PATCH] Support Swagger collectionFormat encodings in Feign (#5266) * Support Swagger collectionFormat encodings in Feign Feign only natively supports the "multi" collectionFormat for encoding lists of parameter values. This change adds manual encoding of the other formats, such as "csv" (the default for collections), "tsv", space-separated, and pipes. * Fix typo in anchor tag. --- .../codegen/languages/JavaClientCodegen.java | 1 + .../libraries/feign/EncodingUtils.mustache | 86 +++++++++++++++++++ .../Java/libraries/feign/api.mustache | 10 ++- .../Java/libraries/feign/pom.mustache | 12 +++ samples/client/petstore/java/feign/pom.xml | 12 +++ .../java/io/swagger/client/EncodingUtils.java | 86 +++++++++++++++++++ .../java/io/swagger/client/api/FakeApi.java | 9 +- .../java/io/swagger/client/api/PetApi.java | 9 +- .../java/io/swagger/client/api/StoreApi.java | 1 + .../java/io/swagger/client/api/UserApi.java | 7 +- .../io/swagger/client/api/PetApiTest.java | 22 +++++ 11 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 modules/swagger-codegen/src/main/resources/Java/libraries/feign/EncodingUtils.mustache create mode 100644 samples/client/petstore/java/feign/src/main/java/io/swagger/client/EncodingUtils.java diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavaClientCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavaClientCodegen.java index 3b8cea940a1..acfd7d78469 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavaClientCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavaClientCodegen.java @@ -176,6 +176,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen if ("feign".equals(getLibrary())) { additionalProperties.put("jackson", "true"); supportingFiles.add(new SupportingFile("ParamExpander.mustache", invokerFolder, "ParamExpander.java")); + supportingFiles.add(new SupportingFile("EncodingUtils.mustache", invokerFolder, "EncodingUtils.java")); } else if ("okhttp-gson".equals(getLibrary()) || StringUtils.isEmpty(getLibrary())) { // the "okhttp-gson" library template requires "ApiCallback.mustache" for async call supportingFiles.add(new SupportingFile("ApiCallback.mustache", invokerFolder, "ApiCallback.java")); diff --git a/modules/swagger-codegen/src/main/resources/Java/libraries/feign/EncodingUtils.mustache b/modules/swagger-codegen/src/main/resources/Java/libraries/feign/EncodingUtils.mustache new file mode 100644 index 00000000000..6f43e1c3c2a --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/Java/libraries/feign/EncodingUtils.mustache @@ -0,0 +1,86 @@ +package {{invokerPackage}}; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** +* Utilities to support Swagger encoding formats in Feign. +*/ +public final class EncodingUtils { + + /** + * Private constructor. Do not construct this class. + */ + private EncodingUtils() {} + + /** + *

Encodes a collection of query parameters according to the Swagger + * collection format.

+ * + *

Of the various collection formats defined by Swagger ("csv", "tsv", + * etc), Feign only natively supports "multi". This utility generates the + * other format types so it will be properly processed by Feign.

+ * + *

Note, as part of reformatting, it URL encodes the parameters as + * well.

+ * @param parameters The collection object to be formatted. This object will + * not be changed. + * @param collectionFormat The Swagger collection format (eg, "csv", "tsv", + * "pipes"). See the + * + * Swagger Spec for more details. + * @return An object that will be correctly formatted by Feign. + */ + public static Object encodeCollection(Collection parameters, + String collectionFormat) { + if (parameters == null) { + return parameters; + } + List stringValues = new ArrayList<>(parameters.size()); + for (Object parameter : parameters) { + // ignore null values (same behavior as Feign) + if (parameter != null) { + stringValues.add(encode(parameter)); + } + } + // Feign natively handles single-element lists and the "multi" format. + if (stringValues.size() < 2 || "multi".equals(collectionFormat)) { + return stringValues; + } + // Otherwise return a formatted String + String[] stringArray = stringValues.toArray(new String[0]); + switch (collectionFormat) { + case "csv": + default: + return StringUtil.join(stringArray, ","); + case "ssv": + return StringUtil.join(stringArray, " "); + case "tsv": + return StringUtil.join(stringArray, "\t"); + case "pipes": + return StringUtil.join(stringArray, "|"); + } + } + + /** + * URL encode a single query parameter. + * @param parameter The query parameter to encode. This object will not be + * changed. + * @return The URL encoded string representation of the parameter. If the + * parameter is null, returns null. + */ + public static String encode(Object parameter) { + if (parameter == null) { + return null; + } + try { + return URLEncoder.encode(parameter.toString(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Should never happen, UTF-8 is always supported + throw new RuntimeException(e); + } + } +} diff --git a/modules/swagger-codegen/src/main/resources/Java/libraries/feign/api.mustache b/modules/swagger-codegen/src/main/resources/Java/libraries/feign/api.mustache index 6aadf2305d1..67a772a8772 100644 --- a/modules/swagger-codegen/src/main/resources/Java/libraries/feign/api.mustache +++ b/modules/swagger-codegen/src/main/resources/Java/libraries/feign/api.mustache @@ -1,6 +1,7 @@ package {{package}}; import {{invokerPackage}}.ApiClient; +import {{invokerPackage}}.EncodingUtils; {{#legacyDates}} import {{invokerPackage}}.ParamExpander; {{/legacyDates}} @@ -71,7 +72,7 @@ public interface {{classname}} extends ApiClient.Api { "{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{#hasMore}}, {{/hasMore}}{{/headerParams}} }) - {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isBodyParam}}{{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap Map queryParams); + {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isBodyParam}}{{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap(encoded=true) Map queryParams); /** * A convenience class for generating query parameters for the @@ -80,7 +81,12 @@ public interface {{classname}} extends ApiClient.Api { public static class {{operationIdCamelCase}}QueryParams extends HashMap { {{#queryParams}} public {{operationIdCamelCase}}QueryParams {{paramName}}(final {{{dataType}}} value) { - put("{{baseName}}", value); + {{#collectionFormat}} + put("{{baseName}}", EncodingUtils.encodeCollection(value, "{{collectionFormat}}")); + {{/collectionFormat}} + {{^collectionFormat}} + put("{{baseName}}", EncodingUtils.encode(value)); + {{/collectionFormat}} return this; } {{/queryParams}} diff --git a/modules/swagger-codegen/src/main/resources/Java/libraries/feign/pom.mustache b/modules/swagger-codegen/src/main/resources/Java/libraries/feign/pom.mustache index da5eb7f93ab..7c05b94ca3e 100644 --- a/modules/swagger-codegen/src/main/resources/Java/libraries/feign/pom.mustache +++ b/modules/swagger-codegen/src/main/resources/Java/libraries/feign/pom.mustache @@ -230,6 +230,18 @@ ${junit-version} test + + com.squareup.okhttp3 + mockwebserver + 3.6.0 + test + + + org.assertj + assertj-core + 1.7.1 + test + {{#java8}}1.8{{/java8}}{{^java8}}1.7{{/java8}} diff --git a/samples/client/petstore/java/feign/pom.xml b/samples/client/petstore/java/feign/pom.xml index 37d2b96f4fa..afc858fcab8 100644 --- a/samples/client/petstore/java/feign/pom.xml +++ b/samples/client/petstore/java/feign/pom.xml @@ -230,6 +230,18 @@ ${junit-version} test + + com.squareup.okhttp3 + mockwebserver + 3.6.0 + test + + + org.assertj + assertj-core + 1.7.1 + test + 1.7 diff --git a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/EncodingUtils.java b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/EncodingUtils.java new file mode 100644 index 00000000000..c474fc62187 --- /dev/null +++ b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/EncodingUtils.java @@ -0,0 +1,86 @@ +package io.swagger.client; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** +* Utilities to support Swagger encoding formats in Feign. +*/ +public final class EncodingUtils { + + /** + * Private constructor. Do not construct this class. + */ + private EncodingUtils() {} + + /** + *

Encodes a collection of query parameters according to the Swagger + * collection format.

+ * + *

Of the various collection formats defined by Swagger ("csv", "tsv", + * etc), Feign only natively supports "multi". This utility generates the + * other format types so it will be properly processed by Feign.

+ * + *

Note, as part of reformatting, it URL encodes the parameters as + * well.

+ * @param parameters The collection object to be formatted. This object will + * not be changed. + * @param collectionFormat The Swagger collection format (eg, "csv", "tsv", + * "pipes"). See the + * + * Swagger Spec for more details. + * @return An object that will be correctly formatted by Feign. + */ + public static Object encodeCollection(Collection parameters, + String collectionFormat) { + if (parameters == null) { + return parameters; + } + List stringValues = new ArrayList<>(parameters.size()); + for (Object parameter : parameters) { + // ignore null values (same behavior as Feign) + if (parameter != null) { + stringValues.add(encode(parameter)); + } + } + // Feign natively handles single-element lists and the "multi" format. + if (stringValues.size() < 2 || "multi".equals(collectionFormat)) { + return stringValues; + } + // Otherwise return a formatted String + String[] stringArray = stringValues.toArray(new String[0]); + switch (collectionFormat) { + case "csv": + default: + return StringUtil.join(stringArray, ","); + case "ssv": + return StringUtil.join(stringArray, " "); + case "tsv": + return StringUtil.join(stringArray, "\t"); + case "pipes": + return StringUtil.join(stringArray, "|"); + } + } + + /** + * URL encode a single query parameter. + * @param parameter The query parameter to encode. This object will not be + * changed. + * @return The URL encoded string representation of the parameter. If the + * parameter is null, returns null. + */ + public static String encode(Object parameter) { + if (parameter == null) { + return null; + } + try { + return URLEncoder.encode(parameter.toString(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Should never happen, UTF-8 is always supported + throw new RuntimeException(e); + } + } +} diff --git a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/FakeApi.java b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/FakeApi.java index ab0b9b2bd4f..77c7ae592b2 100644 --- a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/FakeApi.java +++ b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/FakeApi.java @@ -1,6 +1,7 @@ package io.swagger.client.api; import io.swagger.client.ApiClient; +import io.swagger.client.EncodingUtils; import java.math.BigDecimal; import io.swagger.client.model.Client; @@ -106,7 +107,7 @@ public interface FakeApi extends ApiClient.Api { "enum_header_string: {enumHeaderString}" }) - void testEnumParameters(@Param("enumFormStringArray") List enumFormStringArray, @Param("enumFormString") String enumFormString, @Param("enumHeaderStringArray") List enumHeaderStringArray, @Param("enumHeaderString") String enumHeaderString, @Param("enumQueryDouble") Double enumQueryDouble, @QueryMap Map queryParams); + void testEnumParameters(@Param("enumFormStringArray") List enumFormStringArray, @Param("enumFormString") String enumFormString, @Param("enumHeaderStringArray") List enumHeaderStringArray, @Param("enumHeaderString") String enumHeaderString, @Param("enumQueryDouble") Double enumQueryDouble, @QueryMap(encoded=true) Map queryParams); /** * A convenience class for generating query parameters for the @@ -114,15 +115,15 @@ public interface FakeApi extends ApiClient.Api { */ public static class TestEnumParametersQueryParams extends HashMap { public TestEnumParametersQueryParams enumQueryStringArray(final List value) { - put("enum_query_string_array", value); + put("enum_query_string_array", EncodingUtils.encodeCollection(value, "csv")); return this; } public TestEnumParametersQueryParams enumQueryString(final String value) { - put("enum_query_string", value); + put("enum_query_string", EncodingUtils.encode(value)); return this; } public TestEnumParametersQueryParams enumQueryInteger(final Integer value) { - put("enum_query_integer", value); + put("enum_query_integer", EncodingUtils.encode(value)); return this; } } diff --git a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/PetApi.java b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/PetApi.java index 48987c67f38..5a6813c6198 100644 --- a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/PetApi.java +++ b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/PetApi.java @@ -1,6 +1,7 @@ package io.swagger.client.api; import io.swagger.client.ApiClient; +import io.swagger.client.EncodingUtils; import java.io.File; import io.swagger.client.model.ModelApiResponse; @@ -75,7 +76,7 @@ public interface PetApi extends ApiClient.Api { "Content-Type: application/json", "Accept: application/json", }) - List findPetsByStatus(@QueryMap Map queryParams); + List findPetsByStatus(@QueryMap(encoded=true) Map queryParams); /** * A convenience class for generating query parameters for the @@ -83,7 +84,7 @@ public interface PetApi extends ApiClient.Api { */ public static class FindPetsByStatusQueryParams extends HashMap { public FindPetsByStatusQueryParams status(final List value) { - put("status", value); + put("status", EncodingUtils.encodeCollection(value, "csv")); return this; } } @@ -121,7 +122,7 @@ public interface PetApi extends ApiClient.Api { "Content-Type: application/json", "Accept: application/json", }) - List findPetsByTags(@QueryMap Map queryParams); + List findPetsByTags(@QueryMap(encoded=true) Map queryParams); /** * A convenience class for generating query parameters for the @@ -129,7 +130,7 @@ public interface PetApi extends ApiClient.Api { */ public static class FindPetsByTagsQueryParams extends HashMap { public FindPetsByTagsQueryParams tags(final List value) { - put("tags", value); + put("tags", EncodingUtils.encodeCollection(value, "csv")); return this; } } diff --git a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/StoreApi.java b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/StoreApi.java index 9e1ecddbece..1ad8111d3d6 100644 --- a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/StoreApi.java +++ b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/StoreApi.java @@ -1,6 +1,7 @@ package io.swagger.client.api; import io.swagger.client.ApiClient; +import io.swagger.client.EncodingUtils; import io.swagger.client.model.Order; diff --git a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/UserApi.java b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/UserApi.java index c271b9deda8..e85fca41f8b 100644 --- a/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/UserApi.java +++ b/samples/client/petstore/java/feign/src/main/java/io/swagger/client/api/UserApi.java @@ -1,6 +1,7 @@ package io.swagger.client.api; import io.swagger.client.ApiClient; +import io.swagger.client.EncodingUtils; import io.swagger.client.model.User; @@ -110,7 +111,7 @@ public interface UserApi extends ApiClient.Api { "Content-Type: application/json", "Accept: application/json", }) - String loginUser(@QueryMap Map queryParams); + String loginUser(@QueryMap(encoded=true) Map queryParams); /** * A convenience class for generating query parameters for the @@ -118,11 +119,11 @@ public interface UserApi extends ApiClient.Api { */ public static class LoginUserQueryParams extends HashMap { public LoginUserQueryParams username(final String value) { - put("username", value); + put("username", EncodingUtils.encode(value)); return this; } public LoginUserQueryParams password(final String value) { - put("password", value); + put("password", EncodingUtils.encode(value)); return this; } } diff --git a/samples/client/petstore/java/feign/src/test/java/io/swagger/client/api/PetApiTest.java b/samples/client/petstore/java/feign/src/test/java/io/swagger/client/api/PetApiTest.java index b144cde9ff0..8bf44c41cb5 100644 --- a/samples/client/petstore/java/feign/src/test/java/io/swagger/client/api/PetApiTest.java +++ b/samples/client/petstore/java/feign/src/test/java/io/swagger/client/api/PetApiTest.java @@ -13,17 +13,25 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; import org.junit.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; public class PetApiTest { ApiClient apiClient; PetApi api; + MockWebServer localServer; + ApiClient localClient; @Before public void setup() { apiClient = new ApiClient(); api = apiClient.buildClient(PetApi.class); + localServer = new MockWebServer(); + localClient = new ApiClient(); } @Test @@ -211,6 +219,20 @@ public class PetApiTest { assertTrue(pet1.hashCode() == pet1.hashCode()); } + @Test + public void testCSVDelimitedArray() throws Exception { + localServer.enqueue(new MockResponse().setBody("[{\"id\":5,\"name\":\"rocky\"}]")); + localServer.start(); + PetApi api = localClient.setBasePath(localServer.url("/").toString()).buildClient(PetApi.class); + PetApi.FindPetsByTagsQueryParams queryParams = new PetApi.FindPetsByTagsQueryParams() + .tags(Arrays.asList("friendly","energetic")); + List pets = api.findPetsByTags(queryParams); + assertNotNull(pets); + RecordedRequest request = localServer.takeRequest(); + assertThat(request.getPath()).contains("tags=friendly,energetic"); + localServer.shutdown(); + } + private Pet createRandomPet() { Pet pet = new Pet(); pet.setId(TestUtils.nextId());