diff --git a/.gitignore b/.gitignore index f4611fa..bfc8281 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ project/plugins/project/ # Scala-IDE specific .scala_dependencies .worksheet + +# IntelliJ specific +.idea diff --git a/src/main/scala/frdomain/ch6/domain/app/app.scala b/src/main/scala/frdomain/ch6/domain/app/app.scala index de0a6bf..9533dd9 100644 --- a/src/main/scala/frdomain/ch6/domain/app/app.scala +++ b/src/main/scala/frdomain/ch6/domain/app/app.scala @@ -6,36 +6,33 @@ import scalaz._ import Scalaz._ import Kleisli._ import scala.concurrent._ -import ExecutionContext.Implicits.global import service.interpreter.{ AccountService, InterestPostingService, ReportingService } import repository.interpreter.AccountRepositoryInMemory import service.{ Checking, Savings } -import model.common._ -import model.Account object App { import AccountService._ - import InterestPostingService._ import ReportingService._ + import ExecutionContext.Implicits.global - val opens = + val opens = for { _ <- open("a1234", "a1name", None, None, Checking) _ <- open("a2345", "a2name", None, None, Checking) _ <- open("a3456", "a3name", BigDecimal(5.8).some, None, Savings) _ <- open("a4567", "a4name", None, None, Checking) _ <- open("a5678", "a5name", BigDecimal(2.3).some, None, Savings) - } yield (()) + } yield () - val credits = + val credits = for { _ <- credit("a1234", 1000) _ <- credit("a2345", 2000) _ <- credit("a3456", 3000) _ <- credit("a4567", 4000) - } yield (()) + } yield () val c = for { _ <- opens diff --git a/src/main/scala/frdomain/ch6/domain/app/app2.scala b/src/main/scala/frdomain/ch6/domain/app/app2.scala index ba5aaa6..cf5a2f3 100644 --- a/src/main/scala/frdomain/ch6/domain/app/app2.scala +++ b/src/main/scala/frdomain/ch6/domain/app/app2.scala @@ -4,18 +4,20 @@ package app import scalaz._ import Scalaz._ -import \/._ - import repository.interpreter.AccountRepositoryInMemory import model.common._ -import model.{ Account, Balance } +import model.{Account, Balance} import Account._ +import frdomain.ch6.domain.service.Valid object App2 { + import AccountRepositoryInMemory._ - val account = checkingAccount("a-123", "debasish ghosh", today.some, None, Balance(0)).toOption.get + import scala.concurrent.ExecutionContext.Implicits.global + + val account = checkingAccount("a-123", "debasish ghosh", today.some, None, Balance()).toOption.get val c = for { - b <- updateBalance(account, 10000) + b <- Valid(updateBalance(account, 10000)) c <- store(b) d <- balance(c.no) } yield d diff --git a/src/main/scala/frdomain/ch6/domain/model/Account.scala b/src/main/scala/frdomain/ch6/domain/model/Account.scala index 057141d..18ddb2b 100644 --- a/src/main/scala/frdomain/ch6/domain/model/Account.scala +++ b/src/main/scala/frdomain/ch6/domain/model/Account.scala @@ -28,15 +28,15 @@ sealed trait Account { final case class CheckingAccount (no: String, name: String, dateOfOpen: Option[Date], dateOfClose: Option[Date] = None, balance: Balance = Balance()) extends Account -final case class SavingsAccount (no: String, name: String, rateOfInterest: Amount, +final case class SavingsAccount (no: String, name: String, rateOfInterest: Amount, dateOfOpen: Option[Date], dateOfClose: Option[Date] = None, balance: Balance = Balance()) extends Account object Account { - private def validateAccountNo(no: String) = - if (no.isEmpty || no.size < 5) s"Account No has to be at least 5 characters long: found $no".failureNel[String] + private def validateAccountNo(no: String) = + if (no.isEmpty || no.size < 5) s"Account No has to be at least 5 characters long: found $no".failureNel[String] else no.successNel[String] - private def validateOpenCloseDate(od: Date, cd: Option[Date]) = cd.map { c => + private def validateOpenCloseDate(od: Date, cd: Option[Date]) = cd.map { c => if (c before od) s"Close date [$c] cannot be earlier than open date [$od]".failureNel[(Option[Date], Option[Date])] else (od.some, cd).successNel[String] }.getOrElse { (od.some, cd).successNel[String] } @@ -45,26 +45,26 @@ object Account { if (rate <= BigDecimal(0)) s"Interest rate $rate must be > 0".failureNel[BigDecimal] else rate.successNel[String] - def checkingAccount(no: String, name: String, openDate: Option[Date], closeDate: Option[Date], - balance: Balance): \/[NonEmptyList[String], Account] = { + def checkingAccount(no: String, name: String, openDate: Option[Date], closeDate: Option[Date], + balance: Balance): \/[NonEmptyList[String], Account] = { val od = openDate.getOrElse(today) ( - validateAccountNo(no) |@| + validateAccountNo(no) |@| validateOpenCloseDate(openDate.getOrElse(today), closeDate) ) { (n, d) => CheckingAccount(n, name, d._1, d._2, balance) }.disjunction } - def savingsAccount(no: String, name: String, rate: BigDecimal, openDate: Option[Date], - closeDate: Option[Date], balance: Balance): \/[NonEmptyList[String], Account] = { + def savingsAccount(no: String, name: String, rate: BigDecimal, openDate: Option[Date], + closeDate: Option[Date], balance: Balance): \/[NonEmptyList[String], Account] = { val od = openDate.getOrElse(today) ( - validateAccountNo(no) |@| + validateAccountNo(no) |@| validateOpenCloseDate(openDate.getOrElse(today), closeDate) |@| validateRate(rate) ) { (n, d, r) => @@ -110,5 +110,3 @@ object Account { case _ => None } } - - diff --git a/src/main/scala/frdomain/ch6/domain/package.scala b/src/main/scala/frdomain/ch6/domain/package.scala index e7c5ed0..427e2c4 100644 --- a/src/main/scala/frdomain/ch6/domain/package.scala +++ b/src/main/scala/frdomain/ch6/domain/package.scala @@ -7,4 +7,13 @@ import Scalaz._ package object service { type Valid[A] = EitherT[Future, NonEmptyList[String], A] + + object Valid { + implicit val validAppl = new Applicative[Valid] { + override def point[A](a: => A): Valid[A] = EitherT(Future.successful(a.right)) + override def ap[A, B](fa: => Valid[A])(f: => Valid[(A) => B]): Valid[B] = ??? + } + + def apply[A](block: => NonEmptyList[String] \/ A): Valid[A] = EitherT { Future.successful(block) } + } } diff --git a/src/main/scala/frdomain/ch6/domain/repository/AccountRepository.scala b/src/main/scala/frdomain/ch6/domain/repository/AccountRepository.scala index ffdda0c..cebaa97 100644 --- a/src/main/scala/frdomain/ch6/domain/repository/AccountRepository.scala +++ b/src/main/scala/frdomain/ch6/domain/repository/AccountRepository.scala @@ -3,20 +3,26 @@ package domain package repository import java.util.Date -import scalaz._ -import Scalaz._ -import \/._ -import model.{ Account, Balance } -trait AccountRepository { - def query(no: String): \/[NonEmptyList[String], Option[Account]] - def store(a: Account): \/[NonEmptyList[String], Account] - def balance(no: String): \/[NonEmptyList[String], Balance] = query(no) match { - case \/-(Some(a)) => a.balance.right - case \/-(None) => NonEmptyList(s"No account exists with no $no").left[Balance] - case a @ -\/(_) => a +import frdomain.ch6.domain.model.{Account, Balance} +import frdomain.ch6.domain.service.Valid + +import scala.concurrent.ExecutionContext.Implicits.global +import scalaz.NonEmptyList +import scalaz.Scalaz._ + +trait AccountRepository { + def query(no: String): Valid[Option[Account]] + def store(a: Account): Valid[Account] + def query(openedOn: Date): Valid[Seq[Account]] + def all: Valid[Seq[Account]] + + def balance(no: String): Valid[Balance] = query(no).flatMap { maybeAcc => + Valid { + maybeAcc match { + case Some(acc) => acc.balance.right + case None => NonEmptyList(s"No account exists with no $no").left + } + } } - def query(openedOn: Date): \/[NonEmptyList[String], Seq[Account]] - def all: \/[NonEmptyList[String], Seq[Account]] } - diff --git a/src/main/scala/frdomain/ch6/domain/repository/interpreter/AccountRepository.scala b/src/main/scala/frdomain/ch6/domain/repository/interpreter/AccountRepository.scala deleted file mode 100644 index f3f3fec..0000000 --- a/src/main/scala/frdomain/ch6/domain/repository/interpreter/AccountRepository.scala +++ /dev/null @@ -1,26 +0,0 @@ -package frdomain.ch6 -package domain -package repository -package interpreter - -import java.util.Date -import scala.collection.mutable.{ Map => MMap } -import scalaz._ -import Scalaz._ -import \/._ -import model.{ Account, Balance } - -trait AccountRepositoryInMemory extends AccountRepository { - lazy val repo = MMap.empty[String, Account] - - def query(no: String): \/[NonEmptyList[String], Option[Account]] = repo.get(no).right - def store(a: Account): \/[NonEmptyList[String], Account] = { - val r = repo += ((a.no, a)) - a.right - } - def query(openedOn: Date): \/[NonEmptyList[String], Seq[Account]] = repo.values.filter(_.dateOfOpen == openedOn).toSeq.right - def all: \/[NonEmptyList[String], Seq[Account]] = repo.values.toSeq.right -} - -object AccountRepositoryInMemory extends AccountRepositoryInMemory - diff --git a/src/main/scala/frdomain/ch6/domain/repository/interpreter/AccountRepositoryInMemory.scala b/src/main/scala/frdomain/ch6/domain/repository/interpreter/AccountRepositoryInMemory.scala new file mode 100644 index 0000000..7da0db0 --- /dev/null +++ b/src/main/scala/frdomain/ch6/domain/repository/interpreter/AccountRepositoryInMemory.scala @@ -0,0 +1,32 @@ +package frdomain.ch6 +package domain +package repository +package interpreter + +import java.util.Date + +import frdomain.ch6.domain.model.Account +import frdomain.ch6.domain.service.Valid +import frdomain.ch6.domain.service.Valid._ + +import scala.collection.mutable.{Map => MMap} +import scalaz.Scalaz._ + +trait AccountRepositoryInMemory extends AccountRepository { + lazy val repo = MMap.empty[String, Account] + + override def query(no: String): Valid[Option[Account]] = repo.get(no).pure[Valid] + + override def store(a: Account): Valid[Account] = { + repo += ((a.no, a)) + a.pure[Valid] + } + + override def query(openedOn: Date): Valid[Seq[Account]] = + repo.values.filter(_.dateOfOpen == openedOn).toSeq.pure[Valid] + + override def all: Valid[Seq[Account]] = repo.values.toSeq.pure[Valid] +} + +object AccountRepositoryInMemory extends AccountRepositoryInMemory + diff --git a/src/main/scala/frdomain/ch6/domain/service/AccountService.scala b/src/main/scala/frdomain/ch6/domain/service/AccountService.scala index 77db55f..0aec967 100644 --- a/src/main/scala/frdomain/ch6/domain/service/AccountService.scala +++ b/src/main/scala/frdomain/ch6/domain/service/AccountService.scala @@ -33,6 +33,6 @@ trait AccountService[Account, Amount, Balance] { def transfer(from: String, to: String, amount: Amount): AccountOperation[(Account, Account)] = for { a <- debit(from, amount) b <- credit(to, amount) - } yield ((a, b)) + } yield (a, b) } diff --git a/src/main/scala/frdomain/ch6/domain/service/interpreter/AccountService.scala b/src/main/scala/frdomain/ch6/domain/service/interpreter/AccountService.scala index 94a5b99..d37d7c1 100644 --- a/src/main/scala/frdomain/ch6/domain/service/interpreter/AccountService.scala +++ b/src/main/scala/frdomain/ch6/domain/service/interpreter/AccountService.scala @@ -3,58 +3,48 @@ package domain package service package interpreter -import java.util.{ Date, Calendar } +import java.util.{Date, Calendar} import scalaz._ import Scalaz._ import \/._ import Kleisli._ -import scala.concurrent._ -import ExecutionContext.Implicits.global - -import model.{ Account, Balance } +import model.{Account, Balance} import model.common._ import repository.AccountRepository +import scala.concurrent._ +import ExecutionContext.Implicits.global + class AccountServiceInterpreter extends AccountService[Account, Amount, Balance] { - def open(no: String, - name: String, + def open(no: String, + name: String, rate: Option[BigDecimal], openingDate: Option[Date], accountType: AccountType) = kleisli[Valid, AccountRepository, Account] { (repo: AccountRepository) => - - EitherT { - Future { - repo.query(no) match { - case \/-(Some(a)) => NonEmptyList(s"Already existing account with no $no").left[Account] - case \/-(None) => accountType match { - case Checking => Account.checkingAccount(no, name, openingDate, None, Balance()).flatMap(repo.store) - case Savings => rate map { r => - Account.savingsAccount(no, name, r, openingDate, None, Balance()).flatMap(repo.store) - } getOrElse { - NonEmptyList(s"Rate needs to be given for savings account").left[Account] - } - } - case a @ -\/(_) => a + repo.query(no) flatMap { + case Some(_) => Valid(NonEmptyList(s"Already existing account with no $no").left[Account]) + case None => accountType match { + case Checking => Valid(Account.checkingAccount(no, name, openingDate, None, Balance())).flatMap(repo.store) + case Savings => rate map { r => + Valid(Account.savingsAccount(no, name, r, openingDate, None, Balance())).flatMap(repo.store) + } getOrElse { + Valid(NonEmptyList(s"Rate needs to be given for savings account").left[Account]) } } } } - def close(no: String, closeDate: Option[Date]) = kleisli[Valid, AccountRepository, Account] { (repo: AccountRepository) => - EitherT { - Future { - repo.query(no) match { - case \/-(None) => NonEmptyList(s"Account $no does not exist").left[Account] - case \/-(Some(a)) => - val cd = closeDate.getOrElse(today) - Account.close(a, cd).flatMap(repo.store) - case a @ -\/(_) => a - } + def close(no: String, closeDate: Option[Date]) = kleisli[Valid, AccountRepository, Account] { + (repo: AccountRepository) => + repo.query(no).flatMap { + case None => Valid(NonEmptyList(s"Account $no does not exist").left[Account]) + case Some(a) => + val cd = closeDate.getOrElse(today) + Valid(Account.close(a, cd)).flatMap(repo.store) } - } } def debit(no: String, amount: Amount) = up(no, amount, D) @@ -64,26 +54,20 @@ class AccountServiceInterpreter extends AccountService[Account, Amount, Balance] private case object D extends DC private case object C extends DC - private def up(no: String, amount: Amount, dc: DC): AccountOperation[Account] = kleisli[Valid, AccountRepository, Account] { (repo: AccountRepository) => - EitherT { - Future { - repo.query(no) match { - case \/-(None) => NonEmptyList(s"Account $no does not exist").left[Account] - case \/-(Some(a)) => dc match { - case D => Account.updateBalance(a, -amount).flatMap(repo.store) - case C => Account.updateBalance(a, amount).flatMap(repo.store) - } - case a @ -\/(_) => a + private def up(no: String, amount: Amount, dc: DC): AccountOperation[Account] = kleisli[Valid, AccountRepository, Account] { + (repo: AccountRepository) => + repo.query(no) flatMap { + case None => Valid(NonEmptyList(s"Account $no does not exist").left[Account]) + case Some(a) => dc match { + case D => Valid(Account.updateBalance(a, -amount)).flatMap(repo.store) + case C => Valid(Account.updateBalance(a, amount)).flatMap(repo.store) } } - } } - def balance(no: String) = - kleisli[Valid, AccountRepository, Balance] { (repo: AccountRepository) => - EitherT { - Future { repo.balance(no) } - } + def balance(no: String) = + kleisli[Valid, AccountRepository, Balance] { + (repo: AccountRepository) => repo.balance(no) } } diff --git a/src/main/scala/frdomain/ch6/domain/service/interpreter/ReportingService.scala b/src/main/scala/frdomain/ch6/domain/service/interpreter/ReportingService.scala index 72807f1..a01c60a 100644 --- a/src/main/scala/frdomain/ch6/domain/service/interpreter/ReportingService.scala +++ b/src/main/scala/frdomain/ch6/domain/service/interpreter/ReportingService.scala @@ -16,16 +16,10 @@ import model.common._ class ReportingServiceInterpreter extends ReportingService[Amount] { - def balanceByAccount: ReportOperation[Seq[(String, Amount)]] = kleisli[Valid, AccountRepository, Seq[(String, Amount)]] { (repo: AccountRepository) => - EitherT { - Future { - repo.all match { - case \/-(as) => as.map(a => (a.no, a.balance.amount)).right - case a @ -\/(_) => a - } - } + def balanceByAccount: ReportOperation[Seq[(String, Amount)]] = + kleisli[Valid, AccountRepository, Seq[(String, Amount)]] { (repo: AccountRepository) => + repo.all map { as => as.map(a => (a.no, a.balance.amount)) } } - } -} +} object ReportingService extends ReportingServiceInterpreter