Skip to content

Commit

Permalink
Merge pull request #67 from hmrc/BDOG-175
Browse files Browse the repository at this point in the history
Bdog 175
  • Loading branch information
colin-lamed authored May 13, 2019
2 parents 6254d30 + 2c076a8 commit 7a21d16
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 263 deletions.
183 changes: 123 additions & 60 deletions app/uk/gov/hmrc/cataloguefrontend/DependencyExplorerController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import play.api.http.HttpEntity
import play.api.i18n.MessagesProvider
import play.api.mvc.{Action, AnyContent, MessagesControllerComponents, ResponseHeader, Result}
import uk.gov.hmrc.cataloguefrontend.connector.{SlugInfoFlag, TeamsAndRepositoriesConnector}
import uk.gov.hmrc.cataloguefrontend.connector.model.{ServiceWithDependency, Version, VersionOp}
import uk.gov.hmrc.cataloguefrontend.connector.model.{BobbyVersionRange, ServiceWithDependency, Version}
import uk.gov.hmrc.cataloguefrontend.{ routes => appRoutes }
import uk.gov.hmrc.cataloguefrontend.service.DependenciesService
import uk.gov.hmrc.cataloguefrontend.util.CsvUtils
import uk.gov.hmrc.play.bootstrap.controller.FrontendController
Expand All @@ -36,10 +37,10 @@ import scala.concurrent.{ExecutionContext, Future}

@Singleton
class DependencyExplorerController @Inject()(
mcc : MessagesControllerComponents,
trConnector: TeamsAndRepositoriesConnector,
service : DependenciesService,
page : DependencyExplorerPage
mcc : MessagesControllerComponents
, trConnector: TeamsAndRepositoriesConnector
, service : DependenciesService
, page : DependencyExplorerPage
)(implicit val ec: ExecutionContext
) extends FrontendController(mcc) {

Expand All @@ -51,61 +52,118 @@ class DependencyExplorerController @Inject()(
teams <- trConnector.allTeams.map(_.map(_.name).sorted)
flags = SlugInfoFlag.values
groupArtefacts <- service.getGroupArtefacts
} yield Ok(page(form.fill(SearchForm("", SlugInfoFlag.Latest.s, "", "", "", "0.0.0")), teams, flags, groupArtefacts, searchResults = None, pieData = None))
} yield Ok(page(
form.fill(SearchForm("", SlugInfoFlag.Latest.s, "", "", ""))
, teams
, flags
, groupArtefacts
, versionRange = BobbyVersionRange(None, None, None, "")
, searchResults = None
, pieData = None
))
}


def search =
Action.async { implicit request =>
// first preserve old API
if (request.queryString.contains("versionOp")) {
(for {
version <- EitherT.fromOption[Future](
request.queryString.get("version").flatMap(_.headOption).flatMap(Version.parse)
, Redirect(appRoutes.DependencyExplorerController.landing)
)
versionRange <- EitherT.fromOption[Future](
request.queryString.get("versionOp").flatMap(_.headOption).flatMap { versionOp =>
PartialFunction.condOpt(versionOp) {
case ">=" => s"[$version,)"
case "<=" => s"(,$version]"
case "==" => s"[$version]"
}
}
, Redirect(appRoutes.DependencyExplorerController.landing)
)
queryString = request.queryString - "version" - "versionOp" + ("versionRange" -> Seq(versionRange))

// updating request with new querystring does not update uri!? - build uri manually...
queryStr = queryString.flatMap { case (k, vs) =>
vs.map(v => java.net.URLEncoder.encode(k, "UTF-8") + "=" + java.net.URLEncoder.encode(v, "UTF-8"))
}.mkString("?", "&", "")
} yield Redirect(request.path + queryStr)
).merge
// else continue to new API
} else search2(request)
}

