From adcf7681f4a4128b1e5483190d6fb781933ab401 Mon Sep 17 00:00:00 2001 From: Andy Spaven <16537059+AndySpaven@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:16:20 +0000 Subject: [PATCH 1/6] Scala-3-POC - convert code to work with scala 3 libs --- .scalafix.conf | 2 +- .scalafmt.conf | 2 +- .../hmrc/apiscope/controllers/ScopeController.scala | 4 ++-- app/uk/gov/hmrc/apiscope/models/ErrorCode.scala | 4 ++-- .../hmrc/apiscope/models/ResponseFormatters.scala | 2 +- app/uk/gov/hmrc/apiscope/models/ScopeRequest.scala | 2 +- .../hmrc/apiscope/repository/ScopeRepository.scala | 4 ++-- .../apiscope/services/ScopeJsonFileService.scala | 8 ++++---- build.sbt | 2 +- project/AppDependencies.scala | 4 ++-- .../apiscope/controllers/ScopeControllerSpec.scala | 4 ++-- .../gov/hmrc/apiscope/models/ScopeRequestSpec.scala | 12 ++++++------ test/uk/gov/hmrc/apiscope/repository/MongoApp.scala | 2 +- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.scalafix.conf b/.scalafix.conf index 24f1055..2fce1f6 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -23,4 +23,4 @@ OrganizeImports { importsOrder = Ascii preset = DEFAULT removeUnused = true -} \ No newline at end of file +}OrganizeImports.targetDialect = Scala3 \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index 5ac8eb7..a1a4481 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,5 +1,5 @@ version="3.7.17" -runner.dialect = "scala213" +runner.dialect = scala3 maxColumn = 180 diff --git a/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala b/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala index 9be61d0..9ca84b7 100644 --- a/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala +++ b/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala @@ -26,8 +26,8 @@ import play.api.mvc._ import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController import uk.gov.hmrc.apiscope.models.ErrorCode._ -import uk.gov.hmrc.apiscope.models.ResponseFormatters._ -import uk.gov.hmrc.apiscope.models.{Scope, ScopeData} +import uk.gov.hmrc.apiscope.models.ResponseFormatters.given +import uk.gov.hmrc.apiscope.models._ import uk.gov.hmrc.apiscope.services.ScopeService @Singleton diff --git a/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala b/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala index f9bd94c..b39fd8a 100644 --- a/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala +++ b/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala @@ -17,7 +17,7 @@ package uk.gov.hmrc.apiscope.models import play.api.libs.json._ -import uk.gov.hmrc.apiplatform.modules.common.domain.services.SealedTraitJsonFormatting +import uk.gov.hmrc.apiplatform.modules.common.domain.services.SimpleEnumJsonFormatting sealed trait ErrorCode @@ -32,7 +32,7 @@ object ErrorCode { def unsafeApply(text: String): ErrorCode = apply(text).getOrElse(throw new RuntimeException(s"$text is not a valid Error Code")) - implicit val format: Format[ErrorCode] = SealedTraitJsonFormatting.createFormatFor[ErrorCode]("Error Code", apply) + implicit val format: Format[ErrorCode] = SimpleEnumJsonFormatting.createFormatFor[ErrorCode]("Error Code", apply) } case class ErrorResponse(code: ErrorCode, message: String, details: Option[Seq[ErrorDescription]] = None) diff --git a/app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala b/app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala index 41d018e..294e4e8 100644 --- a/app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala +++ b/app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala @@ -19,5 +19,5 @@ package uk.gov.hmrc.apiscope.models import play.api.libs.json.{Json, OFormat} object ResponseFormatters { - implicit val scopeFormat: OFormat[Scope] = Json.format[Scope] + given OFormat[Scope] = Json.format[Scope] } diff --git a/app/uk/gov/hmrc/apiscope/models/ScopeRequest.scala b/app/uk/gov/hmrc/apiscope/models/ScopeRequest.scala index dc164c0..52670f6 100644 --- a/app/uk/gov/hmrc/apiscope/models/ScopeRequest.scala +++ b/app/uk/gov/hmrc/apiscope/models/ScopeRequest.scala @@ -26,5 +26,5 @@ case class ScopeData(key: String, name: String, description: String, confidenceL } object ScopeData { - implicit val format1: OFormat[ScopeData] = Json.format[ScopeData] + given OFormat[ScopeData] = Json.format[ScopeData] } diff --git a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala index 019dd98..683ab01 100644 --- a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala +++ b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala @@ -53,7 +53,7 @@ private object ScopeFormats { case Some(600) => Some(ConfidenceLevel.L600) case Some(i) => throw new RuntimeException(s"Bad data in confidence level of $i") }) - )(Scope.apply _) + )(Scope.apply) implicit val scopeWrites: OWrites[Scope] = Json.writes[Scope] implicit val scopeFormat: OFormat[Scope] = OFormat(scopeRead, scopeWrites) @@ -95,7 +95,7 @@ class ScopeRepository @Inject() (mongoComponent: MongoComponent)(implicit val ec collection.findOneAndUpdate( equal("key", Codecs.toBson(scope.key)), - update = combine(updateSeq: _*), + update = combine(updateSeq*), options = FindOneAndUpdateOptions().upsert(true).returnDocument(ReturnDocument.AFTER) ).map(_.asInstanceOf[Scope]).head() } diff --git a/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala b/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala index 55c0809..6485249 100644 --- a/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala +++ b/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala @@ -24,20 +24,20 @@ import util.ApplicationLogger import play.api.libs.json.{JsError, JsSuccess, Json} -import uk.gov.hmrc.apiscope.models.ResponseFormatters._ +import uk.gov.hmrc.apiscope.models.ResponseFormatters.given import uk.gov.hmrc.apiscope.models.Scope import uk.gov.hmrc.apiscope.repository.ScopeRepository @Singleton class ScopeJsonFileService @Inject() (scopeRepository: ScopeRepository, fileReader: ScopeJsonFileReader)(implicit val ec: ExecutionContext) extends ApplicationLogger { - private def saveScopes(scopes: Seq[Scope]): Future[Seq[Scope]] = + private def saveScopes(scopes: List[Scope]): Future[List[Scope]] = Future.sequence(scopes.map(scopeRepository.save)) try { fileReader.readFile.map(s => - Json.parse(s).validate[Seq[Scope]] match { - case JsSuccess(scopes: Seq[Scope], _) => + Json.parse(s).validate[List[Scope]] match { + case JsSuccess(scopes: List[Scope] @unchecked, _) => logger.info(s"Inserting ${scopes.size} Scopes from bundled file") saveScopes(scopes) case JsError(errors) => logger.error(s"Unable to parse JSON into Scopes ${errors.mkString("; ")}") diff --git a/build.sbt b/build.sbt index 9708e8d..eede8f4 100644 --- a/build.sbt +++ b/build.sbt @@ -7,7 +7,7 @@ Global / bloopExportJarClassifiers := Some(Set("sources")) ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision -ThisBuild / scalaVersion := "2.13.16" +ThisBuild / scalaVersion := "3.7.4" ThisBuild / majorVersion := 0 ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always diff --git a/project/AppDependencies.scala b/project/AppDependencies.scala index de36d68..1a32e22 100644 --- a/project/AppDependencies.scala +++ b/project/AppDependencies.scala @@ -6,7 +6,7 @@ object AppDependencies { private lazy val bootstrapVersion = "10.5.0" private lazy val hmrcMongoVersion = "2.11.0" - val commonDomainVersion = "0.19.0" + val commonDomainVersion = "1.0.0-SNAPSHOT" private lazy val compile = Seq( "uk.gov.hmrc" %% "bootstrap-backend-play-30" % bootstrapVersion, @@ -18,7 +18,7 @@ object AppDependencies { "uk.gov.hmrc.mongo" %% "hmrc-mongo-test-play-30" % hmrcMongoVersion, "com.softwaremill.sttp.client3" %% "core" % "3.11.0", "uk.gov.hmrc" %% "bootstrap-test-play-30" % bootstrapVersion, - "org.mockito" %% "mockito-scala-scalatest" % "1.17.45", + // "org.mockito" %% "mockito-scala-scalatest" % "1.17.45", "uk.gov.hmrc" %% "api-platform-common-domain-fixtures" % commonDomainVersion ).map(_ % "test") diff --git a/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala b/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala index 9daaf70..5827649 100644 --- a/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala +++ b/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala @@ -32,7 +32,7 @@ import play.api.test.{FakeRequest, StubControllerComponentsFactory, StubPlayBody import play.mvc.Http.Status.{INTERNAL_SERVER_ERROR, NOT_FOUND, NO_CONTENT, OK} import uk.gov.hmrc.auth.core.ConfidenceLevel -import uk.gov.hmrc.apiscope.models.ResponseFormatters._ +import uk.gov.hmrc.apiscope.models.ResponseFormatters.given import uk.gov.hmrc.apiscope.models.{ErrorCode, ErrorDescription, ErrorResponse, Scope} import uk.gov.hmrc.apiscope.services.ScopeService import uk.gov.hmrc.util.AsyncHmrcSpec @@ -56,7 +56,7 @@ class ScopeControllerSpec extends AsyncHmrcSpec val mockScopeService: ScopeService = mock[ScopeService] val controllerComponents: ControllerComponents = stubControllerComponents() - val underTest = new ScopeController(mockScopeService, controllerComponents, stubPlayBodyParsers(materializer)) + val underTest = new ScopeController(mockScopeService, controllerComponents, stubPlayBodyParsers(using materializer)) implicit lazy val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() diff --git a/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala b/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala index 9a1f9cd..a42fe24 100644 --- a/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala +++ b/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala @@ -24,12 +24,12 @@ class ScopeRequestSpec extends HmrcSpec { val scopeRequest = Seq(scopeData) val testCases = Map( - "scope key is empty" -> { s: Seq[ScopeData] => Seq(scopeData.copy(key = "")) }, - "scope key is empty string" -> { s: Seq[ScopeData] => Seq(scopeData.copy(key = " ")) }, - "scope name is empty" -> { s: Seq[ScopeData] => Seq(scopeData.copy(name = "")) }, - "scope name is empty string" -> { s: Seq[ScopeData] => Seq(scopeData.copy(name = " ")) }, - "scope description is empty" -> { s: Seq[ScopeData] => Seq(scopeData.copy(description = "")) }, - "scope description is empty string" -> { s: Seq[ScopeData] => Seq(scopeData.copy(description = " ")) } + "scope key is empty" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(key = "")) }, + "scope key is empty string" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(key = " ")) }, + "scope name is empty" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(name = "")) }, + "scope name is empty string" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(name = " ")) }, + "scope description is empty" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(description = "")) }, + "scope description is empty string" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(description = " ")) } ) "scopeRequest" should { testCases foreach { diff --git a/test/uk/gov/hmrc/apiscope/repository/MongoApp.scala b/test/uk/gov/hmrc/apiscope/repository/MongoApp.scala index bf8aa73..61d2edc 100644 --- a/test/uk/gov/hmrc/apiscope/repository/MongoApp.scala +++ b/test/uk/gov/hmrc/apiscope/repository/MongoApp.scala @@ -21,7 +21,7 @@ import org.scalatest.{BeforeAndAfterEach, Suite, TestSuite} import uk.gov.hmrc.mongo.test.DefaultPlayMongoRepositorySupport trait MongoApp[A] extends DefaultPlayMongoRepositorySupport[A] with BeforeAndAfterEach { - me: Suite with TestSuite => + me: Suite & TestSuite => override def beforeEach(): Unit = { prepareDatabase() From 3ebdafa1c5e64882e0e5041f1fbc760fa2686ef8 Mon Sep 17 00:00:00 2001 From: Andy Spaven <16537059+AndySpaven@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:57:54 +0000 Subject: [PATCH 2/6] Scala-3-POC - Mockito solution --- .scalafix.conf | 3 ++- .../controllers/ScopeController.scala | 8 +++--- .../hmrc/apiscope/controllers/package.scala | 4 +-- .../gov/hmrc/apiscope/models/ErrorCode.scala | 2 +- .../apiscope/repository/ScopeRepository.scala | 4 +-- .../services/ScopeJsonFileService.scala | 4 +-- build.sbt | 27 ++++++++++--------- .../gov/hmrc/apiscope/BaseFeatureSpec.scala | 2 +- project/AppDependencies.scala | 3 ++- .../controllers/ScopeControllerSpec.scala | 16 ++++++----- .../hmrc/apiscope/models/ErrorCodeSpec.scala | 2 +- .../apiscope/models/ScopeRequestSpec.scala | 12 ++++----- .../repository/ScopeRepositorySpec.scala | 8 +++--- .../services/ScopeJsonFileServiceSpec.scala | 5 +++- .../apiscope/services/ScopeServiceSpec.scala | 5 +++- 15 files changed, 61 insertions(+), 44 deletions(-) diff --git a/.scalafix.conf b/.scalafix.conf index 2fce1f6..179b678 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -23,4 +23,5 @@ OrganizeImports { importsOrder = Ascii preset = DEFAULT removeUnused = true -}OrganizeImports.targetDialect = Scala3 \ No newline at end of file + targetDialect = Scala3 +} \ No newline at end of file diff --git a/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala b/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala index 9ca84b7..36871ce 100644 --- a/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala +++ b/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala @@ -21,13 +21,13 @@ import scala.concurrent.{ExecutionContext, Future} import util.ApplicationLogger -import play.api.libs.json._ -import play.api.mvc._ +import play.api.libs.json.* +import play.api.mvc.* import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController -import uk.gov.hmrc.apiscope.models.ErrorCode._ +import uk.gov.hmrc.apiscope.models.* +import uk.gov.hmrc.apiscope.models.ErrorCode.* import uk.gov.hmrc.apiscope.models.ResponseFormatters.given -import uk.gov.hmrc.apiscope.models._ import uk.gov.hmrc.apiscope.services.ScopeService @Singleton diff --git a/app/uk/gov/hmrc/apiscope/controllers/package.scala b/app/uk/gov/hmrc/apiscope/controllers/package.scala index 96f7c17..2090636 100644 --- a/app/uk/gov/hmrc/apiscope/controllers/package.scala +++ b/app/uk/gov/hmrc/apiscope/controllers/package.scala @@ -20,9 +20,9 @@ import scala.concurrent.Future import util.ApplicationLogger +import play.api.libs.json.* import play.api.libs.json.Json.toJson -import play.api.libs.json._ -import play.api.mvc.Results._ +import play.api.mvc.Results.* import play.api.mvc.{Request, Result} import uk.gov.hmrc.http.NotFoundException diff --git a/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala b/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala index b39fd8a..0ac8ec5 100644 --- a/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala +++ b/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala @@ -16,7 +16,7 @@ package uk.gov.hmrc.apiscope.models -import play.api.libs.json._ +import play.api.libs.json.* import uk.gov.hmrc.apiplatform.modules.common.domain.services.SimpleEnumJsonFormatting sealed trait ErrorCode diff --git a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala index 683ab01..854aa49 100644 --- a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala +++ b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala @@ -27,8 +27,8 @@ import org.mongodb.scala.model.Updates.{combine, set} import org.mongodb.scala.model.{FindOneAndUpdateOptions, IndexModel, IndexOptions, ReturnDocument} import play.api.Logger -import play.api.libs.functional.syntax._ -import play.api.libs.json.{Reads, _} +import play.api.libs.functional.syntax.* +import play.api.libs.json.{Reads, *} import uk.gov.hmrc.auth.core.ConfidenceLevel import uk.gov.hmrc.mongo.MongoComponent import uk.gov.hmrc.mongo.play.json.{Codecs, PlayMongoRepository} diff --git a/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala b/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala index 6485249..48a10bb 100644 --- a/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala +++ b/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala @@ -37,10 +37,10 @@ class ScopeJsonFileService @Inject() (scopeRepository: ScopeRepository, fileRead try { fileReader.readFile.map(s => Json.parse(s).validate[List[Scope]] match { - case JsSuccess(scopes: List[Scope] @unchecked, _) => + case JsSuccess(scopes: List[Scope] @unchecked, _) => logger.info(s"Inserting ${scopes.size} Scopes from bundled file") saveScopes(scopes) - case JsError(errors) => logger.error(s"Unable to parse JSON into Scopes ${errors.mkString("; ")}") + case JsError(errors) => logger.error(s"Unable to parse JSON into Scopes ${errors.mkString("; ")}") } ) } catch { diff --git a/build.sbt b/build.sbt index eede8f4..47e9776 100644 --- a/build.sbt +++ b/build.sbt @@ -5,11 +5,14 @@ lazy val appName = "api-scope" Global / bloopAggregateSourceDependencies := true Global / bloopExportJarClassifiers := Some(Set("sources")) -ThisBuild / semanticdbEnabled := true -ThisBuild / semanticdbVersion := scalafixSemanticdb.revision -ThisBuild / scalaVersion := "3.7.4" +inThisBuild( + List( + scalaVersion := "3.7.4", + semanticdbEnabled := true, + semanticdbVersion := scalafixSemanticdb.revision + ) +) ThisBuild / majorVersion := 0 -ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always lazy val microservice = Project(appName, file(".")) .enablePlugins(PlayScala, SbtDistributablesPlugin) @@ -23,14 +26,14 @@ lazy val microservice = Project(appName, file(".")) Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-eT"), Test / unmanagedSourceDirectories += baseDirectory.value / "test-common" ) - .settings( - scalacOptions ++= Seq( - "-Wconf:cat=unused&src=views/.*\\.scala:s", - "-Wconf:cat=unused&src=.*RoutesPrefix\\.scala:s", - "-Wconf:cat=unused&src=.*Routes\\.scala:s", - "-Wconf:cat=unused&src=.*ReverseRoutes\\.scala:s" - ) - ) + // .settings( + // scalacOptions ++= Seq( + // "-Wconf:cat=unused&src=views/.*\\.scala:s", + // "-Wconf:cat=unused&src=.*RoutesPrefix\\.scala:s", + // "-Wconf:cat=unused&src=.*Routes\\.scala:s", + // "-Wconf:cat=unused&src=.*ReverseRoutes\\.scala:s" + // ) + // ) lazy val it = (project in file("it")) .enablePlugins(PlayScala) diff --git a/it/test/uk/gov/hmrc/apiscope/BaseFeatureSpec.scala b/it/test/uk/gov/hmrc/apiscope/BaseFeatureSpec.scala index e9ea618..ae9e0e6 100644 --- a/it/test/uk/gov/hmrc/apiscope/BaseFeatureSpec.scala +++ b/it/test/uk/gov/hmrc/apiscope/BaseFeatureSpec.scala @@ -16,7 +16,7 @@ package uk.gov.hmrc.apiscope -import org.scalatest._ +import org.scalatest.* import org.scalatest.concurrent.ScalaFutures import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers diff --git a/project/AppDependencies.scala b/project/AppDependencies.scala index 1a32e22..183d012 100644 --- a/project/AppDependencies.scala +++ b/project/AppDependencies.scala @@ -18,7 +18,8 @@ object AppDependencies { "uk.gov.hmrc.mongo" %% "hmrc-mongo-test-play-30" % hmrcMongoVersion, "com.softwaremill.sttp.client3" %% "core" % "3.11.0", "uk.gov.hmrc" %% "bootstrap-test-play-30" % bootstrapVersion, - // "org.mockito" %% "mockito-scala-scalatest" % "1.17.45", + "org.scalatestplus" %% "mockito-5-18" % "3.2.19.0", + "uk.gov.hmrc" %% "api-platform-common-domain-fixtures" % commonDomainVersion ).map(_ % "test") diff --git a/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala b/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala index 5827649..6287ff5 100644 --- a/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala +++ b/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala @@ -20,14 +20,17 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future.{failed, successful} import org.apache.pekko.stream.Materializer -import org.scalatest.prop.TableDrivenPropertyChecks._ +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.* +import org.scalatest.prop.TableDrivenPropertyChecks.* import org.scalatest.prop.TableFor2 import org.scalatest.prop.Tables.Table +import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.play.guice.GuiceOneAppPerSuite import play.api.libs.json.{JsDefined, JsString, Json} import play.api.mvc.{AnyContentAsEmpty, ControllerComponents} -import play.api.test.Helpers._ +import play.api.test.Helpers.* import play.api.test.{FakeRequest, StubControllerComponentsFactory, StubPlayBodyParsersFactory} import play.mvc.Http.Status.{INTERNAL_SERVER_ERROR, NOT_FOUND, NO_CONTENT, OK} import uk.gov.hmrc.auth.core.ConfidenceLevel @@ -35,12 +38,13 @@ import uk.gov.hmrc.auth.core.ConfidenceLevel import uk.gov.hmrc.apiscope.models.ResponseFormatters.given import uk.gov.hmrc.apiscope.models.{ErrorCode, ErrorDescription, ErrorResponse, Scope} import uk.gov.hmrc.apiscope.services.ScopeService -import uk.gov.hmrc.util.AsyncHmrcSpec +import uk.gov.hmrc.util.AsyncHmrcSpec; class ScopeControllerSpec extends AsyncHmrcSpec with GuiceOneAppPerSuite with StubControllerComponentsFactory - with StubPlayBodyParsersFactory { + with StubPlayBodyParsersFactory + with MockitoSugar { val scope: Scope = Scope("key1", "name1", "desc1") @@ -60,7 +64,7 @@ class ScopeControllerSpec extends AsyncHmrcSpec implicit lazy val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - when(mockScopeService.saveScopes(any[Seq[Scope]])).thenReturn(successful(Seq())) + when(mockScopeService.saveScopes(any[Seq[Scope]]())).thenReturn(successful(Seq())) when(mockScopeService.fetchScopes(Set(scope.key))).thenReturn(successful(Seq(scope))) } @@ -93,7 +97,7 @@ class ScopeControllerSpec extends AsyncHmrcSpec val result = underTest.createOrUpdateScope()(request.withBody(Json.parse(invalidBody))) status(result) shouldBe expectedResponseCode - verify(mockScopeService, times(0)).saveScopes(*) + verify(mockScopeService, times(0)).saveScopes(any()) } } diff --git a/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala b/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala index f5a1a01..44eb90e 100644 --- a/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala +++ b/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala @@ -16,7 +16,7 @@ package uk.gov.hmrc.apiscope.models -import play.api.libs.json.{JsError, _} +import play.api.libs.json.{JsError, *} import uk.gov.hmrc.apiplatform.modules.common.utils.HmrcSpec class ErrorCodeSpec extends HmrcSpec { diff --git a/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala b/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala index a42fe24..da5ae73 100644 --- a/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala +++ b/test/uk/gov/hmrc/apiscope/models/ScopeRequestSpec.scala @@ -24,12 +24,12 @@ class ScopeRequestSpec extends HmrcSpec { val scopeRequest = Seq(scopeData) val testCases = Map( - "scope key is empty" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(key = "")) }, - "scope key is empty string" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(key = " ")) }, - "scope name is empty" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(name = "")) }, - "scope name is empty string" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(name = " ")) }, - "scope description is empty" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(description = "")) }, - "scope description is empty string" -> { (s: Seq[ScopeData]) => Seq(scopeData.copy(description = " ")) } + "scope key is empty" -> { (_: Seq[ScopeData]) => Seq(scopeData.copy(key = "")) }, + "scope key is empty string" -> { (_: Seq[ScopeData]) => Seq(scopeData.copy(key = " ")) }, + "scope name is empty" -> { (_: Seq[ScopeData]) => Seq(scopeData.copy(name = "")) }, + "scope name is empty string" -> { (_: Seq[ScopeData]) => Seq(scopeData.copy(name = " ")) }, + "scope description is empty" -> { (_: Seq[ScopeData]) => Seq(scopeData.copy(description = "")) }, + "scope description is empty string" -> { (_: Seq[ScopeData]) => Seq(scopeData.copy(description = " ")) } ) "scopeRequest" should { testCases foreach { diff --git a/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala b/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala index b010a81..02c787c 100644 --- a/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala +++ b/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala @@ -23,13 +23,14 @@ import org.mongodb.scala.Document import org.mongodb.scala.MongoClient.DEFAULT_CODEC_REGISTRY import org.mongodb.scala.bson.{BsonDocument, BsonString} import org.scalatest.BeforeAndAfterEach -import org.scalatest.matchers.must.Matchers.convertToAnyMustWrapper +import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.play.guice.GuiceOneAppPerSuite import play.api.Application import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json.{Format, JsObject, Json} import uk.gov.hmrc.auth.core.ConfidenceLevel +import uk.gov.hmrc.mongo.logging.ObservableFutureImplicits.given import uk.gov.hmrc.mongo.play.json.Codecs import uk.gov.hmrc.mongo.test.DefaultPlayMongoRepositorySupport @@ -39,7 +40,8 @@ import uk.gov.hmrc.util.AsyncHmrcSpec class ScopeRepositorySpec extends AsyncHmrcSpec with BeforeAndAfterEach with GuiceOneAppPerSuite - with DefaultPlayMongoRepositorySupport[Scope] { + with DefaultPlayMongoRepositorySupport[Scope] + with MockitoSugar { val basicScope: Scope = Scope("key1", "name1", "description1") val scopeConfidence200: Scope = Scope("key2", "name2", "description2", confidenceLevel = Some(ConfidenceLevel.L200)) @@ -175,7 +177,7 @@ class ScopeRepositorySpec extends AsyncHmrcSpec "have all the indexes" in { val indexes = getIndexes() - indexes.size mustEqual 2 + indexes.size shouldBe 2 indexes.map(ind => ind.get("name")) contains BsonString("keyIndex") indexes.map(ind => ind.get("key")) contains BsonDocument("key" -> 1) diff --git a/test/uk/gov/hmrc/apiscope/services/ScopeJsonFileServiceSpec.scala b/test/uk/gov/hmrc/apiscope/services/ScopeJsonFileServiceSpec.scala index 617cbc0..1fc40d2 100644 --- a/test/uk/gov/hmrc/apiscope/services/ScopeJsonFileServiceSpec.scala +++ b/test/uk/gov/hmrc/apiscope/services/ScopeJsonFileServiceSpec.scala @@ -19,6 +19,9 @@ package uk.gov.hmrc.apiscope.services import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future.successful +import org.mockito.Mockito.{never, verify, when} +import org.scalatestplus.mockito.MockitoSugar + import play.api.libs.json.{JsValue, Json} import uk.gov.hmrc.auth.core.ConfidenceLevel @@ -26,7 +29,7 @@ import uk.gov.hmrc.apiscope.models.Scope import uk.gov.hmrc.apiscope.repository.ScopeRepository import uk.gov.hmrc.util.AsyncHmrcSpec -class ScopeJsonFileServiceSpec extends AsyncHmrcSpec { +class ScopeJsonFileServiceSpec extends AsyncHmrcSpec with MockitoSugar { val scope1 = Scope("key1", "name1", "description1") val scope2 = Scope("key2", "name2", "description2", confidenceLevel = Some(ConfidenceLevel.L200)) diff --git a/test/uk/gov/hmrc/apiscope/services/ScopeServiceSpec.scala b/test/uk/gov/hmrc/apiscope/services/ScopeServiceSpec.scala index 8c5f96e..feea734 100644 --- a/test/uk/gov/hmrc/apiscope/services/ScopeServiceSpec.scala +++ b/test/uk/gov/hmrc/apiscope/services/ScopeServiceSpec.scala @@ -19,13 +19,16 @@ package uk.gov.hmrc.apiscope.services import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future.{failed, successful} +import org.mockito.Mockito.{verify, when} +import org.scalatestplus.mockito.MockitoSugar + import uk.gov.hmrc.auth.core.ConfidenceLevel import uk.gov.hmrc.apiscope.models.Scope import uk.gov.hmrc.apiscope.repository.ScopeRepository import uk.gov.hmrc.util.AsyncHmrcSpec -class ScopeServiceSpec extends AsyncHmrcSpec { +class ScopeServiceSpec extends AsyncHmrcSpec with MockitoSugar { val scope1 = Scope("key1", "name1", "description1") val scope2 = Scope("key2", "name2", "description2", confidenceLevel = Some(ConfidenceLevel.L200)) From 7f1cee1d68b85b9ff996036ab9dcf5de02bfccc4 Mon Sep 17 00:00:00 2001 From: Andy Spaven <16537059+AndySpaven@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:04:32 +0000 Subject: [PATCH 3/6] Scala-3-POC - Fix given/using over implicits --- .scalafix.conf | 2 +- .../controllers/ScopeController.scala | 10 +- .../hmrc/apiscope/controllers/package.scala | 13 ++- .../gov/hmrc/apiscope/models/ErrorCode.scala | 31 +++--- .../apiscope/models/ResponseFormatters.scala | 23 ----- app/uk/gov/hmrc/apiscope/models/Scope.scala | 5 + .../apiscope/repository/ScopeRepository.scala | 21 ++-- .../services/ScopeJsonFileService.scala | 3 +- .../hmrc/apiscope/services/ScopeService.scala | 2 +- build.sbt | 8 -- project/plugins.sbt | 1 - scalastyle-config.xml | 99 ------------------- .../controllers/ScopeControllerSpec.scala | 15 ++- .../hmrc/apiscope/models/ErrorCodeSpec.scala | 7 +- .../repository/ScopeRepositorySpec.scala | 8 +- 15 files changed, 59 insertions(+), 189 deletions(-) delete mode 100644 app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala delete mode 100644 scalastyle-config.xml diff --git a/.scalafix.conf b/.scalafix.conf index 179b678..a7db620 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -22,6 +22,6 @@ OrganizeImports { importSelectorsOrder = Ascii importsOrder = Ascii preset = DEFAULT - removeUnused = true + removeUnused = false targetDialect = Scala3 } \ No newline at end of file diff --git a/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala b/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala index 36871ce..c719203 100644 --- a/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala +++ b/app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala @@ -25,13 +25,11 @@ import play.api.libs.json.* import play.api.mvc.* import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController -import uk.gov.hmrc.apiscope.models.* -import uk.gov.hmrc.apiscope.models.ErrorCode.* -import uk.gov.hmrc.apiscope.models.ResponseFormatters.given +import uk.gov.hmrc.apiscope.models.{ErrorCode, *} import uk.gov.hmrc.apiscope.services.ScopeService @Singleton -class ScopeController @Inject() (scopeService: ScopeService, cc: ControllerComponents, playBodyParsers: PlayBodyParsers)(implicit val ec: ExecutionContext) +class ScopeController @Inject() (scopeService: ScopeService, cc: ControllerComponents, playBodyParsers: PlayBodyParsers)(using ExecutionContext) extends BackendController(cc) with ApplicationLogger { def createOrUpdateScope(): Action[JsValue] = Action.async(playBodyParsers.json) { implicit request => @@ -54,7 +52,7 @@ class ScopeController @Inject() (scopeService: ScopeService, cc: ControllerCompo def fetchScope(key: String): Action[AnyContent] = Action.async { scopeService.fetchScope(key).map { case Some(scope) => Ok(Json.toJson(scope)) - case None => NotFound(error(SCOPE_NOT_FOUND, s"Scope not found with key: $key")) + case None => NotFound(error(ErrorCode.ScopeNotFound, s"Scope not found with key: $key")) } recover recovery } @@ -76,7 +74,7 @@ class ScopeController @Inject() (scopeService: ScopeService, cc: ControllerCompo private def recovery: PartialFunction[Throwable, Result] = { case e => logger.error(s"An unexpected error occurred: ${e.getMessage}", e) - InternalServerError(error(UNKNOWN_ERROR, "An unexpected error occurred")) + InternalServerError(error(ErrorCode.UnknownError, "An unexpected error occurred")) } } diff --git a/app/uk/gov/hmrc/apiscope/controllers/package.scala b/app/uk/gov/hmrc/apiscope/controllers/package.scala index 2090636..cfb0a60 100644 --- a/app/uk/gov/hmrc/apiscope/controllers/package.scala +++ b/app/uk/gov/hmrc/apiscope/controllers/package.scala @@ -26,20 +26,19 @@ import play.api.mvc.Results.* import play.api.mvc.{Request, Result} import uk.gov.hmrc.http.NotFoundException -import uk.gov.hmrc.apiscope.models.ErrorCode.{API_INVALID_JSON, SCOPE_NOT_FOUND} import uk.gov.hmrc.apiscope.models.{ErrorCode, ErrorDescription, ErrorResponse} package object controllers extends ApplicationLogger { - private def validate[T](request: Request[JsValue])(implicit tjs: Reads[T]): Either[Result, JsResult[T]] = { + private def validate[T](request: Request[JsValue])(using Reads[T]): Either[Result, JsResult[T]] = { try { Right(request.body.validate[T]) } catch { - case e: Throwable => Left(UnprocessableEntity(error(ErrorCode.INVALID_REQUEST_PAYLOAD, e.getMessage))) + case e: Throwable => Left(UnprocessableEntity(error(ErrorCode.InvalidRequestPayload, e.getMessage))) } } - def handleRequest[T](request: Request[JsValue])(f: T => Future[Result])(implicit tjs: Reads[T]): Future[Result] = { + def handleRequest[T](request: Request[JsValue])(f: T => Future[Result])(using Reads[T]): Future[Result] = { val either: Either[Result, JsResult[T]] = validate(request) @@ -66,14 +65,14 @@ package object controllers extends ApplicationLogger { } }).toSeq - toJson(ErrorResponse(API_INVALID_JSON, "Json cannot be converted to API Scope", Some(errs))) + toJson(ErrorResponse(ErrorCode.ApiInvalidJson, "Json cannot be converted to API Scope", Some(errs))) } def recovery: PartialFunction[Throwable, Result] = { - case nfe: NotFoundException => NotFound(error(SCOPE_NOT_FOUND, nfe.getMessage)) + case nfe: NotFoundException => NotFound(error(ErrorCode.ScopeNotFound, nfe.getMessage)) case e => logger.error(s"An unexpected error occurred: ${e.getMessage}", e) - InternalServerError(error(ErrorCode.UNKNOWN_ERROR, "An unexpected error occurred")) + InternalServerError(error(ErrorCode.UnknownError, "An unexpected error occurred")) } def error(code: ErrorCode, message: String): JsValue = { diff --git a/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala b/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala index 0ac8ec5..b16b826 100644 --- a/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala +++ b/app/uk/gov/hmrc/apiscope/models/ErrorCode.scala @@ -19,31 +19,28 @@ package uk.gov.hmrc.apiscope.models import play.api.libs.json.* import uk.gov.hmrc.apiplatform.modules.common.domain.services.SimpleEnumJsonFormatting -sealed trait ErrorCode +enum ErrorCode: + case ScopeNotFound, InvalidRequestPayload, UnknownError, ApiInvalidJson, ApiScopeAlreadyInUse object ErrorCode { - case object SCOPE_NOT_FOUND extends ErrorCode - case object INVALID_REQUEST_PAYLOAD extends ErrorCode - case object UNKNOWN_ERROR extends ErrorCode - case object API_INVALID_JSON extends ErrorCode - case object API_SCOPE_ALREADY_IN_USE extends ErrorCode - val values: Set[ErrorCode] = Set(SCOPE_NOT_FOUND, INVALID_REQUEST_PAYLOAD, UNKNOWN_ERROR, API_INVALID_JSON, API_SCOPE_ALREADY_IN_USE) - def apply(text: String): Option[ErrorCode] = ErrorCode.values.find(_.toString() == text.toUpperCase) + def apply(text: String): Option[ErrorCode] = ErrorCode.values.find(_.toString.equalsIgnoreCase(text)) - def unsafeApply(text: String): ErrorCode = apply(text).getOrElse(throw new RuntimeException(s"$text is not a valid Error Code")) + def unsafeApply(text: String): ErrorCode = + apply(text).getOrElse(throw new RuntimeException(s"$text is not a valid Error Code")) - implicit val format: Format[ErrorCode] = SimpleEnumJsonFormatting.createFormatFor[ErrorCode]("Error Code", apply) -} - -case class ErrorResponse(code: ErrorCode, message: String, details: Option[Seq[ErrorDescription]] = None) + import play.api.libs.json.Format -object ErrorResponse { - implicit val format1: OFormat[ErrorDescription] = Json.format[ErrorDescription] - implicit val format3: OFormat[ErrorResponse] = Json.format[ErrorResponse] + given Format[ErrorCode] = SimpleEnumJsonFormatting.createEnumFormatFor[ErrorCode]("Error Code", apply) } case class ErrorDescription(field: String, message: String) +case class ErrorResponse(code: ErrorCode, message: String, details: Option[Seq[ErrorDescription]] = None) + object ErrorDescription { - implicit val format: OFormat[ErrorDescription] = Json.format[ErrorDescription] + given OFormat[ErrorDescription] = Json.format[ErrorDescription] +} + +object ErrorResponse { + given OFormat[ErrorResponse] = Json.format[ErrorResponse] } diff --git a/app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala b/app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala deleted file mode 100644 index 294e4e8..0000000 --- a/app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 HM Revenue & Customs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package uk.gov.hmrc.apiscope.models - -import play.api.libs.json.{Json, OFormat} - -object ResponseFormatters { - given OFormat[Scope] = Json.format[Scope] -} diff --git a/app/uk/gov/hmrc/apiscope/models/Scope.scala b/app/uk/gov/hmrc/apiscope/models/Scope.scala index e639151..cfc4489 100644 --- a/app/uk/gov/hmrc/apiscope/models/Scope.scala +++ b/app/uk/gov/hmrc/apiscope/models/Scope.scala @@ -19,3 +19,8 @@ package uk.gov.hmrc.apiscope.models import uk.gov.hmrc.auth.core.ConfidenceLevel case class Scope(key: String, name: String, description: String, confidenceLevel: Option[ConfidenceLevel] = None) + +object Scope { + import play.api.libs.json.{Json, OFormat} + given OFormat[Scope] = Json.format[Scope] +} diff --git a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala index 854aa49..6411571 100644 --- a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala +++ b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala @@ -23,7 +23,7 @@ import scala.concurrent.{ExecutionContext, Future} import org.bson.conversions.Bson import org.mongodb.scala.model.Filters.equal import org.mongodb.scala.model.Indexes.ascending -import org.mongodb.scala.model.Updates.{combine, set} +import org.mongodb.scala.model.Updates.{combine, set, unset} import org.mongodb.scala.model.{FindOneAndUpdateOptions, IndexModel, IndexOptions, ReturnDocument} import play.api.Logger @@ -37,7 +37,7 @@ import uk.gov.hmrc.apiscope.models.Scope private object ScopeFormats { - implicit val scopeRead: Reads[Scope] = ( + private val rds: Reads[Scope] = ( (JsPath \ "key").read[String] and (JsPath \ "name").read[String] and (JsPath \ "description").read[String] and @@ -55,16 +55,18 @@ private object ScopeFormats { }) )(Scope.apply) - implicit val scopeWrites: OWrites[Scope] = Json.writes[Scope] - implicit val scopeFormat: OFormat[Scope] = OFormat(scopeRead, scopeWrites) + import ConfidenceLevel.jsonFormat + + private val wrts: OWrites[Scope] = Json.writes[Scope] + given OFormat[Scope] = OFormat(rds, wrts) } @Singleton -class ScopeRepository @Inject() (mongoComponent: MongoComponent)(implicit val ec: ExecutionContext) +class ScopeRepository @Inject() (mongoComponent: MongoComponent)(using ExecutionContext) extends PlayMongoRepository[Scope]( mongoComponent = mongoComponent, collectionName = "scope", - domainFormat = ScopeFormats.scopeFormat, + domainFormat = ScopeFormats.given_OFormat_Scope, indexes = Seq(IndexModel( ascending("key"), IndexOptions() @@ -72,8 +74,7 @@ class ScopeRepository @Inject() (mongoComponent: MongoComponent)(implicit val ec .background(true) .unique(true) )), - replaceIndexes = true, - extraCodecs = Seq(Codecs.playFormatCodec(ScopeFormats.scopeFormat)) + replaceIndexes = true ) { private val logger = Logger(this.getClass) override lazy val requiresTtlIndex = false @@ -84,7 +85,9 @@ class ScopeRepository @Inject() (mongoComponent: MongoComponent)(implicit val ec set("name", Codecs.toBson(scope.name)), set("description", Codecs.toBson(scope.description)) ) ++ - (scope.confidenceLevel.fold[Seq[Bson]](Seq.empty)(value => { + (scope.confidenceLevel.fold[Seq[Bson]]( + Seq.empty // or Seq(unset("confidenceLevel")) to set this too + )(value => { logger.info(s"confidenceLevel value id ${value} and value enumeration ${value.level}") Seq( set("confidenceLevel", Codecs.toBson(value)) diff --git a/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala b/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala index 48a10bb..e8e97a0 100644 --- a/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala +++ b/app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala @@ -24,12 +24,11 @@ import util.ApplicationLogger import play.api.libs.json.{JsError, JsSuccess, Json} -import uk.gov.hmrc.apiscope.models.ResponseFormatters.given import uk.gov.hmrc.apiscope.models.Scope import uk.gov.hmrc.apiscope.repository.ScopeRepository @Singleton -class ScopeJsonFileService @Inject() (scopeRepository: ScopeRepository, fileReader: ScopeJsonFileReader)(implicit val ec: ExecutionContext) extends ApplicationLogger { +class ScopeJsonFileService @Inject() (scopeRepository: ScopeRepository, fileReader: ScopeJsonFileReader)(using ExecutionContext) extends ApplicationLogger { private def saveScopes(scopes: List[Scope]): Future[List[Scope]] = Future.sequence(scopes.map(scopeRepository.save)) diff --git a/app/uk/gov/hmrc/apiscope/services/ScopeService.scala b/app/uk/gov/hmrc/apiscope/services/ScopeService.scala index 1cb9008..99d24bb 100644 --- a/app/uk/gov/hmrc/apiscope/services/ScopeService.scala +++ b/app/uk/gov/hmrc/apiscope/services/ScopeService.scala @@ -23,7 +23,7 @@ import uk.gov.hmrc.apiscope.models.Scope import uk.gov.hmrc.apiscope.repository.ScopeRepository @Singleton -class ScopeService @Inject() (scopeRepository: ScopeRepository)(implicit val ec: ExecutionContext) { +class ScopeService @Inject() (scopeRepository: ScopeRepository)(using ExecutionContext) { def saveScopes(scopes: Seq[Scope]): Future[Seq[Scope]] = Future.sequence(scopes.map(scopeRepository.save)) diff --git a/build.sbt b/build.sbt index 47e9776..26050cb 100644 --- a/build.sbt +++ b/build.sbt @@ -26,14 +26,6 @@ lazy val microservice = Project(appName, file(".")) Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-eT"), Test / unmanagedSourceDirectories += baseDirectory.value / "test-common" ) - // .settings( - // scalacOptions ++= Seq( - // "-Wconf:cat=unused&src=views/.*\\.scala:s", - // "-Wconf:cat=unused&src=.*RoutesPrefix\\.scala:s", - // "-Wconf:cat=unused&src=.*Routes\\.scala:s", - // "-Wconf:cat=unused&src=.*ReverseRoutes\\.scala:s" - // ) - // ) lazy val it = (project in file("it")) .enablePlugins(PlayScala) diff --git a/project/plugins.sbt b/project/plugins.sbt index 617a091..173fb41 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,6 @@ addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "3.24.0") addSbtPlugin("uk.gov.hmrc" % "sbt-distributables" % "2.6.0") addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.9") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") -addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "1.0.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.6") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.2") diff --git a/scalastyle-config.xml b/scalastyle-config.xml deleted file mode 100644 index e48b66a..0000000 --- a/scalastyle-config.xml +++ /dev/null @@ -1,99 +0,0 @@ - - Scalastyle standard configuration - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala b/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala index 6287ff5..a15cbed 100644 --- a/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala +++ b/test/uk/gov/hmrc/apiscope/controllers/ScopeControllerSpec.scala @@ -35,10 +35,9 @@ import play.api.test.{FakeRequest, StubControllerComponentsFactory, StubPlayBody import play.mvc.Http.Status.{INTERNAL_SERVER_ERROR, NOT_FOUND, NO_CONTENT, OK} import uk.gov.hmrc.auth.core.ConfidenceLevel -import uk.gov.hmrc.apiscope.models.ResponseFormatters.given import uk.gov.hmrc.apiscope.models.{ErrorCode, ErrorDescription, ErrorResponse, Scope} import uk.gov.hmrc.apiscope.services.ScopeService -import uk.gov.hmrc.util.AsyncHmrcSpec; +import uk.gov.hmrc.util.AsyncHmrcSpec class ScopeControllerSpec extends AsyncHmrcSpec with GuiceOneAppPerSuite @@ -54,15 +53,15 @@ class ScopeControllerSpec extends AsyncHmrcSpec val scopeBodyMissingKeyAndDesc: String = """[{"name":"name1"},{"key":"key2","name":"name2"}]""" val scopeBodyWithInvalidConfidenceLevel: String = """[{"key":"key1", "name":"name1", "description":"desc1", "confidenceLevel":1001}]""" - implicit lazy val materializer: Materializer = mock[Materializer] + val mat: Materializer = mock[Materializer] trait Setup { val mockScopeService: ScopeService = mock[ScopeService] val controllerComponents: ControllerComponents = stubControllerComponents() - val underTest = new ScopeController(mockScopeService, controllerComponents, stubPlayBodyParsers(using materializer)) + val underTest = new ScopeController(mockScopeService, controllerComponents, stubPlayBodyParsers(using mat)) - implicit lazy val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() when(mockScopeService.saveScopes(any[Seq[Scope]]())).thenReturn(successful(Seq())) when(mockScopeService.fetchScopes(Set(scope.key))).thenReturn(successful(Seq(scope))) @@ -132,7 +131,7 @@ class ScopeControllerSpec extends AsyncHmrcSpec val result = underTest.fetchScope("key1")(request) status(result) shouldBe NOT_FOUND - contentAsJson(result) \ "code" shouldEqual JsDefined(JsString(ErrorCode.SCOPE_NOT_FOUND.toString)) + contentAsJson(result) \ "code" shouldEqual JsDefined(JsString("SCOPE_NOT_FOUND")) } "return 500 (internal service error) when the service throws an exception" in new Setup { @@ -211,7 +210,7 @@ class ScopeControllerSpec extends AsyncHmrcSpec contentAsJson(result) shouldBe Json.toJson( ErrorResponse( - ErrorCode.API_INVALID_JSON, + ErrorCode.ApiInvalidJson, "Json cannot be converted to API Scope", Some(Seq( ErrorDescription("(0)/description", "element is missing"), @@ -227,7 +226,7 @@ class ScopeControllerSpec extends AsyncHmrcSpec contentAsJson(result) shouldEqual Json.toJson( ErrorResponse( - ErrorCode.API_INVALID_JSON, + ErrorCode.ApiInvalidJson, "Json cannot be converted to API Scope", Some(Seq( ErrorDescription("(0)/name", "element is missing") diff --git a/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala b/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala index 44eb90e..f3f0531 100644 --- a/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala +++ b/test/uk/gov/hmrc/apiscope/models/ErrorCodeSpec.scala @@ -23,15 +23,14 @@ class ErrorCodeSpec extends HmrcSpec { "read" should { "read valid error code" in { - Json.fromJson[ErrorCode](JsString("SCOPE_NOT_FOUND")) shouldBe JsSuccess(ErrorCode.SCOPE_NOT_FOUND) + Json.fromJson[ErrorCode](JsString("SCOPE_NOT_FOUND")) shouldBe JsSuccess(ErrorCode.ScopeNotFound) } - "report invalid error code" in { Json.fromJson[ErrorCode](JsNumber(0)) should matchPattern { - case e: JsError => + case _: JsError => } Json.fromJson[ErrorCode](JsString("NOT VALID")) should matchPattern { - case e: JsError => + case _: JsError => } } } diff --git a/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala b/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala index 02c787c..ab3b41e 100644 --- a/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala +++ b/test/uk/gov/hmrc/apiscope/repository/ScopeRepositorySpec.scala @@ -47,8 +47,9 @@ class ScopeRepositorySpec extends AsyncHmrcSpec val scopeConfidence200: Scope = Scope("key2", "name2", "description2", confidenceLevel = Some(ConfidenceLevel.L200)) val scopeConfidence500: Scope = Scope("key3", "name3", "description3", confidenceLevel = Some(ConfidenceLevel.L500)) - override val repository: ScopeRepository = app.injector.instanceOf[ScopeRepository] - override implicit lazy val app: Application = appBuilder.build() + override val repository: ScopeRepository = app.injector.instanceOf[ScopeRepository] + + override lazy val app: Application = appBuilder.build() private def getIndexes(): List[BsonDocument] = { await(repository.collection.listIndexes().map(toBsonDocument).toFuture().map(_.toList)) @@ -116,10 +117,11 @@ class ScopeRepositorySpec extends AsyncHmrcSpec await(repository.save(updatedScope1)) await(repository.save(updatedScope2)) - await(repository.fetch(basicScope.key)).get shouldEqual updatedScope1 await(repository.fetch(scopeConfidence200.key)).get shouldEqual updatedScope2 + await(repository.fetch(basicScope.key)).get shouldEqual updatedScope1 } } + "read a scope" should { val scopeName = "some scope name" val scopeKey = "read:some-scope-key" From 5b229580131edadff997c464fb89128de8802fbb Mon Sep 17 00:00:00 2001 From: Andy Spaven <16537059+AndySpaven@users.noreply.github.com> Date: Thu, 12 Feb 2026 08:01:33 +0000 Subject: [PATCH 4/6] Scala-3-POC - Tidy up score format --- .../apiscope/repository/ScopeRepository.scala | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala index 6411571..3147e27 100644 --- a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala +++ b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala @@ -19,11 +19,12 @@ package uk.gov.hmrc.apiscope.repository import javax.inject.{Inject, Singleton} import scala.collection.immutable.Seq import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} import org.bson.conversions.Bson import org.mongodb.scala.model.Filters.equal import org.mongodb.scala.model.Indexes.ascending -import org.mongodb.scala.model.Updates.{combine, set, unset} +import org.mongodb.scala.model.Updates.{combine, set} import org.mongodb.scala.model.{FindOneAndUpdateOptions, IndexModel, IndexOptions, ReturnDocument} import play.api.Logger @@ -32,33 +33,28 @@ import play.api.libs.json.{Reads, *} import uk.gov.hmrc.auth.core.ConfidenceLevel import uk.gov.hmrc.mongo.MongoComponent import uk.gov.hmrc.mongo.play.json.{Codecs, PlayMongoRepository} +import uk.gov.hmrc.play.json.Mappings import uk.gov.hmrc.apiscope.models.Scope private object ScopeFormats { - private val rds: Reads[Scope] = ( - (JsPath \ "key").read[String] and - (JsPath \ "name").read[String] and - (JsPath \ "description").read[String] and - (JsPath \ "confidenceLevel").readNullable[Int] - .map[Option[ConfidenceLevel]](_ match { - case None => None - case Some(50) => Some(ConfidenceLevel.L50) - case Some(100) => Some(ConfidenceLevel.L200) - case Some(200) => Some(ConfidenceLevel.L200) - case Some(250) => Some(ConfidenceLevel.L250) - case Some(300) => Some(ConfidenceLevel.L200) - case Some(500) => Some(ConfidenceLevel.L500) - case Some(600) => Some(ConfidenceLevel.L600) - case Some(i) => throw new RuntimeException(s"Bad data in confidence level of $i") - }) - )(Scope.apply) - - import ConfidenceLevel.jsonFormat - - private val wrts: OWrites[Scope] = Json.writes[Scope] - given OFormat[Scope] = OFormat(rds, wrts) + private def fromIntIncludingOldValues(level: Int): Try[ConfidenceLevel] = level match { + case 600 => Success(ConfidenceLevel.L600) + case 500 => Success(ConfidenceLevel.L500) + case 300 => Success(ConfidenceLevel.L200) + case 250 => Success(ConfidenceLevel.L250) + case 200 => Success(ConfidenceLevel.L200) + case 100 => Success(ConfidenceLevel.L200) + case 50 => Success(ConfidenceLevel.L50) + case _ => Failure(throw new NoSuchElementException(s"Bad data in confidence level of $level")) + } + + private val mapping = Mappings.mapTry[Int, ConfidenceLevel](fromIntIncludingOldValues, _.level) + + given Format[ConfidenceLevel] = mapping.jsonFormat + + given mongoScopeFmt: OFormat[Scope] = Json.format[Scope] } @Singleton @@ -66,7 +62,7 @@ class ScopeRepository @Inject() (mongoComponent: MongoComponent)(using Execution extends PlayMongoRepository[Scope]( mongoComponent = mongoComponent, collectionName = "scope", - domainFormat = ScopeFormats.given_OFormat_Scope, + domainFormat = ScopeFormats.mongoScopeFmt, indexes = Seq(IndexModel( ascending("key"), IndexOptions() From 4d837aadb10203aee51a2795088987a11124643f Mon Sep 17 00:00:00 2001 From: Andy Spaven <16537059+AndySpaven@users.noreply.github.com> Date: Thu, 12 Feb 2026 08:38:35 +0000 Subject: [PATCH 5/6] Scala-3-POC - Minor tweak --- app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala | 1 - build.sbt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala index 3147e27..48a581e 100644 --- a/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala +++ b/app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala @@ -28,7 +28,6 @@ import org.mongodb.scala.model.Updates.{combine, set} import org.mongodb.scala.model.{FindOneAndUpdateOptions, IndexModel, IndexOptions, ReturnDocument} import play.api.Logger -import play.api.libs.functional.syntax.* import play.api.libs.json.{Reads, *} import uk.gov.hmrc.auth.core.ConfidenceLevel import uk.gov.hmrc.mongo.MongoComponent diff --git a/build.sbt b/build.sbt index 26050cb..20a5a98 100644 --- a/build.sbt +++ b/build.sbt @@ -7,12 +7,12 @@ Global / bloopExportJarClassifiers := Some(Set("sources")) inThisBuild( List( + majorVersion := 0, scalaVersion := "3.7.4", semanticdbEnabled := true, semanticdbVersion := scalafixSemanticdb.revision ) ) -ThisBuild / majorVersion := 0 lazy val microservice = Project(appName, file(".")) .enablePlugins(PlayScala, SbtDistributablesPlugin) From 236661cce4d191cace773b18a776e34752eb0099 Mon Sep 17 00:00:00 2001 From: Andy Spaven <16537059+AndySpaven@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:40:08 +0000 Subject: [PATCH 6/6] Scala-3-POC - Bump to non-snapshot --- project/AppDependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/AppDependencies.scala b/project/AppDependencies.scala index 183d012..992e30a 100644 --- a/project/AppDependencies.scala +++ b/project/AppDependencies.scala @@ -6,7 +6,7 @@ object AppDependencies { private lazy val bootstrapVersion = "10.5.0" private lazy val hmrcMongoVersion = "2.11.0" - val commonDomainVersion = "1.0.0-SNAPSHOT" + val commonDomainVersion = "1.0.0" private lazy val compile = Seq( "uk.gov.hmrc" %% "bootstrap-backend-play-30" % bootstrapVersion,