Implemented custom fsm to make the code more readable

This commit is contained in:
Eugene Zadyra
2018-07-02 16:22:07 +02:00
parent 55bae1a221
commit c613d09508
17 changed files with 659 additions and 533 deletions

View File

@@ -27,78 +27,55 @@ import akka.actor.{ActorRef, Props}
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Account._
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login._
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.service.DataService
class Account(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization, router: ActorRef) extends SafeFSM[FSMState, FSMData] with Localizable {
class Account(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization, router: ActorRef) extends Conversation[Unit] with Localizable {
startWith(AskAction, null)
whenSafe(AskAction) {
case Event(Next, _) =>
def askAction: QA =
question { _ =>
val credentials = dataService.getUserCredentials(userId.userId)
val currentAccount = credentials.find(c => c.accountId == userId.accountId).getOrElse(sys.error("Can't determine current account"))
val buttons = Seq(Button(lang.addAccount, -1L), Button(lang.deleteAccount, -2L)) ++ credentials.map(c => Button(s"🔐️ ${c.username}", c.accountId))
bot.sendMessage(userId.source, lang.pleaseChooseAccount(currentAccount.username), inlineKeyboard = createInlineKeyboard(buttons, columns = 1))
goto(AwaitAction)
}
whenSafe(AwaitAction) {
case Event(cmd@Command(_, _, Some(actionStr)), _) =>
val action = actionStr.toLong
action match {
case -1L =>
router ! cmd.copy(message = cmd.message.copy(text = Some("/login")))
goto(AskAction) using null
case -2L =>
bot.sendMessage(userId.source, "Not implemented yet")
goto(AskAction) using null
case accountId =>
val accountMaybe = dataService.findUserCredentialsByAccountId(userId.userId, accountId)
accountMaybe match {
case Some(account) => //account was found
val userMaybe = dataService.findUser(userId.userId)
userMaybe.foreach { user =>
user.activeAccountId = accountId
dataService.saveUser(user)
router ! SwitchUser(UserId(account.userId, account.accountId, userId.source))
bot.sendMessage(userId.source, lang.accountSwitched(account.username))
}
goto(AskAction) using null
case None =>
error(s"This is not user [#${userId.userId}] account [#$accountId]")
goto(AskAction) using null
}
}
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
goto(AskAction) using null
case e: Event =>
error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
} answer {
case Msg(cmd@Command(_, _, Some(actionStr)), _) =>
val action = actionStr.toLong
action match {
case -1L =>
router ! cmd.copy(message = cmd.message.copy(text = Some("/login")))
stay()
case -2L =>
bot.sendMessage(userId.source, "Not implemented yet")
stay()
case accountId =>
val accountMaybe = dataService.findUserCredentialsByAccountId(userId.userId, accountId)
accountMaybe match {
case Some(account) => //account was found
val userMaybe = dataService.findUser(userId.userId)
userMaybe.foreach { user =>
user.activeAccountId = accountId
dataService.saveUser(user)
router ! SwitchUser(UserId(account.userId, account.accountId, userId.source))
bot.sendMessage(userId.source, lang.accountSwitched(account.username))
}
stay()
case None =>
error(s"This is not user [#${userId.userId}] account [#$accountId]")
stay()
}
}
}
entryPoint(askAction)
}
object Account {
def props(userId: UserId, bot: Bot, dataService: DataService, localization: Localization, router: ActorRef): Props =
Props(new Account(userId, bot, dataService, localization, router))
object AskAction extends FSMState
object AwaitAction extends FSMState
object AddAccount extends FSMState
object RemoveAccount extends FSMState
case class SwitchUser(userId: UserId)
}

View File

@@ -34,6 +34,7 @@ import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.DatePicker.{DateFromMode, DateToMode}
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.StaticData.StaticDataConfig
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.service.{ApiService, DataService, MonitoringService}
@@ -81,6 +82,7 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
whenSafe(RequestDateFrom) {
case Event(_, bookingData: BookingData) =>
datePicker ! StartConversation
datePicker ! DateFromMode
datePicker ! bookingData.dateFrom
goto(AwaitDateFrom)
@@ -257,8 +259,8 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
private def reinit() = {
invokeNext()
datePicker ! Init
staticData ! Init
datePicker ! InitConversation
staticData ! InitConversation
termsPager ! Init
goto(RequestCity) using BookingData()
}

View File

@@ -23,95 +23,70 @@
*/
package com.lbs.server.actor
import akka.actor.{PoisonPill, Props}
import akka.actor.Props
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Bug._
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model
import com.lbs.server.service.DataService
import com.lbs.server.util.MessageExtractors
class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorFactory: ByUserIdWithOriginatorActorFactory,
val localization: Localization) extends SafeFSM[FSMState, FSMData] with Localizable {
val localization: Localization) extends Conversation[Unit] with Localizable {
private val bugPager = bugPagerActorFactory(userId, self)
startWith(RequestAction, null)
whenSafe(RequestAction) {
case Event(Next, _) =>
def askAction: Step =
question { _ =>
bot.sendMessage(userId.source, lang.bugAction, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.createNewBug, Tags.SubmitNew), Button(lang.showSubmittedBugs, Tags.ListSubmitted))))
goto(AwaitAction)
}
} answer {
case Msg(Command(_, _, Some(Tags.SubmitNew)), _) =>
goto(askBugDescription)
case Msg(Command(_, _, Some(Tags.ListSubmitted)), _) =>
goto(displaySubmittedBugs)
}
whenSafe(AwaitAction) {
case Event(Command(_, _, Some(Tags.SubmitNew)), _) =>
bot.sendMessage(userId.source, lang.enterIssueDetails)
goto(AwaitBugDescription)
case Event(Command(_, _, Some(Tags.ListSubmitted)), _) =>
invokeNext()
goto(RequestData)
}
whenSafe(RequestData) {
case Event(Next, _) =>
def displaySubmittedBugs: IC =
internalConfig { _ =>
val bugs = dataService.getBugs(userId.userId)
bugPager ! Init
bugPager ! InitConversation
bugPager ! StartConversation
bugPager ! Right[Throwable, Seq[model.Bug]](bugs)
goto(AwaitPage)
}
goto(processResponseFromPager)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
bugPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noSubmittedIssuesFound)
goto(RequestData)
}
def processResponseFromPager: M =
monologue {
case Msg(cmd: Command, _) =>
bugPager ! cmd
stay()
case Msg(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noSubmittedIssuesFound)
end()
}
whenSafe(AwaitBugDescription) {
case Event(Command(_, MessageExtractors.Text(details), _), _) =>
val bugId = dataService.submitBug(userId.userId, userId.source.sourceSystem.id, details)
bot.sendMessage(userId.source, lang.bugHasBeenCreated(bugId.getOrElse(-1L)))
goto(RequestAction) using null
}
def askBugDescription: Step =
question { _ =>
bot.sendMessage(userId.source, lang.enterIssueDetails)
} answer {
case Msg(Command(_, MessageExtractors.Text(details), _), _) =>
val bugId = dataService.submitBug(userId.userId, userId.source.sourceSystem.id, details)
bot.sendMessage(userId.source, lang.bugHasBeenCreated(bugId.getOrElse(-1L)))
end()
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
bugPager ! Init
goto(RequestAction)
}
initialize()
override def postStop(): Unit = {
bugPager ! PoisonPill
super.postStop()
}
entryPoint(askAction)
}
object Bug {
def props(userId: UserId, bot: Bot, dataService: DataService, bugPagerActorFactory: ByUserIdWithOriginatorActorFactory, localization: Localization): Props =
Props(new Bug(userId, bot, dataService, bugPagerActorFactory, localization))
object RequestBugDetails extends FSMState
object AwaitBugDescription extends FSMState
object RequestAction extends FSMState
object AwaitAction extends FSMState
object RequestData extends FSMState
object AwaitPage extends FSMState
object Tags {
val SubmitNew = "submit"
val ListSubmitted = "list"

View File

@@ -28,6 +28,7 @@ import com.lbs.bot.model.Command
import com.lbs.common.Logger
import com.lbs.server.actor.Chat._
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.service.{DataService, MonitoringService}
import com.lbs.server.util.MessageExtractors._
@@ -66,7 +67,8 @@ class Chat(val userId: UserId, dataService: DataService, monitoringService: Moni
when(HistoryChat, historyActor) {
case Event(Command(_, Text("/history"), _), _) =>
historyActor ! Init
historyActor ! InitConversation
historyActor ! StartConversation
stay()
}
@@ -78,25 +80,29 @@ class Chat(val userId: UserId, dataService: DataService, monitoringService: Moni
when(BugChat, bugActor) {
case Event(Command(_, Text("/bug"), _), _) =>
bugActor ! Init
bugActor ! InitConversation
bugActor ! StartConversation
goto(BugChat)
}
when(MonitoringsChat, monitoringsActor) {
case Event(Command(_, Text("/monitorings"), _), _) =>
monitoringsActor ! Init
monitoringsActor ! InitConversation
monitoringsActor ! StartConversation
stay()
}
when(SettingsChat, settingsActor) {
case Event(Command(_, Text("/settings"), _), _) =>
settingsActor ! Init
settingsActor ! InitConversation
settingsActor ! StartConversation
stay()
}
when(AccountChat, accountActor) {
case Event(Command(_, Text("/accounts"), _), _) =>
accountActor ! Init
accountActor ! InitConversation
accountActor ! StartConversation
stay()
}

View File

@@ -29,9 +29,9 @@ import java.time.{LocalTime, ZonedDateTime}
import akka.actor.{ActorRef, Props}
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.DatePicker._
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.lang.{Localizable, Localization}
/**
@@ -42,59 +42,48 @@ import com.lbs.server.lang.{Localizable, Localization}
* ⬇ ⬇ ⬇
*
*/
class DatePicker(val userId: UserId, val bot: Bot, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, ZonedDateTime] with Localizable {
startWith(AwaitMode, null)
class DatePicker(val userId: UserId, val bot: Bot, val localization: Localization, originator: ActorRef) extends Conversation[ZonedDateTime] with Localizable {
private var mode: Mode = DateFromMode
whenSafe(AwaitMode) {
case Event(newMode: Mode, _) =>
mode = newMode
goto(RequestDate)
}
entryPoint(configure)
whenSafe(RequestDate) {
case Event(initialDate: ZonedDateTime, _) =>
def configure: EC =
externalConfig {
case Msg(newMode: Mode, _) =>
mode = newMode
stay()
case Msg(initialDate: ZonedDateTime, _) =>
goto(requestDate) using initialDate
}
def requestDate: QA =
question { initialDate =>
val message = mode match {
case DateFromMode => lang.chooseDateFrom
case DateToMode => lang.chooseDateTo
}
bot.sendMessage(userId.source, message, inlineKeyboard = dateButtons(initialDate))
goto(AwaitDate) using initialDate
}
} answer {
case Msg(Command(_, msg, Some(Tags.Done)), finalDate) =>
val (message, updatedDate) = mode match {
case DateFromMode =>
val startOfTheDay = finalDate.`with`(LocalTime.MIN)
val dateFrom = if (startOfTheDay.isBefore(ZonedDateTime.now())) finalDate else startOfTheDay
lang.dateFromIs(dateFrom) -> dateFrom
case DateToMode =>
val dateTo = finalDate.`with`(LocalTime.MAX).minusHours(2)
lang.dateToIs(dateTo) -> dateTo
}
bot.sendEditMessage(userId.source, msg.messageId, message)
originator ! updatedDate
goto(configure) using null
whenSafe(AwaitDate) {
case Event(Command(_, msg, Some(Tags.Done)), finalDate: ZonedDateTime) =>
val (message, updatedDate) = mode match {
case DateFromMode =>
val startOfTheDay = finalDate.`with`(LocalTime.MIN)
val dateFrom = if (startOfTheDay.isBefore(ZonedDateTime.now())) finalDate else startOfTheDay
lang.dateFromIs(dateFrom) -> dateFrom
case DateToMode =>
val dateTo = finalDate.`with`(LocalTime.MAX).minusHours(2)
lang.dateToIs(dateTo) -> dateTo
}
bot.sendEditMessage(userId.source, msg.messageId, message)
originator ! updatedDate
goto(AwaitMode) using null
case Event(Command(_, msg, Some(tag)), date: ZonedDateTime) =>
val modifiedDate = modifyDate(date, tag)
bot.sendEditMessage(userId.source, msg.messageId, inlineKeyboard = dateButtons(modifiedDate))
stay() using modifiedDate
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(AwaitMode) using null
case e: Event =>
error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
case Msg(Command(_, msg, Some(tag)), date) =>
val modifiedDate = modifyDate(date, tag)
bot.sendEditMessage(userId.source, msg.messageId, inlineKeyboard = dateButtons(modifiedDate))
stay() using modifiedDate
}
private def modifyDate(date: ZonedDateTime, tag: String) = {
val dateModifier = tag match {
@@ -127,12 +116,6 @@ object DatePicker {
def props(userId: UserId, bot: Bot, localization: Localization, originator: ActorRef): Props =
Props(new DatePicker(userId, bot, localization, originator))
object RequestDate extends FSMState
object AwaitDate extends FSMState
object AwaitMode extends FSMState
trait Mode
object DateFromMode extends Mode

View File

@@ -23,17 +23,23 @@
*/
package com.lbs.server.actor
import akka.actor.{Actor, Props}
import akka.actor.Props
import com.lbs.bot.Bot
import com.lbs.bot.model.Command
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.lang.{Localizable, Localization}
class Help(val userId: UserId, bot: Bot, val localization: Localization) extends Actor with Localizable {
override def receive: Receive = {
case _: Command =>
bot.sendMessage(userId.source, lang.help)
}
class Help(val userId: UserId, bot: Bot, val localization: Localization) extends Conversation[Unit] with Localizable {
entryPoint(displayHelp)
def displayHelp: M =
monologue {
case Msg(_: Command, _) =>
bot.sendMessage(userId.source, lang.help)
stay()
}
}
object Help {

View File

@@ -27,44 +27,38 @@ import akka.actor.{PoisonPill, Props}
import com.lbs.api.json.model.HistoricVisit
import com.lbs.bot.Bot
import com.lbs.bot.model.Command
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.History.{AwaitPage, RequestData}
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.service.ApiService
class History(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization, historyPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends SafeFSM[FSMState, FSMData] with Localizable {
class History(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization, historyPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends Conversation[Unit] with Localizable {
private val historyPager = historyPagerActorFactory(userId, self)
startWith(RequestData, null)
entryPoint(prepareData)
whenSafe(RequestData) {
case Event(Next, _) =>
def prepareData: IC =
internalConfig { _ =>
val visits = apiService.visitsHistory(userId.accountId)
historyPager ! InitConversation
historyPager ! StartConversation
historyPager ! visits
goto(AwaitPage)
}
goto(processResponseFromPager)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
historyPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.visitsHistoryIsEmpty)
goto(RequestData)
case Event(_: HistoricVisit, _) =>
goto(RequestData) using null
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
historyPager ! Init
goto(RequestData)
}
initialize()
def processResponseFromPager: M =
monologue {
case Msg(cmd: Command, _) =>
historyPager ! cmd
stay()
case Msg(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.visitsHistoryIsEmpty)
end()
case Msg(_: HistoricVisit, _) =>
end()
}
override def postStop(): Unit = {
historyPager ! PoisonPill
@@ -75,10 +69,4 @@ class History(val userId: UserId, bot: Bot, apiService: ApiService, val localiza
object History {
def props(userId: UserId, bot: Bot, apiService: ApiService, localization: Localization, historyPagerActorFactory: ByUserIdWithOriginatorActorFactory): Props =
Props(new History(userId, bot, apiService, localization, historyPagerActorFactory))
object RequestData extends FSMState
object AwaitPage extends FSMState
}

View File

@@ -26,77 +26,61 @@ package com.lbs.server.actor
import akka.actor.{ActorRef, Props}
import com.lbs.bot.Bot
import com.lbs.bot.model.{Command, MessageSource}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login._
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.service.{ApiService, DataService}
import com.lbs.server.util.MessageExtractors
import org.jasypt.util.text.TextEncryptor
class Login(source: MessageSource, bot: Bot, dataService: DataService, apiService: ApiService, textEncryptor: TextEncryptor, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, FSMData] with Localizable {
class Login(source: MessageSource, bot: Bot, dataService: DataService, apiService: ApiService, textEncryptor: TextEncryptor, val localization: Localization, originator: ActorRef) extends Conversation[LoginData] with Localizable {
protected var userId: UserId = _
startWith(LogIn, LoginData())
entryPoint(logIn)
private var forwardCommand: ForwardCommand = _
whenSafe(LogIn) {
case Event(cmd: Command, LoginData(None, None)) =>
forwardCommand = ForwardCommand(cmd)
invokeNext()
goto(RequestUsername)
case Event(_, LoginData(Some(username), Some(password))) =>
def logIn: M =
monologue {
case Msg(cmd: Command, LoginData(None, None)) =>
forwardCommand = ForwardCommand(cmd)
goto(requestUsername)
}
def requestUsername: QA =
question { _ =>
bot.sendMessage(source, lang.provideUsername)
} answer {
case Msg(Command(_, MessageExtractors.TextOpt(username), _), _) =>
goto(requestPassword) using LoginData(username = username)
}
def requestPassword: QA =
question { _ =>
bot.sendMessage(source, lang.providePassword)
} answer {
case Msg(Command(_, MessageExtractors.TextOpt(password), _), loginData: LoginData) =>
goto(processLoginInformation) using loginData.copy(password = password.map(textEncryptor.encrypt))
}
def processLoginInformation: IC = {
internalConfig { case LoginData(Some(username), Some(password)) =>
val loginResult = apiService.login(username, password)
loginResult match {
case Left(error) =>
bot.sendMessage(source, error.getMessage)
invokeNext()
goto(RequestUsername) using LoginData()
goto(requestUsername)
case Right(loggedIn) =>
val credentials = dataService.saveCredentials(source, username, password)
userId = UserId(credentials.userId, credentials.accountId, source)
apiService.addSession(credentials.accountId, loggedIn.accessToken, loggedIn.tokenType)
bot.sendMessage(source, lang.loginAndPasswordAreOk)
originator ! LoggedIn(forwardCommand, credentials.userId, credentials.accountId)
stay() using null
end()
}
}
}
whenSafe(RequestUsername) {
case Event(Next, _) =>
bot.sendMessage(source, lang.provideUsername)
goto(AwaitUsername)
}
whenSafe(AwaitUsername) {
case Event(Command(_, MessageExtractors.TextOpt(username), _), loginData: LoginData) =>
invokeNext()
goto(RequestPassword) using loginData.copy(username = username)
}
whenSafe(RequestPassword) {
case Event(Next, _) =>
bot.sendMessage(source, lang.providePassword)
goto(AwaitPassword)
}
whenSafe(AwaitPassword) {
case Event(Command(_, MessageExtractors.TextOpt(password), _), loginData: LoginData) =>
invokeNext()
goto(LogIn) using loginData.copy(password = password.map(textEncryptor.encrypt))
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(LogIn) using LoginData()
case e: Event =>
error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
}
object Login {
@@ -104,17 +88,7 @@ object Login {
def props(source: MessageSource, bot: Bot, dataService: DataService, apiService: ApiService, textEncryptor: TextEncryptor, localization: Localization, originator: ActorRef): Props =
Props(new Login(source, bot, dataService, apiService, textEncryptor, localization, originator))
object LogIn extends FSMState
object RequestUsername extends FSMState
object AwaitUsername extends FSMState
object RequestPassword extends FSMState
object AwaitPassword extends FSMState
case class LoginData(username: Option[String] = None, password: Option[String] = None) extends FSMData
case class LoginData(username: Option[String] = None, password: Option[String] = None)
case class ForwardCommand(cmd: Command)

View File

@@ -26,57 +26,54 @@ package com.lbs.server.actor
import akka.actor.{PoisonPill, Props}
import com.lbs.bot._
import com.lbs.bot.model.{Button, Command}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Monitorings.{AwaitDecision, AwaitPage, RequestData, Tags}
import com.lbs.server.actor.Monitorings.Tags
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.service.MonitoringService
class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringService, val localization: Localization, monitoringsPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends SafeFSM[FSMState, Monitoring] with Localizable {
class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringService, val localization: Localization, monitoringsPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends Conversation[Monitoring] with Localizable {
private val monitoringsPager = monitoringsPagerActorFactory(userId, self)
startWith(RequestData, null)
entryPoint(prepareData)
whenSafe(RequestData) {
case Event(Next, _) =>
def prepareData: IC =
internalConfig { _ =>
val monitorings = monitoringService.getActiveMonitorings(userId.accountId)
monitoringsPager ! InitConversation
monitoringsPager ! StartConversation
monitoringsPager ! Right[Throwable, Seq[Monitoring]](monitorings)
goto(AwaitPage)
}
goto(processResponseFromPager)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
monitoringsPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noActiveMonitorings)
goto(RequestData)
case Event(monitoring: Monitoring, _) =>
def processResponseFromPager: M =
monologue {
case Msg(cmd: Command, _) =>
monitoringsPager ! cmd
stay()
case Msg(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noActiveMonitorings)
end()
case Msg(monitoring: Monitoring, _) =>
goto(askToDeactivateMonitoring) using monitoring
}
def askToDeactivateMonitoring: QA =
question { monitoring =>
bot.sendMessage(userId.source, lang.deactivateMonitoring(monitoring), inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))
goto(AwaitDecision) using monitoring
}
whenSafe(AwaitDecision) {
case Event(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.monitoringWasNotDeactivated)
goto(RequestData)
case Event(Command(_, _, Some(Tags.Yes)), monitoring: Monitoring) =>
monitoringService.deactivateMonitoring(monitoring.recordId)
bot.sendMessage(userId.source, lang.deactivated)
goto(RequestData)
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
monitoringsPager ! Init
goto(RequestData)
}
initialize()
} answer {
case Msg(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.monitoringWasNotDeactivated)
end()
case Msg(Command(_, _, Some(Tags.Yes)), monitoring: Monitoring) =>
monitoringService.deactivateMonitoring(monitoring.recordId)
bot.sendMessage(userId.source, lang.deactivated)
end()
}
override def postStop(): Unit = {
monitoringsPager ! PoisonPill
@@ -88,12 +85,6 @@ object Monitorings {
def props(userId: UserId, bot: Bot, monitoringService: MonitoringService, localization: Localization, monitoringsPagerActorFactory: ByUserIdWithOriginatorActorFactory): Props =
Props(new Monitorings(userId, bot, monitoringService, localization, monitoringsPagerActorFactory))
object RequestData extends FSMState
object AwaitPage extends FSMState
object AwaitDecision extends FSMState
object Tags {
val Yes = "yes"
val No = "no"

View File

@@ -27,57 +27,49 @@ import akka.actor.{ActorRef, Props}
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.common.Logger
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Pager.{Tags, _}
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.util.MessageExtractors
class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) => String,
makeHeader: (Int, Int) => String, selectionPrefix: Option[String],
val localization: Localization, originator: ActorRef)
extends SafeFSM[FSMState, FSMData] with Localizable with Logger {
private case class Page(page: Int, pages: Seq[Seq[Data]]) extends FSMData
extends Conversation[(Registry[Data], Option[String])] with Localizable with Logger {
private val Selection = s"/${selectionPrefix.getOrElse("")}_(\\d+)_(\\d+)".r
startWith(PrepareData, null)
entryPoint(awaitForData)
whenSafe(PrepareData) {
case Event(Left(error: Throwable), _) =>
bot.sendMessage(userId.source, error.getMessage)
invokeNext()
goto(PrepareData)
case Event(Right(items: Seq[Data]), _) if items.isEmpty =>
originator ! NoItemsFound
goto(PrepareData) using null
case Event(Right(items: Seq[Data]), _) =>
invokeNext()
goto(RequestData) using Page(0, items.grouped(Pager.PageSize).toList)
}
private def awaitForData: EC =
externalConfig {
case Msg(Left(error: Throwable), _) =>
bot.sendMessage(userId.source, error.getMessage)
end()
case Msg(Right(items: Seq[Data]), _) if items.isEmpty =>
originator ! NoItemsFound
end()
case Msg(Right(items: Seq[Data]), _) =>
goto(displayPage) using Registry(0, items.grouped(Pager.PageSize).toList) -> None
}
whenSafe(RequestData) {
case Event(Next, page: Page) =>
sendPage(page.page, page.pages)
goto(AwaitData)
}
whenSafe(AwaitData) {
case Event(Command(_, msg, Some(Tags.Next)), termsData: Page) =>
val page = termsData.page + 1
sendPage(page, termsData.pages, Some(msg.messageId))
stay() using termsData.copy(page = page)
case Event(Command(_, msg, Some(Tags.Previous)), termsData: Page) =>
val page = termsData.page - 1
sendPage(page, termsData.pages, Some(msg.messageId))
stay() using termsData.copy(page = page)
case Event(Command(_, MessageExtractors.Text(Selection(pageStr, indexStr)), _), termsData: Page) if selectionPrefix.nonEmpty =>
val page = pageStr.toInt
val index = indexStr.toInt
originator ! termsData.pages(page)(index)
goto(PrepareData) using null
}
private def displayPage: QA =
question { case (registry, massageIdMaybe) =>
sendPage(registry.page, registry.pages, massageIdMaybe)
} answer {
case Msg(Command(_, msg, Some(Tags.Next)), (registry, _)) =>
val page = registry.page + 1
goto(displayPage) using registry.copy(page = page) -> Some(msg.messageId)
case Msg(Command(_, msg, Some(Tags.Previous)), (registry, _)) =>
val page = registry.page - 1
goto(displayPage) using registry.copy(page = page) -> Some(msg.messageId)
case Msg(Command(_, MessageExtractors.Text(Selection(pageStr, indexStr)), _), (registry, _)) if selectionPrefix.nonEmpty =>
val page = pageStr.toInt
val index = indexStr.toInt
originator ! registry.pages(page)(index)
end()
}
private def sendPage(page: Int, data: Seq[Seq[Data]], messageId: Option[String] = None): Unit = {
val pages = data.length
@@ -94,31 +86,16 @@ class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) =>
bot.sendMessage(userId.source, message, inlineKeyboard = createInlineKeyboard(buttons))
}
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(PrepareData) using null
case e: Event =>
error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
}
object Pager {
def props[Data](userId: UserId, bot: Bot,
makeMessage: (Data, Int, Int) => String, makeHeader: (Int, Int) => String, dataPrefix: Option[String], localization: Localization, originator: ActorRef): Props =
Props(new Pager[Data](userId, bot, makeMessage, makeHeader, dataPrefix, localization, originator))
val PageSize = 5
object PrepareData extends FSMState
object RequestData extends FSMState
object AwaitData extends FSMState
case class Registry[Data](page: Int, pages: Seq[Seq[Data]])
object NoItemsFound

View File

@@ -26,57 +26,42 @@ package com.lbs.server.actor
import akka.actor.Props
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Settings._
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.lang.{Lang, Localizable, Localization}
import com.lbs.server.service.DataService
class Settings(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization) extends SafeFSM[FSMState, FSMData] with Localizable {
class Settings(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization) extends Conversation[Unit] with Localizable {
startWith(RequestAction, null)
entryPoint(askForAction)
whenSafe(RequestAction) {
case Event(Next, _) =>
def askForAction: QA =
question { _ =>
bot.sendMessage(userId.source, lang.settingsHeader, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.language, Tags.Language))))
goto(AwaitAction)
}
} answer {
case Msg(Command(_, _, Some(Tags.Language)), _) =>
goto(askLanguage)
}
whenSafe(AwaitAction) {
case Event(Command(_, _, Some(Tags.Language)), _) =>
def askLanguage: QA =
question { _ =>
bot.sendMessage(userId.source, lang.chooseLanguage,
inlineKeyboard = createInlineKeyboard(Lang.Langs.map(l => Button(l.label, l.id)), columns = 1))
goto(AwaitLanguage)
}
whenSafe(AwaitLanguage) {
case Event(Command(_, _, Some(langIdStr)), _) =>
val langId = langIdStr.toInt
localization.updateLanguage(userId.userId, Lang(langId))
bot.sendMessage(userId.source, lang.languageUpdated)
goto(RequestAction) using null
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
goto(RequestAction)
}
initialize()
} answer {
case Msg(Command(_, _, Some(langIdStr)), _) =>
val langId = langIdStr.toInt
localization.updateLanguage(userId.userId, Lang(langId))
bot.sendMessage(userId.source, lang.languageUpdated)
end()
}
}
object Settings {
def props(userId: UserId, bot: Bot, dataService: DataService, localization: Localization): Props =
Props(new Settings(userId, bot, dataService, localization))
object AwaitLanguage extends FSMState
object RequestAction extends FSMState
object AwaitAction extends FSMState
object Tags {
val Language = "language"
}

View File

@@ -27,94 +27,73 @@ import akka.actor.{ActorRef, Props}
import com.lbs.api.json.model.IdName
import com.lbs.bot.model.{Button, Command, TaggedButton}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.StaticData._
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.lang.{Localizable, Localization}
class StaticData(val userId: UserId, bot: Bot, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, IdName] with Localizable {
class StaticData(val userId: UserId, bot: Bot, val localization: Localization, originator: ActorRef) extends Conversation[List[TaggedButton]] with Localizable {
private def anySelectOption: List[TaggedButton] = if (config.isAnyAllowed) List(Button(lang.any, -1L)) else List()
startWith(AwaitConfig, null)
private var config: StaticDataConfig = _
private var callbackTags: List[TaggedButton] = List()
entryPoint(AwaitConfig)
whenSafe(AwaitConfig) {
case Event(newConfig: StaticDataConfig, _) =>
config = newConfig
invokeNext()
goto(RequestStaticData)
}
def AwaitConfig: EC =
externalConfig {
case Msg(newConfig: StaticDataConfig, _) =>
config = newConfig
goto(askForLatestOption)
}
whenSafe(RequestStaticData) {
case Event(Next, _) =>
def askForLatestOption: QA =
question { _ =>
originator ! LatestOptions
stay()
case Event(LatestOptions(options), _) if options.isEmpty =>
callbackTags = anySelectOption
bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrAny(config),
inlineKeyboard = createInlineKeyboard(callbackTags))
goto(AwaitStaticData)
case Event(LatestOptions(options), _) if options.nonEmpty =>
callbackTags = anySelectOption ++ options.map(data => Button(data.name, data.id))
} answer {
case Msg(LatestOptions(options), _) if options.isEmpty =>
val callbackTags = anySelectOption
goto(askForUserInput) using callbackTags
case Msg(LatestOptions(options), _) if options.nonEmpty =>
val callbackTags = anySelectOption ++ options.map(data => Button(data.name, data.id))
goto(askForUserInput) using callbackTags
}
def askForUserInput: QA =
question { callbackTags =>
bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrPrevious(config),
inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1))
goto(AwaitStaticData)
}
} answer {
case Msg(Command(_, msg, Some(tag)), callbackTags) =>
val id = tag.toLong
val label = callbackTags.find(_.tag == tag).map(_.label).getOrElse(sys.error("Unable to get callback tag label"))
bot.sendEditMessage(userId.source, msg.messageId, lang.staticDataIs(config, label))
originator ! IdName(id, label)
end()
whenSafe(AwaitStaticData) {
case Event(Command(_, msg, Some(tag)), _) =>
val id = tag.toLong
val label = callbackTags.find(_.tag == tag).map(_.label).getOrElse(sys.error("Unable to get callback tag label"))
bot.sendEditMessage(userId.source, msg.messageId, lang.staticDataIs(config, label))
originator ! IdName(id, label)
goto(AwaitConfig)
case Msg(Command(_, msg, _), _) =>
val searchText = msg.text.get.toLowerCase
originator ! FindOptions(searchText)
stay()
case Event(Command(_, msg, _), _) =>
val searchText = msg.text.get.toLowerCase
originator ! FindOptions(searchText)
stay()
case Msg(FoundOptions(Right(options)), _) if options.nonEmpty =>
val callbackTags = anySelectOption ::: options.map(c => Button(c.name, c.id))
goto(askForUserInput) using callbackTags
case Event(FoundOptions(Right(options)), _) if options.nonEmpty =>
callbackTags = anySelectOption ::: options.map(c => Button(c.name, c.id))
bot.sendMessage(userId.source, lang.pleaseChooseStaticDataNameOrAny(config),
inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1))
stay()
case Msg(FoundOptions(Right(options)), _) if options.isEmpty =>
val callbackTags = anySelectOption
goto(askForUserInput) using callbackTags
case Event(FoundOptions(Right(options)), _) if options.isEmpty =>
callbackTags = anySelectOption
bot.sendMessage(userId.source, lang.staticNotFound(config), inlineKeyboard = createInlineKeyboard(callbackTags))
stay()
case Event(FoundOptions(Left(ex)), _) =>
bot.sendMessage(userId.source, ex.getMessage)
stay()
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(AwaitConfig) using null
case e: Event =>
error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
case Msg(FoundOptions(Left(ex)), _) =>
bot.sendMessage(userId.source, ex.getMessage)
end()
}
}
object StaticData {
def props(userId: UserId, bot: Bot, localization: Localization, originator: ActorRef): Props =
Props(new StaticData(userId, bot, localization, originator))
object AwaitConfig extends FSMState
object RequestStaticData extends FSMState
object AwaitStaticData extends FSMState
case class StaticDataConfig(name: String, example: String, isAnyAllowed: Boolean)
object LatestOptions

View File

@@ -1,26 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.ActorRef
@@ -28,6 +28,7 @@ import com.lbs.api.json.model.IdName
import com.lbs.bot.model.Command
import com.lbs.server.actor.Book.BookingData
import com.lbs.server.actor.StaticData.{FindOptions, FoundOptions, LatestOptions, StaticDataConfig}
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
trait StaticDataForBooking extends SafeFSM[FSMState, FSMData] {
@@ -53,6 +54,8 @@ trait StaticDataForBooking extends SafeFSM[FSMState, FSMData] {
protected def requestStaticData(requestState: FSMState, awaitState: FSMState, staticDataConfig: => StaticDataConfig)(functions: BookingData => FSMState => StateFunction)(requestNext: FSMState): Unit = {
whenSafe(requestState) {
case Event(_, _) =>
staticData ! InitConversation
staticData ! StartConversation
staticData ! staticDataConfig
goto(awaitState)
}

View File

@@ -27,59 +27,56 @@ import akka.actor.{PoisonPill, Props}
import com.lbs.api.json.model.ReservedVisit
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Visits.{AwaitDecision, AwaitPage, RequestData, Tags}
import com.lbs.server.actor.Visits.Tags
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.service.ApiService
class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization,
visitsPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends SafeFSM[FSMState, ReservedVisit] with Localizable {
visitsPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends Conversation[ReservedVisit] with Localizable {
private val reservedVisitsPager = visitsPagerActorFactory(userId, self)
startWith(RequestData, null)
entryPoint(prepareData)
whenSafe(RequestData) {
case Event(Next, _) =>
def prepareData: IC =
internalConfig { _ =>
val visits = apiService.reservedVisits(userId.accountId)
reservedVisitsPager ! InitConversation
reservedVisitsPager ! StartConversation
reservedVisitsPager ! visits
goto(AwaitPage)
}
goto(processResponseFromPager)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
reservedVisitsPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noUpcomingVisits)
goto(RequestData)
case Event(visit: ReservedVisit, _) =>
def processResponseFromPager: M =
monologue {
case Msg(cmd: Command, _) =>
reservedVisitsPager ! cmd
stay()
case Msg(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noUpcomingVisits)
end()
case Msg(visit: ReservedVisit, _) =>
goto(askToCancelVisit) using visit
}
def askToCancelVisit: QA =
question { visit =>
bot.sendMessage(userId.source, lang.areYouSureToCancelAppointment(visit),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))
goto(AwaitDecision) using visit
}
whenSafe(AwaitDecision) {
case Event(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.appointmentWasNotCancelled)
goto(RequestData)
case Event(Command(_, _, Some(Tags.Yes)), visit: ReservedVisit) =>
apiService.deleteReservation(userId.accountId, visit.reservationId) match {
case Left(ex) => bot.sendMessage(userId.source, lang.unableToCancelUpcomingVisit(ex.getMessage))
case Right(r) => bot.sendMessage(userId.source, lang.appointmentHasBeenCancelled)
}
goto(RequestData)
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
reservedVisitsPager ! Init
goto(RequestData)
}
initialize()
} answer {
case Msg(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.appointmentWasNotCancelled)
end()
case Msg(Command(_, _, Some(Tags.Yes)), visit: ReservedVisit) =>
apiService.deleteReservation(userId.accountId, visit.reservationId) match {
case Left(ex) => bot.sendMessage(userId.source, lang.unableToCancelUpcomingVisit(ex.getMessage))
case Right(r) => bot.sendMessage(userId.source, lang.appointmentHasBeenCancelled)
}
end()
}
override def postStop(): Unit = {
reservedVisitsPager ! PoisonPill
@@ -92,12 +89,6 @@ object Visits {
visitsPagerActorFactory: ByUserIdWithOriginatorActorFactory): Props =
Props(new Visits(userId, bot, apiService, localization, visitsPagerActorFactory))
object RequestData extends FSMState
object AwaitPage extends FSMState
object AwaitDecision extends FSMState
object Tags {
val Yes = "yes"
val No = "no"

View File

@@ -0,0 +1,120 @@
package com.lbs.server.actor.conversation
import akka.actor.Actor
import com.lbs.common.Logger
import com.lbs.server.actor.conversation.Conversation.{ContinueConversation, InitConversation, StartConversation}
trait Conversation[D] extends Actor with Domain[D] with Logger {
private var currentData: D = _
private var currentStep: Step = _
private var startWithData: D = _
private var startWithStep: Step = _
private val defaultMsgHandler: AnswerFn = {
case Msg(any, data) =>
debug(s"Unhandled fact message received. [$any, $data]")
NextStep(currentStep, Some(data))
}
private var msgHandler: AnswerFn = defaultMsgHandler
private var runAfterInit: () => Unit = () => {}
override def receive: Receive = {
case InitConversation => init()
case StartConversation | ContinueConversation =>
currentStep match {
case qa: QuestionAnswer => qa.question.questionFn(currentData)
case InternalConfiguration(fn) =>
val nextStep = fn(currentData)
moveToNextStep(nextStep)
case _ => //do nothing
}
case any => makeTransition(any)
}
private def moveToNextStep(nextStep: NextStep): Unit = {
currentStep = nextStep.step
nextStep.data.foreach { data =>
currentData = data
}
}
private def makeTransition(any: Any): Unit = {
def handle[X](unit: X, fn: PartialFunction[X, NextStep], defaultFn: PartialFunction[X, NextStep]): Unit = {
val nextStep = if (fn.isDefinedAt(unit)) fn(unit) else defaultFn(unit)
moveToNextStep(nextStep)
}
currentStep match {
case ExternalConfiguration(fn) =>
val conf = Msg(any, currentData)
handle(conf, fn, msgHandler)
case QuestionAnswer(_, Answer(fn)) =>
val fact = Msg(any, currentData)
handle(fact, fn, msgHandler)
case Monologue(fn) =>
val fact = Msg(any, currentData)
handle(fact, fn, msgHandler)
case _ => //do nothing
}
}
private def init(): Unit = {
require(startWithStep != null, "Entry point must be defined")
currentStep = startWithStep
currentData = startWithData
runAfterInit()
}
override def preStart(): Unit = {
init()
}
protected def monologue(answerFn: AnswerFn): Monologue = Monologue(answerFn)
protected def question(questionFn: D => Unit): Question = Question(questionFn)
protected def externalConfig(receiveConfFunction: ExternalConfigFn): ExternalConfiguration = ExternalConfiguration(receiveConfFunction)
protected def internalConfig(receiveConfFunction: InternalConfigFn): InternalConfiguration = InternalConfiguration(receiveConfFunction)
protected def end(): NextStep = NextStep(End)
protected def goto(step: Step): NextStep = {
self ! ContinueConversation
NextStep(step)
}
protected def stay(): NextStep = NextStep(currentStep)
protected def whenUnhandledMsg(receiveMsgFn: AnswerFn): Unit = {
msgHandler = receiveMsgFn orElse defaultMsgHandler
}
protected def afterInit(fn: => Unit): Unit = {
runAfterInit = () => fn
}
protected def entryPoint(step: Step, data: D): Unit = {
startWithStep = step
startWithData = data
}
protected def entryPoint(step: Step): Unit = {
entryPoint(step, null.asInstanceOf[D])
}
}
object Conversation {
object StartConversation
object ContinueConversation
object InitConversation
}

View File

@@ -0,0 +1,50 @@
package com.lbs.server.actor.conversation
trait Domain[D] {
protected type QuestionFn = D => Unit
protected type AnswerFn = PartialFunction[Msg, NextStep]
protected type ExternalConfigFn = AnswerFn
protected type InternalConfigFn = D => NextStep
protected case class Msg(message: Any, data: D)
sealed trait Step
private[conversation] object End extends Step
protected case class ExternalConfiguration(configFn: ExternalConfigFn) extends Step
protected case class InternalConfiguration(configFn: InternalConfigFn) extends Step
protected case class QuestionAnswer(question: Question, answer: Answer) extends Step
protected case class Monologue(answerFn: AnswerFn) extends Step
private[conversation] case class NextStep(step: Step, data: Option[D] = None)
private[conversation] case class Question(questionFn: QuestionFn)
private[conversation] case class Answer(answerFn: AnswerFn)
protected type QA = QuestionAnswer
protected type EC = ExternalConfiguration
protected type IC = InternalConfiguration
protected type M = Monologue
protected implicit class RichQuestion(question: Question) {
def answer(answerFn: AnswerFn): QuestionAnswer = QuestionAnswer(question, Answer(answerFn))
}
protected implicit class NextStepOps(nextStep: NextStep) {
def using(data: D): NextStep = {
nextStep.copy(data = Some(data))
}
}
}

View File

@@ -0,0 +1,119 @@
package com.lbs.server.actor.conversation
import akka.actor.{ActorRef, Props}
import akka.testkit.TestProbe
import com.lbs.server.actor.AkkaTestKit
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
class ConversationSpec extends AkkaTestKit {
"Actor must send complete data" when {
"conversation is done" in {
case class Data(configured: Boolean = false, hello: String = null, world: String = null, people: String = null)
object Hello
object World
object Dialogue
class TestActor(originator: ActorRef) extends Conversation[Data] {
private var conf: String = _
def configure: EC =
externalConfig {
case Msg(confStr: String, data) =>
conf = confStr
goto(askHello) using data.copy(configured = true)
}
def askHello: QA =
question { data =>
self ! Hello
} answer {
case Msg(Hello, data) =>
goto(askWorld) using data.copy(hello = "hello")
}
def askWorld: QA =
question { data =>
self ! World
} answer {
case Msg(World, data) =>
goto(askDialogue) using data.copy(world = "world")
}
def askDialogue: QA =
question { data =>
self ! Dialogue
} answer {
case Msg(Dialogue, data) =>
originator ! data.copy(people = "dialogue") -> conf
end()
}
entryPoint(configure, Some(Data()))
}
val expected = Data(configured = true, "hello", "world", "dialogue") -> "myconf"
val originator = TestProbe()
val actor = system.actorOf(Props(new TestActor(originator.ref)))
actor ! StartConversation
actor ! expected._2
originator.expectMsg(expected)
//reinit
actor ! InitConversation
actor ! StartConversation
actor ! expected._2
originator.expectMsg(expected)
}
"configuration is done" in {
case class Data(configured: Boolean = false, message1: String = null, message2: String = null)
object InvokeEnrichMessage
class TestActor(originator: ActorRef) extends Conversation[Data] {
def configure1: IC =
internalConfig { _ =>
goto(configure2) using Data(configured = true)
}
def configure2: IC =
internalConfig { data =>
goto(askMessage2) using data.copy(message1 = "hello")
}
def askMessage2: QA =
question { _ =>
self ! InvokeEnrichMessage
} answer {
case Msg(InvokeEnrichMessage, data) =>
originator ! data.copy(message2 = "world")
end()
}
entryPoint(configure1, Some(Data()))
}
val expected = Data(configured = true, message1 = "hello", message2 = "world")
val originator = TestProbe()
val actor = system.actorOf(Props(new TestActor(originator.ref)))
actor ! StartConversation
originator.expectMsg(expected)
//reinit
actor ! InitConversation
actor ! StartConversation
originator.expectMsg(expected)
}
}
}