Skip to content

Commit

Permalink
Support Scala 3, drop Scala 2.12 & Play 2.8
Browse files Browse the repository at this point in the history
See:

* #149 - dropping Scala 2.12
* #161 - supporting Scala 3

### Need to import `play.api.libs.ws._` for the `BodyWritable`

Panda's `OAuth` class posts some url-encoded data (defined as `Map[String, Seq[String]]`
in Scala) with `ws.url(dd.token_endpoint).post`, and this needs an implicit instance of
`BodyWritable[Map[String, Seq[String]]]` in order to work! For some reason, in Scala 2,
the compiler was able to find the correct implicit somewhere, but in Scala 3 we get a
compilation error:

```
[error] 81 |        }.flatMap { response =>
[error]    |         ^
[error]    |Cannot find an instance of Map[K, Seq[String]] to WSBody. Define a BodyWritable[Map[K, Seq[String]]] or extend play.api.libs.ws.ahc.DefaultBodyWritables
[error]    |
[error]    |where:    K is a type variable with constraint >: String
[error]    |
[error]    |One of the following imports might fix the problem:
[error]    |
[error]    |
[error]    |One of the following imports might fix the problem:
[error]    |
[error]    |  import play.api.libs.ws.DefaultBodyWritables.writeableOf_urlEncodedForm
[error]    |  import play.api.libs.ws.WSBodyWritables.writeableOf_urlEncodedForm
[error]    |  import play.api.libs.ws.writeableOf_urlEncodedForm
```

Importing the whole `ws` package fixes the problem:

```
import play.api.libs.ws._
```
  • Loading branch information
rtyley committed Nov 23, 2024
1 parent 2c378ad commit 83e81a2
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 139 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ jobs:
popd
- uses: actions/setup-java@v4
- uses: guardian/setup-scala@v1
- name: Build and Test
run: sbt -v clean +test
- name: Test Summary
uses: test-summary/action@v2
with:
java-version: '21'
distribution: 'corretto'
cache: 'sbt'

- name: Scala Build
run: sbt clean +test
paths: "test-results/**/TEST-*.xml"
if: always()
148 changes: 52 additions & 96 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,30 @@ import sbt.Keys.*
import Dependencies.*
import sbtrelease.*
import ReleaseStateTransformations.*
import xerial.sbt.Sonatype.*
import play.sbt.PlayImport.PlayKeys.*
import sbtversionpolicy.withsbtrelease.ReleaseVersion

val scala212 = "2.12.20"
val scala213 = "2.13.14"
ThisBuild / scalaVersion := "3.3.4"
ThisBuild / crossScalaVersions := Seq(
scalaVersion.value,
"2.13.15"
)

ThisBuild / scalaVersion := scala213
val commonSettings = Seq(
organization := "com.gu",
licenses := Seq(License.Apache2),
Test / fork := false,
scalacOptions := Seq(
"-feature",
"-deprecation",
"-release:11"
),
Test / testOptions +=
Tests.Argument(TestFrameworks.ScalaTest, "-u", s"test-results/scala-${scalaVersion.value}", "-o")
)

val commonSettings =
Seq(
crossScalaVersions := List(scala212, scala213),
organization := "com.gu",
Test / fork := false,
scalacOptions ++= Seq(
"-feature",
"-deprecation",
// upgrade warnings to errors except deprecations
"-Wconf:cat=deprecation:ws,any:e",
"-release:11"
),
licenses := Seq(License.Apache2),
)
def subproject(path: String): Project =
Project(path, file(path)).settings(commonSettings: _*)

lazy val panDomainAuthVerification = subproject("pan-domain-auth-verification")
.settings(
Expand All @@ -34,10 +35,9 @@ lazy val panDomainAuthVerification = subproject("pan-domain-auth-verification")
++ awsDependencies
++ testDependencies
++ loggingDependencies
++ scalaCollectionCompatDependencies,
:+ scalaCollectionCompat,
)


lazy val panDomainAuthCore = subproject("pan-domain-auth-core")
.dependsOn(panDomainAuthVerification)
.settings(
Expand All @@ -46,104 +46,60 @@ lazy val panDomainAuthCore = subproject("pan-domain-auth-core")
++ googleDirectoryApiDependencies
++ cryptoDependencies
++ testDependencies
++ scalaCollectionCompatDependencies,
:+ scalaCollectionCompat,
)

lazy val panDomainAuthPlay_2_8 = subproject("pan-domain-auth-play_2-8")
.settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-play" / "src")
.settings(
libraryDependencies
++= playLibs_2_8
++ scalaCollectionCompatDependencies,
).dependsOn(panDomainAuthCore)

lazy val panDomainAuthPlay_2_9 = subproject("pan-domain-auth-play_2-9")
.settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-play" / "src")
.settings(
crossScalaVersions := Seq(scala213),
libraryDependencies
++= playLibs_2_9
++ scalaCollectionCompatDependencies,
).dependsOn(panDomainAuthCore)
def playBasedProject(playVersion: PlayVersion, projectPrefix: String, srcFolder: String) =
subproject(s"$projectPrefix${playVersion.projectIdSuffix}").settings(
sourceDirectory := (ThisBuild / baseDirectory).value / srcFolder / "src"
)

