Skip to content

Instantly share code, notes, and snippets.

@keynmol
Created September 28, 2023 11:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save keynmol/531cd2b8c93385aa86c3df88dc9d7227 to your computer and use it in GitHub Desktop.
Save keynmol/531cd2b8c93385aa86c3df88dc9d7227 to your computer and use it in GitHub Desktop.
Sample gist showing how to run a HTTP server with Typelevel Scala libraries, and a postgres docker container
//> using dep "org.http4s::http4s-scalatags::0.25.2"
//> using dep "org.http4s::http4s-dsl::0.23.23"
//> using dep "org.http4s::http4s-ember-server::0.23.23"
//> using dep "org.tpolecat::skunk-core::0.6.0"
//> using dep "com.dimafeng::testcontainers-scala-postgresql::0.41.0"
//> using dep "com.outr::scribe-slf4j::3.12.2"
import skunk.*, codec.all.*, syntax.all.*
import cats.effect.*
import scalatags.Text.all.*
import org.http4s.{HttpRoutes}
import org.http4s.scalatags.*
import org.http4s.dsl.io.*
import org.http4s.ember.server.EmberServerBuilder
import com.comcast.ip4s.*
import com.dimafeng.testcontainers.PostgreSQLContainer
import org.testcontainers.utility.DockerImageName
import natchez.Trace
import cats.syntax.all.*
enum EmailAddress:
case Private
case Public(email: String)
def toOption =
this match
case Private => None
case Public(email) => Some(email)
object EmailAddress:
def from(s: Option[String]) =
s match
case None => EmailAddress.Private
case Some(value) => EmailAddress.Public(value)
case class User(id: Int, username: String, email: EmailAddress)
object User:
val codec =
(int4 *: varchar *: varchar.opt.imap(EmailAddress.from)(_.toOption))
.to[User]
def main(a: scalatags.Text.Modifier*) =
html(body(a*))
def queryUserById(id: Int)(using sess: Session[IO]) =
val query = sql"select * from users where id = $int4"
sess.option(query.query(User.codec))(id)
val userNotFoundPage =
p("This user does not exist :(")
def userView(user: User) =
main(
p(s"User ID: ${user.id}"),
p(s"Username: ${user.username}"),
user.email match
case EmailAddress.Private => p("This user has a private email address")
case EmailAddress.Public(email) => p(s"Email address: $email")
)
def renderUserOrNotFound(potentialUser: Option[User]) =
potentialUser.map(userView).getOrElse(userNotFoundPage)
def renderUserHandler(userId: Int)(using Session[IO]) =
queryUserById(userId).map(renderUserOrNotFound)
def handler(using Session[IO]) =
HttpRoutes.of[IO] { case GET -> Root / "users" / IntVar(i) =>
renderUserHandler(i).flatMap(Ok(_))
}
object App extends IOApp.Simple:
val run =
skunkConnection(using Trace.Implicits.noop)
.flatMap { case given Session[IO] =>
EmberServerBuilder
.default[IO]
.withHttpApp(handler.orNotFound.onError { case err =>
cats.data.Kleisli(req => IO(scribe.error(s"[$req] Error: ", err)))
})
.withPort(port"9955")
.build
}
.use(server =>
IO.println(
s"Server running at ${server.baseUri}, press Enter to terminate"
) *> IO.readLine
)
.void
private def postgresContainer =
Resource.make(
IO(
PostgreSQLContainer(
dockerImageNameOverride = DockerImageName("postgres:14"),
mountPostgresDataToTmpfs = true
)
).flatTap(cont => IO(cont.start()))
)(cont => IO(cont.stop()))
end postgresContainer
private def skunkConnection(using natchez.Trace[IO]) =
postgresContainer
.evalMap(cont => parseJDBC(cont.jdbcUrl).map(cont -> _))
.flatMap { case (cont, jdbcUrl) =>
Session.single[IO](
host = jdbcUrl.getHost(),
port = jdbcUrl.getPort(),
user = cont.username,
password = Some(cont.password),
database = cont.databaseName
)
}
.evalTap { sess =>
val commands = Seq(
sql"DROP TABLE IF EXISTS users".command,
sql"CREATE TABLE users (id serial, username varchar not null, email varchar)".command,
sql"INSERT INTO users(username, email) values ('anton', 'bla@bla.com')".command,
sql"INSERT INTO users(username, email) values ('dark_anton', NULL)".command
)
commands.traverse(sess.execute)
}
private def parseJDBC(url: String) = IO(java.net.URI.create(url.substring(5)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment