Skip to content

Commit 3bddf1f

Browse files
author
Adrian Cole
committed
Supports custom expansion of template parameters via Param.Expander
Parameters annotated with `Param` expand based on their `toString`. By specifying a custom `Param.Expander`, users can control this behavior, for example formatting dates. ```java @RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date); ``` Closes OpenFeign#122
1 parent 5801ea9 commit 3bddf1f

10 files changed

Lines changed: 99 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### Version 7.1
22
* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
3+
* Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)`
34
* Adds OkHttp integration
45
* Allows multiple headers with the same name.
56
* Ensures Accept headers default to `*/*`

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ Where possible, Feign configuration uses normal Dagger conventions. For example
228228
};
229229
}
230230
```
231+
232+
#### Custom Parameter Expansion
233+
Parameters annotated with `Param` expand based on their `toString`. By
234+
specifying a custom `Param.Expander`, users can control this behavior,
235+
for example formatting dates.
236+
237+
```java
238+
@RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
239+
```
240+
231241
#### Logging
232242
You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that:
233243
```java

core/src/main/java/feign/Contract.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public interface Contract {
3838
*/
3939
List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring);
4040

41-
public static abstract class BaseContract implements Contract {
41+
abstract class BaseContract implements Contract {
4242

4343
@Override public List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring) {
4444
List<MethodMetadata> metadata = new ArrayList<MethodMetadata>();
@@ -119,7 +119,7 @@ protected void nameParam(MethodMetadata data, String name, int i) {
119119
}
120120
}
121121

122-
static class Default extends BaseContract {
122+
class Default extends BaseContract {
123123

124124
@Override
125125
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
@@ -173,6 +173,12 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[
173173
checkState(emptyToNull(name) != null,
174174
"%s annotation was empty on param %s.", annotationType.getSimpleName(), paramIndex);
175175
nameParam(data, name, paramIndex);
176+
if (annotationType == Param.class) {
177+
Class<? extends Param.Expander> expander = ((Param) annotation).expander();
178+
if (expander != Param.ToStringExpander.class) {
179+
data.indexToExpanderClass().put(paramIndex, expander);
180+
}
181+
}
176182
isHttpAnnotation = true;
177183
String varName = '{' + name + '}';
178184
if (data.template().url().indexOf(varName) == -1 &&

core/src/main/java/feign/MethodMetadata.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package feign;
1717

18+
import feign.Param.Expander;
1819
import java.io.Serializable;
1920
import java.lang.reflect.Type;
2021
import java.util.ArrayList;
@@ -36,6 +37,8 @@ public final class MethodMetadata implements Serializable {
3637
private RequestTemplate template = new RequestTemplate();
3738
private List<String> formParams = new ArrayList<String>();
3839
private Map<Integer, Collection<String>> indexToName = new LinkedHashMap<Integer, Collection<String>>();
40+
private Map<Integer, Class<? extends Expander>> indexToExpanderClass =
41+
new LinkedHashMap<Integer, Class<? extends Expander>>();
3942

4043
/**
4144
* @see Feign#configKey(java.lang.reflect.Method)
@@ -49,9 +52,6 @@ MethodMetadata configKey(String configKey) {
4952
return this;
5053
}
5154

52-
/**
53-
* Method return type.
54-
*/
5555
public Type returnType() {
5656
return returnType;
5757
}
@@ -100,6 +100,9 @@ public Map<Integer, Collection<String>> indexToName() {
100100
return indexToName;
101101
}
102102

103-
private static final long serialVersionUID = 1L;
103+
public Map<Integer, Class<? extends Expander>> indexToExpanderClass() {
104+
return indexToExpanderClass;
105+
}
104106

107+
private static final long serialVersionUID = 1L;
105108
}

core/src/main/java/feign/Param.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,24 @@
2020
import static java.lang.annotation.ElementType.PARAMETER;
2121
import static java.lang.annotation.RetentionPolicy.RUNTIME;
2222

23-
/** The name of a template variable applied to {@link Headers}, {@linkplain RequestLine} or {@linkplain Body} */
23+
/** A named template parameter applied to {@link Headers}, {@linkplain RequestLine} or {@linkplain Body} */
2424
@Retention(RUNTIME)
2525
@java.lang.annotation.Target(PARAMETER)
2626
public @interface Param {
27+
/** The name of the template parameter. */
2728
String value();
29+
30+
/** How to expand the value of this parameter, if {@link ToStringExpander} isn't adequate. */
31+
Class<? extends Expander> expander() default ToStringExpander.class;
32+
33+
interface Expander {
34+
/** Expands the value into a string. Does not accept or return null. */
35+
String expand(Object value);
36+
}
37+
38+
final class ToStringExpander implements Expander {
39+
@Override public String expand(Object value) {
40+
return value.toString();
41+
}
42+
}
2843
}

core/src/main/java/feign/ReflectiveFeign.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import dagger.Provides;
1919
import feign.InvocationHandlerFactory.MethodHandler;
20+
import feign.Param.Expander;
2021
import feign.Request.Options;
2122
import feign.codec.Decoder;
2223
import feign.codec.EncodeException;
@@ -158,9 +159,20 @@ public Map<String, MethodHandler> apply(Target key) {
158159

159160
private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
160161
protected final MethodMetadata metadata;
162+
private final Map<Integer, Expander> indexToExpander = new LinkedHashMap<Integer, Expander>();
161163

162164
private BuildTemplateByResolvingArgs(MethodMetadata metadata) {
163165
this.metadata = metadata;
166+
if (metadata.indexToExpanderClass().isEmpty()) return;
167+
for (Entry<Integer, Class<? extends Expander>> indexToExpanderClass : metadata.indexToExpanderClass().entrySet()) {
168+
try {
169+
indexToExpander.put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance());
170+
} catch (InstantiationException e) {
171+
throw new IllegalStateException(e);
172+
} catch (IllegalAccessException e) {
173+
throw new IllegalStateException(e);
174+
}
175+
}
164176
}
165177

166178
@Override public RequestTemplate create(Object[] argv) {
@@ -172,8 +184,12 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata) {
172184
}
173185
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
174186
for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
187+
int i = entry.getKey();
175188
Object value = argv[entry.getKey()];
176189
if (value != null) { // Null values are skipped.
190+
if (indexToExpander.containsKey(i)) {
191+
value = indexToExpander.get(i).expand(value);
192+
}
177193
for (String name : entry.getValue())
178194
varBuilder.put(name, value);
179195
}

core/src/main/java/feign/RequestTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public RequestTemplate(RequestTemplate toCopy) {
8080
}
8181

8282
/**
83-
* Resolves any templated variables in the requests path, query, or headers
83+
* Resolves any template parameters in the requests path, query, or headers
8484
* against the supplied unencoded arguments.
8585
* <br>
8686
* <br><br><b>relationship to JAXRS 2.0</b><br>

core/src/main/java/feign/codec/Encoder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public interface Encoder {
7171
/**
7272
* Default implementation of {@code Encoder}.
7373
*/
74-
public class Default implements Encoder {
74+
class Default implements Encoder {
7575
@Override
7676
public void encode(Object object, RequestTemplate template) throws EncodeException {
7777
if (object instanceof String) {

core/src/test/java/feign/DefaultContractTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.google.gson.reflect.TypeToken;
1919
import java.net.URI;
20+
import java.util.Date;
2021
import java.util.List;
2122
import javax.inject.Named;
2223
import org.junit.Rule;
@@ -247,6 +248,23 @@ interface HeaderParams {
247248
.containsExactly(entry(0, asList("Auth-Token")));
248249
}
249250

251+
interface CustomExpander {
252+
@RequestLine("POST /?date={date}") void date(@Param(value = "date", expander = DateToMillis.class) Date date);
253+
}
254+
255+
class DateToMillis implements Param.Expander {
256+
@Override public String expand(Object value) {
257+
return String.valueOf(((Date) value).getTime());
258+
}
259+
}
260+
261+
@Test public void customExpander() throws Exception {
262+
MethodMetadata md = contract.parseAndValidatateMetadata(CustomExpander.class.getDeclaredMethod("date", Date.class));
263+
264+
assertThat(md.indexToExpanderClass())
265+
.containsExactly(entry(0, DateToMillis.class));
266+
}
267+
250268
// TODO: remove all of below in 8.x
251269

252270
interface WithPathAndQueryParamsAnnotatedWithNamed {

core/src/test/java/feign/FeignTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.lang.reflect.Type;
3131
import java.net.URI;
3232
import java.util.Arrays;
33+
import java.util.Date;
3334
import java.util.List;
3435
import java.util.Map;
3536
import javax.inject.Singleton;
@@ -70,6 +71,14 @@ void login(
7071

7172
@RequestLine("GET /?1={1}&2={2}") Response queryParams(@Param("1") String one, @Param("2") Iterable<String> twos);
7273

74+
@RequestLine("POST /?date={date}") void expand(@Param(value = "date", expander = DateToMillis.class) Date date);
75+
76+
class DateToMillis implements Param.Expander {
77+
@Override public String expand(Object value) {
78+
return String.valueOf(((Date) value).getTime());
79+
}
80+
}
81+
7382
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
7483
static class Module {
7584
@Provides Decoder defaultDecoder() {
@@ -224,6 +233,18 @@ public void multipleInterceptor() throws IOException, InterruptedException {
224233
.hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign");
225234
}
226235

236+
@Test public void customExpander() throws Exception {
237+
server.enqueue(new MockResponse());
238+
239+
TestInterface api =
240+
Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
241+
242+
api.expand(new Date(1234l));
243+
244+
assertThat(server.takeRequest())
245+
.hasPath("/?date=1234");
246+
}
247+
227248
@Test public void toKeyMethodFormatsAsExpected() throws Exception {
228249
assertEquals("TestInterface#post()", Feign.configKey(TestInterface.class.getDeclaredMethod("post")));
229250
assertEquals("TestInterface#uriParam(String,URI,String)",

0 commit comments

Comments
 (0)