lazy val panDomainAuthPlay_3_0 = subproject("pan-domain-auth-play_3-0")
.settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-play" / "src")
.settings(
crossScalaVersions := Seq(scala213),
libraryDependencies
++= playLibs_3_0
++ scalaCollectionCompatDependencies,
def playSupportFor(playVersion: PlayVersion) =
playBasedProject(playVersion, "pan-domain-auth", "pan-domain-auth-play").settings(
libraryDependencies ++= playVersion.playLibs :+ scalaCollectionCompat
).dependsOn(panDomainAuthCore)

lazy val panDomainAuthHmac_2_8 = subproject("panda-hmac-play_2-8")
.settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-hmac" / "src")
.settings(
libraryDependencies ++= hmacLibs ++ playLibs_2_8 ++ testDependencies,
).dependsOn(panDomainAuthPlay_2_8)
def hmacPlayProject(playVersion: PlayVersion, playSupportProject: Project) =
playBasedProject(playVersion, "panda-hmac", "pan-domain-auth-hmac").settings(
libraryDependencies ++= hmacHeaders +: testDependencies
).dependsOn(playSupportProject)

lazy val panDomainAuthHmac_2_9 = subproject("panda-hmac-play_2-9")
.settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-hmac" / "src")
.settings(
crossScalaVersions := Seq(scala213),
libraryDependencies ++= hmacLibs ++ playLibs_2_9 ++ testDependencies,
).dependsOn(panDomainAuthPlay_2_9)
lazy val panDomainAuthPlay_2_9 = playSupportFor(PlayVersion.V29)
lazy val panDomainAuthHmac_2_9 = hmacPlayProject(PlayVersion.V29, panDomainAuthPlay_2_9)

lazy val panDomainAuthHmac_3_0 = subproject("panda-hmac-play_3-0")
.settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-hmac" / "src")
.settings(
crossScalaVersions := Seq(scala213),
libraryDependencies ++= hmacLibs ++ playLibs_3_0 ++ testDependencies,
).dependsOn(panDomainAuthPlay_3_0)
lazy val panDomainAuthPlay_3_0 = playSupportFor(PlayVersion.V30)
lazy val panDomainAuthHmac_3_0 = hmacPlayProject(PlayVersion.V30, panDomainAuthPlay_3_0)

lazy val exampleApp = subproject("pan-domain-auth-example")
.enablePlugins(PlayScala)
.settings(libraryDependencies ++= (awsDependencies :+ ws))
.dependsOn(panDomainAuthPlay_2_9)
.dependsOn(panDomainAuthPlay_3_0)
.settings(
crossScalaVersions := Seq(scala213),
libraryDependencies ++= awsDependencies :+ ws,
publish / skip := true,
playDefaultPort := 9500
)

lazy val sonatypeReleaseSettings = {
sonatypeSettings ++ Seq(
// sbt and sbt-release implement cross-building support differently. sbt does it better
// (it supports each subproject having different crossScalaVersions), so disable sbt-release's
// implementation, and do the publish step with a `+`,
// ie. (`releaseStepCommandAndRemaining("+publishSigned")`)
// See https://www.scala-sbt.org/1.x/docs/Cross-Build.html#Note+about+sbt-release
// Never run with "release cross" or "+release"! Odd things start happening
releaseCrossBuild := false,
releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value,
releaseProcess := Seq[ReleaseStep](
checkSnapshotDependencies,
inquireVersions,
runClean,
runTest,
setReleaseVersion,
commitReleaseVersion,
tagRelease,
setNextVersion,
commitNextVersion
)
)
}

lazy val root = Project("pan-domain-auth-root", file(".")).aggregate(
panDomainAuthVerification,
panDomainAuthCore,
panDomainAuthPlay_2_8,
panDomainAuthPlay_2_9,
panDomainAuthPlay_3_0,
panDomainAuthHmac_2_8,
panDomainAuthHmac_2_9,
panDomainAuthHmac_3_0,
exampleApp
).settings(sonatypeReleaseSettings)
.settings(
organization := "com.gu",
).settings(
publish / skip := true,
releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value,
releaseCrossBuild := true, // true if you cross-build the project for multiple Scala versions
releaseProcess := Seq[ReleaseStep](
checkSnapshotDependencies,
inquireVersions,
runClean,
runTest,
setReleaseVersion,
commitReleaseVersion,
tagRelease,
setNextVersion,
commitNextVersion
)
)

def subproject(path: String): Project =
Project(path, file(path)).settings(commonSettings: _*)
12 changes: 6 additions & 6 deletions pan-domain-auth-example/app/controllers/AdminController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package controllers

import com.gu.pandomainauth.PanDomainAuthSettingsRefresher
import play.api.Configuration
import play.api.mvc.{AbstractController, ControllerComponents}
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}
import play.api.libs.ws.WSClient


