Last active
November 20, 2016 18:14
-
-
Save harrylaou/a1ffff7a9511b1f15f3d9729c933561f to your computer and use it in GitHub Desktop.
Emm Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package controllers | |
import application.AppComponents.DatabaseSB | |
import cats.data.Xor | |
import cats.implicits._ | |
import emm._ | |
import emm.compat.cats._ | |
import models.User | |
import play.api.Logger.logger | |
import play.api.libs.json.{Format, JsValue, Json, Reads, Writes} | |
import play.api.mvc.{AnyContent, Request, Results, _} | |
import services.postgres.PostgresDriverSB | |
import scala.concurrent.{ExecutionContext, Future} | |
import play.api.mvc.Results.Ok | |
/** | |
* | |
* This code demonstrates the practicality and usefulness of advanced functional programming techniques | |
* like generalized effect composition libraries, in this example Emm. https://github.com/djspiewak/emm | |
* It also proves by example and comparison that even is simple operations like parsing a json | |
* and updating a model in db, the usage of Emm produces much clearer and concrete code. | |
* | |
* We need a definition of the monad stack | |
* type FXE = Future |: Xor[Erratum, ?] |: Base | |
* | |
* and use a for-comprehension by lifting the values in the effect stack using : | |
* | |
* pointM[FXE] – in the case of a Base type , like Unit or User | |
* liftM[FXE] – in the case of a Xor[Erratum, Base] | |
* wrapM[FXE] – Future[Xor[Erratum, Base]] | |
* | |
* You can compare how much more complex the code is when not using Emm , | |
* but with map, flatMap and embedded pattern matching. | |
* | |
* Without using Emm the function is 26-lines of code long | |
* and much more complex because of the embedded pattern matching | |
* | |
* Using Emm, the code becomes very clean and 8-lines long. | |
* | |
* @author harrylaou | |
* | |
*/ | |
class DummyController(implicit db: DatabaseSB, ec: ExecutionContext) extends Controller { | |
import Helper._ | |
/** | |
* demo of using Emm | |
* | |
* - calls functions with different return types | |
* - lifs them in the effect stack | |
* - gets the value from the effect stack | |
* - transforms the asynchronous value to asynchronous result | |
* | |
* @return | |
*/ | |
def user = Action.async { implicit request => | |
val savedUserE: Emm[FXE, User] = for { | |
jsValue <- parseRequest(request).liftM[FXE] | |
user <- fromJson(jsValue).liftM[FXE] | |
savedUser <- upsertXor(user).wrapM[FXE] | |
_ <- logger.debug(s"User $user updated").pointM[FXE] | |
} yield savedUser | |
stackToResult[User](savedUserE.run) | |
} | |
/** | |
* Same functionality without Emm , | |
* much more complex when using map, flatMap and embedded pattern matching. | |
* | |
*/ | |
def userWithoutEmm = Action.async { implicit request => | |
val jsonOpt: Option[JsValue] = request.body.asJson | |
jsonOpt match { | |
case None => | |
logger.error(s" cannot parse json for ${request.body}") | |
Future.successful(UnprocessableEntity(" cannot parse json")) | |
case Some(jsValue) => | |
val userXor = Json.fromJson(jsValue).asEither.toXor | |
userXor match { | |
case Xor.Right(user) => | |
upsert(user).map { (uOpt: Option[User]) => | |
uOpt match { | |
case None => | |
logger.error(s"couldn't write in db user:$user") | |
InternalServerError(s"couldn't write in db user:$user") | |
case Some(u) => | |
logger.debug(s"User $user updated") | |
Ok(Json.toJson(u)) | |
} | |
} | |
case Xor.Left(errorData) => | |
logger.error(s"$errorData") | |
Future.successful(UnprocessableEntity(s"$errorData")) | |
} | |
} | |
} | |
} | |
/** | |
* This is just a holder of functions and implicits. | |
* These can be anywhere in the codebase but for demo reasons | |
* they are in this example. | |
*/ | |
object Helper { | |
/** | |
* extending the default Postgres database | |
*/ | |
type DatabaseSB = PostgresDriverSB#Backend#Database | |
/** | |
* Definition of the monad stack | |
*/ | |
type FXE = Future |: Xor[Erratum, ?] |: Base | |
/** | |
* Generic (polymorphic) function that parses a JsValue and returns the disjunction of an error and a value of A | |
* | |
* @param jsValue | |
* @param reads implicit parameter of a play.api.libs.json.Reads[A] | |
* @tparam A | |
* @return | |
*/ | |
def fromJson[A](jsValue: JsValue)(implicit reads: Reads[A]): Xor[Erratum, A] = | |
Json.fromJson[A](jsValue)(reads).asEither.toXor.leftMap(e => | |
Erratum(status = Results.UnprocessableEntity, message = e.toString())) | |
implicit val userJsonF: Format[User] = Json.format[User] | |
/** | |
* helper function to parse a request and return the Xor (disjunction) of either an error or a JsValue | |
* | |
* @param request | |
* @return Xor[Erratum, JsValue] | |
*/ | |
def parseRequest(request: Request[AnyContent]): Xor[Erratum, JsValue] = { | |
val jsonOpt: Option[JsValue] = request.body.asJson | |
jsonOpt.toRightXor(Erratum(status = Results.UnprocessableEntity, message = s"cannot parse json for ${request.body}")) | |
} | |
/** | |
* this belongs in a slick class , but is here to demo its return type. | |
*/ | |
def upsert(u: User)(implicit db: DatabaseSB, ec: ExecutionContext): Future[Option[User]] = ??? | |
/** | |
* this belongs in a slick class , but is here to demo its return type. | |
*/ | |
def upsertXor(u: User)(implicit db: DatabaseSB, ec: ExecutionContext): Future[Xor[Erratum, User]] = ??? | |
/** | |
* | |
* Polymorphic function that tranforms the stacked monad into an synchronous result. | |
* | |
* @param fxo the stacked monad | |
* @param jsWrites the implicit play.api.libs.json.Writes | |
* @param ec implicit ExecutionContext | |
* @tparam A type parameter of a model | |
* @return | |
*/ | |
def stackToResult[A](fxo: Future[Xor[Erratum, A]])(implicit jsWrites: Writes[A], ec: ExecutionContext): Future[Result] = | |
fxo.map(xo => xo match { | |
case Xor.Left(erratum) => | |
logger.error(erratum.toString) | |
erratum.toResult | |
case Xor.Right(user) => Ok(Json.toJson(user)) | |
}) | |
} | |
/** | |
* Represents an error. | |
* | |
* The name was chosen so as not to class with | |
* a bazillion other `Error` classes in other libraries. | |
* So here the Error (Erratum) is the product of status , which its type is a Play type, | |
* but can be anything else that represents an HTTP status code | |
* and a simple String message | |
* | |
*/ | |
case class Erratum(status: Results.Status, message: String) { | |
def toResult: Result = { | |
status(message) | |
} | |
} | |
object Erratum { | |
def withMessage(msg: String) = Erratum(Results.InternalServerError, msg) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment