Skip to content

Commit

Permalink
EphemeralView implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
DomenicDev committed Dec 14, 2024
1 parent 104d746 commit 9ecb343
Show file tree
Hide file tree
Showing 12 changed files with 388 additions and 0 deletions.
4 changes: 4 additions & 0 deletions application-arrow/api/application-arrow.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
public final class com/fraktalio/fmodel/application/EphemeralViewArrowExtensionKt {
public static final fun handleWithEffect (Lcom/fraktalio/fmodel/application/ViewStateComputation;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class com/fraktalio/fmodel/application/EventSourcingAggregateArrowExtensionKt {
public static final fun handleOptimisticallyWithEffect (Lcom/fraktalio/fmodel/application/EventSourcingLockingAggregate;Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun handleOptimisticallyWithEffect (Lcom/fraktalio/fmodel/application/EventSourcingLockingAggregate;Ljava/lang/Object;Ljava/util/Map;)Lkotlinx/coroutines/flow/Flow;
Expand Down
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()
}
}
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))
}
}
})
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()
4 changes: 4 additions & 0 deletions application-vanilla/api/application-vanilla.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
public final class com/fraktalio/fmodel/application/EphemeralViewExtensionKt {
public static final fun handle (Lcom/fraktalio/fmodel/application/ViewStateComputation;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class com/fraktalio/fmodel/application/EventSourcingAggregateActorExtensionKt {
public static final fun handleConcurrently (Lcom/fraktalio/fmodel/application/EventSourcingAggregate;Lkotlinx/coroutines/flow/Flow;IILkotlinx/coroutines/CoroutineStart;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
public static final fun handleConcurrently (Lcom/fraktalio/fmodel/application/EventSourcingAggregate;Lkotlinx/coroutines/flow/Flow;IILkotlinx/coroutines/CoroutineStart;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
Expand Down
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) }
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))
}
}

})
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()
28 changes: 28 additions & 0 deletions application/api/application.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ public final class com/fraktalio/fmodel/application/ActionPublisher$DefaultImpls
public static fun publish (Lcom/fraktalio/fmodel/application/ActionPublisher;Lkotlinx/coroutines/flow/Flow;Ljava/util/Map;)Lkotlinx/coroutines/flow/Flow;
}

public abstract interface class com/fraktalio/fmodel/application/EphemeralView : com/fraktalio/fmodel/application/EphemeralViewRepository, com/fraktalio/fmodel/application/ViewStateComputation {
}

public final class com/fraktalio/fmodel/application/EphemeralView$DefaultImpls {
public static fun computeNewState (Lcom/fraktalio/fmodel/application/EphemeralView;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
}

public final class com/fraktalio/fmodel/application/EphemeralViewKt {
public static final fun EphemeralView (Lcom/fraktalio/fmodel/domain/IView;Lcom/fraktalio/fmodel/application/EphemeralViewRepository;)Lcom/fraktalio/fmodel/application/EphemeralView;
}

public abstract interface class com/fraktalio/fmodel/application/EphemeralViewRepository {
public abstract fun fetchEvents (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
}

public abstract class com/fraktalio/fmodel/application/Error : com/fraktalio/fmodel/application/Result {
public abstract fun getThrowable ()Ljava/lang/Throwable;
}
Expand Down Expand Up @@ -110,6 +125,19 @@ public final class com/fraktalio/fmodel/application/Error$EventPublishingFailed
public fun toString ()Ljava/lang/String;
}

public final class com/fraktalio/fmodel/application/Error$FetchingEventsFailed : com/fraktalio/fmodel/application/Error {
public fun <init> (Ljava/lang/Object;Ljava/lang/Throwable;)V
public final fun component1 ()Ljava/lang/Object;
public final fun component2 ()Ljava/lang/Throwable;
public final fun copy (Ljava/lang/Object;Ljava/lang/Throwable;)Lcom/fraktalio/fmodel/application/Error$FetchingEventsFailed;
public static synthetic fun copy$default (Lcom/fraktalio/fmodel/application/Error$FetchingEventsFailed;Ljava/lang/Object;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/fraktalio/fmodel/application/Error$FetchingEventsFailed;
public fun equals (Ljava/lang/Object;)Z
public final fun getId ()Ljava/lang/Object;
public fun getThrowable ()Ljava/lang/Throwable;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/fraktalio/fmodel/application/Error$FetchingStateFailed : com/fraktalio/fmodel/application/Error {
public fun <init> (Ljava/lang/Object;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
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 {}
Loading

0 comments on commit 9ecb343

Please sign in to comment.