diff --git a/README.md b/README.md index 2456c3df..e2b90790 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,15 @@ Motif is a DI library that offers a simple API optimized for nested scopes. Unde | [![Maven Central](https://img.shields.io/maven-central/v/com.uber.motif/motif-compiler.svg)](https://search.maven.org/artifact/com.uber.motif/motif-compiler)
[![Maven Central](https://img.shields.io/maven-central/v/com.uber.motif/motif.svg)](https://search.maven.org/artifact/com.uber.motif/motif) |
annotationProcessor 'com.uber.motif:motif-compiler:x.y.z'
implementation 'com.uber.motif:motif:x.y.z'
| |-|:-| +## Proguard + +```proguard +-keepnames @motif.Scope interface * +-keepnames @motif.ScopeImpl class * { + (...); +} +``` + ## The Basics This is a Motif Scope. It serves as a container for objects that can be created by this Scope: @@ -206,6 +215,13 @@ interface MainScope extends Creatable { interface MainDependencies {} ``` +Extending `Creatable` also enables instantiation of a "root" `Scope` without referencing generated code using Motif's `ScopeFactory.create` API: + +```java +MainDependencies dependencies = ...; +MainScope mainScope = ScopeFactory.create(MainScope.class, dependencies) +``` + ## Convenience APIs Factory methods that pass parameters through to a constructor without modification can be converted to parameterless abstract methods: diff --git a/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt b/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt index 2be8a855..2a7d210b 100644 --- a/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt +++ b/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt @@ -22,6 +22,7 @@ import motif.ast.compiler.CompilerMethod import motif.ast.compiler.CompilerType import motif.core.ResolvedGraph import motif.core.ScopeEdge +import motif.internal.Constants import motif.models.* import javax.annotation.processing.ProcessingEnvironment @@ -342,7 +343,7 @@ class ScopeImplFactory private constructor( get() = scopeImplClassNames.computeIfAbsent(this) { scope -> val scopeClassName = scope.clazz.typeName val prefix = scopeClassName.kt.simpleNames.joinToString("") - ClassName.get(scopeClassName.kt.packageName, "${prefix}Impl") + ClassName.get(scopeClassName.kt.packageName, "$prefix${Constants.SCOPE_IMPL_SUFFIX}") } private val Scope.dependenciesClassName: ClassName diff --git a/lib/src/main/java/motif/ScopeFactory.java b/lib/src/main/java/motif/ScopeFactory.java new file mode 100644 index 00000000..bc7d21a3 --- /dev/null +++ b/lib/src/main/java/motif/ScopeFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, 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 motif; + +import motif.internal.Constants; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +public class ScopeFactory { + + private ScopeFactory() {} + + private static final Map, Constructor> scopeClassToCreateMethod = new HashMap<>(); + + public static , D> S create(Class scopeClass, D dependencies) { + Constructor constructor = getConstructor(scopeClass); + try { + return (S) constructor.newInstance(dependencies); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + } + + private synchronized static Constructor getConstructor(Class scopeClass) { + Constructor constructor = scopeClassToCreateMethod.get(scopeClass); + if (constructor == null) { + Class scopeImplClass = getScopeImplClass(scopeClass); + constructor = scopeImplClass.getDeclaredConstructors()[0]; + scopeClassToCreateMethod.put(scopeClass, constructor); + } + return constructor; + } + + private static Class getScopeImplClass(Class scopeClass) { + String scopeImplClassName = getScopeImplClassName(scopeClass); + try { + return Class.forName(scopeImplClassName, true, scopeClass.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException( + "Could not find Scope implementation class " + scopeImplClassName + ". Ensure that the Motif " + + "annotation processor is enabled and that proguard is configured correctly (See README for details)."); + } + } + + // Inspired by https://github.com/square/javapoet/issues/295 + private static String getScopeImplClassName(Class scopeClass) { + StringBuilder sb = new StringBuilder(); + sb.append(Constants.SCOPE_IMPL_SUFFIX); + Class clazz = scopeClass; + while (true) { + sb.insert(0, clazz.getSimpleName()); + Class enclosing = clazz.getEnclosingClass(); + if (enclosing == null) break; + clazz = enclosing; + } + int lastDot = clazz.getName().lastIndexOf('.'); + if (lastDot != -1) { + sb.insert(0, '.'); + sb.insert(0, clazz.getName().substring(0, lastDot)); + } + return sb.toString(); + } +} diff --git a/lib/src/main/java/motif/internal/Constants.java b/lib/src/main/java/motif/internal/Constants.java new file mode 100644 index 00000000..f28daa66 --- /dev/null +++ b/lib/src/main/java/motif/internal/Constants.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, 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 motif.internal; + +public class Constants { + + public static final String SCOPE_IMPL_SUFFIX = "Impl"; + + private Constants() {} +} diff --git a/tests/src/main/java/testcases/T071_scope_factory/GRAPH.txt b/tests/src/main/java/testcases/T071_scope_factory/GRAPH.txt new file mode 100644 index 00000000..062ddcbc --- /dev/null +++ b/tests/src/main/java/testcases/T071_scope_factory/GRAPH.txt @@ -0,0 +1,32 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ---- String ---- + [ Provided By ] + [ Consumed By ] + * Scope | Scope.string() + + ==== Provides ==== + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T071_scope_factory/Scope.java b/tests/src/main/java/testcases/T071_scope_factory/Scope.java new file mode 100644 index 00000000..daeccd64 --- /dev/null +++ b/tests/src/main/java/testcases/T071_scope_factory/Scope.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, 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 testcases.T071_scope_factory; + +import motif.Creatable; + +@motif.Scope +public interface Scope extends Creatable { + + String string(); + + interface Dependencies { + + String s(); + } +} diff --git a/tests/src/main/java/testcases/T071_scope_factory/Test.java b/tests/src/main/java/testcases/T071_scope_factory/Test.java new file mode 100644 index 00000000..afca847f --- /dev/null +++ b/tests/src/main/java/testcases/T071_scope_factory/Test.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, 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 testcases.T071_scope_factory; + +import motif.ScopeFactory; + +import static com.google.common.truth.Truth.assertThat; + +public class Test { + + public static void run() { + Scope scope = ScopeFactory.create(Scope.class, new Scope.Dependencies() { + @Override + public String s() { + return "s"; + } + }); + assertThat(scope.string()).isEqualTo("s"); + } +} diff --git a/tests/src/main/java/testcases/T071_scope_factory/config.pro b/tests/src/main/java/testcases/T071_scope_factory/config.pro new file mode 100644 index 00000000..0c2b750d --- /dev/null +++ b/tests/src/main/java/testcases/T071_scope_factory/config.pro @@ -0,0 +1,9 @@ +-dontshrink +-keep class **Test { + public static void run(); +} + +-keepnames @motif.Scope interface * +-keepnames @motif.ScopeImpl class * { + (...); +} \ No newline at end of file