def search2 =
Action.async { implicit request =>
for {
teams <- trConnector.allTeams.map(_.map(_.name).sorted)
flags = SlugInfoFlag.values
groupArtefacts <- service.getGroupArtefacts
res <- {
def pageWithError(msg: String) = page(form.bindFromRequest().withGlobalError(msg), teams, flags, groupArtefacts, searchResults = None, pieData = None)
def pageWithError(msg: String) = page(
form.bindFromRequest().withGlobalError(msg)
, teams
, flags
, groupArtefacts
, versionRange = BobbyVersionRange(None, None, None, "")
, searchResults = None
, pieData = None
)
form
.bindFromRequest()
.fold(
hasErrors = formWithErrors => Future.successful(BadRequest(page(formWithErrors, teams, flags, groupArtefacts, searchResults = None, pieData = None))),
success = query =>
(for {
versionOp <- EitherT.fromOption[Future](VersionOp.parse(query.versionOp), BadRequest(pageWithError("Invalid version op")))
version <- EitherT.fromOption[Future](Version.parse(query.version), BadRequest(pageWithError("Invalid version")))
team = if (query.team.isEmpty) None else Some(query.team)
flag <- EitherT.fromOption[Future](SlugInfoFlag.parse(query.flag), BadRequest(pageWithError("Invalid flag")))
results <- EitherT.right[Result] {
service
.getServicesWithDependency(team, flag, query.group, query.artefact, versionOp, version)
}
pieData = PieData(
"Version spread",
results
.groupBy(r => s"${r.depGroup}:${r.depArtefact}:${r.depVersion}")
.map(r => r._1 -> r._2.size))
} yield
if (query.asCsv) {
val csv = CsvUtils.toCsv(toRows(results))
val source = Source.single(ByteString(csv, "UTF-8"))
Result(
header = ResponseHeader(200, Map("Content-Disposition" -> "inline; filename=\"depex.csv\"")),
body = HttpEntity.Streamed(source, None, Some("text/csv"))
)
}
else Ok(page(form.bindFromRequest(), teams, flags, groupArtefacts, Some(results), Some(pieData)))
).merge
)
hasErrors = formWithErrors => Future.successful(BadRequest(page(formWithErrors, teams, flags, groupArtefacts, versionRange = BobbyVersionRange(None, None, None, ""), searchResults = None, pieData = None)))
, success = query =>
(for {
versionRange <- EitherT.fromOption[Future](BobbyVersionRange.parse(query.versionRange), BadRequest(pageWithError(s"Invalid version range")))
team = if (query.team.isEmpty) None else Some(query.team)
flag <- EitherT.fromOption[Future](SlugInfoFlag.parse(query.flag), BadRequest(pageWithError("Invalid flag")))
results <- EitherT.right[Result] {
service
.getServicesWithDependency(team, flag, query.group, query.artefact, versionRange)
}
pieData = PieData(
"Version spread"
, results
.groupBy(r => s"${r.depGroup}:${r.depArtefact}:${r.depVersion}")
.map(r => r._1 -> r._2.size)
)
} yield
if (query.asCsv) {
val csv = CsvUtils.toCsv(toRows(results))
val source = Source.single(ByteString(csv, "UTF-8"))
Result(
header = ResponseHeader(200, Map("Content-Disposition" -> "inline; filename=\"depex.csv\"")),
body = HttpEntity.Streamed(source, None, Some("text/csv"))
)
}
else Ok(page(
form.bindFromRequest()
, teams
, flags
, groupArtefacts
, versionRange
, Some(results)
, Some(pieData)
))
).merge
)
}
} yield res
}


/** @param versionRange replaces versionOp and version, supporting Maven version range */
case class SearchForm(
team : String,
flag : String,
group : String,
artefact : String,
versionOp: String,
version : String,
asCsv : Boolean = false)
team : String
, flag : String
, group : String
, artefact : String
, versionRange: String
, asCsv : Boolean = false
)

// Forms.nonEmptyText, but has no constraint info label
def notEmpty = {
Expand All @@ -118,34 +176,39 @@ class DependencyExplorerController @Inject()(
def form(implicit messagesProvider: MessagesProvider) =
Form(
Forms.mapping(
"team" -> Forms.text,
"flag" -> Forms.text.verifying(notEmpty),
"group" -> Forms.text.verifying(notEmpty),
"artefact" -> Forms.text.verifying(notEmpty),
"versionOp" -> Forms.text,
"version" -> Forms.text,
"asCsv" -> Forms.boolean
)(SearchForm.apply)(SearchForm.unapply)
"team" -> Forms.text
, "flag" -> Forms.text.verifying(notEmpty)
, "group" -> Forms.text.verifying(notEmpty)
, "artefact" -> Forms.text.verifying(notEmpty)
, "versionRange" -> Forms.default(Forms.text, "")
, "asCsv" -> Forms.boolean
)(SearchForm.apply)(SearchForm.unapply)
)
}

