Skip to content

Commit

Permalink
Merge pull request #3 from traversals/automatic
Browse files Browse the repository at this point in the history
Automatic injectors
  • Loading branch information
mgouline authored Apr 3, 2017
2 parents a87bf1b + c8c6945 commit 29fe1ee
Show file tree
Hide file tree
Showing 19 changed files with 433 additions and 51 deletions.
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,15 @@ This will provide the same instance of `name` and a new instance of `Manager` fo

### Step 2: Inject properties

Let's say you have a class `Screen` that needs these values. You need to create an injection Kapsule (hence the name) and invoke it as a function to map uninitialized delegates to your properties.
Let's say you have a class `Screen` that needs these values. You need to indicate the target module by implementing `Injects<Module>` interface with the target module and request uninitialized delegates for your properties.

```kotlin
class Screen {
private val kap = Kapsule<Module>()
private val name by kap { name }
private val manager by kap { manager }
class Screen : Injects<Module> {
private val name by required { name }
private val manager by required { manager }

init {
kap.inject(Application.module)
inject(Application.module)
}
}
```
Expand Down Expand Up @@ -139,25 +138,39 @@ object Application {

### Optional delegates

So far you've only seen non-null values, but what happens if you need to inject a nullable value? You can use the `opt` function on your Kapsule:
So far you've only seen non-null values, but what happens if you need to inject a nullable value? You can use the `optional` function on your Kapsule:

```kotlin
val kap = Kapsule<Module>()
val firstName by kap { firstName }
val lastName by kap.opt { lastName }
val firstName by required { firstName }
val lastName by optional { lastName }
```

Given both fields are strings, `firstName` is `String`, while `lastName` is `String?`. The default `kap {}` is actually shorthand for `kap.req {}`, so either can be used interchangeably.
Given both fields are strings, `firstName` is `String`, while `lastName` is `String?`.

Unlike non-null properties, nullable ones can be read even before injection (the former would throw `KotlinNullPointerException`).

### Manual injection

While the more convenient way to inject modules is by implementing the `Injects<Module>` interface, you may want to split the injection of separate modules (e.g. for testing). This can be done by creating separate instances of `Kapsule<Module>` and calling the injection methods on it.

```kotlin
class Screen {
private val kap = Kapsule<Module>()
private val name by kap.required { name }
private val manager by kap.required { manager }

init {
kap.inject(Application.module)
}
}
```

### Variable delegates

In most cases you would make the injected properties `val`, however there's no reason it can't be a `var`, which would allow you to reassign it before or after injection.

```kotlin
val kap = Kapsule<Module>()
var firstName by kap { firstName }
var firstName by required { firstName }

init {
firstName = "before"
Expand All @@ -173,15 +186,15 @@ Note that any delegates can be injected repeatedly, regardless of whether they'r
Kotlin 1.1 infers property types from the delegates, which allows for a simpler definitions:

```kotlin
val firstName by kap { firstName }
val lastName by kap.opt { lastName }
val firstName by required { firstName }
val lastName by optional { lastName }
```

However, when using 1.0, you have to specify the types explicitly:

```kotlin
val firstName by kap<String> { firstName }
val lastName by kap.opt<String?> { lastName }
val firstName by required<String> { firstName }
val lastName by optional<String?> { lastName }
```

## Samples
Expand All @@ -194,7 +207,7 @@ To use Kapsule in your project, include it as a dependency:

```gradle
dependencies {
compile "space.traversal.kapsule:kapsule-core:0.1"
compile "space.traversal.kapsule:kapsule-core:0.2"
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import kotlin.reflect.KProperty
*/
sealed class Delegate<in M, T>(internal val initializer: M.() -> T?) {

internal var value: T? = null
var value: T? = null
internal set

/**
* Initializes value from the injection module.
Expand Down
34 changes: 34 additions & 0 deletions kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2017 Traversal Space
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package space.traversal.kapsule

/**
* Injection interface.
*/
interface Injects<M> {

/**
* Fetches [Kapsule] instance and calls [Kapsule.required].
*/
fun <T> required(initializer: M.() -> T) = Kapsules.get(this).required(initializer)

/**
* Fetches [Kapsule] instance and calls [Kapsule.optional].
*/
fun <T> optional(initializer: M.() -> T?) = Kapsules.get(this).optional(initializer)

/**
* Fetches [Kapsule] instance and calls [Kapsule.inject].
*/
fun <M> Injects<M>.inject(module: M) {
Kapsules.get(this).inject(module)
}
}
11 changes: 7 additions & 4 deletions kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,23 @@ class Kapsule<M> {
* Creates and registers delegate for a required (non-null) injectable property.
*
* @param initializer Initializer function from the module context to value.
* @return Required (non-null) property delegate.
*/
fun <T> req(initializer: M.() -> T) = Delegate.Required(initializer).apply { delegates += this }
fun <T> required(initializer: M.() -> T) = Delegate.Required(initializer).apply { delegates += this }

/**
* Creates and registers delegate for an optional (nullable) injectable property.
*
* @param initializer Initializer function from the module context to value.
* @return Optional (nullable) property delegate.
*/
fun <T> opt(initializer: M.() -> T?) = Delegate.Optional(initializer).apply { delegates += this }
fun <T> optional(initializer: M.() -> T?) = Delegate.Optional(initializer).apply { delegates += this }

/**
* Shortcut for [req] by invoking the class like a function.
* Shortcut for [required] by invoking the class like a function.
*
* @param initializer Initializer function from the module context to value.
* @return Required (non-null) property delegate.
*/
operator fun <T> invoke(initializer: M.() -> T) = req(initializer)
operator fun <T> invoke(initializer: M.() -> T) = required(initializer)
}
38 changes: 38 additions & 0 deletions kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2017 Traversal Space
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package space.traversal.kapsule

import space.traversal.kapsule.util.CallerMap

/**
* Static storage of [Kapsule] instances.
*/
object Kapsules {

internal val instances = CallerMap<Injects<*>, Kapsule<*>>()

/**
* Retrieves active instance of [Kapsule] or creates a new one.
*
* @param caller Injection caller instance, used as lookup key.
* @return Stored or new instance.
*/
fun <M> get(caller: Injects<M>) = fetch(caller) ?: Kapsule<M>().apply { instances[caller] = this }

/**
* Fetches stored instance.
*
* @param caller Injection caller instance, used as lookup key.
* @return Stored instance or null.
*/
@Suppress("UNCHECKED_CAST")
internal fun <M> fetch(caller: Injects<M>) = instances[caller] as? Kapsule<M>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2017 Traversal Space
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package space.traversal.kapsule.util

import java.util.*

/**
* Utility map for storing caller instances.
*
* Implements a cache such that the most recent entry is returned at constant time.
*/
internal class CallerMap<K, V> : WeakHashMap<K, V>() {

@Volatile internal var lastKey: K? = null
@Volatile internal var lastValue: V? = null

override fun containsKey(key: K) = key == lastKey || super.containsKey(key)

override fun containsValue(value: V): Boolean {
return super.containsValue(value)
}

operator override fun get(key: K): V? {
return if (key == lastKey) {
lastValue
} else {
val value = super.get(key)
setLast(key, value)
return value
}
}

operator fun set(key: K, value: V) = put(key, value)

override fun put(key: K, value: V): V? {
setLast(key, value)
return super.put(key, value)
}

override fun remove(key: K): V? {
if (key == lastKey) {
setLast()
}
return super.remove(key)
}

override fun clear() {
super.clear()
setLast()
}

/**
* Set last key-value pair cache.
*
* @param key Map key.
* @param value Map value.
*/
private fun setLast(key: K? = null, value: V? = null) {
lastKey = key
lastValue = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import kotlin.reflect.KProperty
/**
* Test case for [Delegate].
*/
class DelegateTestCase : TestCase() {
class DelegateTest : TestCase() {

@Test fun testInitialize_required() {
val delegate = Delegate.Required<RequiredModule, String> { value }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ import kotlin.reflect.KProperty
/**
* Test case for [Kapsule].
*/
class KapsuleTestCase : TestCase() {
class KapsuleTest : TestCase() {

@Test fun testRequired() {
val kap = Kapsule<MultiModule>()
assertTrue(kap.req { reqInt } is Delegate.Required)
assertTrue(kap.required { reqInt } is Delegate.Required)
assertTrue(kap<Int> { reqInt } is Delegate.Required)
assertTrue(kap.opt<Int?> { reqInt } is Delegate.Optional)
assertTrue(kap.optional<Int?> { reqInt } is Delegate.Optional)
}

@Test fun testDelegates() {
val kap = Kapsule<MultiModule>()
for (i in 0..2) {
val initializer: (MultiModule.() -> Int) = { reqInt }
kap.req(initializer)
kap.required(initializer)
assertEquals(i + 1, kap.delegates.count())
assertEquals(initializer, kap.delegates[i].initializer)
}
Expand All @@ -43,7 +43,7 @@ class KapsuleTestCase : TestCase() {
val kap = Kapsule<MultiModule>()
val prop = Mockito.mock(KProperty::class.java)

val optStringDelegate = kap.opt<String?> { optString }
val optStringDelegate = kap.optional<String?> { optString }
val reqIntDelegate = kap<Int> { reqInt }

listOf(MultiModule("test1", 3, "abc123"),
Expand Down Expand Up @@ -81,7 +81,7 @@ class KapsuleTestCase : TestCase() {
class Target {

val kap = Kapsule<MultiModule>()
var optString by kap.opt<String?> { optString }
var optString by kap.optional<String?> { optString }
val reqInt by kap<Int> { reqInt }

fun inject(module: MultiModule) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2017 Traversal Space
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package space.traversal.kapsule

import junit.framework.TestCase
import org.junit.Test
import org.omg.CORBA.Object

/**
* Test case for [Kapsules].
*/
class KapsulesTest : TestCase() {

@Test fun testFetch() {
Kapsules.instances.clear()
val caller = object : Injects<Object> {}
assertEquals(null, Kapsules.fetch(caller))
assertEquals(0, Kapsules.instances.size)

}

@Test fun testGet() {
Kapsules.instances.clear()
val caller = object : Injects<Object> {}
val kap = Kapsules.get(caller)
assertEquals(kap, Kapsules.get(caller))
assertEquals(1, Kapsules.instances.size)
}
}
Loading

0 comments on commit 29fe1ee

Please sign in to comment.