Skip to content

Commit

Permalink
Add support for query parameters with no value.
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton committed Jan 30, 2017
1 parent 77a65b7 commit e4acf7b
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 27 deletions.
15 changes: 15 additions & 0 deletions retrofit/src/main/java/retrofit2/ParameterHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ static final class Query<T> extends ParameterHandler<T> {
}
}

static final class QueryName<T> extends ParameterHandler<T> {
private final Converter<T, String> nameConverter;
private final boolean encoded;

QueryName(Converter<T, String> nameConverter, boolean encoded) {
this.nameConverter = nameConverter;
this.encoded = encoded;
}

@Override void apply(RequestBuilder builder, T value) throws IOException {
if (value == null) return; // Skip null values.
builder.addQueryParam(nameConverter.convert(value), null, encoded);
}
}

static final class QueryMap<T> extends ParameterHandler<Map<String, T>> {
private final Converter<T, String> valueConverter;
private final boolean encoded;
Expand Down
30 changes: 30 additions & 0 deletions retrofit/src/main/java/retrofit2/ServiceMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import retrofit2.http.Path;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
import retrofit2.http.QueryName;
import retrofit2.http.Url;

/** Adapts an invocation of an interface method into an HTTP call. */
Expand Down Expand Up @@ -429,6 +430,35 @@ private ParameterHandler<?> parseParameterAnnotation(
return new ParameterHandler.Query<>(name, converter, encoded);
}

} else if (annotation instanceof QueryName) {
QueryName query = (QueryName) annotation;
boolean encoded = query.encoded();

Class<?> rawParameterType = Utils.getRawType(type);
gotQuery = true;
if (Iterable.class.isAssignableFrom(rawParameterType)) {
if (!(type instanceof ParameterizedType)) {
throw parameterError(p, rawParameterType.getSimpleName()
+ " must include generic type (e.g., "
+ rawParameterType.getSimpleName()
+ "<String>)");
}
ParameterizedType parameterizedType = (ParameterizedType) type;
Type iterableType = Utils.getParameterUpperBound(0, parameterizedType);
Converter<?, String> converter =
retrofit.stringConverter(iterableType, annotations);
return new ParameterHandler.QueryName<>(converter, encoded).iterable();
} else if (rawParameterType.isArray()) {
Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
Converter<?, String> converter =
retrofit.stringConverter(arrayComponentType, annotations);
return new ParameterHandler.QueryName<>(converter, encoded).array();
} else {
Converter<?, String> converter =
retrofit.stringConverter(type, annotations);
return new ParameterHandler.QueryName<>(converter, encoded);
}

} else if (annotation instanceof QueryMap) {
Class<?> rawParameterType = Utils.getRawType(type);
if (!Map.class.isAssignableFrom(rawParameterType)) {
Expand Down
27 changes: 14 additions & 13 deletions retrofit/src/main/java/retrofit2/http/Query.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,36 @@
* <p>
* Simple Example:
* <pre><code>
* &#64;GET("/list")
* Call&lt;ResponseBody&gt; list(@Query("page") int page);
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@Query("page") int page);
* </code></pre>
* Calling with {@code foo.list(1)} yields {@code /list?page=1}.
* Calling with {@code foo.friends(1)} yields {@code /friends?page=1}.
* <p>
* Example with {@code null}:
* <pre><code>
* &#64;GET("/list")
* Call&lt;ResponseBody&gt; list(@Query("category") String category);
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@Query("group") String group);
* </code></pre>
* Calling with {@code foo.list(null)} yields {@code /list}.
* Calling with {@code foo.friends(null)} yields {@code /friends}.
* <p>
* Array/Varargs Example:
* <pre><code>
* &#64;GET("/list")
* Call&lt;ResponseBody&gt; list(@Query("category") String... categories);
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@Query("group") String... groups);
* </code></pre>
* Calling with {@code foo.list("bar", "baz")} yields
* {@code /list?category=bar&category=baz}.
* Calling with {@code foo.friends("coworker", "bowling")} yields
* {@code /friends?group=coworker&group=bowling}.
* <p>
* Parameter names and values are URL encoded by default. Specify {@link #encoded() encoded=true}
* to change this behavior.
* <pre><code>
* &#64;GET("/search")
* Call&lt;ResponseBody&gt; list(@Query(value="foo", encoded=true) String foo);
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@Query(value="group", encoded=true) String group);
* </code></pre>
* Calling with {@code foo.list("foo+bar"))} yields {@code /search?foo=foo+bar}.
* Calling with {@code foo.friends("foo+bar"))} yields {@code /friends?group=foo+bar}.
*
* @see QueryMap
* @see QueryName
*/
@Documented
@Target(PARAMETER)
Expand Down
17 changes: 9 additions & 8 deletions retrofit/src/main/java/retrofit2/http/QueryMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,25 @@
* <p>
* Simple Example:
* <pre><code>
* &#64;GET("/search")
* Call&lt;ResponseBody&gt; list(@QueryMap Map&lt;String, String&gt; filters);
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@QueryMap Map&lt;String, String&gt; filters);
* </code></pre>
* Calling with {@code foo.list(ImmutableMap.of("foo", "bar", "kit", "kat"))} yields
* {@code /search?foo=bar&kit=kat}.
* Calling with {@code foo.friends(ImmutableMap.of("group", "coworker", "age", "42"))} yields
* {@code /friends?group=coworker&age=42}.
* <p>
* Map keys and values representing parameter values are URL encoded by default. Specify
* {@link #encoded() encoded=true} to change this behavior.
* <pre><code>
* &#64;GET("/search")
* Call&lt;ResponseBody&gt; list(@QueryMap(encoded=true) Map&lt;String, String&gt; filters);
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@QueryMap(encoded=true) Map&lt;String, String&gt; filters);
* </code></pre>
* Calling with {@code foo.list(ImmutableMap.of("foo", "foo+bar"))} yields
* {@code /search?foo=foo+bar}.
* Calling with {@code foo.list(ImmutableMap.of("group", "coworker+bowling"))} yields
* {@code /search?group=coworker+bowling}.
* <p>
* A {@code null} value for the map, as a key, or as a value is not allowed.
*
* @see Query
* @see QueryName
*/
@Documented
@Target(PARAMETER)
Expand Down
65 changes: 65 additions & 0 deletions retrofit/src/main/java/retrofit2/http/QueryName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* 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 retrofit2.http;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Query parameter appended to the URL that has no value.
* <p>
* Passing a {@link java.util.List List} or array will result in a query parameter for each
* non-{@code null} item.
* <p>
* Simple Example:
* <pre><code>
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@QueryName String filter);
* </code></pre>
* Calling with {@code foo.friends("contains(Bob)")} yields {@code /friends?contains(Bob)}.
* <p>
* Array/Varargs Example:
* <pre><code>
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@QueryName String... filters);
* </code></pre>
* Calling with {@code foo.friends("contains(Bob)", "age(42)")} yields
* {@code /friends?contains(Bob)&age(42)}.
* <p>
* Parameter names are URL encoded by default. Specify {@link #encoded() encoded=true} to change
* this behavior.
* <pre><code>
* &#64;GET("/friends")
* Call&lt;ResponseBody&gt; friends(@QueryName(encoded=true) String filter);
* </code></pre>
* Calling with {@code foo.friends("name+age"))} yields {@code /friends?name+age}.
*
* @see Query
* @see QueryMap
*/
@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface QueryName {
/**
* Specifies whether the parameter is already URL encoded.
*/
boolean encoded() default false;
}
100 changes: 94 additions & 6 deletions retrofit/src/test/java/retrofit2/RequestBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import retrofit2.http.Path;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
import retrofit2.http.QueryName;
import retrofit2.http.Url;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -1059,11 +1060,11 @@ Call<ResponseBody> method(@Query("key") List<Object> keys) {
}
}

