Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .scalafix.conf
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ OrganizeImports {
importSelectorsOrder = Ascii
importsOrder = Ascii
preset = DEFAULT
removeUnused = true
removeUnused = false
targetDialect = Scala3
}
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version="3.7.17"
runner.dialect = "scala213"
runner.dialect = scala3

maxColumn = 180

Expand Down
14 changes: 6 additions & 8 deletions app/uk/gov/hmrc/apiscope/controllers/ScopeController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@ 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.ResponseFormatters._
import uk.gov.hmrc.apiscope.models.{Scope, ScopeData}
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 =>
Expand All @@ -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
}

Expand All @@ -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"))
}

}
17 changes: 8 additions & 9 deletions app/uk/gov/hmrc/apiscope/controllers/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,25 @@ 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

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)

Expand All @@ -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 = {
Expand Down
35 changes: 16 additions & 19 deletions app/uk/gov/hmrc/apiscope/models/ErrorCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,31 @@

package uk.gov.hmrc.apiscope.models

import play.api.libs.json._
import uk.gov.hmrc.apiplatform.modules.common.domain.services.SealedTraitJsonFormatting
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] = SealedTraitJsonFormatting.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]
}
23 changes: 0 additions & 23 deletions app/uk/gov/hmrc/apiscope/models/ResponseFormatters.scala

This file was deleted.

5 changes: 5 additions & 0 deletions app/uk/gov/hmrc/apiscope/models/Scope.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
2 changes: 1 addition & 1 deletion app/uk/gov/hmrc/apiscope/models/ScopeRequest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
54 changes: 26 additions & 28 deletions app/uk/gov/hmrc/apiscope/repository/ScopeRepository.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ 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
Expand All @@ -27,53 +28,48 @@ 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.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 {

implicit val scopeRead: 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 _)

implicit val scopeWrites: OWrites[Scope] = Json.writes[Scope]
implicit val scopeFormat: OFormat[Scope] = OFormat(scopeRead, scopeWrites)
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
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.mongoScopeFmt,
indexes = Seq(IndexModel(
ascending("key"),
IndexOptions()
.name("keyIndex")
.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
Expand All @@ -84,7 +80,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))
Expand All @@ -95,7 +93,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()
}
Expand Down
11 changes: 5 additions & 6 deletions app/uk/gov/hmrc/apiscope/services/ScopeJsonFileService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,22 @@ import util.ApplicationLogger

import play.api.libs.json.{JsError, JsSuccess, Json}

import uk.gov.hmrc.apiscope.models.ResponseFormatters._
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: 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("; ")}")
case JsError(errors) => logger.error(s"Unable to parse JSON into Scopes ${errors.mkString("; ")}")
}
)
} catch {
Expand Down
2 changes: 1 addition & 1 deletion app/uk/gov/hmrc/apiscope/services/ScopeService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
21 changes: 8 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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 := "2.13.16"
ThisBuild / majorVersion := 0
ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always
inThisBuild(
List(
majorVersion := 0,
scalaVersion := "3.7.4",
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision
)
)

lazy val microservice = Project(appName, file("."))
.enablePlugins(PlayScala, SbtDistributablesPlugin)
Expand All @@ -23,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)
Expand Down
2 changes: 1 addition & 1 deletion it/test/uk/gov/hmrc/apiscope/BaseFeatureSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading