Service API for Scala

March 22, 2018

Given:

Let:

Service API

import java.sql.Connection
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

type JDBC[A] = Connection => A

case class Error(message: String)

case class Service[A](run: JDBC[Future[Either[Error, A]]]) {

  def map[B](f: A => B)(implicit ec: ExecutionContext): Service[B] =
    Service { c =>
      run(c) map {
        case Left(es) => Left(es)
        case Right(a) => Right(f(a))
      }
    }

  def flatMap[B](f: A => Service[B])(implicit ec: ExecutionContext): Service[B] =
    Service { c =>
      run(c) flatMap {
        case Left(es) => Future(Left(es))
        case Right(a) => f(a).run(c)
      }
    }

  def >>[B](x: Service[B])(implicit ec: ExecutionContext): Service[B] =
    flatMap { _ => x }

  def transact(c: Connection): Future[Either[Error, A]] =
    c synchronized {
      val ac = c.getAutoCommit
      c.setAutoCommit(false)
      val ea =
        run(c) map {
          case r@Right(_) =>
            c.commit()
            r
          case l@Left(_) =>
            c.rollback()
            l
        }
      c.setAutoCommit(ac)
      ea
    }
}

object Service {

  import scala.util.Try
  import scala.util.Success
  import scala.util.Failure

  def success[A](a: => A): Service[A] =
    Service(_ => Future(Right(a)))

  def failure[A](e: => String): Service[A] =
    Service(_ => Future(Left(Error(e))))

  def update(u: String)(implicit ec: ExecutionContext): Service[Unit] =
    Service { c =>
      Future {
        Try {
          val s = c.createStatement()
          s.executeUpdate(u)
          s.close()
        } match {
          case Success(_) => Right(())
          case Failure(t) => Left(Error(t.toString))
        }
      }
    }

  def query[A](k: JDBC[A])(implicit ec: ExecutionContext): Service[A] =
    Service { c =>
      Future {
        Try {
          k(c)
        } match {
          case Success(x) => Right(x)
          case Failure(t) => Left(Error(t.toString))
        }
      }
    }
}

Example

object Messages {

  def init(implicit ec: ExecutionContext): Service[Unit] =
    Service.update("create table messages (lang varchar(5), value varchar(128))") >>
    Service.update("insert into messages (lang, value) values ('en', 'Hello, world!')") >>
    Service.update("insert into messages (lang, value) values ('es', '¡Hola, mundo!')")

  def get(lang: String)(implicit ec: ExecutionContext): Service[String] =
    Service.query { c =>
      val q = "select value from messages where lang = ?"
      val s = c.prepareStatement(q)
      s.setString(1, lang)
      val r = s.executeQuery()
      val vo =
        if (r.next()) {
          Some(r.getString("value"))
        } else {
          None
        }
      s.close()
      vo
    } flatMap {
      case Some(x) => Service.success(x)
      case None => Service.failure(s"no message for lang=${lang}")
    }
}

Usage

import scala.concurrent.duration.Duration
import scala.concurrent.Await

implicit val ec: ExecutionContext = ExecutionContext.global

val succeeds: Service[Map[String, String]] =
  for {
    en <- Messages.get("en")
    es <- Messages.get("es")
  } yield Map( "en" -> en
             , "es" -> es
             )

val fails: Service[Map[String, String]] =
  for {
    fr <- Messages.get("fr")
    es <- Messages.get("es")
  } yield Map( "fr" -> fr
             , "es" -> es
             )
import java.sql.DriverManager

Class.forName("org.h2.Driver")

val c = DriverManager.getConnection("jdbc:h2:mem:db", "sa", "")

def unsafeRun[A](s: Service[A]): Either[Error, A] =
  Await.result(s.transact(c), Duration(100, "millis"))

unsafeRun(Messages.init)
println(unsafeRun(succeeds)) // Right(Map(en -> Hello, world!, es -> ¡Hola, mundo!))
println(unsafeRun(fails)) // Left(Error(no message for lang=fr))

Demo

Usage with Codedown:

/***
libraryDependencies += "com.h2database" % "h2" % "1.4.194"
*/
$ curl -s https://earldouglas.com/posts/scala-service-api.md |
  codedown scala > service-api.scala
$ sbt -Dsbt.main.class=sbt.ScriptMain service-api.scala
Right(Map(en -> Hello, world!, es -> ¡Hola, mundo!))
Left(Error(no message for lang=fr))