Skip to content

Commit 0679979

Browse files
author
Adrian Cole
committed
Introduces feign.@param to annotate template parameters
Feign 8.x will no longer support Dagger, nor interfaces annotated with `javax.inject.@Named`. Users must migrate from `javax.inject.@Named` to `feign.@Param` via Feign v7.1+ before attempting to update to Feign 8.0. For example, the following uses `@Param` as opposed to `@Named` to annotate template parameters. ```java interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@param("owner") String owner, @param("repo") String repo); } ```
1 parent 252559a commit 0679979

File tree

20 files changed

+207
-64
lines changed

20 files changed

+207
-64
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### Version 7.1
2+
* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
3+
14
### Version 7.0
25
* Expose reflective dispatch hook: InvocationHandlerFactory
36
* Add JAXB integration

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Usage typically looks like this, an adaptation of the [canonical Retrofit sample
1616
```java
1717
interface GitHub {
1818
@RequestLine("GET /repos/{owner}/{repo}/contributors")
19-
List<Contributor> contributors(@Named("owner") String owner, @Named("repo") String repo);
19+
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
2020
}
2121

2222
static class Contributor {
@@ -44,7 +44,7 @@ Feign has several aspects that can be customized. For simple cases, you can use
4444
```java
4545
interface Bank {
4646
@RequestLine("POST /account/{id}")
47-
Account getAccountInfo(@Named("id") String id);
47+
Account getAccountInfo(@Param("id") String id);
4848
}
4949
...
5050
Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com");

core/src/main/java/feign/Body.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* <br>
1616
* <pre>
1717
* &#064;Body(&quot;&lt;v01:getResourceRecordsOfZone&gt;&lt;zoneName&gt;{zoneName}&lt;/zoneName&gt;&lt;rrType&gt;0&lt;/rrType&gt;&lt;/v01:getResourceRecordsOfZone&gt;&quot;)
18-
* List&lt;Record&gt; listByZone(&#64;Named(&quot;zoneName&quot;) String zoneName);
18+
* List&lt;Record&gt; listByZone(&#64;Param(&quot;zoneName&quot;) String zoneName);
1919
* </pre>
2020
* <br>
2121
* Note that if you'd like curly braces literally in the body, urlencode

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,12 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA
159159
@Override
160160
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
161161
boolean isHttpAnnotation = false;
162-
for (Annotation parameterAnnotation : annotations) {
163-
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
164-
if (annotationType == Named.class) {
165-
String name = Named.class.cast(parameterAnnotation).value();
166-
checkState(emptyToNull(name) != null, "Named annotation was empty on param %s.", paramIndex);
162+
for (Annotation annotation : annotations) {
163+
Class<? extends Annotation> annotationType = annotation.annotationType();
164+
if (annotationType == Param.class || annotationType == Named.class) {
165+
String name = annotationType == Param.class ? ((Param) annotation).value() : ((Named) annotation).value();
166+
checkState(emptyToNull(name) != null,
167+
"%s annotation was empty on param %s.", annotationType.getSimpleName(), paramIndex);
167168
nameParam(data, name, paramIndex);
168169
isHttpAnnotation = true;
169170
String varName = '{' + name + '}';

core/src/main/java/feign/Headers.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* &#64;Headers({
1919
* "X-Foo: Bar",
2020
* "X-Ping: {token}"
21-
* }) void post(&#64;Named("token") String token);
21+
* }) void post(&#64;Param("token") String token);
2222
* ...
2323
* </pre>
2424
* <br>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2015 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign;
17+
18+
import java.lang.annotation.Retention;
19+
20+
import static java.lang.annotation.ElementType.PARAMETER;
21+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
22+
23+
/** The name of a template variable applied to {@link Headers}, {@linkplain RequestLine} or {@linkplain Body} */
24+
@Retention(RUNTIME)
25+
@java.lang.annotation.Target(PARAMETER)
26+
public @interface Param {
27+
String value();
28+
}

core/src/main/java/feign/RequestLine.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* ...
1616
*
1717
* &#64;RequestLine("GET /servers/{serverId}?count={count}")
18-
* void get(&#64;Named("serverId") String serverId, &#64;Named("count") int count);
18+
* void get(&#64;Param("serverId") String serverId, &#64;Param("count") int count);
1919
* ...
2020
*
2121
* &#64;RequestLine("GET")
@@ -39,7 +39,7 @@
3939
* Feign:
4040
* <pre>
4141
* &#64;RequestLine("GET /servers/{serverId}?count={count}")
42-
* void get(&#64;Named("serverId") String serverId, &#64;Named("count") int count);
42+
* void get(&#64;Param("serverId") String serverId, &#64;Param("count") int count);
4343
* ...
4444
* </pre>
4545
* <br>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
* <pre>
5656
* &#064;POST
5757
* &#064;Path(&quot;/&quot;)
58-
* Session login(@Named(&quot;username&quot;) String username, @Named(&quot;password&quot;) String password);
58+
* Session login(@Param(&quot;username&quot;) String username, @Param(&quot;password&quot;) String password);
5959
* </pre>
6060
*/
6161
public interface Encoder {

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

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ interface BodyWithoutParameters {
164164
}
165165

166166
interface WithURIParam {
167-
@RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two);
167+
@RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two);
168168
}
169169

170170
@Test public void withPathAndURIParam() throws Exception {
@@ -183,8 +183,8 @@ interface WithURIParam {
183183

184184
interface WithPathAndQueryParams {
185185
@RequestLine("GET /domains/{domainId}/records?name={name}&type={type}")
186-
Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String nameFilter,
187-
@Named("type") String typeFilter);
186+
Response recordsByNameAndType(@Param("domainId") int id, @Param("name") String nameFilter,
187+
@Param("type") String typeFilter);
188188
}
189189

190190
@Test public void pathAndQueryParams() throws Exception {
@@ -205,8 +205,8 @@ interface FormParams {
205205
@RequestLine("POST /")
206206
@Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
207207
void login(
208-
@Named("customer_name") String customer,
209-
@Named("user_name") String user, @Named("password") String password);
208+
@Param("customer_name") String customer,
209+
@Param("user_name") String user, @Param("password") String password);
210210
}
211211

212212
@Test public void bodyWithTemplate() throws Exception {
@@ -233,7 +233,7 @@ void login(
233233

234234
interface HeaderParams {
235235
@RequestLine("POST /")
236-
@Headers("Auth-Token: {Auth-Token}") void logout(@Named("Auth-Token") String token);
236+
@Headers("Auth-Token: {Auth-Token}") void logout(@Param("Auth-Token") String token);
237237
}
238238

239239
@Test public void headerParamsParseIntoIndexToName() throws Exception {
@@ -244,4 +244,70 @@ interface HeaderParams {
244244
assertThat(md.indexToName())
245245
.containsExactly(entry(0, asList("Auth-Token")));
246246
}
247+
248+
// TODO: remove all of below in 8.x
249+
250+
interface WithPathAndQueryParamsAnnotatedWithNamed {
251+
@RequestLine("GET /domains/{domainId}/records?name={name}&type={type}")
252+
Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String nameFilter,
253+
@Named("type") String typeFilter);
254+
}
255+
256+
@Test public void pathAndQueryParamsAnnotatedWithNamed() throws Exception {
257+
MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParamsAnnotatedWithNamed.class.getDeclaredMethod
258+
("recordsByNameAndType", int.class, String.class, String.class));
259+
260+
assertThat(md.template())
261+
.hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}")));
262+
263+
assertThat(md.indexToName()).containsExactly(
264+
entry(0, asList("domainId")),
265+
entry(1, asList("name")),
266+
entry(2, asList("type"))
267+
);
268+
}
269+
270+
interface FormParamsAnnotatedWithNamed {
271+
@RequestLine("POST /")
272+
@Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
273+
void login(
274+
@Named("customer_name") String customer,
275+
@Named("user_name") String user, @Named("password") String password);
276+
}
277+
278+
@Test public void bodyWithTemplateAnnotatedWithNamed() throws Exception {
279+
MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class,
280+
String.class, String.class));
281+
282+
assertThat(md.template())
283+
.hasBodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D");
284+
}
285+
286+
@Test public void formParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception {
287+
MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class,
288+
String.class, String.class));
289+
290+
assertThat(md.formParams())
291+
.containsExactly("customer_name", "user_name", "password");
292+
293+
assertThat(md.indexToName()).containsExactly(
294+
entry(0, asList("customer_name")),
295+
entry(1, asList("user_name")),
296+
entry(2, asList("password"))
297+
);
298+
}
299+
300+
interface HeaderParamsAnnotatedWithNamed {
301+
@RequestLine("POST /")
302+
@Headers("Auth-Token: {Auth-Token}") void logout(@Named("Auth-Token") String token);
303+
}
304+
305+
@Test public void headerParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception {
306+
MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParamsAnnotatedWithNamed.class.getDeclaredMethod("logout", String.class));
307+
308+
assertThat(md.template()).hasHeaders(entry("Auth-Token", asList("{Auth-Token}")));
309+
310+
assertThat(md.indexToName())
311+
.containsExactly(entry(0, asList("Auth-Token")));
312+
}
247313
}

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

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@
3333
import java.util.Arrays;
3434
import java.util.List;
3535
import java.util.Map;
36-
import java.util.concurrent.Executor;
37-
import javax.inject.Named;
3836
import javax.inject.Singleton;
3937
import javax.net.ssl.HostnameVerifier;
4038
import javax.net.ssl.SSLSession;
@@ -63,18 +61,18 @@ interface TestInterface {
6361
@RequestLine("POST /")
6462
@Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
6563
void login(
66-
@Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password);
64+
@Param("customer_name") String customer, @Param("user_name") String user, @Param("password") String password);
6765

6866
@RequestLine("POST /") void body(List<String> contents);
6967

7068
@RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List<String> contents);
7169

7270
@RequestLine("POST /") void form(
73-
@Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password);
71+
@Param("customer_name") String customer, @Param("user_name") String user, @Param("password") String password);
7472

75-
@RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two);
73+
@RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two);
7674

