Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
notxcain committed Feb 22, 2018
1 parent 346198e commit 412d7a9
Showing 1 changed file with 112 additions and 53 deletions.
165 changes: 112 additions & 53 deletions README.md
Expand Up @@ -16,20 +16,53 @@ The name `Aecor` (_lat. ocean_) is inspired by a vision of modern distributed ap

### Installing Aecor

To start using Aecor Runtime add the following to your `build.sbt` file:
To start using Aecor Akka Persistence Runtime add the following to your `build.sbt` file:

```scala
scalaOrganization := "org.typelevel"
libraryDependencies += "io.aecor" %% "aecor-core" % "0.15.0"
scalaVersion := "2.12.4"
scalacOptions += "-Ypartial-unification"
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-M10" cross CrossVersion.full)
libraryDependencies += "io.aecor" %% "akka-peristence-runtime" % "0.16.0-SNAPSHOT"
```

### Defining and running behavior
### Entity Behavior Definition

Let's start with defining domain events:
In this short guide I'll show you how to define and deploy your first event sourced behavior on runtime backed by Akka Persistence and Akka Cluster Sharding.

Each entity needs an identity, so let's start with identifier type:

```scala
final case class SubscriptionId(value: java.util.UUID) extends AnyVal
```

Then define what actions we're able to perform on `Subscription`

```scala
import aecor.macros.wireProtocol

@wireProtocol
trait Subscription[F[_]] {
def createSubscription(userId: String, productId: String, planId: String): F[Unit]
def pauseSubscription: F[Unit]
def resumeSubscription: F[Unit]
def cancelSubscription: F[Unit]
}
```

You may notice that there is no `SubscriptionId` involved, and it's okay because this interface describes actions of a concrete `Subscription` and entity behavior should not by any know about its identity, it's behavior should be defined by its state.

