Skip to content

Commit

Permalink
Migrate to Playframework 3 and Pekko (#435)
Browse files Browse the repository at this point in the history
* Migrate to Playframework 3

* Minor changes

* Fix compile
  • Loading branch information
KapStorm authored Nov 9, 2023
1 parent 181f3bc commit 8f7b8ce
Show file tree
Hide file tree
Showing 60 changed files with 253 additions and 3,646 deletions.
61 changes: 17 additions & 44 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import java.nio.file.StandardCopyOption.REPLACE_EXISTING
ThisBuild / scalaVersion := "3.3.0"
ThisBuild / organization := "net.wiringbits"

val playJson = "2.10.0-RC9"
val playJson = "3.0.1"
val sttp = "3.8.15"
val webappUtils = "0.7.2"
val anorm = "2.7.0"
val enumeratum = "1.7.2"
val scalaJavaTime = "2.5.0"
val tapir = "1.5.0"
val tapir = "1.8.5"
val chimney = "0.8.0-RC1"

val consoleDisabledOptions = Seq("-Werror", "-Ywarn-unused", "-Ywarn-unused-import")
Expand Down Expand Up @@ -226,7 +226,7 @@ lazy val common = (crossProject(JSPlatform, JVMPlatform) in file("lib/common"))
)
.jvmSettings(
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-json" % playJson,
"org.playframework" %% "play-json" % playJson,
"net.wiringbits" %% "webapp-common" % webappUtils,
"org.scalatest" %% "scalatest" % "3.2.16" % Test
)
Expand All @@ -238,7 +238,7 @@ lazy val common = (crossProject(JSPlatform, JVMPlatform) in file("lib/common"))
Compile / stMinimize := Selection.All,
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % scalaJavaTime,
"com.typesafe.play" %%% "play-json" % playJson,
"org.playframework" %%% "play-json" % playJson,
"net.wiringbits" %%% "webapp-common" % webappUtils,
"org.scalatest" %%% "scalatest" % "3.2.16" % Test,
"com.beachape" %%% "enumeratum" % enumeratum
Expand All @@ -247,14 +247,14 @@ lazy val common = (crossProject(JSPlatform, JVMPlatform) in file("lib/common"))

// shared apis
lazy val api = (crossProject(JSPlatform, JVMPlatform) in file("lib/api"))
.dependsOn(common, tapirPlayJson)
.dependsOn(common)
.configure(baseLibSettings, commonSettings)
.jsConfigure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin))
.jvmSettings(
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-json" % playJson,
"org.playframework" %% "play-json" % playJson,
"com.softwaremill.sttp.client3" %% "core" % sttp,
"com.softwaremill.sttp.tapir" %% "tapir-core" % tapir,
"com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapir,
"com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % tapir
)
)
Expand All @@ -264,11 +264,11 @@ lazy val api = (crossProject(JSPlatform, JVMPlatform) in file("lib/api"))
stUseScalaJsDom := true,
Compile / stMinimize := Selection.All,
libraryDependencies ++= Seq(
"com.typesafe.play" %%% "play-json" % playJson,
"com.softwaremill.sttp.client3" %%% "core" % sttp,
"org.playframework" %%% "play-json" % playJson,
"org.scalatest" %%% "scalatest" % "3.2.16" % Test,
"com.beachape" %%% "enumeratum" % enumeratum,
"com.softwaremill.sttp.tapir" %%% "tapir-core" % tapir,
"com.softwaremill.sttp.client3" %%% "core" % sttp,
"com.softwaremill.sttp.tapir" %%% "tapir-json-play" % tapir,
"com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % tapir
)
)
Expand Down Expand Up @@ -317,47 +317,17 @@ lazy val ui = (project in file("lib/ui"))
)
)

lazy val tapirServerCore = (project in file("tapir/core"))
.settings(
name := "tapir-server-core",
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-core" % tapir
)

lazy val tapirServerPlay = (project in file("tapir/tapir-play"))
.settings(
name := "tapir-server-play",
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-akka-http-server" % "2.9.0-M6",
"com.softwaremill.sttp.shared" %% "akka" % "1.3.14",
"org.scala-lang.modules" %% "scala-collection-compat" % "2.11.0"
)
)
.dependsOn(tapirServerCore)

lazy val tapirPlayJson = (crossProject(JSPlatform, JVMPlatform) in file("tapir/playjson"))
.settings(
name := "tapir-play-json",
libraryDependencies ++= Seq(
"com.typesafe.play" %%% "play-json" % playJson,
"com.softwaremill.sttp.tapir" %% "tapir-core" % tapir
)
)
.jsSettings(
libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTime
)

