diff --git a/apt-test-generator/README.md b/apt-test-generator/README.md
new file mode 100644
index 00000000..35d99845
--- /dev/null
+++ b/apt-test-generator/README.md
@@ -0,0 +1,66 @@
+# Feign APT test generator
+This module generates mock clients for tests based on feign interfaces
+
+## Usage
+
+Just need to add this module to dependency list and Java [Annotation Processing Tool](https://docs.oracle.com/javase/7/docs/technotes/guides/apt/GettingStarted.html) will automatically pick up the jar and generate test clients.
+
+There are 2 main alternatives to include this to a project:
+
+1. Just add to classpath and java compiler should automaticaly detect and run code generation. On maven this is done like this:
+
+```xml
+
+ io.github.openfeign.experimental
+ feign-apt-test-generator
+ ${feign.version}
+ test
+
+```
+
+1. Use a purpose build tool that allow to pick output location and don't mix dependencies onto classpath
+
+```xml
+
+ com.mysema.maven
+ apt-maven-plugin
+ 1.1.3
+
+
+
+ process
+
+
+ target/generated-test-sources/feign
+ feign.apttestgenerator.GenerateTestStubAPT
+
+
+
+
+
+ io.github.openfeign.experimental
+ feign-apt-test-generator
+ ${feign.version}
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ feign-stubs-source
+ generate-test-sources
+
+ add-test-source
+
+
+
+
+
+
+
+
+
+```
diff --git a/apt-test-generator/pom.xml b/apt-test-generator/pom.xml
new file mode 100644
index 00000000..91c7a597
--- /dev/null
+++ b/apt-test-generator/pom.xml
@@ -0,0 +1,185 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 10.5.2-SNAPSHOT
+
+
+ io.github.openfeign.experimental
+ feign-apt-test-generator
+ Feign APT test generator
+ Feign code generation tool for mocked clients
+
+
+ ${project.basedir}/..
+
+
+
+
+
+ io.github.openfeign
+ feign-bom
+ ${project.version}
+ pom
+ import
+
+
+
+
+
+
+ com.github.jknack
+ handlebars
+ 4.1.2
+
+
+
+ io.github.openfeign
+ feign-example-github
+ ${project.version}
+
+
+
+ com.google.testing.compile
+ compile-testing
+ 0.18
+ test
+
+
+ com.google.guava
+ guava
+ 28.0-jre
+
+
+ com.google.auto.service
+ auto-service
+ 1.0-rc5
+ provided
+
+
+
+
+
+
+ docker
+ true
+
+ ${project.basedir}/docker
+
+
+
+ ${basedir}/src/main/resources
+
+
+ src/main/java
+
+ **/*.java
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.4.3
+
+
+ package
+
+ shade
+
+
+
+
+ feign.aptgenerator.github.GitHubFactoryExample
+
+
+ false
+
+
+
+
+
+ org.skife.maven
+ really-executable-jar-maven-plugin
+ 1.5.0
+
+ github
+
+
+
+ package
+
+ really-executable-jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${maven-surefire-plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+ com.spotify
+ docker-maven-plugin
+
+
+ ${project.build.directory}/classes/docker/
+
+
+ true
+
+ docker-hub
+ https://index.docker.io/v1/
+ feign-apt-generator/test
+
+
+ /
+ ${project.build.directory}
+ ${project.artifactId}-${project.version}.jar
+
+
+
+
+
+
+ post-integration-test
+
+ build
+
+
+
+
+
+
+
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java b/apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java
new file mode 100644
index 00000000..df9a088f
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2012-2019 The Feign 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 feign.apttestgenerator;
+
+public class ArgumentDefinition {
+
+ public final String name;
+ public final String type;
+
+ public ArgumentDefinition(String name, String type) {
+ super();
+ this.name = name;
+ this.type = type;
+ }
+
+}
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java b/apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java
new file mode 100644
index 00000000..2c3eff99
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2012-2019 The Feign 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 feign.apttestgenerator;
+
+public class ClientDefinition {
+
+ public final String jpackage;
+ public final String className;
+ public final String fullQualifiedName;
+
+ public ClientDefinition(String jpackage, String className, String fullQualifiedName) {
+ super();
+ this.jpackage = jpackage;
+ this.className = className;
+ this.fullQualifiedName = fullQualifiedName;
+ }
+
+}
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java b/apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java
new file mode 100644
index 00000000..b6b81c4c
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright 2012-2019 The Feign 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 feign.apttestgenerator;
+
+import com.github.jknack.handlebars.*;
+import com.github.jknack.handlebars.context.FieldValueResolver;
+import com.github.jknack.handlebars.context.JavaBeanValueResolver;
+import com.github.jknack.handlebars.context.MapValueResolver;
+import com.github.jknack.handlebars.io.URLTemplateSource;
+import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableList;
+import java.io.IOError;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.*;
+import java.util.stream.Collectors;
+import javax.annotation.processing.*;
+import javax.lang.model.element.*;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.WildcardType;
+import javax.tools.Diagnostic.Kind;
+import javax.tools.JavaFileObject;
+
+@SupportedAnnotationTypes({
+ "feign.RequestLine"
+})
+@AutoService(Processor.class)
+public class GenerateTestStubAPT extends AbstractProcessor {
+
+ @Override
+ public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ System.out.println(annotations);
+ System.out.println(roundEnv);
+
+ final Map> clientsToGenerate = annotations.stream()
+ .map(roundEnv::getElementsAnnotatedWith)
+ .flatMap(Set::stream)
+ .map(ExecutableElement.class::cast)
+ .collect(Collectors.toMap(
+ annotatedMethod -> TypeElement.class.cast(annotatedMethod.getEnclosingElement()),
+ ImmutableList::of,
+ (list1, list2) -> ImmutableList.builder()
+ .addAll(list1)
+ .addAll(list2)
+ .build()));
+
+ System.out.println("Count: " + clientsToGenerate.size());
+ System.out.println("clientsToGenerate: " + clientsToGenerate);
+
+ final Handlebars handlebars = new Handlebars();
+
+ final URLTemplateSource source =
+ new URLTemplateSource("stub.mustache", getClass().getResource("/stub.mustache"));
+ Template template;
+ try {
+ template = handlebars.with(EscapingStrategy.JS).compile(source);
+ } catch (final IOException e) {
+ throw new IOError(e);
+ }
+
+
+ clientsToGenerate.forEach((type, executables) -> {
+ try {
+ final String jPackage = readPackage(type);
+ final String className = type.getSimpleName().toString();
+ final JavaFileObject builderFile = processingEnv.getFiler()
+ .createSourceFile(jPackage + "." + className + "Stub");
+
+ final ClientDefinition client = new ClientDefinition(
+ jPackage,
+ className,
+ type.toString());
+
+ final List methods = executables.stream()
+ .map(method -> {
+ final String methodName = method.getSimpleName().toString();
+
+ final List args = method.getParameters()
+ .stream()
+ .map(var -> new ArgumentDefinition(var.getSimpleName().toString(),
+ var.asType().toString()))
+ .collect(Collectors.toList());
+ return new MethodDefinition(
+ methodName,
+ method.getReturnType().toString(),
+ method.getReturnType().getKind() == TypeKind.VOID,
+ args);
+ })
+ .collect(Collectors.toList());
+
+ final Context context = Context.newBuilder(template)
+ .combine("client", client)
+ .combine("methods", methods)
+ .resolver(JavaBeanValueResolver.INSTANCE, MapValueResolver.INSTANCE,
+ FieldValueResolver.INSTANCE)
+ .build();
+ final String stubSource = template.apply(context);
+ System.out.println(stubSource);
+
+ builderFile.openWriter().append(stubSource).close();
+ } catch (final Exception e) {
+ e.printStackTrace();
+ processingEnv.getMessager().printMessage(Kind.ERROR,
+ "Unable to generate factory for " + type);
+ }
+ });
+
+ return true;
+ }
+
+
+
+ private Type toJavaType(TypeMirror type) {
+ outType(type.getClass());
+ if (type instanceof WildcardType) {
+
+ }
+ return Object.class;
+ }
+
+ private void outType(Class> class1) {
+ if (Object.class.equals(class1) || class1 == null) {
+ return;
+ }
+ System.out.println(class1);
+ outType(class1.getSuperclass());
+ Arrays.stream(class1.getInterfaces()).forEach(this::outType);
+ }
+
+
+
+ private String readPackage(Element type) {
+ if (type.getKind() == ElementKind.PACKAGE) {
+ return type.toString();
+ }
+
+ if (type.getKind() == ElementKind.CLASS
+ || type.getKind() == ElementKind.INTERFACE) {
+ return readPackage(type.getEnclosingElement());
+ }
+
+ return null;
+ }
+
+}
+
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java b/apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java
new file mode 100644
index 00000000..56ebd7b5
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2012-2019 The Feign 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 feign.apttestgenerator;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Converter;
+import java.util.List;
+
+public class MethodDefinition {
+
+ private static final Converter TO_UPPER_CASE =
+ CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL);
+ private final String name;
+ private final String uname;
+ private final String returnType;
+ private final boolean isVoid;
+ private final List args;
+
+ public MethodDefinition(String name, String returnType, boolean isVoid,
+ List args) {
+ super();
+ this.name = name;
+ this.uname = TO_UPPER_CASE.convert(name);
+ this.returnType = returnType;
+ this.isVoid = isVoid;
+ this.args = args;
+ }
+
+}
diff --git a/apt-test-generator/src/main/resources/stub.mustache b/apt-test-generator/src/main/resources/stub.mustache
new file mode 100644
index 00000000..015fbbad
--- /dev/null
+++ b/apt-test-generator/src/main/resources/stub.mustache
@@ -0,0 +1,62 @@
+package {{client.jpackage}};
+
+import java.util.concurrent.atomic.AtomicInteger;
+import feign.Experimental;
+
+public class {{client.className}}Stub
+ implements {{client.fullQualifiedName}} {
+
+ @Experimental
+ public class {{client.className}}Invokations {
+
+{{#each methods as |method|}}
+
+ private final AtomicInteger {{method.name}} = new AtomicInteger(0);
+
+ public int {{method.name}}() {
+ return {{method.name}}.get();
+ }
+
+{{/each}}
+
+ }
+
+ @Experimental
+ public class {{client.className}}Anwsers {
+
+{{#each methods as |method|}}
+ {{#unless method.isVoid}}
+ private {{method.returnType}} {{method.name}}Default;
+ {{/unless}}
+{{/each}}
+
+ }
+
+ public {{client.className}}Invokations invokations;
+ public {{client.className}}Anwsers answers;
+
+ public {{client.className}}Stub() {
+ this.invokations = new {{client.className}}Invokations();
+ this.answers = new {{client.className}}Anwsers();
+ }
+
+{{#each methods as |method|}}
+ {{#unless method.isVoid}}
+ @Experimental
+ public {{client.className}}Stub with{{method.uname}}({{method.returnType}} {{method.name}}) {
+ answers.{{method.name}}Default = {{method.name}};
+ return this;
+ }
+ {{/unless}}
+
+ @Override
+ public {{method.returnType}} {{method.name}}({{#each method.args as |arg|}}{{arg.type}} {{arg.name}}{{#unless @last}},{{/unless}}{{/each}}) {
+ invokations.{{method.name}}.incrementAndGet();
+{{#unless method.isVoid}}
+ return answers.{{method.name}}Default;
+{{/unless}}
+ }
+
+{{/each}}
+
+}
diff --git a/apt-test-generator/src/test/java/example/github/GitHubStub.java b/apt-test-generator/src/test/java/example/github/GitHubStub.java
new file mode 100644
index 00000000..bdebb319
--- /dev/null
+++ b/apt-test-generator/src/test/java/example/github/GitHubStub.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2012-2019 The Feign 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 example.github;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import feign.Experimental;
+
+public class GitHubStub
+ implements example.github.GitHubExample.GitHub {
+
+ @Experimental
+ public class GitHubInvokations {
+
+ private final AtomicInteger repos = new AtomicInteger(0);
+
+ public int repos() {
+ return repos.get();
+ }
+
+ private final AtomicInteger contributors = new AtomicInteger(0);
+
+ public int contributors() {
+ return contributors.get();
+ }
+
+ private final AtomicInteger createIssue = new AtomicInteger(0);
+
+ public int createIssue() {
+ return createIssue.get();
+ }
+
+ }
+
+ @Experimental
+ public class GitHubAnwsers {
+
+ private java.util.List reposDefault;
+
+ private java.util.List contributorsDefault;
+
+ }
+
+ public GitHubInvokations invokations;
+ public GitHubAnwsers answers;
+
+ public GitHubStub() {
+ this.invokations = new GitHubInvokations();
+ this.answers = new GitHubAnwsers();
+ }
+
+ @Experimental
+ public GitHubStub withRepos(java.util.List repos) {
+ answers.reposDefault = repos;
+ return this;
+ }
+
+ @Override
+ public java.util.List repos(java.lang.String owner) {
+ invokations.repos.incrementAndGet();
+
+ return answers.reposDefault;
+ }
+
+ @Experimental
+ public GitHubStub withContributors(java.util.List contributors) {
+ answers.contributorsDefault = contributors;
+ return this;
+ }
+
+
+ @Override
+ public java.util.List contributors(java.lang.String owner,
+ java.lang.String repo) {
+ invokations.contributors.incrementAndGet();
+
+ return answers.contributorsDefault;
+ }
+
+ @Override
+ public void createIssue(example.github.GitHubExample.GitHub.Issue issue,
+ java.lang.String owner,
+ java.lang.String repo) {
+ invokations.createIssue.incrementAndGet();
+
+ }
+
+}
diff --git a/apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java b/apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java
new file mode 100644
index 00000000..90f32ffb
--- /dev/null
+++ b/apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2012-2019 The Feign 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 feign.apttestgenerator;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+import static com.google.testing.compile.Compiler.javac;
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.JavaFileObjects;
+import org.junit.Test;
+import java.io.File;
+
+/**
+ * Test for {@link GenerateTestStubAPT}
+ */
+public class GenerateTestStubAPTTest {
+
+ private final File main = new File("../example-github/src/main/java/").getAbsoluteFile();
+
+ @Test
+ public void test() throws Exception {
+ final Compilation compilation =
+ javac()
+ .withProcessors(new GenerateTestStubAPT())
+ .compile(JavaFileObjects.forResource(
+ new File(main, "example/github/GitHubExample.java")
+ .toURI()
+ .toURL()));
+ assertThat(compilation).succeeded();
+ assertThat(compilation)
+ .generatedSourceFile("example.github.GitHubStub")
+ .hasSourceEquivalentTo(JavaFileObjects.forResource(
+ new File("src/test/java/example/github/GitHubStub.java")
+ .toURI()
+ .toURL()));
+ }
+
+}
diff --git a/example-github/src/main/java/example/github/GitHubExample.java b/example-github/src/main/java/example/github/GitHubExample.java
index 7c9f9f02..d2cc5c06 100644
--- a/example-github/src/main/java/example/github/GitHubExample.java
+++ b/example-github/src/main/java/example/github/GitHubExample.java
@@ -30,17 +30,17 @@ public class GitHubExample {
private static final String GITHUB_TOKEN = "GITHUB_TOKEN";
- interface GitHub {
+ public interface GitHub {
- class Repository {
+ public class Repository {
String name;
}
- class Contributor {
+ public class Contributor {
String login;
}
- class Issue {
+ public class Issue {
Issue() {
diff --git a/pom.xml b/pom.xml
index 3c413dc4..3cd25c42 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,7 @@
example-github
example-wikipedia
mock
+ apt-test-generator
benchmark