There is an abstract type `F[_]` which stays for an effect (see [Rob Norris, Functional Programming with Effects](https://www.youtube.com/watch?v=po3wmq4S15A)) that would be performed during each action invocation.

Also being polymorphic in effect improves the reuse of this interface, you'll see it later.

`@wireProtocol` - is a macro annotation that automates derivation of a `WireProtocol`, which is used by Akka Runtime to encode and decode actions and corresponding responses.

We are event sourced, so let's define our events:

```scala
sealed abstract class SubscriptionEvent
import aecor.runtime.akkapersistence.serialization._

sealed abstract class SubscriptionEvent extends Product with Serializable
object SubscriptionEvent {
final case class SubscriptionCreated(userId: String, productId: String, planId: String) extends SubscriptionEvent
final case object SubscriptionPaused extends SubscriptionEvent
Expand All @@ -39,79 +72,105 @@ object SubscriptionEvent {
implicit val persistentEncoder: PersistentEncoder[SubscriptionEvent] = ???
implicit val persistentDecoder: PersistentDecoder[SubscriptionEvent] = ???
}
```

sealed trait SubscriptionStatus
object SubscriptionStatus {
case object Active extends SubscriptionStatus
case object Paused extends SubscriptionStatus
case object Cancelled extends SubscriptionStatus
}
I've intentionally omitted implementation of `PersistentEncoder` and `PersistentDecoder`, because providing generic JSON encoding would be careless as persistent event schema requires your attention and I would recommend to use Protobuf or other formats that support evolution.

Let's define a state on which `Subscription` operate.

```scala
import aecor.data.Folded.syntax._
import SubscriptionState._

final case class SubscriptionState(status: SubscriptionStatus) {
final case class SubscriptionState(status: Status) {
def applyEvent(e: SubscriptionEvent): Folded[Subscription] = e match {
case e: SubscriptionCreated =>
case SubscriptionCreated(_, _, _) =>
impossible
case e: SubscriptionPaused =>
case SubscriptionPaused =>
subscription.copy(status = Paused).next
case e: SubscriptionResumed =>
case SubscriptionResumed =>
subscription.copy(status = Active).next
case e: SubscriptionCancelled =>
case SubscriptionCancelled =>
subscription.copy(status = Cancelled).next
}
}

object SubscriptionState {
import SubscriptionStatus._
sealed abstract class Status extends Product with Serializable
object Status {
final case object Active extends Status
final case object Paused extends Status
final case object Cancelled extends Status
}
def init(e: SubscriptionEvent): Folded[SubscriptionState] = e match {
case SubscriptionCreated(subscriptionId, userId, productId, planId) =>
case SubscriptionCreated(userId, productId, planId) =>
Subscription(Active).next
case _ => impossible
}
}

@wireProtocol
trait Subscription[F[_]] {
def createSubscription(userId: String, productId: String, planId: String): F[Unit]
def pauseSubscription: F[Unit]
def resumeSubscription: F[Unit]
def cancelSubscription: F[Unit]
}
```

Pay attention to `Folded` datatype, it has to constructor:
- `Impossible` is used to express impossible folds of events, so that you don't throw exceptions.
- `Next(a: A)` is used to express successful event application.


Now, the final part before we launch.'

As I said earlier `Subscription[F[_]]` is polymorphic in its effect type.

Our effect would be and `Actions[S, E, A]` which says that given some state `S` it will produce a `List[E]` of events and a result `A`
Other stuff like state recovery and event persistence is held by Akka Persistence Runtime.

So lets define `SubscritpionActions`

```scala
object SubscriptionActions extends Subscription[Action[Option[Subscription], SubscriptionEvent, ?]] {
def createSubscription(userId: String, productId: String, planId: String): F[Unit] = Action {
case Some(subscription) =>
// Do nothing reply with ()
List.empty -> ()
case None =>
// Produce event and reply with ()
List(SubscriptionCreated(userId, productId, planId)) -> ()
}
def pauseSubscription: F[Unit] = Action {
case Some(subscription) if subscription.status == Active =>
List(SubscriptionPaused) -> ()
case _ =>
List.empty -> ()
}
def resumeSubscription: F[Unit] = Action {
case Some(subscription) if subscription.status == Paused =>
List(SubscriptionResumed) -> ()
case _ =>
List.empty -> ()
}
def createSubscription(userId: String, productId: String, planId: String): Action[Option[Subscription], SubscriptionEvent, Unit] =
Action {
case Some(subscription) =>
// Do nothing reply with ()
List.empty -> ()
case None =>
// Produce event and reply with ()
List(SubscriptionCreated(userId, productId, planId)) -> ()
}
def pauseSubscription: Action[Option[Subscription], SubscriptionEvent, Unit] =
Action {
case Some(subscription) if subscription.status == Active =>
List(SubscriptionPaused) -> ()
case _ =>
List.empty -> ()
}
def resumeSubscription: Action[Option[Subscription], SubscriptionEvent, Unit] =
Action {
case Some(subscription) if subscription.status == Paused =>
List(SubscriptionResumed) -> ()
case _ =>
List.empty -> ()
}

def cancelSubscription: F[Unit] = Action {
case Some(subscription) =>
List(SubscriptionCancelled) -> ()
case _ =>
List.empty -> ()
}
def cancelSubscription: Action[Option[Subscription], SubscriptionEvent, Unit] =
Action {
case Some(subscription) =>
List(SubscriptionCancelled) -> ()
case _ =>
List.empty -> ()
}
}
```

Now that actions are define we're ready to deploy

```scala

import monix.eval.Task
import aecor.runtime.akkapersistence.AkkaPersistenceRuntime

val system = ActorSystem("system")

val runtime = AkkaPersistenctRuntime(system)
val runtime = AkkaPersistenceRuntime(system)

val behavior = EventsourcedBehavior.optional(
SubscriptionActions,
Expand Down

0 comments on commit 412d7a9

Please sign in to comment.