lazy val server = (project in file("server"))
.dependsOn(common.jvm, api.jvm, tapirServerPlay, tapirPlayJson.jvm)
.dependsOn(common.jvm, api.jvm)
.configure(baseServerSettings, commonSettings, playSettings)
.settings(
name := "wiringbits-server",
fork := true,
Test / fork := true, // allows for graceful shutdown of containers once the tests have finished running
libraryDependencies ++= Seq(
"org.playframework.anorm" %% "anorm" % anorm,
"org.playframework.anorm" %% "anorm-akka" % anorm,
"org.playframework.anorm" %% "anorm-postgres" % anorm,
"com.typesafe.play" %% "play-json" % playJson,
"org.playframework" %% "play-json" % playJson,
"org.postgresql" % "postgresql" % "42.6.0",
"de.svenkubiak" % "jBCrypt" % "0.4.3",
"commons-validator" % "commons-validator" % "1.7",
Expand All @@ -375,8 +345,11 @@ lazy val server = (project in file("server"))
"javax.el" % "javax.el-api" % "3.0.0",
"org.glassfish" % "javax.el" % "3.0.0",
"com.beachape" %% "enumeratum" % enumeratum,
"io.scalaland" %% "chimney" % chimney,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapir,
"io.scalaland" %% "chimney" % chimney
"com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapir,
"com.softwaremill.sttp.tapir" %% "tapir-play-server" % tapir,
"org.apache.pekko" %% "pekko-stream" % "1.0.1"
)
)

Expand Down Expand Up @@ -431,7 +404,7 @@ lazy val web = (project in file("web"))
"@types/react-google-recaptcha" -> "2.1.0"
),
libraryDependencies ++= Seq(
"com.typesafe.play" %%% "play-json" % playJson,
"org.playframework" %%% "play-json" % playJson,
"com.softwaremill.sttp.client3" %%% "core" % sttp,
"org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1",
"com.olvind.st-material-ui" %%% "st-material-ui-icons-slinky" % "5.11.16",
Expand Down
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ evictionErrorLevel := sbt.util.Level.Warn

addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1")

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0-M6")
addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.0")

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1")

Expand Down
201 changes: 201 additions & 0 deletions server/src/main/scala/PekkoStream.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package anorm

import java.sql.Connection
import scala.util.control.NonFatal
import scala.concurrent.{Future, Promise}
import org.apache.pekko.stream.scaladsl.Source

import scala.annotation.nowarn

/** Anorm companion for the Pekko Streams.
*
* @define materialization
* It materializes a [[scala.concurrent.Future]] of [[scala.Int]] containing the number of rows read from the source
* upon completion, and a possible exception if row parsing failed.
* @define sqlParam
* the SQL query
* @define connectionParam
* the JDBC connection, which must not be closed until the source is materialized.
* @define columnAliaserParam
* the column aliaser
*/
// From https://github.com/playframework/anorm/blob/main/pekko/src/main/scala/anorm/PekkoStream.scala
// We are copying this because the anorm.pekko isn't published yet
// TODO: remove after anorm.pekko is published
object PekkoStream {

/** Returns the rows parsed from the `sql` query as a reactive source.
*
* $materialization
*
* @tparam T
* the type of the result elements
* @param sql
* $sqlParam
* @param parser
* the result (row) parser
* @param as
* $columnAliaserParam
* @param connection
* $connectionParam
*
* {{{
* import java.sql.Connection
*
* import scala.concurrent.Future
*
* import org.apache.pekko.stream.scaladsl.Source
*
* import anorm._
*
* def resultSource(implicit con: Connection): Source[String, Future[Int]] = PekkoStream.source(SQL"SELECT * FROM Test", SqlParser.scalar[String], ColumnAliaser.empty)
* }}}
*/
@SuppressWarnings(Array("UnusedMethodParameter"))
def source[T](sql: => Sql, parser: RowParser[T], as: ColumnAliaser)(implicit
con: Connection
): Source[T, Future[Int]] = Source.fromGraph(new ResultSource[T](con, sql, as, parser))

/** Returns the rows parsed from the `sql` query as a reactive source.
*
* $materialization
*
* @tparam T
* the type of the result elements
* @param sql
* $sqlParam
* @param parser
* the result (row) parser
* @param connection
* $connectionParam
*/
@SuppressWarnings(Array("UnusedMethodParameter"))
def source[T](sql: => Sql, parser: RowParser[T])(implicit con: Connection): Source[T, Future[Int]] =
source[T](sql, parser, ColumnAliaser.empty)

/** Returns the result rows from the `sql` query as an enumerator. This is equivalent to `source[Row](sql,
* RowParser.successful, as)`.
*
* $materialization
*
* @param sql
* $sqlParam
* @param as
* $columnAliaserParam
* @param connection
* $connectionParam
*/
def source(sql: => Sql, as: ColumnAliaser)(implicit connection: Connection): Source[Row, Future[Int]] =
source(sql, RowParser.successful, as)

/** Returns the result rows from the `sql` query as an enumerator. This is equivalent to `source[Row](sql,
* RowParser.successful, ColumnAliaser.empty)`.
*
* $materialization
*
* @param sql
* $sqlParam
* @param connection
* $connectionParam
*/
def source(sql: => Sql)(implicit connnection: Connection): Source[Row, Future[Int]] =
source(sql, RowParser.successful, ColumnAliaser.empty)

// Internal stages

import org.apache.pekko.stream.stage.{GraphStageLogic, GraphStageWithMaterializedValue, OutHandler}
import org.apache.pekko.stream.{Attributes, Outlet, SourceShape}

import java.sql.ResultSet
import scala.util.{Failure, Success}

private[anorm] class ResultSource[T](connection: Connection, sql: Sql, as: ColumnAliaser, parser: RowParser[T])
extends GraphStageWithMaterializedValue[SourceShape[T], Future[Int]] {

@SuppressWarnings(Array("org.wartremover.warts.Null"))
private[anorm] var resultSet: ResultSet = _

override val toString = "AnormQueryResult"
val out: Outlet[T] = Outlet(s"${toString}.out")
val shape: SourceShape[T] = SourceShape(out)

override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[Int]) = {
val result = Promise[Int]()

val logic = new GraphStageLogic(shape) with OutHandler {
private var cursor: Option[Cursor] = None
private var counter: Int = 0

private def failWith(cause: Throwable): Unit = {
result.failure(cause)
fail(out, cause)
()
}

override def preStart(): Unit = {
try {
resultSet = sql.unsafeResultSet(connection)
nextCursor()
} catch {
case NonFatal(cause) => failWith(cause)
}
}

override def postStop() = release()

private def release(): Unit = {
val stmt: Option[java.sql.Statement] = {
if (resultSet != null && !resultSet.isClosed) {
val s = resultSet.getStatement
resultSet.close()
Option(s)
} else None
}

stmt.foreach { s =>
if (!s.isClosed) s.close()
}
}

private def nextCursor(): Unit = {
cursor = Sql.unsafeCursor(resultSet, sql.resultSetOnFirstRow, as)
}

def onPull(): Unit = cursor match {
case Some(c) =>
c.row.as(parser) match {
case Success(parsed) => {
counter += 1
push(out, parsed)
nextCursor()
}

case Failure(cause) =>
failWith(cause)
}

case _ => {
result.success(counter)
complete(out)
}
}

@nowarn
override def onDownstreamFinish() = {
result.tryFailure(new InterruptedException("Downstream finished"))
release()
super.onDownstreamFinish()
}

setHandler(out, this)
}

logic -> result.future
}
}

}
4 changes: 2 additions & 2 deletions server/src/main/scala/controllers/AdminController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import net.wiringbits.common.models.Email
import net.wiringbits.services.AdminService
import org.slf4j.LoggerFactory
import sttp.capabilities.WebSockets
import sttp.capabilities.akka.AkkaStreams
import sttp.capabilities.pekko.PekkoStreams
import sttp.tapir.server.ServerEndpoint

