Created
October 1, 2018 08:03
-
-
Save loicknuchel/2185d22621baf482846225a41e69964d to your computer and use it in GitHub Desktop.
Try to remove boilerplate code created by many similar case classes
This file contains hidden or 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
import java.sql.Timestamp | |
import java.time.Instant | |
import slick.basic.DatabaseConfig | |
import slick.jdbc.JdbcProfile | |
import slick.lifted.ProvenShape | |
import scala.concurrent.ExecutionContext.Implicits.global | |
import scala.util.{Failure, Success, Try} | |
/* | |
Problem: | |
- I define each state of my state diagram as a case class so they have a lot in common | |
- There is lot of boilerplate to do if I want to add a new field to the created case class for example : | |
- add this field to other case classes | |
- use it on state transitions | |
- add it to Row class | |
- change every from & to methods in Row to pass this new field | |
What I wish: | |
- create new case classes from other ones, for example: `extends(Created)(accepted: Instant, acceptedBy: String)` | |
- automate conversion with some subtilities: | |
- T can become Option[T] | |
Solutions: | |
- scalameta to generate case classes | |
- shapeless for conversions | |
- https://github.com/kailuowang/henkan | |
*/ | |
sealed trait State { | |
val id: State.Id | |
} | |
object State { | |
case class Id(value: String) extends AnyVal | |
object Id { | |
def from(str: String): Try[Id] = | |
if (str.isEmpty) Failure(new Exception("Id can't be empty")) | |
else Success(Id(str)) | |
} | |
case class Created(id: Id, | |
content: String, | |
created: Instant, | |
createdBy: String) extends State { | |
def accept(by: String, now: Instant): Accepted = Accepted(id, content, created, createdBy, now, by) | |
} | |
case class Accepted(id: Id, | |
content: String, | |
created: Instant, | |
createdBy: String, | |
accepted: Instant, | |
acceptedBy: String) extends State { | |
def succeed(now: Instant): Succeeded = Succeeded(id, content, created, createdBy, accepted, acceptedBy, now) | |
def fail(now: Instant): Failed = Failed(id, content, created, createdBy, accepted, acceptedBy, now) | |
} | |
case class Succeeded(id: Id, | |
content: String, | |
created: Instant, | |
createdBy: String, | |
accepted: Instant, | |
acceptedBy: String, | |
succeeded: Instant) extends State | |
case class Failed(id: Id, | |
content: String, | |
created: Instant, | |
createdBy: String, | |
accepted: Instant, | |
acceptedBy: String, | |
failed: Instant) extends State | |
} | |
trait StateTable { | |
protected val dbConfig: DatabaseConfig[JdbcProfile] | |
import State._ | |
import StateTable._ | |
import Utils._ | |
import dbConfig.profile.api._ | |
protected class Mapper(tag: Tag) extends Table[Row](tag, "state") { | |
def id: Rep[String] = column[String]("id", O.PrimaryKey) | |
def status: Rep[String] = column[String]("status") | |
def content: Rep[String] = column[String]("content") | |
def created: Rep[Timestamp] = column[Timestamp]("created") | |
def createdBy: Rep[String] = column[String]("created_by") | |
def accepted: Rep[Option[Timestamp]] = column[Option[Timestamp]]("accepted") | |
def acceptedBy: Rep[Option[String]] = column[Option[String]]("accepted_by") | |
def succeeded: Rep[Option[Timestamp]] = column[Option[Timestamp]]("succeeded") | |
def failed: Rep[Option[Timestamp]] = column[Option[Timestamp]]("failed") | |
def * : ProvenShape[Row] = (id, status, content, created, createdBy, accepted, acceptedBy, succeeded, failed) <> ((Row.apply _).tupled, Row.unapply) | |
} | |
private val table = slick.lifted.TableQuery[Mapper] | |
protected val stateTable = table // expose this to child classes | |
protected def insert(c: Created): DBIOAction[Created, NoStream, Effect.Write] = | |
(table += Row.from(c)).flatMap { | |
case 1 => DBIO.successful(c) | |
case r => DBIO.failed(new Exception(s"Insertion failed with code $r: enable to create a ${State.getClass.getSimpleName}")) | |
} | |
protected def update(c: State): DBIOAction[Id, NoStream, Effect.Write] = | |
table.filter(_.id === c.id.value).update(Row.build(c)).flatMap { | |
case 1 => DBIO.successful(c.id) | |
case r => DBIO.failed(new Exception(s"Update failed with code $r: enable to update ${State.getClass.getSimpleName} ${c.id}")) | |
} | |
protected def getById(id: Id): DBIOAction[Option[State], NoStream, Effect.All] = | |
table.filter(r => r.id === id.value).result.headOption.flatMap(parseState) | |
protected def getStates(): DBIOAction[Seq[State], NoStream, Effect.All] = | |
table.sortBy(_.created).result.flatMap(parseState) | |
protected def getStatesByStatus(status: String): DBIOAction[Seq[State], NoStream, Effect.All] = | |
table.filter(_.status === status).sortBy(_.created).result.flatMap(parseState) | |
protected def parseState(v: Option[Row]): DBIO[Option[State]] = toDBIO(sequence(v.map(_.format))) | |
protected def parseState(v: Seq[Row]): DBIO[Seq[State]] = toDBIO(sequence(v.map(_.format))) | |
} | |
object StateTable { | |
import State._ | |
import Utils._ | |
case class Row(id: String, | |
status: String, | |
content: String, | |
created: Timestamp, | |
createdBy: String, | |
accepted: Option[Timestamp], | |
acceptedBy: Option[String], | |
succeeded: Option[Timestamp], | |
failed: Option[Timestamp]) { | |
def toCreated: Try[Created] = for { | |
id <- Id.from(id) | |
} yield Created(id, content, created.toInstant, createdBy) | |
def toAccepted: Try[Accepted] = for { | |
id <- Id.from(id) | |
accepted <- toTry(accepted, new Exception("Missing accepted field")) | |
acceptedBy <- toTry(acceptedBy, new Exception("Missing acceptedBy field")) | |
} yield Accepted(id, content, created.toInstant, createdBy, accepted.toInstant, acceptedBy) | |
def toSucceeded: Try[Succeeded] = for { | |
id <- Id.from(id) | |
accepted <- toTry(accepted, new Exception("Missing accepted field")) | |
acceptedBy <- toTry(acceptedBy, new Exception("Missing acceptedBy field")) | |
succeeded <- toTry(succeeded, new Exception("Missing succeeded field")) | |
} yield Succeeded(id, content, created.toInstant, createdBy, accepted.toInstant, acceptedBy, succeeded.toInstant) | |
def toFailed: Try[Failed] = for { | |
id <- Id.from(id) | |
accepted <- toTry(accepted, new Exception("Missing accepted field")) | |
acceptedBy <- toTry(acceptedBy, new Exception("Missing acceptedBy field")) | |
failed <- toTry(failed, new Exception("Missing failed field")) | |
} yield Failed(id, content, created.toInstant, createdBy, accepted.toInstant, acceptedBy, failed.toInstant) | |
def format: Try[State] = status match { | |
case "Created" => toCreated | |
case "Accepted" => toAccepted | |
case "Succeeded" => toSucceeded | |
case "Failed" => toFailed | |
} | |
} | |
object Row { | |
def from(s: Created): Row = Row( | |
id = s.id.value, | |
status = s.getClass.getSimpleName, | |
content = s.content, | |
created = Timestamp.from(s.created), | |
createdBy = s.createdBy, | |
accepted = None, | |
acceptedBy = None, | |
succeeded = None, | |
failed = None) | |
def from(s: Accepted): Row = Row( | |
id = s.id.value, | |
status = s.getClass.getSimpleName, | |
content = s.content, | |
created = Timestamp.from(s.created), | |
createdBy = s.createdBy, | |
accepted = Some(Timestamp.from(s.accepted)), | |
acceptedBy = Some(s.acceptedBy), | |
succeeded = None, | |
failed = None) | |
def from(s: Succeeded): Row = Row( | |
id = s.id.value, | |
status = s.getClass.getSimpleName, | |
content = s.content, | |
created = Timestamp.from(s.created), | |
createdBy = s.createdBy, | |
accepted = Some(Timestamp.from(s.accepted)), | |
acceptedBy = Some(s.acceptedBy), | |
succeeded = Some(Timestamp.from(s.succeeded)), | |
failed = None) | |
def from(s: Failed): Row = Row( | |
id = s.id.value, | |
status = s.getClass.getSimpleName, | |
content = s.content, | |
created = Timestamp.from(s.created), | |
createdBy = s.createdBy, | |
accepted = Some(Timestamp.from(s.accepted)), | |
acceptedBy = Some(s.acceptedBy), | |
succeeded = None, | |
failed = Some(Timestamp.from(s.failed))) | |
def build(s: State): Row = s match { | |
case v: Created => from(v) | |
case v: Accepted => from(v) | |
case v: Succeeded => from(v) | |
case v: Failed => from(v) | |
} | |
} | |
} | |
object Utils { | |
import slick.dbio.DBIO | |
def sequence[A](seq: Seq[Try[A]]): Try[Seq[A]] = | |
Try(seq.map(_.get)) | |
def sequence[A](opt: Option[Try[A]]): Try[Option[A]] = opt match { | |
case Some(Success(v)) => Success(Some(v)) | |
case Some(Failure(e)) => Failure(e) | |
case None => Success(None) | |
} | |
def toTry[A](opt: Option[A], e: => Throwable): Try[A] = opt match { | |
case Some(v) => Success(v) | |
case None => Failure(e) | |
} | |
def toDBIO[A](t: Try[A]): DBIO[A] = t match { | |
case Success(v) => DBIO.successful(v) | |
case Failure(e) => DBIO.failed(e) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment