Skip to content

Commit

Permalink
Merge pull request #1008 from scalacenter/yoo/add-artifact-data-endpoint
Browse files Browse the repository at this point in the history
Adding `/api/artifacts/<group_id>/<artifact_id>` endpoint
  • Loading branch information
adpi2 authored Apr 19, 2022
2 parents 557005e + c063136 commit 561acb6
Show file tree
Hide file tree
Showing 16 changed files with 186 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,14 @@ trait ArtifactEndpointSchema extends PaginationSchema {
.xmap[ArtifactResponse] { case (groupId, artifactId) => ArtifactResponse(groupId, artifactId) } {
case ArtifactResponse(groupId, artifactId) => (groupId, artifactId)
}

implicit val artifactMetadataResponseSchema: JsonSchema[ArtifactMetadataResponse] =
field[String]("version")
.zip(optField[String]("projectReference"))
.zip(field[String]("releaseDate"))
.zip(field[String]("language"))
.zip(field[String]("platform"))
.xmap[ArtifactMetadataResponse](ArtifactMetadataResponse.tupled)(
Function.unlift(ArtifactMetadataResponse.unapply)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,29 @@ trait ArtifactEndpoints
docs = Some("Filter the results matching the given platform only (e.g., 'jvm', 'sjs1', 'native0.4', 'sbt1.0')")
)).xmap((ArtifactParams.apply _).tupled)(Function.unlift(ArtifactParams.unapply))

val artifactMetadataEndpointParams: Path[ArtifactMetadataParams] = (segment[String](
name = "group_id",
docs = Some(
"Filter the results matching the given group id only (e.g., 'org.typelevel', 'org.scala-lang', 'org.apache.spark')"
)
) / segment[String](
name = "artifact_id",
docs = Some(
"Filter the results matching the given artifact id only (e.g., 'cats-core_3', 'cats-core_sjs0.6_2.13')"
)
)).xmap(ArtifactMetadataParams.tupled)(Function.unlift(ArtifactMetadataParams.unapply))

// Artifact endpoint definition
val artifact: Endpoint[ArtifactParams, Page[ArtifactResponse]] =
endpoint(
get(path / "api" / "artifacts" /? artifactEndpointParams),
ok(jsonResponse[Page[ArtifactResponse]])
)

