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/T070_scope_factory/GRAPH.txt b/tests/src/main/java/testcases/T070_scope_factory/GRAPH.txt
new file mode 100644
index 00000000..062ddcbc
--- /dev/null
+++ b/tests/src/main/java/testcases/T070_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/T070_scope_factory/Scope.java b/tests/src/main/java/testcases/T070_scope_factory/Scope.java
new file mode 100644
index 00000000..2379e920
--- /dev/null
+++ b/tests/src/main/java/testcases/T070_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.T070_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/T070_scope_factory/Test.java b/tests/src/main/java/testcases/T070_scope_factory/Test.java
new file mode 100644
index 00000000..2373f9bb
--- /dev/null
+++ b/tests/src/main/java/testcases/T070_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.T070_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/T070_scope_factory/config.pro b/tests/src/main/java/testcases/T070_scope_factory/config.pro
new file mode 100644
index 00000000..0c2b750d
--- /dev/null
+++ b/tests/src/main/java/testcases/T070_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