Skip to content

Commit

Permalink
SNAPSHOT: second draft presentation
Browse files Browse the repository at this point in the history
  • Loading branch information
carltonwhitehead committed Jun 2, 2024
1 parent 35baa65 commit 1fcf9ff
Show file tree
Hide file tree
Showing 18 changed files with 222 additions and 111 deletions.
14 changes: 6 additions & 8 deletions presentation/presentation-library-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,19 @@
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
</dependency>

<dependency>
<groupId>tech.coner.trailer</groupId>
<artifactId>presentation-library</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>test</scope>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>tech.coner.trailer</groupId>
<artifactId>presentation-library-testsupport</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>tech.coner.trailer</groupId>
<artifactId>io</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand All @@ -53,6 +47,10 @@
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-test-jvm</artifactId>
</dependency>
<dependency>
<groupId>app.cash.turbine</groupId>
<artifactId>turbine-jvm</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.

interface FooService {

suspend fun create(create: Foo): Result<Foo>
suspend fun findById(id: Foo.Id): Result<Foo>
suspend fun update(update: Foo): Result<Foo>
suspend fun deleteById(id: Foo.Id): Result<Foo>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import tech.coner.trailer.toolkit.konstraints.CompositeConstraint
class FooConstraint : CompositeConstraint<Foo>() {

val valueIsInRange = propertyConstraint(
property = Foo::value,
property = Foo::id,
assessFn = {
when (it.value) {
in Foo.values().indices -> true
when (it.id.value) {
in 0..4 -> true
else -> false
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package tech.coner.trailer.presentation.library.testsupport.fooapp.domain.exception

class AlreadyExistsException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package tech.coner.trailer.presentation.library.testsupport.fooapp.domain.service

import tech.coner.trailer.presentation.library.testsupport.fooapp.data.service.FooService
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.FOO_ID_BAR
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.FOO_ID_BAT
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.FOO_ID_BAZ
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.FOO_ID_FOO
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.Foo
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.exception.AlreadyExistsException
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.exception.NotFoundException
import tech.coner.trailer.toolkit.konstraints.Constraint

class TestableFooService(private val constraint: Constraint<Foo>) : FooService {

private val map: MutableMap<Foo.Id, Foo> = arrayOf(
FOO_ID_FOO to "foo",
FOO_ID_BAR to "bar",
FOO_ID_BAZ to "baz",
FOO_ID_BAT to "bat",
)
.associate { (idValue, name) -> Foo.Id(idValue).let { it to Foo(it, name) } }
.toMutableMap()

override suspend fun create(create: Foo): Result<Foo> {
if (map.containsKey(create.id)) {
return Result.failure(AlreadyExistsException())
}
return constraint(create)
.map { map[create.id] = create; create }
}

override suspend fun findById(id: Foo.Id): Result<Foo> {
return map[id]?.let { Result.success(it) }
?: Result.failure(NotFoundException())
}

override suspend fun update(update: Foo): Result<Foo> {
return findById(update.id)
.mapCatching { constraint(update).getOrThrow() }
.map { map[update.id] = update; update }
}

override suspend fun deleteById(id: Foo.Id): Result<Foo> {
return map.remove(id)
?.let { Result.success(it) }
?: Result.failure(NotFoundException())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.
import tech.coner.trailer.presentation.library.adapter.LoadableItemAdapter
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.Foo
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.model.FooModel
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.state.FooState

class FooAdapter : LoadableItemAdapter<
Foo.Id,
FooState,
Foo,
Unit,
FooModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tech.coner.trailer.presentation.library.model.BaseItemModel
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.constraint.FooConstraint
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.*
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.Foo

class FooModel(override val original: Foo) : BaseItemModel<Foo, FooConstraint>() {
override val constraints = FooConstraint()
Expand All @@ -13,6 +13,10 @@ class FooModel(override val original: Foo) : BaseItemModel<Foo, FooConstraint>()
it.name.capitalizeFirstChar()
}

fun setName(name: String) {
updateItem { it.copy(name = name) }
}

private fun String.capitalizeFirstChar(): String {
return when (length) {
0 -> this
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.presenter

import kotlin.coroutines.CoroutineContext
import tech.coner.trailer.presentation.library.presenter.LoadableItemPresenter
import tech.coner.trailer.presentation.library.state.LoadableItem
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.model.FooModel
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.state.FooState
import tech.coner.trailer.presentation.library.testsupport.fooapp.data.service.FooService
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.Foo
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.adapter.FooAdapter
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.model.FooModel

class FooPresenter(override val argument: Foo.Id) : LoadableItemPresenter<
class FooDetailPresenter(
override val argument: Foo.Id,
private val service: FooService,
override val coroutineContext: CoroutineContext
) : LoadableItemPresenter<
Foo.Id,
FooState,
Foo,
Unit,
FooModel
>() {
override val initialState = FooState(LoadableItem.Empty())
override val adapter = FooAdapter()

override suspend fun performLoad(): Result<Foo> {
return service.findById(argument)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package tech.coner.trailer.presentation.library.presenter

import app.cash.turbine.test
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.prop
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import tech.coner.trailer.presentation.library.model.LoadableModel
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.constraint.FooConstraint
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.FOO_ID_FOO
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.entity.Foo
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.exception.NotFoundException
import tech.coner.trailer.presentation.library.testsupport.fooapp.domain.service.TestableFooService
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.model.FooModel
import tech.coner.trailer.presentation.library.testsupport.fooapp.presentation.presenter.FooDetailPresenter

class FooDetailPresenterTest {

@Test
fun itsModelFlowShouldBeAdaptedFromInitialState() = runTest {
val id = Foo.Id(FOO_ID_FOO)
val presenter = createPresenter(id)

presenter.modelFlow.test {
assertThat(expectMostRecentItem())
.isEqualTo(LoadableModel.Empty(null))
}
}

@Test
fun itsModelFlowShouldEmitWhenLoadingAndLoaded() = runTest {
val id = Foo.Id(FOO_ID_FOO)
val presenter = createPresenter(id)

presenter.modelFlow.test {
skipItems(1)

presenter.load()

assertThat(awaitItem())
.isInstanceOf<LoadableModel.Loading<Unit, FooModel>>()
assertThat(awaitItem())
.isInstanceOf<LoadableModel.Loaded<Unit, FooModel>>()
}
}

@Test
fun itsModelFlowShouldEmitWhenLoadingAndLoadFailed() = runTest {
val id = Foo.Id(Int.MAX_VALUE)
val presenter = createPresenter(id)

presenter.modelFlow.test {
skipItems(1)

presenter.load()

assertThat(awaitItem())
.isInstanceOf<LoadableModel.Loading<Unit, FooModel>>()
assertThat(awaitItem())
.isInstanceOf<LoadableModel.LoadFailed<Unit, FooModel>>()
.prop(LoadableModel.LoadFailed<Unit, FooModel>::cause)
.isNotNull()
.isInstanceOf<NotFoundException>()
}
}

}

private fun TestScope.createPresenter(argument: Foo.Id): FooDetailPresenter {
return FooDetailPresenter(
argument = argument,
service = TestableFooService(
constraint = FooConstraint()
),
coroutineContext = coroutineContext + Job() + CoroutineName("FooDetailPresenter")
)
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import tech.coner.trailer.presentation.library.model.LoadableModel
import tech.coner.trailer.presentation.library.state.LoadableItem
import tech.coner.trailer.presentation.library.state.LoadableItemState

abstract class LoadableItemAdapter<ARGUMENT, STATE, ITEM, ARGUMENT_MODEL, ITEM_MODEL>
where STATE : LoadableItemState<ITEM>,
ITEM_MODEL : ItemModel<ITEM> {
abstract class LoadableItemAdapter<ARGUMENT, ITEM, ARGUMENT_MODEL, ITEM_MODEL>
where ITEM_MODEL : ItemModel<ITEM> {

protected abstract val argumentModelAdapter: ((ARGUMENT) -> ARGUMENT_MODEL)?
protected abstract val partialItemAdapter: ((ARGUMENT, STATE) -> ITEM_MODEL)?
protected abstract val partialItemAdapter: ((ARGUMENT, LoadableItemState<ITEM>) -> ITEM_MODEL)?
protected abstract val itemAdapter: (ITEM) -> ITEM_MODEL

operator fun invoke(argument: ARGUMENT, state: STATE): LoadableModel<ARGUMENT_MODEL, ITEM_MODEL> {
operator fun invoke(argument: ARGUMENT, state: LoadableItemState<ITEM>): LoadableModel<ARGUMENT_MODEL, ITEM_MODEL> {
val argumentModel: ARGUMENT_MODEL? = argumentModelAdapter?.invoke(argument)
return when (val loadable = state.loadable) {
is LoadableItem.Empty<ITEM> -> LoadableModel.Empty(
Expand Down
Loading

0 comments on commit 1fcf9ff

Please sign in to comment.