Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EphemeralView implementation #338

Merged
merged 1 commit into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading