diff --git a/vee-core/src/main/java/com/slambang/vee_core/Vee.kt b/vee-core/src/main/java/com/slambang/vee_core/Vee.kt index 3653d02..487af9c 100644 --- a/vee-core/src/main/java/com/slambang/vee_core/Vee.kt +++ b/vee-core/src/main/java/com/slambang/vee_core/Vee.kt @@ -5,6 +5,8 @@ import com.slambang.vee_core.exceptions.VeeCircularDependencyException import com.slambang.vee_core.exceptions.VeeClassAlreadyProvidedException import com.slambang.vee_core.exceptions.VeeClassNotProvidedException import com.slambang.vee_core.exceptions.VeeNotStartedException +import com.slambang.vee_core.internal.VeeImpl +import com.slambang.vee_core.internal.VeeProductionRegistry import com.slambang.vee_core.internal.veeInstance /** @@ -62,9 +64,18 @@ interface Vee { /** * Public accessor for Vee. * + * A default (production-friendly) registry is passed here. + * @see [VeeProductionRegistry]. + * + * @param registry The [VeeRegistry] to use. * @return The Vee instance. */ @JvmStatic - fun instance(): Vee = veeInstance + fun instance(registry: VeeRegistry = VeeProductionRegistry()): Vee { + synchronized(Vee::class) { + if (veeInstance == null) veeInstance = VeeImpl(registry, mutableMapOf()) + return veeInstance!! + } + } } } diff --git a/vee-core/src/main/java/com/slambang/vee_core/VeeExt.kt b/vee-core/src/main/java/com/slambang/vee_core/VeeExt.kt index 3c2b39d..ab69351 100644 --- a/vee-core/src/main/java/com/slambang/vee_core/VeeExt.kt +++ b/vee-core/src/main/java/com/slambang/vee_core/VeeExt.kt @@ -2,11 +2,12 @@ package com.slambang.vee_core -import com.slambang.vee_core.internal.newInjectDelegate +import com.slambang.vee_core.internal.newVeeInjectDelegate /** * Runs a block to work with the [Vee] instance. * Automatically calls [Vee.start] after the block returns. + * Uses the default [VeeRegistry] which caches nothing. * * @param block The callback [Vee] function */ @@ -46,4 +47,4 @@ inline fun get(): T = Vee.instance().get(T::class.java) * * @return A [Lazy] (caching) delegate which resolves the required object */ -inline fun inject(): Lazy = newInjectDelegate(T::class.java) +inline fun inject(): Lazy = newVeeInjectDelegate(T::class.java) diff --git a/vee-core/src/main/java/com/slambang/vee_core/VeeRegistry.kt b/vee-core/src/main/java/com/slambang/vee_core/VeeRegistry.kt new file mode 100644 index 0000000..9d94a6c --- /dev/null +++ b/vee-core/src/main/java/com/slambang/vee_core/VeeRegistry.kt @@ -0,0 +1,17 @@ +package com.slambang.vee_core + +/** + * A Vee registry manages instances that are constructed with [Vee.get]. + * + * @see [VeeTestRegistry]. + */ +interface VeeRegistry { + + /** + * Each time Vee constructs an instance, that instance is passed here. + * + * @param clazz The class of the instance that was just constructed + * @param instance The instance that was just constructed + */ + fun onInstance(clazz: Class, instance: T): T +} diff --git a/vee-core/src/main/java/com/slambang/vee_core/internal/VeeImpl.kt b/vee-core/src/main/java/com/slambang/vee_core/internal/VeeImpl.kt index 6a14277..2c0e524 100644 --- a/vee-core/src/main/java/com/slambang/vee_core/internal/VeeImpl.kt +++ b/vee-core/src/main/java/com/slambang/vee_core/internal/VeeImpl.kt @@ -6,9 +6,10 @@ import com.slambang.vee_core.exceptions.VeeClassAlreadyProvidedException import com.slambang.vee_core.exceptions.VeeClassNotProvidedException import com.slambang.vee_core.exceptions.VeeNotStartedException -internal val veeInstance = VeeImpl(mutableMapOf()) +internal var veeInstance: Vee? = null internal class VeeImpl( + private val registry: VeeRegistry, private val graph: MutableMap, Vertex<*>> ) : Vee { @@ -29,39 +30,43 @@ internal class VeeImpl( } @Suppress("UNCHECKED_CAST") - return vertex.producer() as T + return registry.onInstance(clazz, vertex.producer() as T) } // 2. Examine the graph after all vertices have been provided. @Synchronized override fun start() { for (vertex in graph.values) { - validateVertices(vertex, vertex, mutableListOf()) + validateVertices(vertex, vertex,0, mutableListOf()) } isStarted = true } - private fun validateVertices( + private tailrec fun validateVertices( rootVertex: Vertex<*>, nextVertex: Vertex<*>, + edgeIndex: Int, path: MutableList> ) { + if (nextVertex.edges.isEmpty()) return + // Optimisation: better performance when a class directly references itself. if (nextVertex.edges.contains(nextVertex.clazz)) { throw VeeCircularDependencyException(listOf(nextVertex.clazz)) } - nextVertex.edges.forEach { edge -> - val destinationVertex = graph[edge] ?: throw VeeClassNotProvidedException(edge) - - when (destinationVertex.edges.contains(rootVertex.clazz)) { - true -> throw VeeCircularDependencyException(path + destinationVertex.clazz) - false -> path.add(nextVertex.clazz) - } + val nextEdge = nextVertex.edges[edgeIndex] + val destinationVertex = graph[nextEdge] ?: throw VeeClassNotProvidedException(nextEdge) - path.add(destinationVertex.clazz) - validateVertices(rootVertex, destinationVertex, path) + when (destinationVertex.edges.contains(rootVertex.clazz)) { + true -> throw VeeCircularDependencyException(path + destinationVertex.clazz) + false -> path.add(nextVertex.clazz) } + + path.add(destinationVertex.clazz) + + val nextEdgeIndex = if (edgeIndex + 1 == nextVertex.edges.size) 0 else edgeIndex + 1 + validateVertices(rootVertex, destinationVertex, nextEdgeIndex, path) } private fun getEdges(clazz: Class<*>): List> = diff --git a/vee-core/src/main/java/com/slambang/vee_core/internal/VeeLazyInjectDelegate.kt b/vee-core/src/main/java/com/slambang/vee_core/internal/VeeLazyInjectDelegate.kt index 29fecb7..6df6f94 100644 --- a/vee-core/src/main/java/com/slambang/vee_core/internal/VeeLazyInjectDelegate.kt +++ b/vee-core/src/main/java/com/slambang/vee_core/internal/VeeLazyInjectDelegate.kt @@ -3,7 +3,7 @@ package com.slambang.vee_core.internal import com.slambang.vee_core.Vee // Hides the VeeLazyInjectDelegate internal constructor -fun newInjectDelegate(clazz: Class): VeeLazyInjectDelegate = +fun newVeeInjectDelegate(clazz: Class): VeeLazyInjectDelegate = VeeLazyInjectDelegate(Vee.instance(), clazz) class VeeLazyInjectDelegate internal constructor( @@ -21,5 +21,6 @@ class VeeLazyInjectDelegate internal constructor( } } + @Synchronized override fun isInitialized(): Boolean = instance != null } diff --git a/vee-core/src/main/java/com/slambang/vee_core/internal/VeeProductionRegistry.kt b/vee-core/src/main/java/com/slambang/vee_core/internal/VeeProductionRegistry.kt new file mode 100644 index 0000000..dfafec7 --- /dev/null +++ b/vee-core/src/main/java/com/slambang/vee_core/internal/VeeProductionRegistry.kt @@ -0,0 +1,12 @@ +package com.slambang.vee_core.internal + +import com.slambang.vee_core.VeeRegistry + +/** + * This is the default registry which immediately returns the instance. + * Caches nothing. + */ +internal class VeeProductionRegistry: VeeRegistry { + + override fun onInstance(clazz: Class, instance: T): T = instance +} diff --git a/vee-core/src/main/java/com/slambang/vee_core/test/VeeTestExt.kt b/vee-core/src/main/java/com/slambang/vee_core/test/VeeTestExt.kt new file mode 100644 index 0000000..72af656 --- /dev/null +++ b/vee-core/src/main/java/com/slambang/vee_core/test/VeeTestExt.kt @@ -0,0 +1,20 @@ +package com.slambang.vee_core.test + +import com.slambang.vee_core.Vee +import com.slambang.vee_core.VeeRegistry +import com.slambang.vee_core.vee + +/** + * Runs a block to work with the [Vee] instance. + * Automatically calls [Vee.start] after the block returns. + * Uses a test-friendly [VeeRegistry] which caches all instances. + * Useful for when mocks need to be injected into the application and controlled from tests. + * + * @see [vee]. + */ +fun veeTest(block: Vee.() -> Unit) { + with (Vee.instance(VeeTestRegistry())) { + apply(block) + start() + } +} diff --git a/vee-core/src/main/java/com/slambang/vee_core/test/VeeTestRegistry.kt b/vee-core/src/main/java/com/slambang/vee_core/test/VeeTestRegistry.kt new file mode 100644 index 0000000..c8eb3fd --- /dev/null +++ b/vee-core/src/main/java/com/slambang/vee_core/test/VeeTestRegistry.kt @@ -0,0 +1,17 @@ +package com.slambang.vee_core.test + +import com.slambang.vee_core.VeeRegistry + +class VeeTestRegistry: VeeRegistry { + + private val instanceCache = mutableMapOf, Any>() + + override fun onInstance(clazz: Class, instance: T): T { + if (instanceCache[clazz] == null) { + instanceCache[clazz] = instance as Any + } + + @Suppress("UNCHECKED_CAST") + return instanceCache[clazz] as T + } +} diff --git a/vee-core/src/test/java/com/slambang/vee_core/internal/VeeImplTest.kt b/vee-core/src/test/java/com/slambang/vee_core/internal/VeeImplTest.kt index 9d9ca38..ab19491 100644 --- a/vee-core/src/test/java/com/slambang/vee_core/internal/VeeImplTest.kt +++ b/vee-core/src/test/java/com/slambang/vee_core/internal/VeeImplTest.kt @@ -5,7 +5,6 @@ import com.slambang.vee_core.exceptions.VeeClassAlreadyProvidedException import com.slambang.vee_core.exceptions.VeeClassNotProvidedException import com.slambang.vee_core.exceptions.VeeNotStartedException import org.junit.Before - import org.junit.Test class VeeImplTest { @@ -16,7 +15,7 @@ class VeeImplTest { @Before fun setup() { veeMap = mutableMapOf() - sut = VeeImpl(veeMap) + sut = VeeImpl(VeeProductionRegistry(), veeMap) } @Test @@ -156,4 +155,31 @@ class VeeImplTest { // When sut.start() } + + @Test + fun `charArray is supported`() { + // Given + sut.provide(CharArray::class.java) { "abc".toCharArray() } + sut.start() + + // When + val actual = sut.get(CharArray::class.java).concatToString() + + // Then + + assert(actual == "abc") + } + + @Test + fun `int is supported`() { + // Given + sut.provide(Int::class.java) { 123 } + sut.start() + + // When + val actual = sut.get(Int::class.java) + + // Then + assert(actual == 123) + } } diff --git a/vee-core/src/test/java/com/slambang/vee_core/internal/VeeLazyInjectDelegateTest.kt b/vee-core/src/test/java/com/slambang/vee_core/internal/VeeLazyInjectDelegateTest.kt index 75e3582..17b1a74 100644 --- a/vee-core/src/test/java/com/slambang/vee_core/internal/VeeLazyInjectDelegateTest.kt +++ b/vee-core/src/test/java/com/slambang/vee_core/internal/VeeLazyInjectDelegateTest.kt @@ -4,7 +4,7 @@ import org.junit.Test class VeeLazyInjectDelegateTest { - private val vee = VeeImpl(mutableMapOf()) + private val vee = VeeImpl(VeeProductionRegistry(), mutableMapOf()) @Test fun `value returns the same instance multiple times`() { diff --git a/vee-sample/build.gradle b/vee-sample/build.gradle index bd1f549..a4dfadb 100644 --- a/vee-sample/build.gradle +++ b/vee-sample/build.gradle @@ -14,7 +14,7 @@ android { versionCode 1 versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "com.slambang.vee.sample.VeeSampleAppRunner" } buildTypes { @@ -36,11 +36,9 @@ android { dependencies { implementation project(":vee-core") - - implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'com.google.android.material:material:1.7.0' - testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + androidTestImplementation 'org.mockito:mockito-android:2.25.1' } diff --git a/vee-sample/src/androidTest/java/com/slambang/vee/sample/InjectMockTest.kt b/vee-sample/src/androidTest/java/com/slambang/vee/sample/InjectMockTest.kt new file mode 100644 index 0000000..de3c852 --- /dev/null +++ b/vee-sample/src/androidTest/java/com/slambang/vee/sample/InjectMockTest.kt @@ -0,0 +1,36 @@ +package com.slambang.vee.sample + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.slambang.vee_core.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.BDDMockito.given + +@RunWith(AndroidJUnit4::class) +class InjectMockTest { + + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + @Test + fun test_mocks() { + // Given + activityRule.scenario.onActivity { + val mockedUseCase: MyUseCase = get() + given(mockedUseCase.getData()).willReturn("Hello, mock!") + } + + // When + onView(withId(com.slambang.vee.R.id.button)).perform(click()) + + // Then + onView(withId(com.slambang.vee.R.id.text_view)).check(matches(withText("Hello, mock!"))) + } +} diff --git a/vee-sample/src/androidTest/java/com/slambang/vee/sample/TestSampleApp.kt b/vee-sample/src/androidTest/java/com/slambang/vee/sample/TestSampleApp.kt new file mode 100644 index 0000000..9954520 --- /dev/null +++ b/vee-sample/src/androidTest/java/com/slambang/vee/sample/TestSampleApp.kt @@ -0,0 +1,18 @@ +package com.slambang.vee.sample + +import android.app.Application +import com.slambang.vee_core.provide +import com.slambang.vee_core.test.veeTest +import org.mockito.Mockito.mock + +class TestSampleApp : Application() { + + override fun onCreate() { + super.onCreate() + + veeTest { + provide { mock(MyRepository::class.java) } + provide { mock(MyUseCase::class.java) } + } + } +} diff --git a/vee-sample/src/androidTest/java/com/slambang/vee/sample/VeeSampleAppRunner.kt b/vee-sample/src/androidTest/java/com/slambang/vee/sample/VeeSampleAppRunner.kt new file mode 100644 index 0000000..3742e91 --- /dev/null +++ b/vee-sample/src/androidTest/java/com/slambang/vee/sample/VeeSampleAppRunner.kt @@ -0,0 +1,16 @@ +package com.slambang.vee.sample + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner + +@Suppress("UNUSED") +class VeeSampleAppRunner : AndroidJUnitRunner() { + + @Throws(Exception::class) + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application = super.newApplication(cl, TestSampleApp::class.java.name, context) +} diff --git a/vee-sample/src/main/java/com/slambang/vee/sample/Dependencies.kt b/vee-sample/src/main/java/com/slambang/vee/sample/Dependencies.kt index 319777a..99dd2be 100644 --- a/vee-sample/src/main/java/com/slambang/vee/sample/Dependencies.kt +++ b/vee-sample/src/main/java/com/slambang/vee/sample/Dependencies.kt @@ -5,10 +5,18 @@ package com.slambang.vee.sample * @see [SampleApp] To begin. */ -class MyRepository { - fun getDataFromServer(): String = "Hello, world!" +interface MyRepository { + fun getDataFromServer(): String } -class MyUseCase(private val repository: MyRepository) { - fun getData(): String = repository.getDataFromServer() +class MyRepositoryImpl : MyRepository { + override fun getDataFromServer(): String = "Hello, world!" +} + +interface MyUseCase { + fun getData(): String +} + +class MyUseCaseImpl(private val repository: MyRepository) : MyUseCase { + override fun getData(): String = repository.getDataFromServer() } diff --git a/vee-sample/src/main/java/com/slambang/vee/sample/SampleApp.kt b/vee-sample/src/main/java/com/slambang/vee/sample/SampleApp.kt index c9a664c..fed8408 100644 --- a/vee-sample/src/main/java/com/slambang/vee/sample/SampleApp.kt +++ b/vee-sample/src/main/java/com/slambang/vee/sample/SampleApp.kt @@ -1,9 +1,7 @@ package com.slambang.vee.sample import android.app.Application -import com.slambang.vee_core.provide -import com.slambang.vee_core.vee -import com.slambang.vee_core.get +import com.slambang.vee_core.* /** * Step 1/2: provide. @@ -18,8 +16,8 @@ class SampleApp : Application() { super.onCreate() vee { - provide { MyRepository() } - provide { MyUseCase(repository = get()) } + provide { MyRepositoryImpl() } + provide { MyUseCaseImpl(repository = get()) } } } }