77-
@RequestLine("GET /?1={1}&2={2}") Response queryParams(@Named("1") String one, @Named("2") Iterable<String> twos);
75+
@RequestLine("GET /?1={1}&2={2}") Response queryParams(@Param("1") String one, @Param("2") Iterable<String> twos);
7876

7977
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
8078
static class Module {
@@ -116,22 +114,12 @@ interface OtherTestInterface {
116114
@RequestLine("POST /") void binaryRequestBody(byte[] contents);
117115
}
118116

119-
@Module(library = true, overrides = true)
120-
static class RunSynchronous {
121-
@Provides @Singleton @Named("http") Executor httpExecutor() {
122-
return new Executor() {
123-
@Override public void execute(Runnable command) {
124-
command.run();
125-
}
126-
};
127-
}
128-
}
129-
130117
@Test
131118
public void postTemplateParamsResolve() throws IOException, InterruptedException {
132119
server.enqueue(new MockResponse().setBody("foo"));
133120

134-
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
121+
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
122+
new TestInterface.Module());
135123

136124
api.login("netflix", "denominator", "password");
137125

@@ -155,7 +143,8 @@ public void responseCoercesToStringBody() throws IOException, InterruptedExcepti
155143
public void postFormParams() throws IOException, InterruptedException {
156144
server.enqueue(new MockResponse().setBody("foo"));
157145

158-
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
146+
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
147+
new TestInterface.Module());
159148

160149
api.form("netflix", "denominator", "password");
161150

@@ -180,7 +169,8 @@ public void postBodyParam() throws IOException, InterruptedException {
180169
public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
181170
server.enqueue(new MockResponse().setBody("foo"));
182171

183-
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
172+
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
173+
new TestInterface.Module());
184174

185175
api.gzipBody(Arrays.asList("netflix", "denominator", "password"));
186176

@@ -239,8 +229,8 @@ public void multipleInterceptor() throws IOException, InterruptedException {
239229

240230
@Test public void toKeyMethodFormatsAsExpected() throws Exception {
241231
assertEquals("TestInterface#post()", Feign.configKey(TestInterface.class.getDeclaredMethod("post")));
242-
assertEquals("TestInterface#uriParam(String,URI,String)", Feign.configKey(
243-
TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)));
232+
assertEquals("TestInterface#uriParam(String,URI,String)",
233+
Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)));
244234
}
245235

246236
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
@@ -266,7 +256,7 @@ public void canOverrideErrorDecoder() throws IOException, InterruptedException {
266256
thrown.expectMessage("zone not found");
267257

268258
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
269-
new IllegalArgumentExceptionOn404());
259+
new IllegalArgumentExceptionOn404());
270260

271261
api.post();
272262
}

0 commit comments

Comments
 (0)