Expand All @@ -14,32 +14,32 @@ class AdminController(
) extends AbstractController(controllerComponents) with ExampleAuthActions {

// No authentication
def index = Action{
def index: Action[AnyContent] = Action {
Ok("hello")
}

// This is a normal user-interactive request that will redirect to the OAuth provider
// to re-negotiate a login on expiry.
def showUser = AuthAction { req =>
def showUser: Action[AnyContent] = AuthAction { req =>
// The user information is available as a field on the request
Ok(req.user.toString)
}

// This is a request that is issued from JS. If the user has expired it will return an
// error code that can be handled by the front-end webapp.
def showUserApi = APIAuthAction { req =>
def showUserApi: Action[AnyContent] = APIAuthAction { req =>
Ok(req.user.toString)
}

// Required to allow the provider to redirect back to us so we can issue the new cookie
// This route must be added to the provider whitelist
def oauthCallback = Action.async { implicit request =>
def oauthCallback: Action[AnyContent] = Action.async { implicit request =>
processOAuthCallback()
}

// Note: this is potentially confusing depending on your use-case as currently only the
// panda cookie is removed and the user is not logged out of the OAuth provider
def logout = Action { implicit request =>
def logout: Action[AnyContent] = Action { implicit request =>
processLogout(request)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.gu.pandomainauth.service

import com.gu.pandomainauth.model.{AuthenticatedUser, OAuthSettings, User}
import play.api.libs.json.JsValue
import play.api.libs.ws.{WSClient, WSResponse}
import play.api.libs.ws._
import play.api.mvc.Results.Redirect
import play.api.mvc.{RequestHeader, Result}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ object PanDomain {
*/
def authStatus(cookieData: String, verification: Verification, validateUser: AuthenticatedUser => Boolean,
apiGracePeriod: Long, system: String, cacheValidation: Boolean, forceExpiry: Boolean): AuthenticationStatus = {
CookieUtils.parseCookieData(cookieData, verification).fold(InvalidCookie, { authedUser =>
CookieUtils.parseCookieData(cookieData, verification).fold(InvalidCookie(_), { authedUser =>
checkStatus(authedUser, validateUser, apiGracePeriod, system, cacheValidation, forceExpiry)
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ object CryptoConf {
case class SettingsReader(settingMap: Map[String,String]) {
def setting(key: String): SettingsResult[String] = settingMap.get(key).toRight(MissingSetting(key))

def signingAndVerificationConf: SettingsResult[SigningAndVerification] = makeConfWith(activeKeyPair)(SigningAndVerification)
def verificationConf: SettingsResult[Verification] = makeConfWith(activePublicKey)(OnlyVerification)
def signingAndVerificationConf: SettingsResult[SigningAndVerification] = makeConfWith(activeKeyPair)(SigningAndVerification(_, _))
def verificationConf: SettingsResult[Verification] = makeConfWith(activePublicKey)(OnlyVerification(_, _))

val activePublicKey: SettingsResult[PublicKey] = setting("publicKey").flatMap(publicKeyFor)

Expand Down
39 changes: 15 additions & 24 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,24 @@ object Dependencies {

val awsDependencies = Seq("com.amazonaws" % "aws-java-sdk-s3" % "1.12.772")

val playLibs_2_8 = {
val version = "2.8.19"
Seq(
"com.typesafe.play" %% "play" % version % "provided",
"com.typesafe.play" %% "play-ws" % version % "provided"
)
case class PlayVersion(
majorVersion: Int,
minorVersion: Int,
groupId: String,
exactPlayVersion: String
) {
val projectIdSuffix = s"-play_$majorVersion-$minorVersion"

val playLibs: Seq[ModuleID] =
Seq("play", "play-ws").map(artifact => groupId %% artifact % exactPlayVersion)
}

val playLibs_2_9 = {
val version = "2.9.0"
Seq(
"com.typesafe.play" %% "play" % version % "provided",
"com.typesafe.play" %% "play-ws" % version % "provided"
)
object PlayVersion {
val V29 = PlayVersion(2, 9, "com.typesafe.play", "2.9.2")
val V30 = PlayVersion(3, 0, "org.playframework", "3.0.5")
}

val playLibs_3_0 = {
val version = "3.0.0"
Seq(
"org.playframework" %% "play" % version % "provided",
"org.playframework" %% "play-ws" % version % "provided"
)
}

val hmacLibs = Seq(
"com.gu" %% "hmac-headers" % "2.0.0"
)
val hmacHeaders = "com.gu" %% "hmac-headers" % "2.0.1"

val googleDirectoryApiDependencies = Seq(
"com.google.apis" % "google-api-services-admin-directory" % "directory_v1-rev20240903-2.0.0",
Expand All @@ -49,5 +40,5 @@ object Dependencies {

// provide compatibility between scala 2.12 and 2.13
// see https://github.com/scala/scala-collection-compat/issues/208
val scalaCollectionCompatDependencies = Seq("org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0")
val scalaCollectionCompat = "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0"
}
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.9.9
sbt.version=1.10.5
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Use the Play sbt plugin for Play projects
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.5")
addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.5")

addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0")

Expand Down

0 comments on commit 83e81a2

Please sign in to comment.