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:
Robert Stoll
2018-10-19 15:11:43 +02:00
parent 5c66f45c1b
commit 9de2cd16ea
16 changed files with 220 additions and 104 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ out
.gradle
.idea
*.iml
node_modules

View File

@@ -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'

View File

@@ -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')
}

View File

@@ -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
}

View File

@@ -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
)
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -0,0 +1,8 @@
package ch.tutteli.atrium.core
interface InterfaceWithOneImplementation
class SingleService : InterfaceWithOneImplementation
interface InterfaceWithTwoImplementation
class Service1 : InterfaceWithTwoImplementation
class Service2 : InterfaceWithTwoImplementation

View File

@@ -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')
}

View File

@@ -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)
}

View File

@@ -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'
}
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -1 +1 @@
ch.tutteli.atrium.core.A
ch.tutteli.atrium.core.SingleService

View File

@@ -1,2 +1,2 @@
ch.tutteli.atrium.core.A1
ch.tutteli.atrium.core.A2
ch.tutteli.atrium.core.Service1
ch.tutteli.atrium.core.Service2