Skip to content
Permalink
Browse files

hey, look, singletons work!

  • Loading branch information...
ThatJoeMoore committed Jun 25, 2019
1 parent 69c50a5 commit 5cb961a975f9cd868c09861e13558aec5ebef503
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinScriptingSettings">
<option name="isAutoReloadEnabled" value="true" />
</component>
</project>
@@ -1,7 +1,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.3.31"
kotlin("jvm") version "1.3.40"
}

group = "com.thatjoemoore.kobble"
@@ -14,6 +14,8 @@ repositories {
dependencies {
implementation(kotlin("stdlib-jdk8"))
testImplementation("org.junit.jupiter:junit-jupiter:5.4.2")
testImplementation(kotlin("reflect"))
testImplementation(kotlin("test-junit5"))
}

tasks.withType<KotlinCompile> {
@@ -0,0 +1,26 @@
package com.thatjoemoore.kobble

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

interface KobbleBinding<T> : ReadOnlyProperty<KobbleModule, T>

internal abstract class BaseBinding<T> : KobbleBinding<T> {
}

internal abstract class BaseSingletonBinding<T>: BaseBinding<T>() {
protected abstract val value: T

override fun getValue(thisRef: KobbleModule, property: KProperty<*>): T {
return value
}
}

internal class LazySingletonBinding<T>(supplier: Supplier<T>) : BaseSingletonBinding<T>() {
override val value by lazy(supplier)
}

// TODO: Eventually, make the eagerness a little less eager - only call once we start retrieving bindings
internal class EagerSingletonBinding<T>(supplier: Supplier<T>) : BaseSingletonBinding<T>() {
override val value = supplier()
}
@@ -0,0 +1,3 @@
package com.thatjoemoore.kobble

typealias Supplier<T> = () -> T
@@ -0,0 +1,45 @@
package com.thatjoemoore.kobble

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

abstract class KobbleModule {

protected fun <T> singleton(
lazy: Boolean = true,
supplier: Supplier<T>
): BindingFactory<T> {
return buildBinding {
if (lazy) {
LazySingletonBinding(supplier)
} else {
EagerSingletonBinding(supplier)
}
}
}

private val _bindings: MutableBindings = mutableMapOf()

internal val bindings: Bindings = _bindings

private inline fun <T> buildBinding(crossinline buildBinding: () -> KobbleBinding<T>): BindingFactory<T> {
return object : BindingFactory<T> {
override fun provideDelegate(thisRef: KobbleModule, prop: KProperty<*>): ReadOnlyProperty<KobbleModule, T> {
val binding = buildBinding()
thisRef._bindings[prop.name] = binding
return binding
}
}
}

}

interface BindingFactory<T> {
operator fun provideDelegate(
thisRef: KobbleModule,
prop: KProperty<*>
): ReadOnlyProperty<KobbleModule, T>
}

internal typealias Bindings = Map<String, KobbleBinding<*>>
internal typealias MutableBindings = MutableMap<String, KobbleBinding<*>>
@@ -0,0 +1,40 @@
package com.thatjoemoore.kobble

import org.junit.jupiter.api.Test
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

internal class EagerSingletonBindingTest {

@Test
fun `always returns same instance`() {
val fixture = EagerSingletonBinding { Any() }

val result = fixture.testInvoke()
assertNotNull(result)
assertSame(result, fixture.testInvoke(), "expected fixture.foo to return the same object")
}

@Test
fun `eagerly calls the initializer`() {
var called = false

val fixture = EagerSingletonBinding {
called = true
Any()
}

assertTrue(called, "Expected initializer to have been called")
}

private fun <T> EagerSingletonBinding<T>.testInvoke(): T {
return this.getValue(FakeModule, FakeModule::foo)
}

private object FakeModule : KobbleModule() {
val foo: Any = Any()
}

}
@@ -0,0 +1,65 @@
package com.thatjoemoore.kobble

import com.thatjoemoore.kobble.test.openDelegate
import org.junit.jupiter.api.Test
import kotlin.reflect.KProperty

import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

internal class KobbleModuleTest {

@Test
fun `singleton() creates a lazy singleton binding`() {
val obj = Any()
val fixture = object: KobbleModule() {
val foo by singleton { obj }
}

val delegate = fixture::foo.openDelegate()
assertTrue(delegate is LazySingletonBinding<*>, "expected LazySingletonBinding, got $delegate")
}

@Test
fun `singleton(lazy = false) creates an eager singleton binding`() {
val obj = Any()
val fixture = object: KobbleModule() {
val foo by singleton(lazy = false) { obj }
}

val delegate = fixture::foo.openDelegate()
assertTrue(delegate is EagerSingletonBinding<*>, "expected EagerSingletonBinding, got $delegate")
}

@Test
fun `module keeps track of registered bindings`() {
val fixture = object: KobbleModule() {
val foo by singleton(lazy = false) { Any() }
val bar: String by singleton { "baz" }
}

val result = fixture.bindings

assertEquals(2, result.size)

assertHasBinding<Any, EagerSingletonBinding<Any>>(
result,
fixture::foo
)
assertHasBinding<String, LazySingletonBinding<String>>(
result,
fixture::bar
)
}

private inline fun <reified T, reified B: KobbleBinding<T>> assertHasBinding(
bindings: Bindings,
prop: KProperty<T>
) {
val binding = bindings[prop.name]
assertNotNull(binding, "expected property ${prop.name} to have been bound")
assertTrue(binding is B, "Expected binding to be of type ${B::class.simpleName}, but was ${binding::class.simpleName}")
}

}
@@ -0,0 +1,44 @@
package com.thatjoemoore.kobble

import org.junit.jupiter.api.Test
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

internal class LazySingletonBindingTest {

@Test
fun `always returns same instance`() {
val fixture = LazySingletonBinding { Any() }

val result = fixture.testInvoke()
assertNotNull(result)
assertSame(result, fixture.testInvoke(), "expected fixture.foo to return the same object")
}

@Test
fun `lazily calls the initializer`() {
var called = false

val fixture = LazySingletonBinding {
called = true
Any()
}

assertFalse(called, "Expected initializer to not have been called yet")

fixture.testInvoke()

assertTrue(called, "Expected initializer to have been called")
}

private fun <T> LazySingletonBinding<T>.testInvoke(): T {
return this.getValue(FakeModule, FakeModule::foo)
}

private object FakeModule : KobbleModule() {
val foo: Any = Any()
}

}
@@ -0,0 +1,14 @@
package com.thatjoemoore.kobble.test

import kotlin.reflect.KProperty0
import kotlin.reflect.jvm.isAccessible

fun KProperty0<*>.openDelegate(): Any? {
val acc = isAccessible
try {
isAccessible = true
return getDelegate()
} finally {
isAccessible = acc
}
}

0 comments on commit 5cb961a

Please sign in to comment.
You can’t perform that action at this time.