Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion vee-core/src/main/java/com/slambang/vee_core/Vee.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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!!
}
}
}
}
5 changes: 3 additions & 2 deletions vee-core/src/main/java/com/slambang/vee_core/VeeExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -46,4 +47,4 @@ inline fun <reified T> get(): T = Vee.instance().get(T::class.java)
*
* @return A [Lazy] (caching) delegate which resolves the required object
*/
inline fun <reified T> inject(): Lazy<T> = newInjectDelegate(T::class.java)
inline fun <reified T> inject(): Lazy<T> = newVeeInjectDelegate(T::class.java)
17 changes: 17 additions & 0 deletions vee-core/src/main/java/com/slambang/vee_core/VeeRegistry.kt
Original file line number Diff line number Diff line change
@@ -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 <T> onInstance(clazz: Class<T>, instance: T): T
}
31 changes: 18 additions & 13 deletions vee-core/src/main/java/com/slambang/vee_core/internal/VeeImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Class<*>, Vertex<*>>
) : Vee {

Expand All @@ -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<Class<*>>
) {
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<Class<*>> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.slambang.vee_core.internal
import com.slambang.vee_core.Vee

// Hides the VeeLazyInjectDelegate internal constructor
fun <T> newInjectDelegate(clazz: Class<T>): VeeLazyInjectDelegate<T> =
fun <T> newVeeInjectDelegate(clazz: Class<T>): VeeLazyInjectDelegate<T> =
VeeLazyInjectDelegate(Vee.instance(), clazz)

class VeeLazyInjectDelegate<T> internal constructor(
Expand All @@ -21,5 +21,6 @@ class VeeLazyInjectDelegate<T> internal constructor(
}
}

@Synchronized
override fun isInitialized(): Boolean = instance != null
}
Original file line number Diff line number Diff line change
@@ -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 <T> onInstance(clazz: Class<T>, instance: T): T = instance
}
20 changes: 20 additions & 0 deletions vee-core/src/main/java/com/slambang/vee_core/test/VeeTestExt.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.slambang.vee_core.test

import com.slambang.vee_core.VeeRegistry

class VeeTestRegistry: VeeRegistry {

private val instanceCache = mutableMapOf<Class<*>, Any>()

override fun <T> onInstance(clazz: Class<T>, instance: T): T {
if (instanceCache[clazz] == null) {
instanceCache[clazz] = instance as Any
}

@Suppress("UNCHECKED_CAST")
return instanceCache[clazz] as T
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -16,7 +15,7 @@ class VeeImplTest {
@Before
fun setup() {
veeMap = mutableMapOf()
sut = VeeImpl(veeMap)
sut = VeeImpl(VeeProductionRegistry(), veeMap)
}

@Test
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`() {
Expand Down
8 changes: 3 additions & 5 deletions vee-sample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ android {
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "com.slambang.vee.sample.VeeSampleAppRunner"
}

buildTypes {
Expand All @@ -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'
}
Original file line number Diff line number Diff line change
@@ -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!")))
}
}
Original file line number Diff line number Diff line change
@@ -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) }
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 12 additions & 4 deletions vee-sample/src/main/java/com/slambang/vee/sample/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Loading