Skip to content

Commit

Permalink
Add Badge showing notable Scala support/Artifact versions
Browse files Browse the repository at this point in the history
Fixes scalacenter#659 - adding
support for a badge that summarises, for a given artifact, which versions
of Scala it supports (and what the latest artifact version is for each
of those Scala versions).


```
$ curl --silent -I http://localhost:8080/typelevel/cats/cats-core/latest-by-scala-version.svg | grep Location
Location: https://img.shields.io/badge/cats--core-0.9.0_(Scala_2.12,_2.11,_2.10)-green.svg?

$ curl --silent -I http://localhost:8080/typelevel/cats/cats-core/latest-by-scala-version.svg | grep Location
Location: https://img.shields.io/badge/cats--core-0.9.0_(Scala_2.12,_2.11,_2.10)-green.svg?
```
  • Loading branch information
rtyley committed May 3, 2021
1 parent d001bdc commit 7414782
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 7 deletions.
16 changes: 16 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ read the [developer guide](/CONTRIBUTING.md)

## Badges API

### Show the versions of Scala supported by your project!

You can add this badge to the README.MD of your own GitHub projects to show
the versions of Scala they support:

[![Scala version Badge](https://index.scala-lang.org/typelevel/cats/cats-core/latest-by-scala-version.svg)

...the badge above only summarises latest JVM artifacts, if you'd like a badge
for Scala JS or Scala Native, add a `targetType=...` query-string parameter:

[![Scala version Badge](https://index.scala-lang.org/typelevel/cats/cats-core/latest-by-scala-version.svg?targetType=js)

[![Scala version Badge](https://index.scala-lang.org/typelevel/cats/cats-core/latest-by-scala-version.svg?targetType=native)

### Smaller, shorter badges

[![Count Badge](https://index.scala-lang.org/count.svg?q=depends-on:typelevel/cats&subject=cats&color=orange&style=flat-square)](https://index.scala-lang.org/search?q=dependencies:typelevel/cats)

[![Latest version](https://index.scala-lang.org/typelevel/cats/cats-core/latest.svg?color=orange&style=flat-square)](https://index.scala-lang.org/typelevel/cats/cats-core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import fastparse._
* It can be either a [[ScalaVersion]] or a [[Scala3Version]]
*/
sealed trait LanguageVersion {
val version: BinaryVersion

/**
* When indexing, all dotty versions are regrouped under the 'dotty' keyword.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ object ScalaTargetType {

implicit val ordering: Ordering[ScalaTargetType] = Ordering.by(All.indexOf(_))

def ofName(name: String): Option[ScalaTargetType] =
All.find(_.getClass.getSimpleName.stripSuffix("$").equalsIgnoreCase(name))
}

sealed trait ScalaTargetType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.scalatest.prop.TableDrivenPropertyChecks
class ScalaTargetTests
extends FunSpec
with Matchers
with OptionValues
with TableDrivenPropertyChecks {
it("should be ordered") {
val js067 = PatchBinary(0, 6, 7)
Expand Down Expand Up @@ -51,4 +52,9 @@ class ScalaTargetTests
ScalaTarget.parse(input) should contain(target)
}
}

it("should parse a string to yield a ScalaTargetType") {
ScalaTargetType.ofName("Js").value shouldBe Js
ScalaTargetType.ofName("Jvm").value shouldBe Jvm
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package ch.epfl.scala.index.server

import ch.epfl.scala.index.model.{Release, SemanticVersion}
import ch.epfl.scala.index.model.release._
import ch.epfl.scala.index.server.ArtifactScalaVersionSupport.ScalaSupport

import scala.collection.immutable.{SortedMap, SortedSet}

case class ArtifactScalaVersionSupport(
scalaTargetsForAllArtifactVersions: Map[SemanticVersion, Seq[ScalaTarget]]
) {

val allTargets: Set[ScalaTarget] =
scalaTargetsForAllArtifactVersions.values.flatten.toSet

val latestScalaVersionsOfAvailableBinaryVersions: Set[LanguageVersion] =
allTargets
.map(_.languageVersion)
.groupBy(_.family)
.view
.mapValues(_.max)
.values
.toSet

val latestArtifactVersionByLanguageVersion
: Map[LanguageVersion, SemanticVersion] = (for {
(artifactVersion, scalaTargets) <- scalaTargetsForAllArtifactVersions.toSeq
scalaTarget <- scalaTargets
if latestScalaVersionsOfAvailableBinaryVersions.contains(
scalaTarget.languageVersion
)
} yield scalaTarget.languageVersion -> artifactVersion)
.groupMap(_._1)(_._2)
.view
.mapValues(_.max)
.toMap

val scalaSupportByLatestSupportingArtifactVersion
: SortedMap[SemanticVersion, ScalaSupport] =
SortedMap.from(
latestArtifactVersionByLanguageVersion.groupMap(_._2)(_._1).view.map {
case (artifactVersion, notableLanguageVersions) =>
val notableLanguageVersionsSet = notableLanguageVersions.toSet
artifactVersion -> ScalaSupport(
scalaTargetsForAllArtifactVersions(artifactVersion)
.filter(st => notableLanguageVersionsSet(st.languageVersion))
.toSet
)
}
)(SemanticVersion.ordering.reverse)

val summaryOfLatestArtifactsSupportingScalaVersions: String = (for (
(artifactVersion, scalaSupport) <-
scalaSupportByLatestSupportingArtifactVersion
) yield s"$artifactVersion (${scalaSupport.summary})").mkString(", ")
}

object ArtifactScalaVersionSupport {
def forSpecifiedArtifactAndTargetType(
allAvailableReleases: Seq[Release],
specificArtifact: String,
specificTargetType: ScalaTargetType
): ArtifactScalaVersionSupport = {

val scalaTargetsByArtifactVersion = (for {
release <- allAvailableReleases if release.isValid
ref = release.reference if ref.artifact == specificArtifact
scalaTarget <- ref.target if scalaTarget.targetType == specificTargetType
} yield ref.version -> scalaTarget).groupMap(_._1)(_._2)

ArtifactScalaVersionSupport(scalaTargetsByArtifactVersion)
}

case class ScalaSupport(
scalaTargets: Set[ScalaTarget]
) {
val scalaTargetsByLanguageVersion: Map[LanguageVersion, Set[ScalaTarget]] =
scalaTargets.groupBy(_.languageVersion)

val languageVersions: SortedSet[LanguageVersion] =
SortedSet.from(scalaTargetsByLanguageVersion.keySet)(
LanguageVersion.ordering.reverse
)

val platformEditionsSupportedForAllLanguageVersions: Set[PlatformEdition] =
scalaTargetsByLanguageVersion.values
.map(_.collect { case st: ScalaTargetWithPlatformBinaryVersion =>
st.platformEdition
})
.reduce(_ & _)

val platformBinaryVersionsByTargetType
: SortedMap[ScalaTargetType, SortedSet[BinaryVersion]] =
SortedMap.from(
platformEditionsSupportedForAllLanguageVersions
.groupMap(_.targetType)(_.version)
.view
.mapValues(SortedSet.from(_)(BinaryVersion.ordering.reverse))
)

val targetsSummary: Option[String] =
Option.when(platformBinaryVersionsByTargetType.nonEmpty)(
" - " + platformBinaryVersionsByTargetType
.map { case (targetType, platformBinaryVersions) =>
s"$targetType ${platformBinaryVersions.mkString("+")}"
}
.mkString(", ")
)

val summary: String =
s"Scala ${languageVersions.mkString(", ")}${targetsSummary.mkString}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package ch.epfl.scala.index
package server
package routes

import model._
import release._
import akka.http.scaladsl._
import server.Directives._
import model.StatusCodes._
import model.headers._
import model.headers.CacheDirectives._
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.model.headers.CacheDirectives._
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.Directives._
import ch.epfl.scala.index.model._
import ch.epfl.scala.index.model.release._
import ch.epfl.scala.index.search.DataRepository

import scala.collection.immutable.{SortedMap, SortedSet}

class Badges(dataRepository: DataRepository) {

private val shields = parameters(
Expand Down Expand Up @@ -103,6 +104,40 @@ class Badges(dataRepository: DataRepository) {
}
}

def latestByScalaVersion(
organization: String,
repository: String,
artifact: String
) = {
parameter("targetType".?) { targetTypeString =>
shields { (color, style, logo, logoWidth) =>
val targetType =
targetTypeString.flatMap(ScalaTargetType.ofName).getOrElse(Jvm)
onSuccess {
dataRepository.getProjectReleases(
Project.Reference(organization, repository)
)
} { allAvailableReleases =>
val notableScalaSupport =
ArtifactScalaVersionSupport.forSpecifiedArtifactAndTargetType(
allAvailableReleases,
artifact,
targetType
)

shieldsSvg(
artifact,
notableScalaSupport.summaryOfLatestArtifactsSupportingScalaVersions,
color,
style,
logo,
logoWidth
)
}
}
}
}

val routes =
get(
concat(
Expand All @@ -116,6 +151,11 @@ class Badges(dataRepository: DataRepository) {
)
)
},
pathPrefix(
Segment / Segment / Segment / "latest-by-scala-version.svg"
) { (organization, repository, artifact) =>
latestByScalaVersion(organization, repository, artifact)
},
path("count.svg")(
parameter("q")(query =>
shieldsSubject((color, style, logo, logoWidth, subject) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package ch.epfl.scala.index.server

import ch.epfl.scala.index.model.release.ScalaVersion._
import ch.epfl.scala.index.model.release.{
Js,
LanguageVersion,
Native,
PreReleaseBinary,
Scala3Version,
ScalaJs,
ScalaJvm,
ScalaNative
}
import ch.epfl.scala.index.model.{Milestone, ReleaseCandidate, SemanticVersion}
import org.apache.commons.lang3.StringUtils.countMatches
import org.scalatest.{FunSpec, Matchers}

class ArtifactScalaVersionSupportTest extends FunSpec with Matchers {

val `3.0.0-M3`: LanguageVersion = Scala3Version(
PreReleaseBinary(3, 0, Some(0), Milestone(3))
)
val `3.0.0-RC2`: LanguageVersion = Scala3Version(
PreReleaseBinary(3, 0, Some(0), ReleaseCandidate(2))
)
val `3.0.0-RC3`: LanguageVersion = Scala3Version(
PreReleaseBinary(3, 0, Some(0), ReleaseCandidate(3))
)

val `7.0.0`: SemanticVersion = SemanticVersion(7, 0, 0)
val `7.1.0`: SemanticVersion = SemanticVersion(7, 1, 0)
val `7.2.0`: SemanticVersion = SemanticVersion(7, 2, 0)
val `7.3.0`: SemanticVersion = SemanticVersion(7, 3, 0)

it("should provide a concise summary of Scala support by the artifact") {
ArtifactScalaVersionSupport(
Map(
`7.0.0` -> Seq(ScalaJvm(`2.11`)),
`7.1.0` -> Seq(ScalaJvm(`2.11`), ScalaJvm(`2.12`)),
`7.2.0` -> Seq(ScalaJvm(`2.12`), ScalaJvm(`2.13`))
)
).summaryOfLatestArtifactsSupportingScalaVersions shouldBe "7.2.0 (Scala 2.13, 2.12), 7.1.0 (Scala 2.11)"
}

it(
"should concisely convey which versions of Scala are supported, most recent Scala version first"
) {
ArtifactScalaVersionSupport(
Map(
`7.0.0` -> Seq(
ScalaJvm(`2.12`),
ScalaJvm(`2.13`),
ScalaJvm(`3.0.0-RC3`)
)
)
).summaryOfLatestArtifactsSupportingScalaVersions should include(
"Scala 3.0.0-RC3, 2.13, 2.12"
)
}

it(
"should convey the latest artifact version available for each Scala language version"
) {
val summary = ArtifactScalaVersionSupport(
Map(
`7.0.0` -> Seq(ScalaJvm(`2.11`)),
`7.1.0` -> Seq(ScalaJvm(`2.11`)),
`7.2.0` -> Seq(ScalaJvm(`2.12`)),
`7.3.0` -> Seq(ScalaJvm(`2.12`))
)
).summaryOfLatestArtifactsSupportingScalaVersions

// these artifact versions are not the latest available support for any Scala language version, so uninteresting:
summary should not(include("7.0.0") or include("7.2.0"))

summary should include("7.1.0 (Scala 2.11)")
summary should include("7.3.0 (Scala 2.12)")
}

it(
"should, for brevity, not mention a Scala language version more than once, even if it occurs in multiple artifact versions being mentioned"
) {
val summary = ArtifactScalaVersionSupport(
Map(
`7.1.0` -> Seq(ScalaJvm(`2.11`), ScalaJvm(`2.12`)),
`7.2.0` -> Seq(ScalaJvm(`2.12`))
)
).summaryOfLatestArtifactsSupportingScalaVersions

// it happens that two artifact versions that support Scala 2.12 will be mentioned...
assert(summary.contains("7.1.0") && summary.contains("7.2.0"))

// ...but for brevity no Scala language version (eg Scala 2.12 in this case) should be mentioned more than once...
countMatches(summary, "2.12") shouldBe 1

// ...specifically it should be listed against the *latest* artifact that supports that Scala language version
summary should include("7.2.0 (Scala 2.12)")
}

it(
"should, for brevity, only mention the *latest* Scala language versions available for any given Scala binary version family"
) {
ArtifactScalaVersionSupport(
Map(
`7.0.0` -> Seq(ScalaJvm(`2.13`), ScalaJvm(`3.0.0-M3`)),
`7.1.0` -> Seq(ScalaJvm(`2.13`), ScalaJvm(`3.0.0-RC2`)),
`7.2.0` -> Seq(ScalaJvm(`2.13`), ScalaJvm(`3.0.0-RC3`))
)
).summaryOfLatestArtifactsSupportingScalaVersions shouldBe "7.2.0 (Scala 3.0.0-RC3, 2.13)"
}

it(
"should list the Scala platform editions that support all cited versions of the Scala language"
) {
ArtifactScalaVersionSupport(
Map(
`7.1.0` -> Seq(
ScalaNative(`3.0.0-M3`, Native.`0.3`),
ScalaNative(`2.13`, Native.`0.3`),
ScalaNative(`3.0.0-M3`, Native.`0.4`),
ScalaNative(`2.13`, Native.`0.4`)
)
)
).summaryOfLatestArtifactsSupportingScalaVersions shouldBe "7.1.0 (Scala 3.0.0-M3, 2.13 - Native 0.4+0.3)"

}

}

0 comments on commit 7414782

Please sign in to comment.