Skip to content

Commit

Permalink
Merge pull request #1484 from adpi2/add-stable-api
Browse files Browse the repository at this point in the history
Add stable API v1
  • Loading branch information
adpi2 authored Oct 22, 2024
2 parents a306d3c + 5d123ed commit e9ec728
Show file tree
Hide file tree
Showing 114 changed files with 1,902 additions and 1,654 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,17 @@ package scaladex.core.api

import java.time.Instant

import scaladex.core.model.Artifact
import scaladex.core.model.Language
import scaladex.core.model.License
import scaladex.core.model.Platform
import scaladex.core.model.Project
import scaladex.core.model.SemanticVersion
import scaladex.core.model._

final case class ArtifactResponse(
groupId: Artifact.GroupId,
artifactId: String,
version: SemanticVersion,
artifactId: Artifact.ArtifactId,
version: Version,
artifactName: Artifact.Name,
binaryVersion: BinaryVersion,
language: Language,
platform: Platform,
project: Project.Reference,
releaseDate: Instant,
licenses: Seq[License],
language: Language,
platform: Platform
licenses: Seq[License]
)

object ArtifactResponse {
def apply(artifact: Artifact): ArtifactResponse = ArtifactResponse(
artifact.groupId,
artifact.artifactId,
artifact.version,
artifact.artifactName,
artifact.projectRef,
artifact.releaseDate,
artifact.licenses.toSeq,
artifact.language,
artifact.platform
)
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package scaladex.core.api

import scaladex.core.model.Project
import scaladex.core.model.UserState
import scaladex.core.model._
import scaladex.core.model.search.SearchParams
import scaladex.core.model.search.Sorting

case class AutocompletionParams(
query: String,
topics: Seq[String],
languages: Seq[String],
platforms: Seq[String],
languages: Seq[Language],
platforms: Seq[Platform],
contributingSearch: Boolean,
you: Boolean
) {
Expand Down
160 changes: 127 additions & 33 deletions modules/core/shared/src/main/scala/scaladex/core/api/Endpoints.scala
Original file line number Diff line number Diff line change
@@ -1,61 +1,155 @@
package scaladex.core.api

import scaladex.core.model.Artifact
import scaladex.core.model.Project
import endpoints4s.Validated
import scaladex.core.model._

trait Endpoints
extends JsonSchemas
with endpoints4s.algebra.Endpoints
with endpoints4s.algebra.JsonEntitiesFromSchemas {

val v0 = None
val v1: Some[String] = Some("v1")

private val root: Path[Unit] = path / "api"
private def api(version: Option[String]): Path[Unit] = version.fold(root)(version => root / version)
private val projectsPath: Path[Unit] = staticPathSegment("projects")
private val artifactsPath: Path[Unit] = staticPathSegment("artifacts")

private val organizationSegment: Path[Project.Organization] =
segment[Project.Organization]("organization")(stringSegment.xmap(Project.Organization(_))(_.value))
private val repositorySegment: Path[Project.Repository] =
segment[Project.Repository]("repository")(stringSegment.xmap(Project.Repository(_))(_.value))
private val groupIdSegment: Path[Artifact.GroupId] =
segment[Artifact.GroupId]("groupId")(stringSegment.xmap(Artifact.GroupId(_))(_.value))
private val artifactIdSegment: Path[Artifact.ArtifactId] =
segment[Artifact.ArtifactId]("artifactId")(stringSegment.xmap(Artifact.ArtifactId(_))(_.value))
private val versionSegment: Path[Version] = segment[Version]("version")(stringSegment.xmap(Version(_))(_.value))

private val projectPath: Path[Project.Reference] =
(segment[String]("organization") / segment[String]("repository"))
.xmap { case (org, repo) => Project.Reference.from(org, repo) }(ref =>
(ref.organization.value, ref.repository.value)
)

private val artifactPath: Path[Artifact.MavenReference] =
(segment[String]("groupId") / segment[String]("artifactId") / segment[String]("version"))
.xmap { case (groupId, artifactId, version) => Artifact.MavenReference(groupId, artifactId, version) }(ref =>
(ref.groupId, ref.artifactId, ref.version)
)

private val autocompletionQueryString: QueryString[AutocompletionParams] = (
(projectsPath / organizationSegment / repositorySegment)
.xmap((Project.Reference.apply _).tupled)(Function.unlift(Project.Reference.unapply))

private val projectVersionsPath: Path[Project.Reference] = projectPath / "versions"
private val projectArtifactsPath: Path[Project.Reference] = projectPath / "artifacts"

private val artifactPath: Path[Artifact.Reference] =
(artifactsPath / groupIdSegment / artifactIdSegment / versionSegment)
.xmap((Artifact.Reference.apply _).tupled)(Function.unlift(Artifact.Reference.unapply))

private implicit val platformQueryString: QueryStringParam[Platform] = stringQueryString
.xmapPartial(v => Validated.fromOption(Platform.parse(v))(s"Cannot parse $v"))(_.value)
private implicit val languageQueryString: QueryStringParam[Language] = stringQueryString
.xmapPartial(v => Validated.fromOption(Language.parse(v))(s"Cannot parse $v"))(_.value)
private implicit val binaryVersionQueryString: QueryStringParam[BinaryVersion] = stringQueryString
.xmapPartial(v => Validated.fromOption(BinaryVersion.parse(v))(s"Cannot parse $v"))(_.value)

private val languageFilters = qs[Seq[Language]](
"language",
qsDoc("Filter on language versions", Seq("3", "2.13", "2.12", "2.11", "java"))
)

private val platformFilters = qs[Seq[Platform]](
"platform",
qsDoc("Filter on platform versions", Seq("jvm", "sjs1", "native0.5", "sbt1.0", "mill0.11"))
)

private val binaryVersionFilters: QueryString[Seq[BinaryVersion]] =
qs(
"binary-version",
qsDoc("Filter on binary versions", Seq("_2.13", "_3", "_sjs1_3", "_2.12_1.0", "_mill0.11_2.13"))
)

private val binaryVersionFilter: QueryString[Option[BinaryVersion]] =
qs[Option[BinaryVersion]](
"binary-version",
qsDoc("Filter on binary version", Seq("_2.13", "_3", "_sjs1_3", "_2.12_1.0", "_mill0.11_2.13"))
)

private val artifactNameFilters = qs[Seq[String]](
"artifact-name",
qsDoc("Filter on artifact names", Seq("cats-core", "cats-free"))
).xmap(_.map(Artifact.Name.apply))(_.map(_.value))

private val artifactNameFilter = qs[Option[String]](
"artifact-name",
qsDoc("Filter on artifact name", Seq("cats-core", "cats-free"))
).xmap(_.map(Artifact.Name.apply))(_.map(_.value))

private val stableOnlyFilter =
qs[Option[Boolean]]("stable-only", Some("Keep only stable versions. (Default is true)"))
.xmap(o => o.getOrElse(true))(b => Option.when(!b)(b))

private val projectsParams: QueryString[ProjectsParams] =
(languageFilters & platformFilters)
.xmap((ProjectsParams.apply _).tupled)(Function.unlift(ProjectsParams.unapply))

private val projectVersionsParams: QueryString[ProjectVersionsParams] =
(binaryVersionFilters & artifactNameFilters & stableOnlyFilter)
.xmap((ProjectVersionsParams.apply _).tupled)(Function.unlift(ProjectVersionsParams.unapply))

private val projectArtifactsParams: QueryString[ProjectArtifactsParams] =
(binaryVersionFilter & artifactNameFilter & stableOnlyFilter)
.xmap((ProjectArtifactsParams.apply _).tupled)(Function.unlift(ProjectArtifactsParams.unapply))

private val autocompletionParams: QueryString[AutocompletionParams] = (
qs[String]("q", docs = Some("Main query (e.g., 'json', 'testing', etc.)")) &
qs[Seq[String]]("topics", docs = Some("Filter on Github topics")) &
qs[Seq[String]](
"languages",
docs = Some("Filter on language versions (e.g., '3', '2.13', '2.12', '2.11', 'java')")
) &
qs[Seq[String]](
"platforms",
docs = Some("Filter on runtime platforms (e.g., 'jvm', 'sjs1', 'native0.4', 'sbt1.0')")
) &
qs[Seq[String]]("topic", docs = Some("Filter on Github topics")) &
languageFilters &
platformFilters &
qs[Option[Boolean]]("contributingSearch").xmap(_.getOrElse(false))(Option.when(_)(true)) &
qs[Option[String]]("you", docs = Some("internal usage")).xmap[Boolean](_.contains(""))(Option.when(_)(""))
).xmap((AutocompletionParams.apply _).tupled)(Function.unlift(AutocompletionParams.unapply))

val listProjects: Endpoint[Unit, Seq[Project.Reference]] =
def getProjects(v: Option[String]): Endpoint[ProjectsParams, Seq[Project.Reference]] =
endpoint(
get(path / "api" / "projects"),
get(api(v) / projectsPath /? projectsParams),
ok(jsonResponse[Seq[Project.Reference]])
)

val listProjectArtifacts: Endpoint[Project.Reference, Seq[Artifact.MavenReference]] =
val getProjectV1: Endpoint[Project.Reference, Option[ProjectResponse]] =
endpoint(get(api(v1) / projectPath), ok(jsonResponse[ProjectResponse]).orNotFound())

val getProjectVersionsV1: Endpoint[(Project.Reference, ProjectVersionsParams), Seq[Version]] =
endpoint(get(api(v1) / projectVersionsPath /? projectVersionsParams), ok(jsonResponse[Seq[Version]]))

val getLatestProjectVersionV1: Endpoint[Project.Reference, Seq[Artifact.Reference]] =
endpoint(get(api(v1) / projectVersionsPath / "latest"), ok(jsonResponse[Seq[Artifact.Reference]]))

val getProjectVersionV1: Endpoint[(Project.Reference, Version), Seq[Artifact.Reference]] =
endpoint(get(api(v1) / projectVersionsPath / versionSegment), ok(jsonResponse[Seq[Artifact.Reference]]))

def getProjectArtifacts(
v: Option[String]
): Endpoint[(Project.Reference, ProjectArtifactsParams), Seq[Artifact.Reference]] =
endpoint(
get(api(v) / projectArtifactsPath /? projectArtifactsParams),
ok(jsonResponse[Seq[Artifact.Reference]])
)

def getArtifactVersions(
v: Option[String]
): Endpoint[(Artifact.GroupId, Artifact.ArtifactId, Boolean), Seq[Version]] =
endpoint(
get(path / "api" / "projects" / projectPath / "artifacts"),
ok(jsonResponse[Seq[Artifact.MavenReference]])
get(api(v) / artifactsPath / groupIdSegment / artifactIdSegment /? stableOnlyFilter),
ok(jsonResponse[Seq[Version]])
)

val getArtifact: Endpoint[Artifact.MavenReference, Option[ArtifactResponse]] =
def getLatestArtifactV1: Endpoint[(Artifact.GroupId, Artifact.ArtifactId), Option[ArtifactResponse]] =
endpoint(
get(path / "api" / "artifacts" / artifactPath),
get(api(v1) / artifactsPath / groupIdSegment / artifactIdSegment / "latest"),
ok(jsonResponse[ArtifactResponse]).orNotFound()
)

def getArtifact(v: Option[String]): Endpoint[Artifact.Reference, Option[ArtifactResponse]] =
endpoint(get(api(v) / artifactPath), ok(jsonResponse[ArtifactResponse]).orNotFound())

val autocomplete: Endpoint[AutocompletionParams, Seq[AutocompletionResponse]] =
endpoint(
get(path / "api" / "autocomplete" /? autocompletionQueryString),
ok(jsonResponse[Seq[AutocompletionResponse]])
endpoint(get(root / "autocomplete" /? autocompletionParams), ok(jsonResponse[Seq[AutocompletionResponse]]))

private def qsDoc(desc: String, examples: Seq[String]): Some[String] =
Some(
s"""|$desc.
|Examples: ${examples.mkString(", ")}""".stripMargin
)
}
112 changes: 71 additions & 41 deletions modules/core/shared/src/main/scala/scaladex/core/api/JsonSchemas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,83 @@ package scaladex.core.api

import java.time.Instant

import scaladex.core.model.Artifact
import scaladex.core.model.Language
import scaladex.core.model.License
import scaladex.core.model.Platform
import scaladex.core.model.Project
import scaladex.core.model.SemanticVersion
import endpoints4s.Validated
import scaladex.core.model._

/**
* The Json schema of the Scaladex API
*/
trait JsonSchemas extends endpoints4s.algebra.JsonSchemas {
private val stringJson = stringJsonSchema(None)

implicit val organizationSchema: JsonSchema[Project.Organization] = stringJson.xmap(Project.Organization(_))(_.value)
implicit val repositorySchema: JsonSchema[Project.Repository] = stringJson.xmap(Project.Repository(_))(_.value)
implicit val groupIdSchema: JsonSchema[Artifact.GroupId] = stringJson.xmap(Artifact.GroupId(_))(_.value)
implicit val artifactIdSchema: JsonSchema[Artifact.ArtifactId] = stringJson.xmap(Artifact.ArtifactId(_))(_.value)
implicit val versionSchema: JsonSchema[Version] = stringJson.xmap(Version(_))(_.value)
implicit val urlSchema: JsonSchema[Url] = stringJson.xmap(Url(_))(_.target)
implicit val licenseSchema: JsonSchema[License] = stringJson
.xmapPartial(l => Validated.fromOption(License.get(l))(s"Unknown license $l"))(_.shortName)
implicit val artifactNameSchema: JsonSchema[Artifact.Name] = stringJson.xmap(Artifact.Name(_))(_.value)
implicit val categorySchema: JsonSchema[Category] = stringJson
.xmapPartial(c => Validated.fromOption(Category.byLabel.get(c))(s"Unknown category $c"))(_.label)
implicit val binaryVersionSchema: JsonSchema[BinaryVersion] = stringJson
.xmapPartial(l => Validated.fromOption(BinaryVersion.parse(l))(s"Unknown binary version $l"))(_.value)
implicit val language: JsonSchema[Language] = stringJson
.xmapPartial(l => Validated.fromOption(Language.parse(l))(s"Unknown language $l"))(_.value)
implicit val platform: JsonSchema[Platform] = stringJson
.xmapPartial(l => Validated.fromOption(Platform.parse(l))(s"Unknown platform $l"))(_.value)

implicit val projectReferenceSchema: JsonSchema[Project.Reference] =
field[String]("organization")
.zip(field[String]("repository"))
.xmap(Function.tupled(Project.Reference.from(_, _)))(ref => (ref.organization.value, ref.repository.value))

implicit val mavenReferenceSchema: JsonSchema[Artifact.MavenReference] =
field[String]("groupId")
.zip(field[String]("artifactId"))
.zip(field[String]("version"))
.xmap(Function.tupled(Artifact.MavenReference.apply _))(Function.unlift(Artifact.MavenReference.unapply))

implicit val getArtifactResponseSchema: JsonSchema[ArtifactResponse] =
field[String]("groupId")
.xmap(Artifact.GroupId.apply)(_.value)
.zip(field[String]("artifactId"))
.zip(field[String]("version").xmap(SemanticVersion.from)(_.encode))
.zip(field[String]("artifactName").xmap(Artifact.Name.apply)(_.value))
.zip(field[String]("project").xmap(Project.Reference.from)(_.toString))
.zip(field[Long]("releaseDate").xmap(Instant.ofEpochMilli)(_.toEpochMilli))
.zip(field[Seq[String]]("licenses").xmap(_.flatMap(License.get))(_.map(_.shortName)))
.zip(field[String]("language").xmap(Language.fromLabel(_).get)(_.label))
.zip(field[String]("platform").xmap(Platform.fromLabel(_).get)(_.label))
.xmap {
case (groupId, artifactId, version, artifactName, project, releaseDate, licenses, language, platform) =>
ArtifactResponse(
groupId,
artifactId,
version,
artifactName,
project,
releaseDate,
licenses,
language,
platform
)
}(Function.unlift(ArtifactResponse.unapply))
field[Project.Organization]("organization")
.zip(field[Project.Repository]("repository"))
.xmap { case (org, repo) => Project.Reference(org, repo) }(ref => (ref.organization, ref.repository))

implicit val artifactReferenceSchema: JsonSchema[Artifact.Reference] =
field[Artifact.GroupId]("groupId")
.zip(field[Artifact.ArtifactId]("artifactId"))
.zip(field[Version]("version"))
.xmap((Artifact.Reference.apply _).tupled)(Function.unlift(Artifact.Reference.unapply))

implicit val documentationPatternSchema: JsonSchema[DocumentationPattern] =
field[String]("label")
.zip(field[String]("pattern"))
.xmap { case (label, pattern) => DocumentationPattern(label, pattern) }(p => (p.label, p.pattern))

implicit val projectResponseSchema: JsonSchema[ProjectResponse] =
field[Project.Organization]("organization")
.zip(field[Project.Repository]("repository"))
.zip(optField[Url]("homepage"))
.zip(optField[String]("description"))
.zip(optField[Url]("logo"))
.zip(optField[Int]("stars"))
.zip(optField[Int]("forks"))
.zip(optField[Int]("issues"))
.zip(field[Set[String]]("topics"))
.zip(optField[Url]("contributingGuide"))
.zip(optField[Url]("codeOfConduct"))
.zip(optField[License]("license"))
.zip(optField[Artifact.Name]("defaultArtifact"))
.zip(optField[String]("customScalaDoc"))
.zip(field[Seq[DocumentationPattern]]("documentationLinks"))
.zip(field[Boolean]("contributorsWanted"))
.zip(field[Set[Artifact.Name]]("cliArtifacts"))
.zip(optField[Category]("category"))
.zip(optField[String]("chatroom"))
.xmap((ProjectResponse.apply _).tupled)(Function.unlift(ProjectResponse.unapply))

implicit val artifactResponseSchema: JsonSchema[ArtifactResponse] =
field[Artifact.GroupId]("groupId")
.zip(field[Artifact.ArtifactId]("artifactId"))
.zip(field[Version]("version"))
.zip(field[Artifact.Name]("name"))
.zip(field[BinaryVersion]("binaryVersion"))
.zip(field[Language]("language"))
.zip(field[Platform]("platform"))
.zip(field[Project.Reference]("project"))
.zip(field[Instant]("releaseDate"))
.zip(field[Seq[License]]("licenses"))
.xmap((ArtifactResponse.apply _).tupled)(Function.unlift(ArtifactResponse.unapply))

implicit val autocompletionResponseSchema: JsonSchema[AutocompletionResponse] =
field[String]("organization")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package scaladex.core.api

import scaladex.core.model.Artifact
import scaladex.core.model.BinaryVersion

final case class ProjectArtifactsParams(
binaryVersion: Option[BinaryVersion],
artifactName: Option[Artifact.Name],
stableOnly: Boolean
)

This file was deleted.

Loading

0 comments on commit e9ec728

Please sign in to comment.