mirror of
https://github.com/jlengrand/atrium.git
synced 2026-03-10 08:01:19 +00:00
implement loadService for JS platform
- move checking for single service to common
- update kbox to 0.11.1 so that we can use forEachRemaining in common
module
- remove SingleServiceLoader from jvm incl Spec
- transform Spec into JUnitTest and implement in common module
- introduce private multi-map serviceRegistry (have to check if this
naive implementation suffices later on)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ out
|
||||
.gradle
|
||||
.idea
|
||||
*.iml
|
||||
node_modules
|
||||
|
||||
@@ -5,7 +5,7 @@ buildscript {
|
||||
def translationProjects = subprojects.findAll { it.projectDir.path.contains("translations") }
|
||||
ext {
|
||||
// main
|
||||
kbox_version = '0.10.0'
|
||||
kbox_version = '0.11.1'
|
||||
kbox = { "ch.tutteli.kbox:kbox:$kbox_version" }
|
||||
kotlin_version = '1.2.71'
|
||||
mockito_kotlin_version = '1.5.0'
|
||||
|
||||
@@ -2,4 +2,8 @@ description = 'API of the core of Atrium as common module'
|
||||
|
||||
dependencies {
|
||||
compile "ch.tutteli.kbox:kbox-common:$kbox_version", excludeKotlin
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-common"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common"
|
||||
testCompile prefixedProject('api-cc-infix-en_GB-common')
|
||||
testCompile prefixedProject('verbs-internal-common')
|
||||
}
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
package ch.tutteli.atrium.core.polyfills
|
||||
|
||||
import ch.tutteli.kbox.forEachRemaining
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Loads the service for the given [kClass] and throws an [IllegalStateException] if it finds more than one.
|
||||
*
|
||||
* @return The loaded service.
|
||||
* @throws IllegalStateException in case more than one service is found.
|
||||
*
|
||||
* @throws NoSuchElementException in case there is no service found for [kClass].
|
||||
* @throws IllegalStateException in case there is more than one service found for [kClass].
|
||||
*/
|
||||
expect fun <T: Any> loadSingleService(kClass: KClass<T>): T
|
||||
expect fun <T : Any> loadSingleService(kClass: KClass<T>): T
|
||||
|
||||
/**
|
||||
* Loads all available service for the given [kClass].
|
||||
*
|
||||
* @return The loaded services as a [Sequence].
|
||||
*/
|
||||
expect fun <T: Any> loadServices(kClass: KClass<T>): Sequence<T>
|
||||
expect fun <T : Any> loadServices(kClass: KClass<T>): Sequence<T>
|
||||
|
||||
/**
|
||||
* Returns the single service contained in [itr] and throws if there is any ore more than one.
|
||||
*
|
||||
* @throws NoSuchElementException in case there is no service found for [kClass].
|
||||
* @throws IllegalStateException in case there is more than one service found for [kClass].
|
||||
*/
|
||||
fun <T : Any> useSingleService(kClass: KClass<T>, itr: Iterator<T>): T {
|
||||
if (!itr.hasNext()) throw NoSuchElementException("Could not find any implementation for ${kClass.fullName}")
|
||||
|
||||
val service = itr.next()
|
||||
check(!itr.hasNext()) {
|
||||
val sb = StringBuilder()
|
||||
itr.forEachRemaining {
|
||||
sb.appendln()
|
||||
sb.append(it::class.fullName)
|
||||
}
|
||||
"Found more than one implementation for ${kClass.fullName}:\n${service::class.fullName}$sb"
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package ch.tutteli.atrium.core
|
||||
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.*
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.keywords.Empty
|
||||
import ch.tutteli.atrium.core.polyfills.fullName
|
||||
import ch.tutteli.atrium.core.polyfills.loadServices
|
||||
import ch.tutteli.atrium.core.polyfills.loadSingleService
|
||||
import ch.tutteli.atrium.verbs.internal.assert
|
||||
import ch.tutteli.atrium.verbs.internal.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class LoadServicesTest {
|
||||
@Test
|
||||
fun noServiceFound_EmptySequence() {
|
||||
assert(loadServices(LoadServicesTest::class).toList()) toBe Empty
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneServiceFound_ReturnsTheService() {
|
||||
assert(loadServices(InterfaceWithOneImplementation::class)).asIterable().and {
|
||||
containsStrictly { isA<SingleService> {} }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoServicesFound_ThrowsIllegalStateException() {
|
||||
assert(loadServices(InterfaceWithTwoImplementation::class)).asIterable() contains Entries(
|
||||
{ isA<Service1> {} },
|
||||
{ isA<Service2> {} }
|
||||
)
|
||||
expect {
|
||||
loadSingleService(InterfaceWithTwoImplementation::class)
|
||||
}.toThrow<IllegalStateException> {
|
||||
this messageContains Values(
|
||||
"Found more than one implementation ",
|
||||
Service1::class.fullName,
|
||||
Service2::class.fullName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ch.tutteli.atrium.core
|
||||
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.Values
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.isA
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.messageContains
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.toThrow
|
||||
import ch.tutteli.atrium.core.polyfills.fullName
|
||||
import ch.tutteli.atrium.core.polyfills.loadSingleService
|
||||
import ch.tutteli.atrium.verbs.internal.assert
|
||||
import ch.tutteli.atrium.verbs.internal.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class LoadSingleServiceTest {
|
||||
@Test
|
||||
fun noServiceFound_ThrowsNoSuchElementException() {
|
||||
expect {
|
||||
loadSingleService(LoadSingleServiceTest::class)
|
||||
}.toThrow<NoSuchElementException> {
|
||||
this messageContains Values("Could not find any implementation", LoadSingleServiceTest::class.fullName)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneServiceFound_ReturnsTheService() {
|
||||
val service = loadSingleService(InterfaceWithOneImplementation::class)
|
||||
assert(service).isA<SingleService> { }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoServicesFound_ThrowsIllegalStateException() {
|
||||
expect {
|
||||
loadSingleService(InterfaceWithTwoImplementation::class)
|
||||
}.toThrow<IllegalStateException> {
|
||||
this messageContains Values(
|
||||
"Found more than one implementation ",
|
||||
Service1::class.fullName,
|
||||
Service2::class.fullName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ch.tutteli.atrium.core
|
||||
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.Values
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.isA
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.messageContains
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.toThrow
|
||||
import ch.tutteli.atrium.core.polyfills.fullName
|
||||
import ch.tutteli.atrium.core.polyfills.useSingleService
|
||||
import ch.tutteli.atrium.verbs.internal.assert
|
||||
import ch.tutteli.atrium.verbs.internal.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class UseSingleServiceTest {
|
||||
@Test
|
||||
fun emptyIterator_ThrowsNoSuchElementException() {
|
||||
expect {
|
||||
useSingleService(InterfaceWithOneImplementation::class, listOf<InterfaceWithOneImplementation>().iterator())
|
||||
}.toThrow<NoSuchElementException> {
|
||||
this messageContains Values(
|
||||
"Could not find any implementation",
|
||||
InterfaceWithOneImplementation::class.fullName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneServiceFound_ReturnsTheService() {
|
||||
val service = useSingleService(InterfaceWithOneImplementation::class, listOf(SingleService()).iterator())
|
||||
assert(service).isA<SingleService> { }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoServiceFound_ReturnsTheService() {
|
||||
expect {
|
||||
useSingleService(InterfaceWithTwoImplementation::class, listOf(Service1(), Service2()).iterator())
|
||||
}.toThrow<IllegalStateException> {
|
||||
this messageContains Values(
|
||||
"Found more than one implementation ",
|
||||
Service1::class.fullName,
|
||||
Service2::class.fullName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package ch.tutteli.atrium.core
|
||||
|
||||
interface InterfaceWithOneImplementation
|
||||
class SingleService : InterfaceWithOneImplementation
|
||||
|
||||
interface InterfaceWithTwoImplementation
|
||||
class Service1 : InterfaceWithTwoImplementation
|
||||
class Service2 : InterfaceWithTwoImplementation
|
||||
@@ -2,4 +2,8 @@ description = 'API of the core of Atrium for the JS platform.'
|
||||
|
||||
dependencies {
|
||||
compile "ch.tutteli.kbox:kbox-js:$kbox_version", excludeKotlin
|
||||
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-js"
|
||||
testCompile prefixedProject('api-cc-infix-en_GB-js')
|
||||
testCompile prefixedProject('verbs-internal-js')
|
||||
}
|
||||
|
||||
@@ -2,10 +2,27 @@ package ch.tutteli.atrium.core.polyfills
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
actual fun <T : Any> loadSingleService(kClass: KClass<T>): T {
|
||||
TODO("need a concept for Services")
|
||||
}
|
||||
private val serviceRegistry = mutableMapOf<KClass<*>, HashSet<Any>>()
|
||||
|
||||
actual fun <T : Any> loadSingleService(kClass: KClass<T>): T =
|
||||
useSingleService(kClass, loadServices(kClass).iterator())
|
||||
|
||||
|
||||
actual fun <T : Any> loadServices(kClass: KClass<T>): Sequence<T> {
|
||||
TODO("need a concept for Services")
|
||||
@Suppress("UNCHECKED_CAST" /* we have a homogeneous map but make sure insertions are kclass */)
|
||||
val set = serviceRegistry[kClass] as Set<T>?
|
||||
return set?.asSequence() ?: emptySequence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given [service] for the service of type [T].
|
||||
*/
|
||||
inline fun <reified T : Any> registerService(service: T) = registerService(T::class, service)
|
||||
|
||||
/**
|
||||
* Registers the given [service] for the given [serviceInterface].
|
||||
*/
|
||||
fun <T : Any> registerService(serviceInterface: KClass<T>, service: T) {
|
||||
val services = serviceRegistry.getOrPut(serviceInterface) { hashSetOf() }
|
||||
services.add(service)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,16 @@ description = 'API of the core of Atrium for the JVM platform.'
|
||||
dependencies {
|
||||
compile kbox(), excludeKotlin
|
||||
compile kotlinReflect()
|
||||
|
||||
testCompile prefixedProject('cc-infix-en_GB-robstoll')
|
||||
testCompile prefixedProject('verbs-internal-jvm')
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit5"
|
||||
testRuntime "org.junit.jupiter:junit-jupiter-engine:5.3.1"
|
||||
}
|
||||
|
||||
junitPlatform.filters {
|
||||
engines {
|
||||
include 'junit-jupiter'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package ch.tutteli.atrium.core
|
||||
|
||||
import ch.tutteli.atrium.core.polyfills.appendln
|
||||
import java.util.*
|
||||
import kotlin.NoSuchElementException
|
||||
|
||||
/**
|
||||
* Loads a service vai [ServiceLoader] for a given [Class] and throws an [IllegalStateException]
|
||||
* in case it finds more than one service.
|
||||
*/
|
||||
object SingleServiceLoader {
|
||||
|
||||
/**
|
||||
* Loads a service vai [ServiceLoader] for a given [Class] and throws an [IllegalStateException]
|
||||
* in case it finds more than one service.
|
||||
*
|
||||
* @param clazz The service represented by a [Class].
|
||||
* @return The loaded service
|
||||
*
|
||||
* @throws IllegalStateException in case none or more than one service is found for the given [clazz]
|
||||
*/
|
||||
fun <T : Any> load(clazz: Class<T>): T {
|
||||
val itr = ServiceLoader.load(clazz).iterator()
|
||||
if (!itr.hasNext()) throw NoSuchElementException("Could not find any implementation for ${clazz.name}")
|
||||
|
||||
val service = itr.next()
|
||||
check(!itr.hasNext()) {
|
||||
val sb = StringBuilder()
|
||||
itr.forEachRemaining {
|
||||
sb.appendln()
|
||||
sb.append(it::class.java.name)
|
||||
}
|
||||
"Found more than one implementation for ${clazz.name}:\n${service::class.java.name}$sb"
|
||||
}
|
||||
return service
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
package ch.tutteli.atrium.core.polyfills
|
||||
|
||||
import ch.tutteli.atrium.core.SingleServiceLoader
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
actual fun <T: Any> loadSingleService(kClass: KClass<T>): T
|
||||
= SingleServiceLoader.load(kClass.java)
|
||||
/**
|
||||
* Loads a service via [ServiceLoader] for a given [kClass] and throws if there is none or more services.
|
||||
*
|
||||
* @param kClass The service type.
|
||||
* @return The loaded service
|
||||
*
|
||||
* @throws NoSuchElementException in case there is no service found for [kClass].
|
||||
* @throws IllegalStateException in case there is more than one service found for [kClass].
|
||||
*/
|
||||
actual fun <T : Any> loadSingleService(kClass: KClass<T>): T =
|
||||
useSingleService(kClass, ServiceLoader.load(kClass.java).iterator())
|
||||
|
||||
actual fun <T : Any> loadServices(kClass: KClass<T>): Sequence<T>
|
||||
= ServiceLoader.load(kClass.java).asSequence()
|
||||
actual fun <T : Any> loadServices(kClass: KClass<T>): Sequence<T> =
|
||||
ServiceLoader.load(kClass.java).asSequence()
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package ch.tutteli.atrium.core
|
||||
|
||||
import ch.tutteli.atrium.api.cc.infix.en_GB.*
|
||||
import ch.tutteli.atrium.verbs.internal.assert
|
||||
import ch.tutteli.atrium.verbs.internal.expect
|
||||
import org.jetbrains.spek.api.Spek
|
||||
import org.jetbrains.spek.api.dsl.given
|
||||
import org.jetbrains.spek.api.dsl.it
|
||||
|
||||
object SingleServiceLoaderSpec : Spek({
|
||||
|
||||
given("no service") {
|
||||
it("throws an NoSuchElementException") {
|
||||
expect {
|
||||
SingleServiceLoader.load(SingleServiceLoaderSpec::class.java)
|
||||
}.toThrow<NoSuchElementException> {
|
||||
message {
|
||||
this contains Values("Could not find any implementation", SingleServiceLoaderSpec::class.java.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
given("single service") {
|
||||
it("loads the corresponding implementation") {
|
||||
val service = SingleServiceLoader.load(InterfaceWithOneImplementation::class.java)
|
||||
assert(service).isA<A> { }
|
||||
}
|
||||
}
|
||||
|
||||
given("more than one service") {
|
||||
it("throws an IllegalStateException") {
|
||||
expect {
|
||||
SingleServiceLoader.load(InterfaceWithTwoImplementation::class.java)
|
||||
}.toThrow<IllegalStateException> {
|
||||
this messageContains Values(
|
||||
"Found more than one implementation ",
|
||||
A1::class.java.name,
|
||||
A2::class.java.name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
interface InterfaceWithOneImplementation
|
||||
class A: InterfaceWithOneImplementation
|
||||
|
||||
interface InterfaceWithTwoImplementation
|
||||
class A1: InterfaceWithTwoImplementation
|
||||
class A2: InterfaceWithTwoImplementation
|
||||
@@ -1 +1 @@
|
||||
ch.tutteli.atrium.core.A
|
||||
ch.tutteli.atrium.core.SingleService
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ch.tutteli.atrium.core.A1
|
||||
ch.tutteli.atrium.core.A2
|
||||
ch.tutteli.atrium.core.Service1
|
||||
ch.tutteli.atrium.core.Service2
|
||||
|
||||
Reference in New Issue
Block a user