diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/OrElseSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/OrElseSpec.scala new file mode 100644 index 0000000000..a7198d704b --- /dev/null +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/OrElseSpec.scala @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2017-2018 Lightbend Inc. + */ + +package akka.actor.typed + +import scala.util.control.NoStackTrace + +import akka.actor.typed.scaladsl.Behaviors +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl._ +import org.scalatest.WordSpecLike + +object OrElseSpec { + sealed trait Ping + case class Ping1(replyTo: ActorRef[Pong]) extends Ping + case class Ping2(replyTo: ActorRef[Pong]) extends Ping + case class Ping3(replyTo: ActorRef[Pong]) extends Ping + case class Pong(counter: Int) + + def ping(counters: Map[String, Int]): Behavior[Ping] = { + + val ping1: Behavior[Ping] = Behaviors.receiveMessagePartial { + case Ping1(replyTo: ActorRef[Pong]) ⇒ + val newCounters = counters.updated("ping1", counters.getOrElse("ping1", 0) + 1) + replyTo ! Pong(newCounters("ping1")) + ping(newCounters) + } + + val ping2: Behavior[Ping] = Behaviors.receiveMessage { + case Ping2(replyTo: ActorRef[Pong]) ⇒ + val newCounters = counters.updated("ping2", counters.getOrElse("ping2", 0) + 1) + replyTo ! Pong(newCounters("ping2")) + ping(newCounters) + case _ ⇒ Behaviors.unhandled + } + + val ping3: Behavior[Ping] = Behaviors.receiveMessagePartial { + case Ping3(replyTo: ActorRef[Pong]) ⇒ + val newCounters = counters.updated("ping3", counters.getOrElse("ping3", 0) + 1) + replyTo ! Pong(newCounters("ping3")) + ping(newCounters) + } + + ping1.orElse(ping2).orElse(ping3) + } + +} + +class OrElseSpec extends WordSpecLike with TypedAkkaSpec { + + import OrElseSpec._ + + "Behavior.orElse" must { + + "use first matching behavior" in { + val inbox = TestInbox[Pong]("reply") + val testkit = BehaviorTestKit(ping(Map.empty)) + testkit.run(Ping1(inbox.ref)) + inbox.receiveMessage() should ===(Pong(1)) + testkit.run(Ping1(inbox.ref)) + inbox.receiveMessage() should ===(Pong(2)) + + testkit.run(Ping2(inbox.ref)) + inbox.receiveMessage() should ===(Pong(1)) + testkit.run(Ping3(inbox.ref)) + inbox.receiveMessage() should ===(Pong(1)) + testkit.run(Ping2(inbox.ref)) + inbox.receiveMessage() should ===(Pong(2)) + testkit.run(Ping3(inbox.ref)) + inbox.receiveMessage() should ===(Pong(2)) + + testkit.run(Ping1(inbox.ref)) + inbox.receiveMessage() should ===(Pong(3)) + } + } + +} + diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/Behavior.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/Behavior.scala index 25a95e236a..c573f3c447 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/Behavior.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/Behavior.scala @@ -6,8 +6,9 @@ package akka.actor.typed import akka.actor.InvalidMessageException import akka.actor.typed.internal.BehaviorImpl - import scala.annotation.tailrec + +import akka.actor.typed.internal.BehaviorImpl.OrElseBehavior import akka.util.{ LineNumbers, OptionVal } import akka.annotation.{ DoNotInherit, InternalApi } import akka.actor.typed.scaladsl.{ ActorContext ⇒ SAC } @@ -41,6 +42,15 @@ sealed abstract class Behavior[T] { behavior ⇒ * (which cannot be expressed directly due to type inference problems). */ final def narrow[U <: T]: Behavior[U] = this.asInstanceOf[Behavior[U]] + + /** + * Composes this `Behavior with a fallback `Behavior` which + * is used when this `Behavior` doesn't handle the message or signal, i.e. + * when `unhandled` is returned. + * + * @param that the fallback `Behavior` + */ + final def orElse(that: Behavior[T]): Behavior[T] = new OrElseBehavior[T](this, that) } /** diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/BehaviorImpl.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/BehaviorImpl.scala index 554061a467..c0d3e0fc63 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/BehaviorImpl.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/BehaviorImpl.scala @@ -178,4 +178,20 @@ import scala.reflect.ClassTag override def toString = s"$toStringPrefix(${LineNumbers(beforeOnMessage)},${LineNumbers(beforeOnSignal)},$behavior)" } + class OrElseBehavior[T](first: Behavior[T], second: Behavior[T]) extends ExtensibleBehavior[T] { + override def receive(ctx: AC[T], msg: T): Behavior[T] = { + Behavior.interpretMessage(first, ctx, msg) match { + case _: UnhandledBehavior.type ⇒ Behavior.interpretMessage(second, ctx, msg) + case handled ⇒ handled + } + } + + override def receiveSignal(ctx: AC[T], msg: Signal): Behavior[T] = { + Behavior.interpretSignal(first, ctx, msg) match { + case _: UnhandledBehavior.type ⇒ Behavior.interpretSignal(second, ctx, msg) + case handled ⇒ handled + } + } + } + } diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala index 96f73d9e0e..69fb42b008 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala @@ -115,6 +115,8 @@ object Behaviors { /** * Construct an immutable actor behavior from a partial message handler which treats undefined messages as unhandled. + * + * Behaviors can also be composed with [[Behavior#orElse]]. */ def receivePartial[T](onMessage: PartialFunction[(ActorContext[T], T), Behavior[T]]): Receive[T] = Behaviors.receive[T] { (ctx, t) ⇒ @@ -123,6 +125,8 @@ object Behaviors { /** * Construct an immutable actor behavior from a partial message handler which treats undefined messages as unhandled. + * + * Behaviors can also be composed with [[Behavior#orElse]]. */ def receiveMessagePartial[T](onMessage: PartialFunction[T, Behavior[T]]): Receive[T] = Behaviors.receive[T] { (_, t) ⇒