-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
104d746
commit 9ecb343
Showing
12 changed files
with
388 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
...row/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewArrowExtension.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package com.fraktalio.fmodel.application | ||
|
||
import arrow.core.Either | ||
import arrow.core.raise.catch | ||
import arrow.core.raise.either | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.fold | ||
|
||
/** | ||
* Extension function - Handles the query of type [Q] | ||
* | ||
* @param query Query of type [Q] to be handled | ||
* @return [Either] (either [Error] or State of type [S]) | ||
* | ||
* @author Domenic Cassisi | ||
*/ | ||
suspend fun <S, E, Q, EV> EV.handleWithEffect(query: Q): Either<Error, S> | ||
where EV : ViewStateComputation<S, E>, EV : EphemeralViewRepository<E, Q> { | ||
|
||
fun Q.fetchEventsWithEffect(): Either<Error, Flow<E>> = | ||
either { | ||
catch({ | ||
fetchEvents() | ||
}) { | ||
raise(Error.FetchingEventsFailed(query, it)) | ||
} | ||
} | ||
|
||
suspend fun Flow<E>.computeStateWithEffect(): Either<Error, S> = | ||
either { | ||
catch({ | ||
fold(initialState) { s, e -> evolve(s, e) } | ||
}) { | ||
raise(Error.CalculatingNewViewStateFailed(this@computeStateWithEffect, it)) | ||
} | ||
} | ||
|
||
return either { | ||
query.fetchEventsWithEffect().bind() | ||
.computeStateWithEffect().bind() | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
...ication-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package com.fraktalio.fmodel.application | ||
|
||
import arrow.core.Either | ||
import com.fraktalio.fmodel.application.examples.numbers.even.query.EvenNumberEphemeralViewRepository | ||
import com.fraktalio.fmodel.application.examples.numbers.even.query.evenNumberEphemeralViewRepository | ||
import com.fraktalio.fmodel.domain.IView | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.Description | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.EvenNumberState | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue | ||
import com.fraktalio.fmodel.domain.examples.numbers.even.query.evenNumberView | ||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.matchers.shouldBe | ||
import io.kotest.matchers.types.shouldBeInstanceOf | ||
|
||
/** | ||
* DSL - Given | ||
*/ | ||
private suspend fun <S, E, Q> IView<S, E>.given(repository: EphemeralViewRepository<E, Q>, query: () -> Q): Either<Error, S> = | ||
EphemeralView( | ||
view = this, | ||
ephemeralViewRepository = repository | ||
).handleWithEffect(query()) | ||
|
||
/** | ||
* DSL - When | ||
*/ | ||
private fun <Q> whenQuery(query: Q): Q = query | ||
|
||
/** | ||
* DSL - Then | ||
*/ | ||
private infix fun <S> Either<Error, S>.thenState(expected: S) { | ||
val state = when (this) { | ||
is Either.Right -> value | ||
is Either.Left -> throw AssertionError("Expected Either.Right, but found Either.Left with value $value") | ||
} | ||
state shouldBe expected | ||
} | ||
|
||
private fun <S> Either<Error, S>.thenError() { | ||
val error = when (this) { | ||
is Either.Right -> throw AssertionError("Expected Either.Left, but found Either.Right with value $value") | ||
is Either.Left -> value | ||
} | ||
error.shouldBeInstanceOf<Error>() | ||
} | ||
|
||
/** | ||
* Ephemeral View Test | ||
*/ | ||
class EphemeralViewTest : FunSpec({ | ||
val evenView = evenNumberView() | ||
val ephemeralViewRepository = evenNumberEphemeralViewRepository() as EvenNumberEphemeralViewRepository | ||
|
||
test("Ephemeral View - load number flow 1") { | ||
with(evenView) { | ||
given(ephemeralViewRepository) { | ||
whenQuery(1) | ||
} thenState EvenNumberState(Description("Initial state, Number 2, Number 4"), NumberValue(6)) | ||
} | ||
} | ||
|
||
test("Ephemeral View - load number flow 2") { | ||
with(evenView) { | ||
given(ephemeralViewRepository) { | ||
whenQuery(2) | ||
} thenState EvenNumberState(Description("Initial state, Number 4, Number 2"), NumberValue(2)) | ||
} | ||
} | ||
|
||
test("Ephemeral View - load number flow 3 - with error") { | ||
with(evenView) { | ||
given(ephemeralViewRepository) { | ||
whenQuery(3) | ||
}.thenError() | ||
} | ||
} | ||
|
||
test("Ephemeral View - load non-existing number flow") { | ||
with(evenView) { | ||
given(ephemeralViewRepository) { | ||
whenQuery(4) | ||
} thenState EvenNumberState(Description("Initial state"), NumberValue(0)) | ||
} | ||
} | ||
}) |
44 changes: 44 additions & 0 deletions
44
...talio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.fraktalio.fmodel.application.examples.numbers.even.query | ||
|
||
import com.fraktalio.fmodel.application.EphemeralViewRepository | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.Description | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.NumberEvent | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.emptyFlow | ||
import kotlinx.coroutines.flow.flowOf | ||
|
||
/** | ||
* Simple flows of events to represent previously stored events in the event store | ||
*/ | ||
private var numberFlow1 = flowOf( | ||
NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 2"), NumberValue(2)), | ||
NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)) | ||
) | ||
|
||
private var numberFlow2 = flowOf( | ||
NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)), | ||
NumberEvent.EvenNumberEvent.EvenNumberSubtracted(Description("Number 2"), NumberValue(2)) | ||
) | ||
|
||
/** | ||
* Even number ephemeral view implementation | ||
*/ | ||
class EvenNumberEphemeralViewRepository : EphemeralViewRepository<NumberEvent.EvenNumberEvent?, Int> { | ||
|
||
override fun Int.fetchEvents(): Flow<NumberEvent.EvenNumberEvent?> { | ||
return when (this) { | ||
1 -> numberFlow1 | ||
2 -> numberFlow2 | ||
3 -> throw RuntimeException("Some fake error while fetching events.") | ||
else -> emptyFlow() | ||
} | ||
} | ||
|
||
} | ||
|
||
/** | ||
* Helper function to create an [EvenNumberEphemeralViewRepository] | ||
*/ | ||
fun evenNumberEphemeralViewRepository(): EphemeralViewRepository<NumberEvent.EvenNumberEvent?, Int> = | ||
EvenNumberEphemeralViewRepository() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
...-vanilla/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewExtension.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.fraktalio.fmodel.application | ||
|
||
import kotlinx.coroutines.flow.fold | ||
|
||
/** | ||
* Extension function - Handles the query of type [Q] | ||
* | ||
* @param query Query of type [Q] to be handled | ||
* @return State of type [S] | ||
* | ||
* @author Domenic Cassisi | ||
*/ | ||
suspend fun <S, E, Q, EV> EV.handle(query: Q): S where EV : ViewStateComputation<S, E>, EV : EphemeralViewRepository<E, Q> = | ||
query.fetchEvents().fold(initialState) { s, e -> evolve(s, e) } |
60 changes: 60 additions & 0 deletions
60
...ation-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package com.fraktalio.fmodel.application | ||
|
||
import com.fraktalio.fmodel.application.examples.numbers.even.query.EvenNumberEphemeralViewRepository | ||
import com.fraktalio.fmodel.application.examples.numbers.even.query.evenNumberEphemeralViewRepository | ||
import com.fraktalio.fmodel.domain.IView | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.Description | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.EvenNumberState | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue | ||
import com.fraktalio.fmodel.domain.examples.numbers.even.query.evenNumberView | ||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.matchers.shouldBe | ||
|
||
/** | ||
* DSL - Given | ||
*/ | ||
private suspend fun <S, E, Q> IView<S, E>.given(repository: EphemeralViewRepository<E, Q>, query: () -> Q): S = | ||
EphemeralView( | ||
view = this, | ||
ephemeralViewRepository = repository | ||
).handle(query()) | ||
|
||
/** | ||
* DSL - When | ||
*/ | ||
private fun <Q> whenQuery(query: Q): Q = query | ||
|
||
/** | ||
* DSL - Then | ||
*/ | ||
private infix fun <S> S.thenState(expected: S) = shouldBe(expected) | ||
|
||
class EphemeralViewTest : FunSpec({ | ||
val evenView = evenNumberView() | ||
val ephemeralViewRepository = evenNumberEphemeralViewRepository() as EvenNumberEphemeralViewRepository | ||
|
||
test("Ephemeral View - load number flow 1") { | ||
with(evenView) { | ||
given(ephemeralViewRepository) { | ||
whenQuery(1) | ||
} thenState EvenNumberState(Description("Initial state, Number 2, Number 4"), NumberValue(6)) | ||
} | ||
} | ||
|
||
test("Ephemeral View - load number flow 2") { | ||
with(evenView) { | ||
given(ephemeralViewRepository) { | ||
whenQuery(2) | ||
} thenState EvenNumberState(Description("Initial state, Number 4, Number 2"), NumberValue(2)) | ||
} | ||
} | ||
|
||
test("Ephemeral View - load non-existing number flow") { | ||
with(evenView) { | ||
given(ephemeralViewRepository) { | ||
whenQuery(3) | ||
} thenState EvenNumberState(Description("Initial state"), NumberValue(0)) | ||
} | ||
} | ||
|
||
}) |
43 changes: 43 additions & 0 deletions
43
...talio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package com.fraktalio.fmodel.application.examples.numbers.even.query | ||
|
||
import com.fraktalio.fmodel.application.EphemeralViewRepository | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.Description | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.NumberEvent | ||
import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.emptyFlow | ||
import kotlinx.coroutines.flow.flowOf | ||
|
||
/** | ||
* Simple flows of events to represent previously stored events in the event store | ||
*/ | ||
private var numberFlow1 = flowOf( | ||
NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 2"), NumberValue(2)), | ||
NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)) | ||
) | ||
|
||
private var numberFlow2 = flowOf( | ||
NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)), | ||
NumberEvent.EvenNumberEvent.EvenNumberSubtracted(Description("Number 2"), NumberValue(2)) | ||
) | ||
|
||
/** | ||
* Even number ephemeral view implementation | ||
*/ | ||
class EvenNumberEphemeralViewRepository : EphemeralViewRepository<NumberEvent.EvenNumberEvent?, Int> { | ||
|
||
override fun Int.fetchEvents(): Flow<NumberEvent.EvenNumberEvent?> { | ||
return when (this) { | ||
1 -> numberFlow1 | ||
2 -> numberFlow2 | ||
else -> emptyFlow() | ||
} | ||
} | ||
|
||
} | ||
|
||
/** | ||
* Helper function to create an [EvenNumberEphemeralViewRepository] | ||
*/ | ||
fun evenNumberEphemeralViewRepository(): EphemeralViewRepository<NumberEvent.EvenNumberEvent?, Int> = | ||
EvenNumberEphemeralViewRepository() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralView.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package com.fraktalio.fmodel.application | ||
|
||
import com.fraktalio.fmodel.domain.IView | ||
|
||
/** | ||
* EphemeralView is using/delegating a `view` / [ViewStateComputation]<[S], [E]> to handle events of type [E] without maintaining a state of projection(s). | ||
* | ||
* [EphemeralView] extends [ViewStateComputation] and [EphemeralViewRepository] interfaces, | ||
* clearly communicating that it is composed out of these two behaviours. | ||
* | ||
* @param S Ephemeral View state of type [S] | ||
* @param E Events of type [E] that are handled by this Ephemeral View | ||
* @param Q Query of type [Q] | ||
* | ||
* @author Domenic Cassisi | ||
*/ | ||
interface EphemeralView<S, E, Q> : ViewStateComputation<S, E>, EphemeralViewRepository<E, Q> | ||
|
||
/** | ||
* Ephemeral View constructor-like function. | ||
* | ||
* The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin supports it natively requiring zero boilerplate code. | ||
* | ||
* @param S Ephemeral View state of type [S] | ||
* @param E Events of type [E] that are used internally to build/fold new state | ||
* @param Q Identifier of type [Q] | ||
* @property view A view component of type [IView]<[S], [E]> | ||
* @property ephemeralViewRepository Interface for fetching events for [Q] - dependencies by delegation | ||
* @return An object/instance of type [EphemeralView]<[S], [E], [Q]> | ||
* | ||
* @author Domenic Cassisi | ||
*/ | ||
fun <S, E, Q> EphemeralView( | ||
view: IView<S, E>, | ||
ephemeralViewRepository: EphemeralViewRepository<E, Q> | ||
): EphemeralView<S, E, Q> = | ||
object : EphemeralView<S, E, Q>, | ||
EphemeralViewRepository<E, Q> by ephemeralViewRepository, | ||
IView<S, E> by view {} |
Oops, something went wrong.