From 9fd42d8a5eca224eb9fa112937d2f2677ec7c1f3 Mon Sep 17 00:00:00 2001 From: Leland Takamine Date: Wed, 2 Oct 2019 10:34:22 -0700 Subject: [PATCH] Add ScopeFactory API --- CHANGELOG.md | 1 + README.md | 16 ++++ .../kotlin/motif/compiler/ScopeImplFactory.kt | 3 +- lib/src/main/java/motif/ScopeFactory.java | 94 +++++++++++++++++++ .../main/java/motif/internal/Constants.java | 23 +++++ .../motif/sample/app/root/RootActivity.java | 4 +- .../motif/sample/app/root/RootFactory.java | 10 +- .../testcases/T071_scope_factory/GRAPH.txt | 32 +++++++ .../testcases/T071_scope_factory/Scope.java | 29 ++++++ .../testcases/T071_scope_factory/Test.java | 33 +++++++ .../testcases/T071_scope_factory/config.pro | 9 ++ .../testcases/T072_scope_factory/GRAPH.txt | 32 +++++++ .../testcases/T072_scope_factory/Scope.java | 29 ++++++ .../testcases/T072_scope_factory/Test.java | 33 +++++++ .../testcases/T072_scope_factory/config.pro | 9 ++ .../GRAPH.txt | 32 +++++++ .../Scope.java | 33 +++++++ .../Test.java | 29 ++++++ .../config.pro | 9 ++ 19 files changed, 452 insertions(+), 8 deletions(-) create mode 100644 lib/src/main/java/motif/ScopeFactory.java create mode 100644 lib/src/main/java/motif/internal/Constants.java create mode 100644 tests/src/main/java/testcases/T071_scope_factory/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T071_scope_factory/Scope.java create mode 100644 tests/src/main/java/testcases/T071_scope_factory/Test.java create mode 100644 tests/src/main/java/testcases/T071_scope_factory/config.pro create mode 100644 tests/src/main/java/testcases/T072_scope_factory/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T072_scope_factory/Scope.java create mode 100644 tests/src/main/java/testcases/T072_scope_factory/Test.java create mode 100644 tests/src/main/java/testcases/T072_scope_factory/config.pro create mode 100644 tests/src/main/java/testcases/T073_scope_factory_no_dependencies/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Scope.java create mode 100644 tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Test.java create mode 100644 tests/src/main/java/testcases/T073_scope_factory_no_dependencies/config.pro diff --git a/CHANGELOG.md b/CHANGELOG.md index 6db36cc1..41a68801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 0.2.2-SNAPSHOT +* Replace `@motif.Dependencies` API with `ScopeFactory.create`. See https://github.com/uber/motif/issues/125 for details. * Do not use computeIfAbsent in ScopeFactory in order to avoid ConcurrentModificationException in the IntelliJ plugin * Allow graph processing to continue even when a ParsingError is encountered. A malformed Scope will simply not be included in the ResolvedGraph. * New `ResolvedGraph.getParentEdges(Scope)` and `ResolvedGraph.getScope(IrType)` APIs 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..430b9b16 --- /dev/null +++ b/lib/src/main/java/motif/ScopeFactory.java @@ -0,0 +1,94 @@ +/* + * 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 NoDependencies NO_DEPENDENCIES = new NoDependencies() {}; + + private static final Map, Constructor> scopeClassToCreateMethod = new HashMap<>(); + + public static > S create(Class scopeClass) { + return create(scopeClass, NO_DEPENDENCIES); + } + + public static , D> S create(Class scopeClass, D dependencies) { + Constructor constructor = getConstructor(scopeClass); + try { + if (constructor.getParameterTypes().length == 0) { + return (S) constructor.newInstance(); + } else { + 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/samples/sample/src/main/java/motif/sample/app/root/RootActivity.java b/samples/sample/src/main/java/motif/sample/app/root/RootActivity.java index ba311b5b..9db37acd 100644 --- a/samples/sample/src/main/java/motif/sample/app/root/RootActivity.java +++ b/samples/sample/src/main/java/motif/sample/app/root/RootActivity.java @@ -19,13 +19,15 @@ import android.os.Bundle; import androidx.annotation.Nullable; +import motif.ScopeFactory; public class RootActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - RootScope rootScope = new RootFactoryImpl().create(this); + RootFactory rootFactory = ScopeFactory.create(RootFactory.class); + RootScope rootScope = rootFactory.create(this); setContentView(rootScope.view()); } } diff --git a/samples/sample/src/main/java/motif/sample/app/root/RootFactory.java b/samples/sample/src/main/java/motif/sample/app/root/RootFactory.java index 48c131f8..7df14dc3 100644 --- a/samples/sample/src/main/java/motif/sample/app/root/RootFactory.java +++ b/samples/sample/src/main/java/motif/sample/app/root/RootFactory.java @@ -17,18 +17,16 @@ import android.content.Context; import motif.Creatable; +import motif.NoDependencies; import motif.Scope; /** * This is a convenience Scope defined simply to build the RootScope. This additional layer is useful since RootScope - * requires a Context that must be provided from outside of the graph. RootFactory can declare an empty dependencies - * interface and pass the ViewGroup in to the RootScope child method. In RootActivity, we now have a nice API to - * instantiate the RootScope. + * requires a Context that must be provided from outside of the graph. RootFactory itself does not require any + * dependencies of its own and can be instantiated in RootActivity via ScopeFactory.create(RootFactory.class). */ @Scope -public interface RootFactory extends Creatable { +public interface RootFactory extends Creatable { RootScope create(Context context); - - interface Dependencies {} } 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 diff --git a/tests/src/main/java/testcases/T072_scope_factory/GRAPH.txt b/tests/src/main/java/testcases/T072_scope_factory/GRAPH.txt new file mode 100644 index 00000000..062ddcbc --- /dev/null +++ b/tests/src/main/java/testcases/T072_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/T072_scope_factory/Scope.java b/tests/src/main/java/testcases/T072_scope_factory/Scope.java new file mode 100644 index 00000000..0946a6b6 --- /dev/null +++ b/tests/src/main/java/testcases/T072_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.T072_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/T072_scope_factory/Test.java b/tests/src/main/java/testcases/T072_scope_factory/Test.java new file mode 100644 index 00000000..46ee286e --- /dev/null +++ b/tests/src/main/java/testcases/T072_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.T072_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/T072_scope_factory/config.pro b/tests/src/main/java/testcases/T072_scope_factory/config.pro new file mode 100644 index 00000000..0c2b750d --- /dev/null +++ b/tests/src/main/java/testcases/T072_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 diff --git a/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/GRAPH.txt b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/GRAPH.txt new file mode 100644 index 00000000..1f2f1811 --- /dev/null +++ b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/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 ==== + + ==== Provides ==== + + ---- String | Objects.string ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.string() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Scope.java b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Scope.java new file mode 100644 index 00000000..5f08b4f9 --- /dev/null +++ b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Scope.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.T073_scope_factory_no_dependencies; + +import motif.Creatable; +import motif.NoDependencies; + +@motif.Scope +public interface Scope extends Creatable { + + String string(); + + @motif.Objects + class Objects { + + String string() { + return "s"; + } + } +} diff --git a/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Test.java b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Test.java new file mode 100644 index 00000000..7a529c43 --- /dev/null +++ b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/Test.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.T073_scope_factory_no_dependencies; + +import motif.ScopeFactory; + +import static com.google.common.truth.Truth.assertThat; + +public class Test { + + + public static void run() { + Scope scope = ScopeFactory.create(Scope.class); + assertThat(scope.string()).isEqualTo("s"); + } +} diff --git a/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/config.pro b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/config.pro new file mode 100644 index 00000000..0c2b750d --- /dev/null +++ b/tests/src/main/java/testcases/T073_scope_factory_no_dependencies/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