Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework badges #1526

Merged
merged 1 commit into from
Jan 13, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,6 @@ case class ArtifactSelection(
private def filterAll(artifact: Artifact.Reference): Boolean =
binaryVersion.forall(_ == artifact.binaryVersion) && artifactNames.forall(_ == artifact.name)

def defaultArtifact(artifacts: Seq[Artifact.Reference], project: Project): Option[Artifact.Reference] =
val filteredArtifacts = artifacts.view.filter(filterAll)

filteredArtifacts.maxByOption { artifact =>
(
// default artifact (ex: akka-actors is the default for akka/akka)
project.settings.defaultArtifact.contains(artifact.name),
// not deprecated
!project.settings.deprecatedArtifacts.contains(artifact.name),
// project repository (ex: shapeless)
project.repository.value == artifact.name.value,
// alphabetically
artifact.name,
// stable version first
project.settings.preferStableVersion && !artifact.version.isStable,
artifact.version,
artifact.binaryVersion
)
}(
Ordering.Tuple7(
Ordering[Boolean],
Ordering[Boolean],
Ordering[Boolean],
Ordering[Artifact.Name].reverse,
Ordering[Boolean].reverse,
Ordering[Version],
Ordering[BinaryVersion]
)
)
end defaultArtifact

def filterArtifacts(artifacts: Seq[Artifact.Reference], project: Project): Seq[Artifact.Reference] =
artifacts
.filter(filterAll)
Expand Down Expand Up @@ -70,6 +39,3 @@ case class ArtifactSelection(
.reverse
)
end ArtifactSelection

object ArtifactSelection:
def empty = new ArtifactSelection(None, None)
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ final case class ProjectHeader(
def allArtifactNames: Seq[Artifact.Name] = artifacts.map(_.name).distinct.sorted
def platforms(artifactName: Artifact.Name): Seq[Platform] =
artifacts.filter(_.name == artifactName).map(_.platform).distinct.sorted(Platform.ordering.reverse)
def artifacts(artifactName: Artifact.Name, platform: Platform): Seq[Artifact] =
artifacts.filter(a => a.name == artifactName && a.platform == platform)

def versionsUrl: String = artifactsUrl(getDefaultArtifact(None, None), withBinaryVersion = false)

Expand All @@ -43,19 +45,30 @@ final case class ProjectHeader(
val queryParams = if filters.nonEmpty then "?" + filters.mkString("&") else ""
s"/$ref/artifacts/${defaultArtifact.name}$queryParams"

def getDefaultArtifact0(binaryVersion: Option[BinaryVersion], artifactName: Option[Artifact.Name]): Artifact =
getDefaultArtifact(binaryVersion.map(_.language), binaryVersion.map(_.platform), artifactName)

def getDefaultArtifact(language: Option[Language], platform: Option[Platform]): Artifact =
getDefaultArtifact(language, platform, None)

/** getDefaultArtifact is split in two steps: first we get the default artifact name and then, the latest version. The
* reason is, we cannot use the latest version of all artifacts to get the default artifact if they don't share the
* same versioning. Instead we use the latest release date. But once we have the artifact with the latest release
* date, we really want to get the latest version of that artifact, which is not necessarily the latest one released
* because of back-publishing.
*/
def getDefaultArtifact(language: Option[Language], platform: Option[Platform]): Artifact =
val artifactName = getDefaultArtifactName(language, platform)
def getDefaultArtifact(
language: Option[Language],
platform: Option[Platform],
artifactName: Option[Artifact.Name]
): Artifact =
val defaultArtifactName = artifactName.getOrElse(getDefaultArtifactName(language, platform))
val filteredArtifacts = artifacts.filter { a =>
a.name == artifactName && language.forall(_ == a.language) && platform.forall(_ == a.platform)
a.name == defaultArtifactName && language.forall(_ == a.language) && platform.forall(_ == a.platform)
}
if preferStableVersion then filteredArtifacts.maxBy(a => (a.version.isStable, a.version))
else filteredArtifacts.maxBy(_.version)
end getDefaultArtifact

private def getDefaultArtifactName(language: Option[Language], platform: Option[Platform]): Artifact.Name =
val filteredArtifacts = artifacts.filter(a => language.forall(_ == a.language) && platform.forall(_ == a.platform))
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion modules/server/src/main/scala/scaladex/server/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ object Server extends LazyLogging:
val publishApi = new PublishApi(githubAuth, publishProcess)
val apiEndpoints = new ApiEndpointsImpl(projectService, artifactService, searchEngine)
val oldSearchApi = new OldSearchApi(searchEngine, webDatabase)
val badges = new Badges(webDatabase)
val badges = new Badges(projectService)
val authentication = new AuthenticationApi(config.oAuth2.clientId, config.session, githubAuth, webDatabase)

val route: Route =
Expand Down
145 changes: 48 additions & 97 deletions modules/server/src/main/scala/scaladex/server/route/Badges.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,18 @@ package scaladex.server.route
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scaladex.core.model.Artifact
import scaladex.core.model.ArtifactSelection
import scaladex.core.model.BinaryVersion
import scaladex.core.model.Jvm
import scaladex.core.model.Platform
import scaladex.core.model.Project
import scaladex.core.model.SbtPlugin
import scaladex.core.model.Scala
import scaladex.core.model.ScalaJs
import scaladex.core.model.ScalaNative
import scaladex.core.model.Version
import scaladex.core.model.Version.PreferStable
import scaladex.core.service.WebDatabase
import scaladex.core.model.*
import scaladex.core.service.ProjectService

import org.apache.pekko.http.scaladsl.model.StatusCodes.*
import org.apache.pekko.http.scaladsl.model.headers.*
import org.apache.pekko.http.scaladsl.model.headers
import org.apache.pekko.http.scaladsl.model.headers.CacheDirectives.*
import org.apache.pekko.http.scaladsl.server.Directives.*
import org.apache.pekko.http.scaladsl.server.RequestContext
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.server.RouteResult

class Badges(database: WebDatabase)(using ExecutionContext):
class Badges(projectService: ProjectService)(using ExecutionContext):

private val shields =
parameters("color".?, "style".?, "logo".?, "logoWidth".as[Int].?)
Expand All @@ -51,140 +40,102 @@ class Badges(database: WebDatabase)(using ExecutionContext):
logo: Option[String],
logoWidth: Option[Int]
) =

def shieldEscape(in: String): String =
in.replace("-", "--")
.replace("_", "__")
.replace(" ", "_")
in.replace("-", "--").replace("_", "__").replace(" ", "_")

val subject = shieldEscape(rawSubject)
val status = shieldEscape(rawStatus)

val color = rawColor.getOrElse("green")

// we need a specific encoding
val query = List(
style.map(("style", _)),
logo.map(l =>
(
"logo",
java.net.URLEncoder
.encode(l, "ascii")
.replace("+", "%2B")
)
),
logo.map(l => ("logo", java.net.URLEncoder.encode(l, "ascii").replace("+", "%2B"))),
logoWidth.map(w => ("logoWidth", w.toString))
).flatten.map { case (k, v) => k + "=" + v }.mkString("?", "&", "")

respondWithHeaders(`Cache-Control`(`no-cache`), ETag(status)) {
redirect(
s"https://img.shields.io/badge/$subject-$status-$color.svg$query",
TemporaryRedirect
)
respondWithHeaders(headers.`Cache-Control`(`no-cache`), headers.ETag(status)) {
redirect(s"https://img.shields.io/badge/$subject-$status-$color.svg$query", TemporaryRedirect)
}
end shieldsSvg

def latest(
ref: Project.Reference,
artifactName: Option[Artifact.Name]
): RequestContext => Future[RouteResult] =
def latest(ref: Project.Reference, artifactName: Option[Artifact.Name]): RequestContext => Future[RouteResult] =
parameter("target".?) { binaryVersion =>
shieldsOptionalSubject { (color, style, logo, logoWidth, subjectOpt) =>
val subject = subjectOpt.orElse(artifactName.map(_.value)).getOrElse(ref.repository.value)
def error(msg: String) =
shieldsSvg(subject, msg, color.orElse(Some("lightgrey")), style, logo, logoWidth)
def error(msg: String) = shieldsSvg(subject, msg, color.orElse(Some("lightgrey")), style, logo, logoWidth)

val res = database.getProject(ref).flatMap {
val res = projectService.getProject(ref).flatMap {
case None => Future.successful(error("project not found"))
case Some(project) =>
val bv = binaryVersion.flatMap(BinaryVersion.parse)
getDefaultArtifact(project, bv, artifactName).map {
case None => error("no published artifacts")
case Some(artifact) =>
shieldsSvg(subject, artifact.version.toString, color, style, logo, logoWidth)
case Some(artifact) => shieldsSvg(subject, artifact.version.toString, color, style, logo, logoWidth)
}

}
onSuccess(res)(identity)
}
}

def latestByScalaVersion(
reference: Project.Reference,
artifactName: Artifact.Name
): RequestContext => Future[RouteResult] =
// targetType paramater is kept for forward compatibility
def latestByScalaVersion(ref: Project.Reference, artifactName: Artifact.Name): RequestContext => Future[RouteResult] =
// targetType parameter is kept for forward compatibility
// in case targetType is defined we choose the most recent corresponding platform
parameters("targetType".?, "platform".?) { (targetTypeParam, platformParam) =>
shields { (color, style, logo, logoWidth) =>
// TODO use projectHeader
val artifactsF = database.getProjectArtifactRefs(reference, artifactName)
onSuccess(artifactsF) { artifacts =>
val availablePlatforms = artifacts.map(_.binaryVersion.platform).distinct
val headerF = projectService.getHeader(ref).map(_.get)
onSuccess(headerF) { header =>
val platforms = header.platforms(artifactName)
val platform = platformParam
.flatMap(Platform.parse)
.orElse {
targetTypeParam.map(_.toUpperCase).flatMap {
case "JVM" => Some(Jvm)
case "JS" =>
val jsPlatforms =
availablePlatforms.collect { case p: ScalaJs => p }
Option.when(jsPlatforms.nonEmpty)(jsPlatforms.max[Platform])
case "NATIVE" =>
val nativePlatforms =
availablePlatforms.collect { case v: ScalaNative => v }
Option.when(nativePlatforms.nonEmpty)(nativePlatforms.max[Platform])
case "SBT" =>
val sbtPlatforms =
availablePlatforms.collect { case v: SbtPlugin => v }
Option.when(sbtPlatforms.nonEmpty)(sbtPlatforms.max[Platform])
case _ => None
}
}
.getOrElse(availablePlatforms.max)

val platformArtifacts = artifacts.filter(_.binaryVersion.platform == platform)
val summary = Badges.summaryOfLatestVersions(platformArtifacts)

.orElse(targetTypeParam.flatMap(selectPlatformFromTargetType(_, platforms)))
.getOrElse(platforms.max)
val artifacts = header.artifacts(artifactName, platform)
val summary = Badges.summaryOfLatestVersions(artifacts.map(_.reference), platform)
shieldsSvg(s"$artifactName - $platform", summary, color, style, logo, logoWidth)
}
}
}

private def selectPlatformFromTargetType(targetType: String, platforms: Seq[Platform]): Option[Platform] =
targetType.toUpperCase match
case "JVM" => platforms.find(_ == Jvm)
case "JS" => platforms.collect { case p: ScalaJs => p }.maxOption
case "NATIVE" => platforms.collect { case v: ScalaNative => v }.maxOption
case "SBT" => platforms.collect { case v: SbtPlugin => v }.maxOption
case _ => None

private def getDefaultArtifact(
project: Project,
binaryVersion: Option[BinaryVersion],
artifact: Option[Artifact.Name]
): Future[Option[Artifact.Reference]] =
val artifactSelection = ArtifactSelection(binaryVersion, artifact)
// TODO use projectHeader
database.getProjectArtifactRefs(project.reference, stableOnly = false).map { artifacts =>
val (stableArtifacts, nonStableArtifacts) = artifacts.partition(_.version.isStable)
artifactSelection
.defaultArtifact(stableArtifacts, project)
.orElse(artifactSelection.defaultArtifact(nonStableArtifacts, project))
}
end getDefaultArtifact
projectService.getHeader(project.reference).map(_.map(_.getDefaultArtifact0(binaryVersion, artifact).reference))
end Badges

object Badges:
private def summaryOfLatestVersions(artifacts: Seq[Artifact.Reference]): String =
val versionsByScalaVersions = artifacts
.groupMap(_.binaryVersion.language)(_.version)
.collect { case (Scala(v), version) => Scala(v) -> version }
summaryOfLatestVersions(versionsByScalaVersions)

private[route] def summaryOfLatestVersions(versionsByScalaVersions: Map[Scala, Seq[Version]]): String =
versionsByScalaVersions.view
.mapValues(_.max(PreferStable))
.groupMap { case (_, latestVersion) => latestVersion } { case (scalaVersion, _) => scalaVersion }
private def summaryOfLatestVersions(artifacts: Seq[Artifact.Reference], platform: Platform): String =
platform match
case _: (SbtPlugin | MillPlugin) =>
val latestVersion = artifacts.map(_.version).max
latestVersion.toString
case _ =>
val latestVersions = artifacts.groupMapReduce(_.binaryVersion.language)(_.version)(Version.ordering.max)
summaryByLanguageVersion(latestVersions)

private[route] def summaryByLanguageVersion(latestVersions: Map[Language, Version]): String =
latestVersions.toSeq
.groupMap { case (_, latestVersion) => latestVersion } { case (language, _) => language }
.toSeq
.sortBy(_._1)(Version.ordering.reverse)
.map {
case (latestVersion, scalaVersions) =>
val scalaVersionsStr =
scalaVersions.map(_.version).toSeq.sorted(Version.ordering.reverse).mkString(", ")
s"$latestVersion (Scala $scalaVersionsStr)"
case (version, Seq(Java)) => s"$version"
case (version, languages) =>
// there is more than one language, we ignore Java
val scalaVersions =
languages.collect { case Scala(v) => v }.toSeq.sorted(Version.ordering.reverse).mkString(", ")
s"$version (Scala $scalaVersions)"
}
.mkString(", ")
end Badges
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,8 @@ class OldSearchApi(searchEngine: SearchEngine, database: WebDatabase)(using Exec
binaryVersion: Option[BinaryVersion],
artifact: Option[String]
): Future[Option[OldSearchApi.ArtifactOptions]] =
val selection = new ArtifactSelection(
binaryVersion = binaryVersion,
artifactNames = artifact.map(Artifact.Name.apply)
)
val selection =
new ArtifactSelection(binaryVersion = binaryVersion, artifactNames = artifact.map(Artifact.Name.apply))
for
projectOpt <- database.getProject(projectRef)
stableOnly = projectOpt.map(_.settings.preferStableVersion).getOrElse(false)
Expand Down
Loading
Loading