import java.util.UUID
Expand Down Expand Up @@ -42,7 +42,7 @@ class AdminController @Inject() (
} yield Right(maskedResponse)
}

def routes: List[ServerEndpoint[AkkaStreams with WebSockets, Future]] = {
def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {
List(
AdminEndpoints.getUserLogsEndpoint.serverLogic(getUserLogs),
AdminEndpoints.getUsersEndpoint.serverLogic(getUsers)
Expand Down
7 changes: 5 additions & 2 deletions server/src/main/scala/controllers/ApiRouter.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package controllers

import akka.stream.Materializer
import net.wiringbits.api.endpoints.*
import net.wiringbits.config.SwaggerConfig
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.routing.Router.Routes
import play.api.routing.SimpleRouter
import sttp.apispec.openapi.Info
Expand All @@ -21,8 +22,10 @@ class ApiRouter @Inject() (
usersController: UsersController,
environmentConfigController: EnvironmentConfigController,
swaggerConfig: SwaggerConfig
)(implicit materializer: Materializer, ec: ExecutionContext)
)(using ExecutionContext)
extends SimpleRouter {
given ActorSystem = ActorSystem("ApiRouter")

private val swagger = SwaggerInterpreter(
swaggerUIOptions = SwaggerUIOptions.default.copy(contextPath = List(swaggerConfig.basePath))
)
Expand Down
Loading

1 comment on commit 8f7b8ce

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preview ready at https://master.sssppa.wiringbits.dev

Powered by https://codepreview.io community edition.

Please sign in to comment.