object DependencyExplorerController {
case class PieData(
title : String,
results: Map[String, Int])
title : String
, results: Map[String, Int]
)


def toRows(seq: Seq[ServiceWithDependency]): Seq[Map[String, String]] =
seq.flatMap { serviceWithDependency =>
val m = Map(
"slugName" -> serviceWithDependency.slugName,
"slugVersion" -> serviceWithDependency.slugVersion,
"team" -> "",
"depGroup" -> serviceWithDependency.depGroup,
"depArtefact" -> serviceWithDependency.depArtefact,
"depVersion" -> serviceWithDependency.depVersion,
"depSemanticVersion" -> serviceWithDependency.depSemanticVersion.map(_.toString).getOrElse(""))
"slugName" -> serviceWithDependency.slugName
, "slugVersion" -> serviceWithDependency.slugVersion
, "team" -> ""
, "depGroup" -> serviceWithDependency.depGroup
, "depArtefact" -> serviceWithDependency.depArtefact
, "depVersion" -> serviceWithDependency.depVersion
, "depSemanticVersion" -> serviceWithDependency.depSemanticVersion.map(_.toString).getOrElse("")
)
if (serviceWithDependency.teams.isEmpty) Seq(m)
else serviceWithDependency.teams.map { team => m + ("team" -> team) }
else serviceWithDependency.teams.map(team => m + ("team" -> team))
}

def search(team: String = "", flag: SlugInfoFlag, group: String, artefact: String, versionRange: BobbyVersionRange): String =
uk.gov.hmrc.cataloguefrontend.routes.DependencyExplorerController.search() +
s"?team=$team&flag=${flag.s}&group=$group&artefact=$artefact&versionRange=${versionRange.range}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,19 @@ class ServiceDependenciesConnector @Inject()(
}

def getServicesWithDependency(
flag : SlugInfoFlag,
group : String,
artefact: String)(implicit hc: HeaderCarrier): Future[Seq[ServiceWithDependency]] = {
flag : SlugInfoFlag,
group : String,
artefact : String,
versionRange: BobbyVersionRange)(implicit hc: HeaderCarrier): Future[Seq[ServiceWithDependency]] = {
implicit val r = ServiceWithDependency.reads
http
.GET[Seq[ServiceWithDependency]](
s"$servicesDependenciesBaseUrl/serviceDeps",
queryParams = Seq(
"flag" -> flag.s,
"group" -> group,
"artefact" -> artefact))
"flag" -> flag.s,
"group" -> group,
"artefact" -> artefact,
"versionRange" -> versionRange.range))
}

def getGroupArtefacts(implicit hc: HeaderCarrier): Future[List[GroupArtefacts]] = {
Expand Down
26 changes: 17 additions & 9 deletions app/uk/gov/hmrc/cataloguefrontend/connector/model/BobbyRule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,27 @@ package uk.gov.hmrc.cataloguefrontend.connector.model

import java.time.LocalDate

import play.api.libs.json.{Json, Reads}
import play.api.libs.json.{Json, Reads, __}
import play.api.libs.functional.syntax._

case class BobbyRule(organisation: String, name: String, range: BobbyVersionRange, reason: String, from: LocalDate) {
val groupArtifactName: String = {
val wildcard = "*"
if (organisation == wildcard && name == wildcard) "*" else s"$organisation:$name"
}
}

case class BobbyRule(
group : String
, artefact: String
, range : BobbyVersionRange
, reason : String
, from : LocalDate
)

object BobbyRule {
val reads: Reads[BobbyRule] = {
implicit val bvrr = BobbyVersionRange.reads
Json.reads[BobbyRule]
implicit val bvrf = BobbyVersionRange.format
( (__ \ "organisation").read[String]
~ (__ \ "name" ).read[String]
~ (__ \ "range" ).read[BobbyVersionRange]
~ (__ \ "reason" ).read[String]
~ (__ \ "from" ).read[LocalDate]
)(BobbyRule.apply _)
}
}

Expand Down
Loading

0 comments on commit 7a21d16

Please sign in to comment.