Skip to content

Instantly share code, notes, and snippets.

@loicknuchel
Created October 1, 2018 08:03
Show Gist options
  • Save loicknuchel/2185d22621baf482846225a41e69964d to your computer and use it in GitHub Desktop.
Save loicknuchel/2185d22621baf482846225a41e69964d to your computer and use it in GitHub Desktop.
Try to remove boilerplate code created by many similar case classes
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