List<Object> values = Arrays.<Object>asList(1, 2, null, "three");
List<Object> values = Arrays.<Object>asList(1, 2, null, "three", "1");
Request request = buildRequest(Example.class, values);
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?key=1&key=2&key=three");
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?key=1&key=2&key=three&key=1");
assertThat(request.body()).isNull();
}

Expand All @@ -1075,11 +1076,11 @@ Call<ResponseBody> method(@Query("key") Object[] keys) {
}
}

Object[] values = { 1, 2, null, "three" };
Object[] values = { 1, 2, null, "three", "1" };
Request request = buildRequest(Example.class, new Object[] { values });
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?key=1&key=2&key=three");
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?key=1&key=2&key=three&key=1");
assertThat(request.body()).isNull();
}

Expand All @@ -1091,11 +1092,98 @@ Call<ResponseBody> method(@Query("key") int[] keys) {
}
}

int[] values = { 1, 2, 3 };
int[] values = { 1, 2, 3, 1 };
Request request = buildRequest(Example.class, new Object[] { values });
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?key=1&key=2&key=3&key=1");
assertThat(request.body()).isNull();
}

@Test public void getWithQueryNameParam() {
class Example {
@GET("/foo/bar/") //
Call<ResponseBody> method(@QueryName String ping) {
return null;
}
}
Request request = buildRequest(Example.class, "pong");
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?pong");
assertThat(request.body()).isNull();
}

@Test public void getWithEncodedQueryNameParam() {
class Example {
@GET("/foo/bar/") //
Call<ResponseBody> method(@QueryName(encoded = true) String ping) {
return null;
}
}
Request request = buildRequest(Example.class, "p%20o%20n%20g");
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?p%20o%20n%20g");
assertThat(request.body()).isNull();
}

@Test public void queryNameParamOptionalOmitsQuery() {
class Example {
@GET("/foo/bar/") //
Call<ResponseBody> method(@QueryName String ping) {
return null;
}
}
Request request = buildRequest(Example.class, new Object[] { null });
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/");
}

@Test public void getWithQueryNameParamList() {
class Example {
@GET("/foo/bar/") //
Call<ResponseBody> method(@QueryName List<Object> keys) {
return null;
}
}

List<Object> values = Arrays.<Object>asList(1, 2, null, "three", "1");
Request request = buildRequest(Example.class, values);
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?1&2&three&1");
assertThat(request.body()).isNull();
}

@Test public void getWithQueryNameParamArray() {
class Example {
@GET("/foo/bar/") //
Call<ResponseBody> method(@QueryName Object[] keys) {
return null;
}
}

Object[] values = { 1, 2, null, "three", "1" };
Request request = buildRequest(Example.class, new Object[] { values });
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?1&2&three&1");
assertThat(request.body()).isNull();
}

@Test public void getWithQueryNameParamPrimitiveArray() {
class Example {
@GET("/foo/bar/") //
Call<ResponseBody> method(@QueryName int[] keys) {
return null;
}
}

int[] values = { 1, 2, 3, 1 };
Request request = buildRequest(Example.class, new Object[] { values });
assertThat(request.method()).isEqualTo("GET");
assertThat(request.headers().size()).isZero();
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?key=1&key=2&key=3");
assertThat(request.url().toString()).isEqualTo("http://example.com/foo/bar/?1&2&3&1");
assertThat(request.body()).isNull();
}

Expand Down

0 comments on commit e4acf7b

Please sign in to comment.