// Artifact metadata endpoint definition
val artifactMetadata: Endpoint[ArtifactMetadataParams, Page[ArtifactMetadataResponse]] =
endpoint(
get(path / "api" / "artifacts" / artifactMetadataEndpointParams),
ok(jsonResponse[Page[ArtifactMetadataResponse]])
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package scaladex.core.api.artifact

final case class ArtifactMetadataParams(groupId: String, artifactId: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package scaladex.core.api.artifact

final case class ArtifactMetadataResponse(
version: String,
projectReference: Option[String],
releaseDate: String,
language: String,
platform: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.time.Instant
import fastparse.P
import fastparse.Start
import fastparse._
import scaladex.core.api.artifact.ArtifactMetadataResponse
import scaladex.core.model.PatchVersion
import scaladex.core.model.Project.DocumentationLink
import scaladex.core.util.Parsers._
Expand Down Expand Up @@ -243,4 +244,12 @@ object Artifact {
s"http://search.maven.org/#artifactdetails|$groupId|$artifactId|$version|jar"
}

def toMetadataResponse(artifact: Artifact): ArtifactMetadataResponse =
ArtifactMetadataResponse(
version = artifact.version.toString,
projectReference = Some(artifact.projectRef.toString),
releaseDate = artifact.releaseDate.toString,
language = artifact.language.toString,
platform = artifact.platform.toString
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ case class Page[A](pagination: Pagination, items: Seq[A]) {

def flatMap[B](f: A => Iterable[B]): Page[B] = Page(pagination, items.flatMap(f))
}

object Page {

def empty[A]: Page[A] =
Page(
pagination = Pagination(current = 1, pageCount = 1, totalSize = 0),
items = Seq.empty
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ trait WebDatabase {
def getProject(projectRef: Project.Reference): Future[Option[Project]]
def getFormerReferences(projectRef: Project.Reference): Future[Seq[Project.Reference]]
def countInverseProjectDependencies(projectRef: Project.Reference): Future[Int]
def getArtifacts(groupId: Artifact.GroupId, artifactId: Artifact.ArtifactId): Future[Seq[Artifact]]
def getArtifacts(projectRef: Project.Reference): Future[Seq[Artifact]]
def getArtifactsByName(projectRef: Project.Reference, artifactName: Artifact.Name): Future[Seq[Artifact]]
def getArtifactByMavenReference(mavenRef: Artifact.MavenReference): Future[Option[Artifact]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ class InMemoryDatabase extends SchedulerDatabase {
override def getProject(projectRef: Project.Reference): Future[Option[Project]] =
Future.successful(projects.get(projectRef))

override def getArtifacts(groupId: Artifact.GroupId, artifactId: Artifact.ArtifactId): Future[Seq[Artifact]] =
Future.successful {
artifacts.values.flatten.filter { artifact: Artifact =>
artifact.groupId == groupId && artifact.artifactId == artifactId.value
}.toSeq
}

override def getArtifacts(projectRef: Project.Reference): Future[Seq[Artifact]] =
Future.successful(artifacts.getOrElse(projectRef, Nil))

Expand Down
3 changes: 3 additions & 0 deletions modules/infra/src/main/scala/scaladex/infra/SqlDatabase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ class SqlDatabase(datasource: HikariDataSource, xa: doobie.Transactor[IO]) exten
override def getProject(projectRef: Project.Reference): Future[Option[Project]] =
run(ProjectTable.selectByReference.option(projectRef))

override def getArtifacts(groupId: Artifact.GroupId, artifactId: Artifact.ArtifactId): Future[Seq[Artifact]] =
run(ArtifactTable.selectArtifactByGroupIdAndArtifactId.to[Seq](groupId, artifactId))

override def getArtifacts(projectRef: Project.Reference): Future[Seq[Artifact]] =
run(ArtifactTable.selectArtifactByProject.to[Seq](projectRef))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ object ArtifactTable {
val selectArtifactByLanguageAndPlatform: Query[(Language, Platform), Artifact] =
selectRequest(table, fields, keys = Seq("language_version", "platform"))

val selectArtifactByGroupIdAndArtifactId: Query[(Artifact.GroupId, Artifact.ArtifactId), Artifact] =
selectRequest(table, fields, Seq("group_id", "artifact_id"))

val selectArtifactByProject: Query[Project.Reference, Artifact] = {
val where = projectReferenceFields.map(f => s"$f=?").mkString(" AND ")
selectRequest1(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ object DoobieUtils {
Meta[String].timap(_.split(",").filter(_.nonEmpty).map(Artifact.Name.apply).toSet)(_.mkString(","))
implicit val semanticVersionMeta: Meta[SemanticVersion] =
Meta[String].timap(SemanticVersion.parse(_).get)(_.encode)
implicit val artifactIdMeta: Meta[Artifact.ArtifactId] =
Meta[String].timap(Artifact.ArtifactId.parse(_).get)(_.value)
implicit val binaryVersionMeta: Meta[BinaryVersion] =
Meta[String].timap { x =>
BinaryVersion
Expand Down
40 changes: 40 additions & 0 deletions modules/infra/src/test/scala/scaladex/infra/SqlDatabaseTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,44 @@ class SqlDatabaseTests extends AsyncFunSpec with BaseDatabaseSuite with Matchers
)
} yield storedArtifacts.size shouldBe 0
}

it("should not return any artifacts when the database is empty, given a group id and artifact id") {
val testArtifact = Cats.`core_3:4`
val testArtifactId = Artifact.ArtifactId
.parse(testArtifact.artifactId)
.getOrElse(fail("Parsing an artifact id should not have failed"))
for {
retrievedArtifacts <- database.getArtifacts(testArtifact.groupId, testArtifactId)
} yield retrievedArtifacts.size shouldBe 0
}

it("should return an artifact, given a group id an artifact id of a stored artifact") {
val testArtifact = Cats.`core_3:4`
val testArtifactId = Artifact.ArtifactId
.parse(testArtifact.artifactId)
.getOrElse(fail("Parsing an artifact id should not have failed"))
for {
isStoredSuccessfully <- database.insertArtifact(testArtifact, dependencies = Cats.dependencies, now)
retrievedArtifacts <- database.getArtifacts(testArtifact.groupId, testArtifactId)
} yield {
isStoredSuccessfully shouldBe true
retrievedArtifacts.size shouldBe 1
retrievedArtifacts.headOption shouldBe Some(testArtifact)
}
}

it("should return all versions of an artifact given a group id and an artifact id") {
val testArtifacts = Seq(Cats.`core_3:4`, Cats.`core_3:2.7.0`)
val groupId = Artifact.GroupId("org.typelevel")
val artifactId = Artifact.ArtifactId
.parse("cats-core_3")
.getOrElse(fail("Parsing an artifact id should not have failed"))
for {
_ <- database.insertArtifacts(testArtifacts)
retrievedArtifacts <- database.getArtifacts(groupId, artifactId)
} yield {
retrievedArtifacts.size shouldBe 2
retrievedArtifacts should contain theSameElementsAs testArtifacts
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class ArtifactTableTests extends AnyFunSpec with BaseDatabaseSuite with Matchers
it("check selectArtifactByLanguage")(check(ArtifactTable.selectArtifactByLanguage))
it("check selectArtifactByPlatform")(check(ArtifactTable.selectArtifactByPlatform))
it("check selectArtifactByLanguageAndPlatform")(check(ArtifactTable.selectArtifactByLanguageAndPlatform))
it("check selectArtifactByGroupIdAndArtifactId")(check(ArtifactTable.selectArtifactByGroupIdAndArtifactId))
it("check selectArtifactByProject")(check(ArtifactTable.selectArtifactByProject))
it("check selectArtifactByProjectAndName")(check(ArtifactTable.selectArtifactByProjectAndName))
it("check findOldestArtifactsPerProjectReference")(check(ArtifactTable.selectOldestByProject))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ object ApiDocumentation
title = "Scaladex API",
version = "0.1.0"
)
)(autocomplete, artifact)
)(autocomplete, artifact, artifactMetadata)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package scaladex.server.route.api

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
import endpoints4s.akkahttp.server
import scaladex.core.api.artifact.ArtifactEndpoints
import scaladex.core.api.artifact.ArtifactMetadataParams
import scaladex.core.api.artifact.ArtifactMetadataResponse
import scaladex.core.api.artifact.ArtifactParams
import scaladex.core.api.artifact.ArtifactResponse
import scaladex.core.model.Artifact
import scaladex.core.model.Language
import scaladex.core.model.Platform
import scaladex.core.model.search.Page
Expand Down Expand Up @@ -41,6 +46,26 @@ class ArtifactApi(database: WebDatabase)(
items = distinctArtifacts
)
}
} ~ artifactMetadata.implementedByAsync {
case ArtifactMetadataParams(groupId, artifactId) =>
val parsedGroupId = Artifact.GroupId(groupId)
Artifact.ArtifactId.parse(artifactId).fold(Future.successful(Page.empty[ArtifactMetadataResponse])) {
parsedArtifactId =>
val futureArtifacts = database.getArtifacts(parsedGroupId, parsedArtifactId)
val futureResponses = futureArtifacts.map(_.map(Artifact.toMetadataResponse))
futureResponses.map { resp =>
// TODO: The values below are placeholders, will need to populate them w. real data.
// See: https://github.com/scalacenter/scaladex/pull/992#discussion_r841500215
Page(
pagination = Pagination(
current = 1,
pageCount = 1,
totalSize = resp.size
),
items = resp
)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import akka.http.scaladsl.server.Route
import cats.implicits.toTraverseOps
import org.scalatest.BeforeAndAfterEach
import play.api.libs.json.Reads
import scaladex.core.api.artifact.ArtifactMetadataResponse
import scaladex.core.api.artifact.ArtifactResponse
import scaladex.core.model.Artifact
import scaladex.core.model.Language
Expand All @@ -25,7 +26,10 @@ class ArtifactApiTests extends ControllerBaseSuite with BeforeAndAfterEach with
val artifactRoute: Route = ArtifactApi(database).routes

implicit val jsonPaginationReader: Reads[Pagination] = PlayJsonCodecs.paginationSchema.reads
implicit def jsonPageReader: Reads[Page[ArtifactResponse]] = PlayJsonCodecs.pageSchema[ArtifactResponse].reads
implicit def jsonArtifactResponsePageReader: Reads[Page[ArtifactResponse]] =
PlayJsonCodecs.pageSchema[ArtifactResponse].reads
implicit def jsonArtifactMetadataResponsePageReader: Reads[Page[ArtifactMetadataResponse]] =
PlayJsonCodecs.pageSchema[ArtifactMetadataResponse].reads

override protected def beforeAll(): Unit = Await.result(insertAllCatsArtifacts(), Duration.Inf)

Expand Down Expand Up @@ -114,16 +118,50 @@ class ArtifactApiTests extends ControllerBaseSuite with BeforeAndAfterEach with
}
}

it("should not return any artifacts given a group id and artifact id for an artifact not stored") {
Get("/api/artifacts/ca.ubc.cs/test-package_3") ~> artifactRoute ~> check {
status shouldBe StatusCodes.OK
val response = responseAs[Page[ArtifactMetadataResponse]]
response shouldBe Page.empty[ArtifactMetadataResponse]
}
}

it("should not return any artifacts given a valid group id and an artifact id that does not parse") {
val malformedArtifactId = "badArtifactId"
Get(s"/api/artifacts/org.apache.spark/$malformedArtifactId") ~> artifactRoute ~> check {
status shouldBe StatusCodes.OK
val response = responseAs[Page[ArtifactMetadataResponse]]
response shouldBe Page.empty[ArtifactMetadataResponse]
}
}

it("should not return any artifacts given an invalid group id and a valid artifact id") {
Get("/api/artifacts/ca.ubc.cs/cats-core_3") ~> artifactRoute ~> check {
status shouldBe StatusCodes.OK
val response = responseAs[Page[ArtifactMetadataResponse]]
response shouldBe Page.empty[ArtifactMetadataResponse]
}
}

it("should return artifacts with the given group id and artifact id") {
Get("/api/artifacts/org.typelevel/cats-core_3") ~> artifactRoute ~> check {
status shouldBe StatusCodes.OK
val response = responseAs[Page[ArtifactMetadataResponse]]
val expectedArtifactMetadata = Seq(Cats.`core_3:2.6.1`, Cats.`core_3:2.7.0`).map(Artifact.toMetadataResponse)
response match {
case Page(_, artifacts) =>
artifacts.size shouldBe 2
artifacts should contain theSameElementsAs expectedArtifactMetadata
}
}
}

it("should not return artifacts if the database is empty") {
database.reset()
Get(s"/api/artifacts") ~> artifactRoute ~> check {
status shouldBe StatusCodes.OK
val response = responseAs[Page[ArtifactResponse]]
response match {
case Page(Pagination(_, _, numStoredArtifacts), artifacts) =>
numStoredArtifacts shouldBe 0
artifacts.size shouldBe 0
}
response shouldBe Page.empty[ArtifactResponse]
}
}
}
Expand Down

0 comments on commit 561acb6

Please sign in to comment.