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 + + + + target/generated-test-sources/feign + + + + + +``` 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 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