From 7710e1b4910d0241ad08d29f4fbbaf13ce377903 Mon Sep 17 00:00:00 2001 From: Mike Gouline Date: Sat, 1 Apr 2017 17:03:30 +1100 Subject: [PATCH 1/4] Working prototype of automatic injector --- .../kotlin/space/traversal/kapsule/Injects.kt | 17 ++++++++++ .../space/traversal/kapsule/Kapsules.kt | 33 +++++++++++++++++++ .../space/traversal/kapsule/demo/Demo.kt | 13 ++++---- 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt create mode 100644 kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt diff --git a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt new file mode 100644 index 0000000..246e00d --- /dev/null +++ b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt @@ -0,0 +1,17 @@ +/* + * 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 { +} \ No newline at end of file diff --git a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt new file mode 100644 index 0000000..1616e34 --- /dev/null +++ b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt @@ -0,0 +1,33 @@ +/* + * 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 java.util.* + +/** + * Created by mgouline on 1/4/17. + */ +object Kapsules { + + private val instances = WeakHashMap, Kapsule<*>>() + + /** + * + */ + @Suppress("UNCHECKED_CAST") + fun get(caller: Injects) = + instances[caller] as? Kapsule ?: Kapsule().apply { instances[caller] = this } +} + +/** + * + */ +val Injects.kap: Kapsule get() = Kapsules.get(this) diff --git a/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt b/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt index fef2160..3cb250a 100644 --- a/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt +++ b/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt @@ -10,8 +10,9 @@ package space.traversal.kapsule.demo -import space.traversal.kapsule.Kapsule +import space.traversal.kapsule.Injects import space.traversal.kapsule.demo.di.Module +import space.traversal.kapsule.kap fun main(args: Array) { val demo = Demo(Context()) @@ -23,13 +24,11 @@ fun main(args: Array) { /** * Demo app definition. */ -class Demo(context: Context) { +class Demo(context: Context) : Injects { - private val kap = Kapsule() - - var firstName by kap { firstName } - val lastName by kap.opt { lastName } - val emails by kap { emails } + var firstName by kap.req { firstName } + val lastName by kap.opt { lastName } + val emails by kap.req { emails } init { kap.inject(context.module) From 2e0d70e75ab37d963be4b96f2dba51f406668b47 Mon Sep 17 00:00:00 2001 From: Mike Gouline Date: Sat, 1 Apr 2017 23:43:26 +1100 Subject: [PATCH 2/4] Caller map, tests and docs --- README.md | 45 +++++++----- .../kotlin/space/traversal/kapsule/Injects.kt | 21 +++++- .../kotlin/space/traversal/kapsule/Kapsule.kt | 11 +-- .../space/traversal/kapsule/Kapsules.kt | 25 ++++--- .../space/traversal/kapsule/util/CallerMap.kt | 70 +++++++++++++++++++ .../{DelegateTestCase.kt => DelegateTest.kt} | 2 +- .../{KapsuleTestCase.kt => KapsuleTest.kt} | 12 ++-- .../space/traversal/kapsule/KapsulesTest.kt | 37 ++++++++++ .../traversal/kapsule/util/CallerMapTest.kt | 62 ++++++++++++++++ .../traversal/kapsule/demo/data/MainDao.kt | 9 ++- .../kapsule/demo/presenter/HomePresenter.kt | 8 +-- .../kapsule/demo/view/HomeActivity.kt | 8 +-- .../space/traversal/kapsule/demo/Demo.kt | 9 ++- 13 files changed, 262 insertions(+), 57 deletions(-) create mode 100644 kapsule-core/src/main/kotlin/space/traversal/kapsule/util/CallerMap.kt rename kapsule-core/src/test/kotlin/space/traversal/kapsule/{DelegateTestCase.kt => DelegateTest.kt} (98%) rename kapsule-core/src/test/kotlin/space/traversal/kapsule/{KapsuleTestCase.kt => KapsuleTest.kt} (90%) create mode 100644 kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsulesTest.kt create mode 100644 kapsule-core/src/test/kotlin/space/traversal/kapsule/util/CallerMapTest.kt diff --git a/README.md b/README.md index cd3c11b..5627e39 100644 --- a/README.md +++ b/README.md @@ -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` interface with the target module and request uninitialized delegates for your properties. ```kotlin -class Screen { - private val kap = Kapsule() - private val name by kap { name } - private val manager by kap { manager } +class Screen : Injects { + private val name by required { name } + private val manager by required { manager } init { - kap.inject(Application.module) + inject(Application.module) } } ``` @@ -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() -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` 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` and calling the injection methods on it. + +```kotlin +class Screen { + private val kap = Kapsule() + 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() -var firstName by kap { firstName } +var firstName by required { firstName } init { firstName = "before" @@ -174,14 +187,14 @@ Kotlin 1.1 infers property types from the delegates, which allows for a simpler ```kotlin val firstName by kap { firstName } -val lastName by kap.opt { lastName } +val lastName by kap.optional { lastName } ``` However, when using 1.0, you have to specify the types explicitly: ```kotlin val firstName by kap { firstName } -val lastName by kap.opt { lastName } +val lastName by kap.optional { lastName } ``` ## Samples @@ -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" } ``` diff --git a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt index 246e00d..41015bd 100644 --- a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt +++ b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Injects.kt @@ -13,5 +13,22 @@ package space.traversal.kapsule /** * Injection interface. */ -interface Injects { -} \ No newline at end of file +interface Injects { + + /** + * Fetches [Kapsule] instance and calls [Kapsule.required]. + */ + fun required(initializer: M.() -> T) = Kapsules.get(this).required(initializer) + + /** + * Fetches [Kapsule] instance and calls [Kapsule.optional]. + */ + fun optional(initializer: M.() -> T?) = Kapsules.get(this).optional(initializer) + + /** + * Fetches [Kapsule] instance and calls [Kapsule.inject]. + */ + fun Injects.inject(module: M) { + Kapsules.get(this).inject(module) + } +} diff --git a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsule.kt b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsule.kt index d6b9868..2500406 100644 --- a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsule.kt +++ b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsule.kt @@ -32,20 +32,23 @@ class Kapsule { * 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 req(initializer: M.() -> T) = Delegate.Required(initializer).apply { delegates += this } + fun 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 opt(initializer: M.() -> T?) = Delegate.Optional(initializer).apply { delegates += this } + fun 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 invoke(initializer: M.() -> T) = req(initializer) + operator fun invoke(initializer: M.() -> T) = required(initializer) } diff --git a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt index 1616e34..4021b06 100644 --- a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt +++ b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Kapsules.kt @@ -10,24 +10,29 @@ package space.traversal.kapsule -import java.util.* +import space.traversal.kapsule.util.CallerMap /** - * Created by mgouline on 1/4/17. + * Static storage of [Kapsule] instances. */ object Kapsules { - private val instances = WeakHashMap, Kapsule<*>>() + internal val instances = CallerMap, 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 get(caller: Injects) = fetch(caller) ?: Kapsule().apply { instances[caller] = this } + + /** + * Fetches stored instance. + * + * @param caller Injection caller instance, used as lookup key. + * @return Stored instance or null. */ @Suppress("UNCHECKED_CAST") - fun get(caller: Injects) = - instances[caller] as? Kapsule ?: Kapsule().apply { instances[caller] = this } + internal fun fetch(caller: Injects) = instances[caller] as? Kapsule } - -/** - * - */ -val Injects.kap: Kapsule get() = Kapsules.get(this) diff --git a/kapsule-core/src/main/kotlin/space/traversal/kapsule/util/CallerMap.kt b/kapsule-core/src/main/kotlin/space/traversal/kapsule/util/CallerMap.kt new file mode 100644 index 0000000..6ff5433 --- /dev/null +++ b/kapsule-core/src/main/kotlin/space/traversal/kapsule/util/CallerMap.kt @@ -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 : WeakHashMap() { + + @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 + } +} \ No newline at end of file diff --git a/kapsule-core/src/test/kotlin/space/traversal/kapsule/DelegateTestCase.kt b/kapsule-core/src/test/kotlin/space/traversal/kapsule/DelegateTest.kt similarity index 98% rename from kapsule-core/src/test/kotlin/space/traversal/kapsule/DelegateTestCase.kt rename to kapsule-core/src/test/kotlin/space/traversal/kapsule/DelegateTest.kt index 764b32a..ae9cfe8 100644 --- a/kapsule-core/src/test/kotlin/space/traversal/kapsule/DelegateTestCase.kt +++ b/kapsule-core/src/test/kotlin/space/traversal/kapsule/DelegateTest.kt @@ -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 { value } diff --git a/kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsuleTestCase.kt b/kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsuleTest.kt similarity index 90% rename from kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsuleTestCase.kt rename to kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsuleTest.kt index 97c090f..1faa23b 100644 --- a/kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsuleTestCase.kt +++ b/kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsuleTest.kt @@ -18,20 +18,20 @@ import kotlin.reflect.KProperty /** * Test case for [Kapsule]. */ -class KapsuleTestCase : TestCase() { +class KapsuleTest : TestCase() { @Test fun testRequired() { val kap = Kapsule() - assertTrue(kap.req { reqInt } is Delegate.Required) + assertTrue(kap.required { reqInt } is Delegate.Required) assertTrue(kap { reqInt } is Delegate.Required) - assertTrue(kap.opt { reqInt } is Delegate.Optional) + assertTrue(kap.optional { reqInt } is Delegate.Optional) } @Test fun testDelegates() { val kap = Kapsule() 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) } @@ -43,7 +43,7 @@ class KapsuleTestCase : TestCase() { val kap = Kapsule() val prop = Mockito.mock(KProperty::class.java) - val optStringDelegate = kap.opt { optString } + val optStringDelegate = kap.optional { optString } val reqIntDelegate = kap { reqInt } listOf(MultiModule("test1", 3, "abc123"), @@ -81,7 +81,7 @@ class KapsuleTestCase : TestCase() { class Target { val kap = Kapsule() - var optString by kap.opt { optString } + var optString by kap.optional { optString } val reqInt by kap { reqInt } fun inject(module: MultiModule) { diff --git a/kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsulesTest.kt b/kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsulesTest.kt new file mode 100644 index 0000000..aeae856 --- /dev/null +++ b/kapsule-core/src/test/kotlin/space/traversal/kapsule/KapsulesTest.kt @@ -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 {} + assertEquals(null, Kapsules.fetch(caller)) + assertEquals(0, Kapsules.instances.size) + + } + + @Test fun testGet() { + Kapsules.instances.clear() + val caller = object : Injects {} + val kap = Kapsules.get(caller) + assertEquals(kap, Kapsules.get(caller)) + assertEquals(1, Kapsules.instances.size) + } +} \ No newline at end of file diff --git a/kapsule-core/src/test/kotlin/space/traversal/kapsule/util/CallerMapTest.kt b/kapsule-core/src/test/kotlin/space/traversal/kapsule/util/CallerMapTest.kt new file mode 100644 index 0000000..465b17a --- /dev/null +++ b/kapsule-core/src/test/kotlin/space/traversal/kapsule/util/CallerMapTest.kt @@ -0,0 +1,62 @@ +/* + * 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 junit.framework.TestCase +import org.junit.Test + +/** + * Test case for [CallerMap]. + */ +class CallerMapTest : TestCase() { + + @Test fun testPut() { + val map = CallerMap() + assertEquals(null, map.lastKey) + assertEquals(null, map.lastValue) + + map["key1"] = "value1" + assertEquals("key1", map.lastKey) + assertEquals("value1", map.lastValue) + + map["key2"] = "value2" + assertEquals("key2", map.lastKey) + assertEquals("value2", map.lastValue) + } + + @Test fun testGet() { + val map = CallerMap() + + map["key1"] = "value1" + map["key2"] = "value2" + map["key1"] + assertEquals("key1", map.lastKey) + assertEquals("value1", map.lastValue) + } + + @Test fun testRemove() { + val map = CallerMap() + + map["key1"] = "value1" + map.remove("key1") + assertEquals(null, map.lastKey) + assertEquals(null, map.lastValue) + } + + @Test fun testClear() { + val map = CallerMap() + + map["key1"] = "value1" + map.clear() + assertEquals(null, map.lastKey) + assertEquals(null, map.lastValue) + } +} diff --git a/samples/android/src/main/java/space/traversal/kapsule/demo/data/MainDao.kt b/samples/android/src/main/java/space/traversal/kapsule/demo/data/MainDao.kt index db47179..df64ca0 100644 --- a/samples/android/src/main/java/space/traversal/kapsule/demo/data/MainDao.kt +++ b/samples/android/src/main/java/space/traversal/kapsule/demo/data/MainDao.kt @@ -11,24 +11,23 @@ package space.traversal.kapsule.demo.data import android.content.Context -import space.traversal.kapsule.Kapsule +import space.traversal.kapsule.Injects import space.traversal.kapsule.demo.App import space.traversal.kapsule.demo.di.Module /** * Main implementation of [Dao]. */ -class MainDao(context: Context) : Dao { +class MainDao(context: Context) : Dao, Injects { private companion object { private val KEY_COUNT = "count" } - private val kap = Kapsule() - private val prefs by kap { sharedPreferences } + private val prefs by required { sharedPreferences } init { - kap.inject(App.module(context)) + inject(App.module(context)) } override fun fetchCount() = prefs.getInt(KEY_COUNT, 0) diff --git a/samples/android/src/main/java/space/traversal/kapsule/demo/presenter/HomePresenter.kt b/samples/android/src/main/java/space/traversal/kapsule/demo/presenter/HomePresenter.kt index d731079..89e9404 100644 --- a/samples/android/src/main/java/space/traversal/kapsule/demo/presenter/HomePresenter.kt +++ b/samples/android/src/main/java/space/traversal/kapsule/demo/presenter/HomePresenter.kt @@ -11,6 +11,7 @@ package space.traversal.kapsule.demo.presenter import android.content.Context +import space.traversal.kapsule.Injects import space.traversal.kapsule.Kapsule import space.traversal.kapsule.demo.App import space.traversal.kapsule.demo.di.Module @@ -18,13 +19,12 @@ import space.traversal.kapsule.demo.di.Module /** * Presenter for home screen. */ -class HomePresenter(context: Context) : Presenter() { +class HomePresenter(context: Context) : Presenter(), Injects { - private val kap = Kapsule() - private val dao by kap { dao } + private val dao by required { dao } init { - kap.inject(App.module(context)) + inject(App.module(context)) } /** diff --git a/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt b/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt index 7182198..8ad7e88 100644 --- a/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt +++ b/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt @@ -13,6 +13,7 @@ package space.traversal.kapsule.demo.view import android.os.Bundle import android.support.v7.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_main.* +import space.traversal.kapsule.Injects import space.traversal.kapsule.Kapsule import space.traversal.kapsule.demo.App import space.traversal.kapsule.demo.R @@ -23,17 +24,16 @@ import space.traversal.kapsule.demo.presenter.HomeView /** * View for [HomePresenter]. */ -class HomeActivity : AppCompatActivity(), HomeView { +class HomeActivity : AppCompatActivity(), HomeView, Injects { - private val kap = Kapsule() - private val inflater by kap { layoutInflater } + private val inflater by required { layoutInflater } private lateinit var presenter: HomePresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - kap.inject(App.module(this)) + inject(App.module(this)) presenter = HomePresenter(this).also { it.attach(this) } diff --git a/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt b/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt index 3cb250a..5503232 100644 --- a/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt +++ b/samples/simple/src/main/kotlin/space/traversal/kapsule/demo/Demo.kt @@ -12,7 +12,6 @@ package space.traversal.kapsule.demo import space.traversal.kapsule.Injects import space.traversal.kapsule.demo.di.Module -import space.traversal.kapsule.kap fun main(args: Array) { val demo = Demo(Context()) @@ -26,11 +25,11 @@ fun main(args: Array) { */ class Demo(context: Context) : Injects { - var firstName by kap.req { firstName } - val lastName by kap.opt { lastName } - val emails by kap.req { emails } + var firstName by required { firstName } + val lastName by optional { lastName } + val emails by required { emails } init { - kap.inject(context.module) + inject(context.module) } } From 2dfe3081d306df983c06e90b46819545686ea3f0 Mon Sep 17 00:00:00 2001 From: Mike Gouline Date: Sat, 1 Apr 2017 23:51:54 +1100 Subject: [PATCH 3/4] Update docs --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5627e39..b0963ce 100644 --- a/README.md +++ b/README.md @@ -186,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.optional { 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 { firstName } -val lastName by kap.optional { lastName } +val firstName by required { firstName } +val lastName by optional { lastName } ``` ## Samples From c8c69452c608b0a91689983b93f9368e70e4d360 Mon Sep 17 00:00:00 2001 From: Mike Gouline Date: Sun, 2 Apr 2017 15:37:54 +1000 Subject: [PATCH 4/4] Improve Java support --- .../space/traversal/kapsule/Delegate.kt | 3 +- samples/android/src/main/AndroidManifest.xml | 3 + .../kapsule/demo/view/CounterActivity.java | 65 +++++++++++++++++++ .../kapsule/demo/view/HomeActivity.kt | 16 +++++ .../src/main/res/layout/activity_counter.xml | 20 ++++++ samples/android/src/main/res/menu/main.xml | 20 ++++++ .../android/src/main/res/values/strings.xml | 3 + 7 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 samples/android/src/main/java/space/traversal/kapsule/demo/view/CounterActivity.java create mode 100644 samples/android/src/main/res/layout/activity_counter.xml create mode 100644 samples/android/src/main/res/menu/main.xml diff --git a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Delegate.kt b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Delegate.kt index 42f1d97..2fd9299 100644 --- a/kapsule-core/src/main/kotlin/space/traversal/kapsule/Delegate.kt +++ b/kapsule-core/src/main/kotlin/space/traversal/kapsule/Delegate.kt @@ -21,7 +21,8 @@ import kotlin.reflect.KProperty */ sealed class Delegate(internal val initializer: M.() -> T?) { - internal var value: T? = null + var value: T? = null + internal set /** * Initializes value from the injection module. diff --git a/samples/android/src/main/AndroidManifest.xml b/samples/android/src/main/AndroidManifest.xml index 751d2e3..59bbee8 100644 --- a/samples/android/src/main/AndroidManifest.xml +++ b/samples/android/src/main/AndroidManifest.xml @@ -26,6 +26,9 @@ + \ No newline at end of file diff --git a/samples/android/src/main/java/space/traversal/kapsule/demo/view/CounterActivity.java b/samples/android/src/main/java/space/traversal/kapsule/demo/view/CounterActivity.java new file mode 100644 index 0000000..5769718 --- /dev/null +++ b/samples/android/src/main/java/space/traversal/kapsule/demo/view/CounterActivity.java @@ -0,0 +1,65 @@ +/* + * 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.demo.view; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; +import android.widget.TextView; +import kotlin.jvm.functions.Function1; +import space.traversal.kapsule.Delegate; +import space.traversal.kapsule.Kapsule; +import space.traversal.kapsule.demo.App; +import space.traversal.kapsule.demo.R; +import space.traversal.kapsule.demo.data.Dao; +import space.traversal.kapsule.demo.di.Module; + +/** + * Counter activity showcasing that Kapsule still works with Java, even if it's not pretty. + */ +public class CounterActivity extends AppCompatActivity { + + private final Kapsule mKap = new Kapsule<>(); + private final Delegate mDao = mKap.required(new Function1() { + @Override + public Dao invoke(Module module) { + return module.getDao(); + } + }); + + private TextView mCounterTextView; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_counter); + mKap.inject(App.Companion.module(this)); + + mCounterTextView = (TextView) findViewById(R.id.txt_counter); + } + + @Override + protected void onStart() { + super.onStart(); + + updateCount(); + } + + /** + * Fetches count from the DAO and applies it to the text view. + */ + private void updateCount() { + if (mDao.getValue() != null) { + String text = Integer.toString(mDao.getValue().fetchCount()); + mCounterTextView.setText(text); + } + } +} diff --git a/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt b/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt index 8ad7e88..d6baf3e 100644 --- a/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt +++ b/samples/android/src/main/java/space/traversal/kapsule/demo/view/HomeActivity.kt @@ -10,8 +10,11 @@ package space.traversal.kapsule.demo.view +import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity +import android.view.Menu +import android.view.MenuItem import kotlinx.android.synthetic.main.activity_main.* import space.traversal.kapsule.Injects import space.traversal.kapsule.Kapsule @@ -43,6 +46,19 @@ class HomeActivity : AppCompatActivity(), HomeView, Injects { presenter.load() } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem?) = when (item?.itemId) { + R.id.menu_counter -> { + startActivity(Intent(this, CounterActivity::class.java)) + true + } + else -> super.onOptionsItemSelected(item) + } + override fun onDestroy() { presenter.detach() super.onDestroy() diff --git a/samples/android/src/main/res/layout/activity_counter.xml b/samples/android/src/main/res/layout/activity_counter.xml new file mode 100644 index 0000000..f7e7bf5 --- /dev/null +++ b/samples/android/src/main/res/layout/activity_counter.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/samples/android/src/main/res/menu/main.xml b/samples/android/src/main/res/menu/main.xml new file mode 100644 index 0000000..f1dcbeb --- /dev/null +++ b/samples/android/src/main/res/menu/main.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/android/src/main/res/values/strings.xml b/samples/android/src/main/res/values/strings.xml index e328485..b8e609a 100644 --- a/samples/android/src/main/res/values/strings.xml +++ b/samples/android/src/main/res/values/strings.xml @@ -11,6 +11,9 @@ Kapsule Demo + + Counter + Add Remove