From 0eb3c85003fcd73f19b97f4fdfb6261b69930065 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 28 Feb 2024 11:16:42 +0100 Subject: [PATCH] Rework the Scaladex API `GET /api/projects` to get the lists of all project references, except the ones that are moved or deleted on Github. `GET /api/projects///artifacts` to get the lists of artifact refs of a project. `GET /api/artifacts///` to get some info about an artifact (language, platform, licenses, release date etc). There are no filter anymore. Later we can (re-)introduce the `language` and `platform` filters. There is no pagination and I don't plan on working on it. --- build.sbt | 1 - .../scaladex/core/api/ArtifactResponse.scala | 35 ++++ .../core/api/AutocompletionParams.scala | 4 +- .../scala/scaladex/core/api/Endpoints.scala | 61 +++++++ .../scala/scaladex/core/api/JsonSchemas.scala | 50 ++++++ .../scaladex/core/api/PaginationSchema.scala | 25 --- .../scaladex/core/api/ProjectParams.scala | 3 + .../scaladex/core/api/SearchEndpoints.scala | 45 ----- .../api/artifact/ArtifactEndpointSchema.scala | 23 --- .../core/api/artifact/ArtifactEndpoints.scala | 45 ----- .../api/artifact/ArtifactMetadataParams.scala | 3 - .../artifact/ArtifactMetadataResponse.scala | 9 - .../core/api/artifact/ArtifactParams.scala | 3 - .../core/api/artifact/ArtifactResponse.scala | 3 - .../scala/scaladex/core/model/Artifact.scala | 10 -- .../scaladex/core/model/GithubStatus.scala | 11 +- .../scaladex/core/model/SemanticVersion.scala | 2 + .../core/model/search/SearchParams.scala | 4 +- .../scaladex/core/service/SearchEngine.scala | 2 +- .../scaladex/core/test/InMemoryDatabase.scala | 6 +- .../main/scala/scaladex/infra/Codecs.scala | 2 +- .../main/scala/scaladex/server/Server.scala | 8 +- .../server/route/api/ApiDocumentation.scala | 19 +- .../server/route/api/ApiEndpointsImpl.scala | 54 ++++++ .../server/route/api/ArtifactApi.scala | 77 -------- .../route/api/DocumentationRoutes.scala | 4 +- .../scaladex/server/route/api/SearchApi.scala | 33 ---- .../server/service/SearchSynchronizer.scala | 3 +- .../route/api/ApiEndpointsImplTests.scala | 73 ++++++++ .../server/route/api/ArtifactApiTests.scala | 168 ------------------ .../route/api/DocumentationRoutesTests.scala | 3 +- ...ApiTests.scala => OldSearchApiTests.scala} | 0 .../scaladex/server/util/PlayJsonCodecs.scala | 6 - .../scaladex/client/Autocompletion.scala | 2 +- .../src/main/scala/scaladex/client/Dom.scala | 4 +- .../src/main/scala/scaladex/client/RPC.scala | 8 + .../main/scala/scaladex/client/rpc/RPC.scala | 8 - 37 files changed, 322 insertions(+), 495 deletions(-) create mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/ArtifactResponse.scala create mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/Endpoints.scala create mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/JsonSchemas.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/PaginationSchema.scala create mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/ProjectParams.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/SearchEndpoints.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpointSchema.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpoints.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataParams.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataResponse.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactParams.scala delete mode 100644 modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactResponse.scala create mode 100644 modules/server/src/main/scala/scaladex/server/route/api/ApiEndpointsImpl.scala delete mode 100644 modules/server/src/main/scala/scaladex/server/route/api/ArtifactApi.scala delete mode 100644 modules/server/src/main/scala/scaladex/server/route/api/SearchApi.scala create mode 100644 modules/server/src/test/scala/scaladex/server/route/api/ApiEndpointsImplTests.scala delete mode 100644 modules/server/src/test/scala/scaladex/server/route/api/ArtifactApiTests.scala rename modules/server/src/test/scala/scaladex/server/route/api/{SearchApiTests.scala => OldSearchApiTests.scala} (100%) delete mode 100644 modules/server/src/test/scala/scaladex/server/util/PlayJsonCodecs.scala create mode 100644 modules/webclient/src/main/scala/scaladex/client/RPC.scala delete mode 100644 modules/webclient/src/main/scala/scaladex/client/rpc/RPC.scala diff --git a/build.sbt b/build.sbt index 5de2192da..c3f92cad1 100644 --- a/build.sbt +++ b/build.sbt @@ -200,7 +200,6 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) "io.github.cquiroz" %%% "scala-java-time" % "2.5.0", "com.typesafe.play" %%% "play-json" % V.playJson, "org.endpoints4s" %%% "algebra" % "1.11.1", - "org.endpoints4s" %% "json-schema-playjson" % "1.11.1" % Test, "org.scalatest" %%% "scalatest" % V.scalatest % Test, "org.jsoup" % "jsoup" % "1.17.2" ) ++ Seq( diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/ArtifactResponse.scala b/modules/core/shared/src/main/scala/scaladex/core/api/ArtifactResponse.scala new file mode 100644 index 000000000..ecd598a10 --- /dev/null +++ b/modules/core/shared/src/main/scala/scaladex/core/api/ArtifactResponse.scala @@ -0,0 +1,35 @@ +package scaladex.core.api + +import scaladex.core.model.Artifact +import scaladex.core.model.SemanticVersion +import scaladex.core.model.Project +import java.time.Instant +import scaladex.core.model.License +import scaladex.core.model.Platform +import scaladex.core.model.Language + +final case class ArtifactResponse( + groupId: Artifact.GroupId, + artifactId: String, + version: SemanticVersion, + artifactName: Artifact.Name, + project: Project.Reference, + releaseDate: Instant, + licenses: Seq[License], + language: Language, + platform: Platform +) + +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 + ) +} \ No newline at end of file diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/AutocompletionParams.scala b/modules/core/shared/src/main/scala/scaladex/core/api/AutocompletionParams.scala index afcbeee33..ee1410d5b 100644 --- a/modules/core/shared/src/main/scala/scaladex/core/api/AutocompletionParams.scala +++ b/modules/core/shared/src/main/scala/scaladex/core/api/AutocompletionParams.scala @@ -7,11 +7,11 @@ import scaladex.core.model.search.Sorting case class AutocompletionParams( query: String, - you: Boolean, topics: Seq[String], languages: Seq[String], platforms: Seq[String], - contributingSearch: Boolean + contributingSearch: Boolean, + you: Boolean ) { def withUser(user: Option[UserState]): SearchParams = { val userRepos = if (you) user.map(_.repos).getOrElse(Set.empty) else Set.empty[Project.Reference] diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/Endpoints.scala b/modules/core/shared/src/main/scala/scaladex/core/api/Endpoints.scala new file mode 100644 index 000000000..f54c2cad3 --- /dev/null +++ b/modules/core/shared/src/main/scala/scaladex/core/api/Endpoints.scala @@ -0,0 +1,61 @@ +package scaladex.core.api + +import scaladex.core.model.Project +import scaladex.core.model.Artifact + +trait Endpoints + extends JsonSchemas + with endpoints4s.algebra.Endpoints + with endpoints4s.algebra.JsonEntitiesFromSchemas { + + 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] = ( + 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[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]] = + endpoint( + get(path / "api" / "projects"), + ok(jsonResponse[Seq[Project.Reference]]) + ) + + val listProjectArtifacts: Endpoint[Project.Reference, Seq[Artifact.MavenReference]] = + endpoint( + get(path / "api" / "projects" / projectPath / "artifacts"), + ok(jsonResponse[Seq[Artifact.MavenReference]]) + ) + + val getArtifact: Endpoint[Artifact.MavenReference, Option[ArtifactResponse]] = + endpoint( + get(path / "api" / "artifacts" / artifactPath), + ok(jsonResponse[ArtifactResponse]).orNotFound() + ) + + val autocomplete: Endpoint[AutocompletionParams, Seq[AutocompletionResponse]] = + endpoint( + get(path / "api" / "autocomplete" /? autocompletionQueryString), + ok(jsonResponse[Seq[AutocompletionResponse]]) + ) +} diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/JsonSchemas.scala b/modules/core/shared/src/main/scala/scaladex/core/api/JsonSchemas.scala new file mode 100644 index 000000000..c85aa51b5 --- /dev/null +++ b/modules/core/shared/src/main/scala/scaladex/core/api/JsonSchemas.scala @@ -0,0 +1,50 @@ +package scaladex.core.api + +import scaladex.core.model.Artifact +import scaladex.core.model.SemanticVersion +import scaladex.core.model.Project +import java.time.Instant +import scaladex.core.model.License +import scaladex.core.model.Language +import scaladex.core.model.Platform + +/** + * The Json schema of the Scaladex API + */ +trait JsonSchemas extends endpoints4s.algebra.JsonSchemas { + 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)) + + implicit val autocompletionResponseSchema: JsonSchema[AutocompletionResponse] = + field[String]("organization") + .zip(field[String]("repository")) + .zip(field[String]("description")) + .xmap[AutocompletionResponse] { + case (organization, repository, description) => AutocompletionResponse(organization, repository, description) + } { autocompletionResponse => + (autocompletionResponse.organization, autocompletionResponse.repository, autocompletionResponse.description) + } +} diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/PaginationSchema.scala b/modules/core/shared/src/main/scala/scaladex/core/api/PaginationSchema.scala deleted file mode 100644 index 4e4e2d6de..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/PaginationSchema.scala +++ /dev/null @@ -1,25 +0,0 @@ -package scaladex.core.api - -import scaladex.core.model.search.Page -import scaladex.core.model.search.Pagination - -/** - * An API schema that supports pagination should extend this trait. - */ -trait PaginationSchema extends endpoints4s.algebra.JsonSchemas { - - implicit val paginationSchema: JsonSchema[Pagination] = - field[Int]("current") - .zip(field[Int]("pageCount")) - .zip(field[Long]("totalSize")) - .xmap[Pagination] { case (current, pageCount, totalSize) => Pagination(current, pageCount, totalSize) } { - case Pagination(current, pageCount, totalSize) => (current, pageCount, totalSize) - } - - implicit def pageSchema[A: JsonSchema]: JsonSchema[Page[A]] = - field[Pagination]("pagination") - .zip(field[Seq[A]]("items")) - .xmap[Page[A]] { case (pagination, items) => Page(pagination, items) } { - case Page(pagination, items) => (pagination, items) - } -} diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/ProjectParams.scala b/modules/core/shared/src/main/scala/scaladex/core/api/ProjectParams.scala new file mode 100644 index 000000000..5c59f7144 --- /dev/null +++ b/modules/core/shared/src/main/scala/scaladex/core/api/ProjectParams.scala @@ -0,0 +1,3 @@ +package scaladex.core.api + +final case class ProjectParams(language: Option[String], platform: Option[String]) diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/SearchEndpoints.scala b/modules/core/shared/src/main/scala/scaladex/core/api/SearchEndpoints.scala deleted file mode 100644 index 25fd26613..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/SearchEndpoints.scala +++ /dev/null @@ -1,45 +0,0 @@ -package scaladex.core.api - -import endpoints4s.algebra - -// Autocompletion endpoints are implemented by the server and invoked by the web client. -// There definition is shared here, for consistency. -trait SearchEndpoints extends algebra.Endpoints with algebra.JsonEntitiesFromSchemas { - - implicit val autocompletionResponse: JsonSchema[AutocompletionResponse] = - field[String]("organization") - .zip(field[String]("repository")) - .zip(field[String]("description")) - .xmap[AutocompletionResponse] { - case (organization, repository, description) => AutocompletionResponse(organization, repository, description) - } { autocompletionResponse => - (autocompletionResponse.organization, autocompletionResponse.repository, autocompletionResponse.description) - } - - // Definition of the autocompletion query format - val autocompletionParams: QueryString[AutocompletionParams] = ( - qs[String]("q", docs = Some("Main query (e.g., 'json', 'testing', etc.)")) & - qs[Option[String]]("you", docs = Some("Used internally by Scaladex web user interface")) - .xmap[Boolean](_.contains("✓"))(Option.when(_)("✓")) & - qs[Seq[String]]("topics", docs = Some("Filter the results matching the given topics only")) & - qs[Seq[String]]( - "languages", - docs = Some( - "Filter the results matching the given language versions only (e.g., '3', '2.13', '2.12', '2.11', 'java')" - ) - ) & - qs[Seq[String]]( - "platforms", - docs = Some("Filter the results matching the given platforms only (e.g., 'jvm', 'sjs1', 'native0.4', 'sbt1.0')") - ) & - qs[Option[Boolean]]("contributingSearch").xmap(_.getOrElse(false))(Option.when(_)(true)) - ).xmap((AutocompletionParams.apply _).tupled)(Function.unlift(AutocompletionParams.unapply)) - - // Autocomplete endpoint definition - val autocomplete: Endpoint[AutocompletionParams, Seq[AutocompletionResponse]] = - endpoint( - get(path / "api" / "autocomplete" /? autocompletionParams), - ok(jsonResponse[Seq[AutocompletionResponse]]) - ) - -} diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpointSchema.scala b/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpointSchema.scala deleted file mode 100644 index d1d1e2d62..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpointSchema.scala +++ /dev/null @@ -1,23 +0,0 @@ -package scaladex.core.api.artifact - -import scaladex.core.api.PaginationSchema - -trait ArtifactEndpointSchema extends PaginationSchema { - - implicit val artifactResponseSchema: JsonSchema[ArtifactResponse] = - field[String]("groupId") - .zip(field[String]("artifactId")) - .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) - ) -} diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpoints.scala b/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpoints.scala deleted file mode 100644 index 9a6c69a4b..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactEndpoints.scala +++ /dev/null @@ -1,45 +0,0 @@ -package scaladex.core.api.artifact - -import scaladex.core.model.search.Page - -trait ArtifactEndpoints - extends ArtifactEndpointSchema - with endpoints4s.algebra.Endpoints - with endpoints4s.algebra.JsonEntitiesFromSchemas { - - val artifactEndpointParams: QueryString[ArtifactParams] = (qs[Option[String]]( - name = "language", - docs = Some( - "Filter the results matching the given language version only (e.g., '3', '2.13', '2.12', '2.11', 'java')" - ) - ) & qs[Option[String]]( - name = "platform", - 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]]) - ) -} diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataParams.scala b/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataParams.scala deleted file mode 100644 index f8985e4a1..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataParams.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scaladex.core.api.artifact - -final case class ArtifactMetadataParams(groupId: String, artifactId: String) diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataResponse.scala b/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataResponse.scala deleted file mode 100644 index b6872c20d..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactMetadataResponse.scala +++ /dev/null @@ -1,9 +0,0 @@ -package scaladex.core.api.artifact - -final case class ArtifactMetadataResponse( - version: String, - projectReference: Option[String], - releaseDate: String, - language: String, - platform: String -) diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactParams.scala b/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactParams.scala deleted file mode 100644 index 355a9cb4d..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactParams.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scaladex.core.api.artifact - -final case class ArtifactParams(maybeLanguage: Option[String], maybePlatform: Option[String]) diff --git a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactResponse.scala b/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactResponse.scala deleted file mode 100644 index 8945819de..000000000 --- a/modules/core/shared/src/main/scala/scaladex/core/api/artifact/ArtifactResponse.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scaladex.core.api.artifact - -final case class ArtifactResponse(groupId: String, artifactId: String) diff --git a/modules/core/shared/src/main/scala/scaladex/core/model/Artifact.scala b/modules/core/shared/src/main/scala/scaladex/core/model/Artifact.scala index 0bd1e9027..80d4ea54d 100644 --- a/modules/core/shared/src/main/scala/scaladex/core/model/Artifact.scala +++ b/modules/core/shared/src/main/scala/scaladex/core/model/Artifact.scala @@ -7,7 +7,6 @@ import java.time.format.DateTimeFormatter import fastparse.P import fastparse.Start import fastparse._ -import scaladex.core.api.artifact.ArtifactMetadataResponse import scaladex.core.model.PatchVersion import scaladex.core.util.Parsers._ @@ -294,13 +293,4 @@ object Artifact { def repoUrl: String = s"https://repo1.maven.org/maven2/${groupId.replace('.', '/')}/$artifactId/$version/" } - - 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 - ) } diff --git a/modules/core/shared/src/main/scala/scaladex/core/model/GithubStatus.scala b/modules/core/shared/src/main/scala/scaladex/core/model/GithubStatus.scala index 9880a61fb..a1287bd2e 100644 --- a/modules/core/shared/src/main/scala/scaladex/core/model/GithubStatus.scala +++ b/modules/core/shared/src/main/scala/scaladex/core/model/GithubStatus.scala @@ -6,22 +6,27 @@ sealed trait GithubStatus extends Ordered[GithubStatus] { val updateDate: Instant def isOk: Boolean = this match { - case GithubStatus.Ok(_) => true + case _: GithubStatus.Ok => true case _ => false } def isMoved: Boolean = this match { - case GithubStatus.Moved(_, _) => true + case _: GithubStatus.Moved => true case _ => false } + def isUnknown: Boolean = this match { + case _: GithubStatus.Unknown => true + case _ => false + } + def isNotFound: Boolean = this match { case GithubStatus.NotFound(_) => true case _ => false } def isFailed: Boolean = this match { - case GithubStatus.Failed(_, _, _) => true + case _: GithubStatus.Failed => true case _ => false } diff --git a/modules/core/shared/src/main/scala/scaladex/core/model/SemanticVersion.scala b/modules/core/shared/src/main/scala/scaladex/core/model/SemanticVersion.scala index 9fbbeeb6a..d69fddd7a 100644 --- a/modules/core/shared/src/main/scala/scaladex/core/model/SemanticVersion.scala +++ b/modules/core/shared/src/main/scala/scaladex/core/model/SemanticVersion.scala @@ -133,4 +133,6 @@ object SemanticVersion { case Parsed.Success(v, _) => Some(v) case _ => None } + + def from(version: String): SemanticVersion = parse(version).get } diff --git a/modules/core/shared/src/main/scala/scaladex/core/model/search/SearchParams.scala b/modules/core/shared/src/main/scala/scaladex/core/model/search/SearchParams.scala index 0cdc98522..5045dbb9b 100644 --- a/modules/core/shared/src/main/scala/scaladex/core/model/search/SearchParams.scala +++ b/modules/core/shared/src/main/scala/scaladex/core/model/search/SearchParams.scala @@ -14,10 +14,10 @@ case class SearchParams( ) { def toAutocomplete: AutocompletionParams = AutocompletionParams( queryString, - userRepos.nonEmpty, topics, languages, platforms, - contributingSearch + contributingSearch, + userRepos.nonEmpty ) } diff --git a/modules/core/shared/src/main/scala/scaladex/core/service/SearchEngine.scala b/modules/core/shared/src/main/scala/scaladex/core/service/SearchEngine.scala index 957ffc53d..5167f98c4 100644 --- a/modules/core/shared/src/main/scala/scaladex/core/service/SearchEngine.scala +++ b/modules/core/shared/src/main/scala/scaladex/core/service/SearchEngine.scala @@ -2,6 +2,7 @@ package scaladex.core.service import scala.concurrent.Future +import scaladex.core.model.search.Page import scaladex.core.model.BinaryVersion import scaladex.core.model.Category import scaladex.core.model.Language @@ -9,7 +10,6 @@ import scaladex.core.model.Platform import scaladex.core.model.Project import scaladex.core.model.TopicCount import scaladex.core.model.search.AwesomeParams -import scaladex.core.model.search.Page import scaladex.core.model.search.PageParams import scaladex.core.model.search.ProjectDocument import scaladex.core.model.search.ProjectHit diff --git a/modules/core/shared/src/test/scala/scaladex/core/test/InMemoryDatabase.scala b/modules/core/shared/src/test/scala/scaladex/core/test/InMemoryDatabase.scala index a091d0f1b..b4328d780 100644 --- a/modules/core/shared/src/test/scala/scaladex/core/test/InMemoryDatabase.scala +++ b/modules/core/shared/src/test/scala/scaladex/core/test/InMemoryDatabase.scala @@ -88,7 +88,8 @@ class InMemoryDatabase extends SchedulerDatabase { .filter(_.artifactName == artifactName) ) - override def getArtifactByMavenReference(mavenRef: Artifact.MavenReference): Future[Option[Artifact]] = ??? + override def getArtifactByMavenReference(mavenRef: Artifact.MavenReference): Future[Option[Artifact]] = + Future.successful(allArtifacts.values.iterator.flatten.find(a => a.mavenReference == mavenRef)) override def getAllArtifacts( maybeLanguage: Option[Language], @@ -115,7 +116,8 @@ class InMemoryDatabase extends SchedulerDatabase { override def countArtifacts(): Future[Long] = Future.successful(allArtifacts.values.flatten.size) - override def getAllProjectsStatuses(): Future[Map[Project.Reference, GithubStatus]] = ??? + override def getAllProjectsStatuses(): Future[Map[Project.Reference, GithubStatus]] = + Future.successful(allProjects.view.mapValues(p => p.githubStatus).toMap) override def getAllProjects(): Future[Seq[Project]] = ??? diff --git a/modules/infra/src/main/scala/scaladex/infra/Codecs.scala b/modules/infra/src/main/scala/scaladex/infra/Codecs.scala index d6f188b9a..448aa0c79 100644 --- a/modules/infra/src/main/scala/scaladex/infra/Codecs.scala +++ b/modules/infra/src/main/scala/scaladex/infra/Codecs.scala @@ -56,7 +56,7 @@ object Codecs { implicit val projectCodec: Codec[Project] = deriveCodec implicit val groupIdCodec: Codec[Artifact.GroupId] = fromString(_.value, Artifact.GroupId.apply) - implicit val semanticVersionCodec: Codec[SemanticVersion] = fromString(_.encode, SemanticVersion.parse(_).get) + implicit val semanticVersionCodec: Codec[SemanticVersion] = fromString(_.encode, SemanticVersion.from) implicit val platformCodec: Codec[Platform] = fromString(_.label, Platform.fromLabel(_).get) implicit val languageCodec: Codec[Language] = fromString(_.label, Language.fromLabel(_).get) implicit val resolverCodec: Codec[Resolver] = deriveCodec diff --git a/modules/server/src/main/scala/scaladex/server/Server.scala b/modules/server/src/main/scala/scaladex/server/Server.scala index d7bf80ed7..f63e42d52 100644 --- a/modules/server/src/main/scala/scaladex/server/Server.scala +++ b/modules/server/src/main/scala/scaladex/server/Server.scala @@ -145,8 +145,7 @@ object Server extends LazyLogging { val artifactPages = new ArtifactPages(config.env, webDatabase) val awesomePages = new AwesomePages(config.env, searchEngine) val publishApi = new PublishApi(githubAuth, publishProcess) - val searchApi = new SearchApi(searchEngine) - val artifactApi = ArtifactApi(webDatabase) + val apiEndpoints = new ApiEndpointsImpl(webDatabase, searchEngine) val oldSearchApi = new OldSearchApi(searchEngine, webDatabase) val badges = new Badges(webDatabase) val authentication = new AuthenticationApi(config.oAuth2.clientId, config.session, githubAuth, webDatabase) @@ -155,13 +154,12 @@ object Server extends LazyLogging { authentication.optionalUser { user => val apiRoute = concat( publishApi.routes, - searchApi.route(user), - artifactApi.routes, + apiEndpoints.routes(user), oldSearchApi.routes, Assets.routes, badges.route, authentication.routes, - DocumentationRoutes.routes + DocumentationRoute.route ) concat( diff --git a/modules/server/src/main/scala/scaladex/server/route/api/ApiDocumentation.scala b/modules/server/src/main/scala/scaladex/server/route/api/ApiDocumentation.scala index 15cc47ad6..88972dceb 100644 --- a/modules/server/src/main/scala/scaladex/server/route/api/ApiDocumentation.scala +++ b/modules/server/src/main/scala/scaladex/server/route/api/ApiDocumentation.scala @@ -3,22 +3,19 @@ package scaladex.server.route.api import endpoints4s.openapi import endpoints4s.openapi.model.Info import endpoints4s.openapi.model.OpenApi -import scaladex.core.api.SearchEndpoints -import scaladex.core.api.artifact.ArtifactEndpoints +import scaladex.core.api.Endpoints /** * Documentation of the public HTTP API of Scaladex */ object ApiDocumentation - extends SearchEndpoints - with ArtifactEndpoints + extends Endpoints with openapi.Endpoints with openapi.JsonEntitiesFromSchemas { - - val api: OpenApi = openApi( - Info( - title = "Scaladex API", - version = "0.1.0" - ) - )(autocomplete, artifact, artifactMetadata) + val api: OpenApi = openApi(Info(title = "Scaladex API", version = "0.1.0"))( + listProjects, + listProjectArtifacts, + getArtifact, + autocomplete + ) } diff --git a/modules/server/src/main/scala/scaladex/server/route/api/ApiEndpointsImpl.scala b/modules/server/src/main/scala/scaladex/server/route/api/ApiEndpointsImpl.scala new file mode 100644 index 000000000..e8fa8e293 --- /dev/null +++ b/modules/server/src/main/scala/scaladex/server/route/api/ApiEndpointsImpl.scala @@ -0,0 +1,54 @@ +package scaladex.server.route.api + +import scala.concurrent.ExecutionContext + +import endpoints4s.pekkohttp.server +import org.apache.pekko.http.cors.scaladsl.CorsDirectives.cors +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.Route +import scaladex.core.api.Endpoints +import scaladex.core.api.ArtifactResponse +import scaladex.core.service.WebDatabase +import scaladex.core.model.UserState +import scaladex.core.service.SearchEngine +import scaladex.core.api.AutocompletionResponse + +class ApiEndpointsImpl(database: WebDatabase, searchEngine: SearchEngine)( + implicit ec: ExecutionContext +) extends Endpoints + with server.Endpoints + with server.JsonEntitiesFromSchemas { + + def routes(user: Option[UserState]): Route = + cors()( + concat( + listProjects.implementedByAsync { _ => + for (projectStatuses <- database.getAllProjectsStatuses()) yield { + projectStatuses.iterator + .collect { case (ref, status) if status.isOk || status.isFailed || status.isUnknown => ref } + .toSeq + } + }, + listProjectArtifacts.implementedByAsync { ref => + for (artifacts <- database.getArtifacts(ref)) + yield artifacts.map(_.mavenReference) + }, + getArtifact.implementedByAsync { mavenRef => + for (artifactOpt <- database.getArtifactByMavenReference(mavenRef)) + yield artifactOpt.map(ArtifactResponse.apply) + }, + autocomplete.implementedByAsync { params => + val searchParams = params.withUser(user) + for (projects <- searchEngine.autocomplete(searchParams, 5)) + yield projects.map { project => + AutocompletionResponse( + project.organization.value, + project.repository.value, + project.githubInfo.flatMap(_.description).getOrElse("") + ) + } + } + ) + ) + +} diff --git a/modules/server/src/main/scala/scaladex/server/route/api/ArtifactApi.scala b/modules/server/src/main/scala/scaladex/server/route/api/ArtifactApi.scala deleted file mode 100644 index 620faadd2..000000000 --- a/modules/server/src/main/scala/scaladex/server/route/api/ArtifactApi.scala +++ /dev/null @@ -1,77 +0,0 @@ -package scaladex.server.route.api - -import scala.concurrent.ExecutionContext -import scala.concurrent.Future - -import endpoints4s.pekkohttp.server -import org.apache.pekko.http.cors.scaladsl.CorsDirectives.cors -import org.apache.pekko.http.scaladsl.server.Directives._ -import org.apache.pekko.http.scaladsl.server.Route -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 -import scaladex.core.model.search.Pagination -import scaladex.core.service.WebDatabase - -class ArtifactApi(database: WebDatabase)( - implicit ec: ExecutionContext -) extends ArtifactEndpoints - with server.Endpoints - with server.JsonEntitiesFromSchemas { - - val routes: Route = - cors() { - artifact.implementedByAsync { - case ArtifactParams(maybeLanguageStr, maybePlatformStr) => - val maybeLanguage = maybeLanguageStr.flatMap(Language.fromLabel) - val maybePlatform = maybePlatformStr.flatMap(Platform.fromLabel) - for (artifacts <- database.getAllArtifacts(maybeLanguage, maybePlatform)) - yield { - val distinctArtifacts = - artifacts.map(artifact => ArtifactResponse(artifact.groupId.value, artifact.artifactId)).distinct - // 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 = distinctArtifacts.size.toLong - ), - 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 - ) - } - } - } - } -} - -object ArtifactApi { - - def apply(database: WebDatabase)(implicit ec: ExecutionContext): ArtifactApi = - new ArtifactApi(database) -} diff --git a/modules/server/src/main/scala/scaladex/server/route/api/DocumentationRoutes.scala b/modules/server/src/main/scala/scaladex/server/route/api/DocumentationRoutes.scala index 611df6096..a774da807 100644 --- a/modules/server/src/main/scala/scaladex/server/route/api/DocumentationRoutes.scala +++ b/modules/server/src/main/scala/scaladex/server/route/api/DocumentationRoutes.scala @@ -8,8 +8,8 @@ import org.apache.pekko.http.scaladsl.server.Route /** * Akka-Http routes serving the documentation of the public HTTP API of Scaladex */ -object DocumentationRoutes extends server.Endpoints with server.JsonEntitiesFromEncodersAndDecoders { - val routes: Route = cors() { +object DocumentationRoute extends server.Endpoints with server.JsonEntitiesFromEncodersAndDecoders { + val route: Route = cors() { endpoint( get(path / "api" / "open-api.json"), ok(jsonResponse[OpenApi]) diff --git a/modules/server/src/main/scala/scaladex/server/route/api/SearchApi.scala b/modules/server/src/main/scala/scaladex/server/route/api/SearchApi.scala deleted file mode 100644 index daea5eb61..000000000 --- a/modules/server/src/main/scala/scaladex/server/route/api/SearchApi.scala +++ /dev/null @@ -1,33 +0,0 @@ -package scaladex.server.route.api - -import scala.concurrent.ExecutionContext - -import endpoints4s.pekkohttp.server -import org.apache.pekko.http.cors.scaladsl.CorsDirectives._ -import org.apache.pekko.http.scaladsl.server.Route -import scaladex.core.api.AutocompletionResponse -import scaladex.core.api.SearchEndpoints -import scaladex.core.model.UserState -import scaladex.core.service.SearchEngine - -class SearchApi(searchEngine: SearchEngine)( - implicit val executionContext: ExecutionContext -) extends SearchEndpoints - with server.Endpoints - with server.JsonEntitiesFromSchemas { - - def route(user: Option[UserState]): Route = - cors() { - autocomplete.implementedByAsync { params => - val searchParams = params.withUser(user) - for (projects <- searchEngine.autocomplete(searchParams, 5)) - yield projects.map { project => - AutocompletionResponse( - project.organization.value, - project.repository.value, - project.githubInfo.flatMap(_.description).getOrElse("") - ) - } - } - } -} diff --git a/modules/server/src/main/scala/scaladex/server/service/SearchSynchronizer.scala b/modules/server/src/main/scala/scaladex/server/service/SearchSynchronizer.scala index 7dfc7e447..22d137882 100644 --- a/modules/server/src/main/scala/scaladex/server/service/SearchSynchronizer.scala +++ b/modules/server/src/main/scala/scaladex/server/service/SearchSynchronizer.scala @@ -30,8 +30,7 @@ class SearchSynchronizer(database: WebDatabase, searchEngine: SearchEngine)(impl allProjectsAndStatus.collect { case (p, GithubStatus.NotFound(_)) => p.reference } projectsToSync = allProjectsAndStatus .collect { - case (p, GithubStatus.Ok(_) | GithubStatus.Unknown(_) | GithubStatus.Failed(_, _, _)) - if !deprecatedProjects.contains(p.reference) => + case (p, status) if status.isOk || status.isUnknown || status.isFailed && !deprecatedProjects.contains(p.reference) => p } diff --git a/modules/server/src/test/scala/scaladex/server/route/api/ApiEndpointsImplTests.scala b/modules/server/src/test/scala/scaladex/server/route/api/ApiEndpointsImplTests.scala new file mode 100644 index 000000000..2ced1b321 --- /dev/null +++ b/modules/server/src/test/scala/scaladex/server/route/api/ApiEndpointsImplTests.scala @@ -0,0 +1,73 @@ +package scaladex.server.route.api + +import scala.concurrent.Await +import scala.concurrent.Future +import scala.concurrent.duration.Duration + +import org.apache.pekko.http.scaladsl.model.MediaTypes +import org.apache.pekko.http.scaladsl.model.StatusCodes +import org.apache.pekko.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshaller +import org.scalatest.BeforeAndAfterEach +import scaladex.core.api.ArtifactResponse +import scaladex.core.model.Artifact +import scaladex.core.model.Project +import scaladex.core.test.Values._ +import scaladex.server.route.ControllerBaseSuite + +class ApiEndpointsImplTests extends ControllerBaseSuite with BeforeAndAfterEach { + val endpoints = new ApiEndpointsImpl(database, searchEngine) + import endpoints._ + + override protected def beforeAll(): Unit = { + val insertCatsArtifacts = Future.traverse(Cats.allArtifacts)(database.insertArtifact(_, Seq.empty, now)) + Await.result(insertCatsArtifacts, Duration.Inf) + } + + implicit def jsonCodecToUnmarshaller[T: JsonCodec]: FromEntityUnmarshaller[T] = + Unmarshaller.stringUnmarshaller + .forContentTypes(MediaTypes.`application/json`) + .map(data => stringCodec[T].decode(data).toEither.toOption.get) + + it("list all project references") { + Get("/api/projects") ~> routes(None) ~> check { + status shouldBe StatusCodes.OK + val artifacts = responseAs[Seq[Project.Reference]] + val expected = Seq(Cats.reference) + artifacts shouldBe expected + } + } + + it("list all artifact references of project") { + Get(s"/api/projects/${Cats.reference}/artifacts") ~> routes(None) ~> check { + status shouldBe StatusCodes.OK + val artifacts = responseAs[Seq[Artifact.MavenReference]] + val expected = Cats.allArtifacts.map(_.mavenReference) + artifacts.size shouldBe expected.size + artifacts.forall(expected.contains) shouldBe true + } + } + + it("empty list of artifacts for unknown project") { + Get("/api/projects/unknown/unknown/artifacts") ~> routes(None) ~> check { + status shouldBe StatusCodes.OK // TODO this should be not found + val artifacts = responseAs[Seq[Artifact.MavenReference]] + artifacts.size shouldBe 0 + } + } + + it("unknown artifact") { + Get("/api/artifacts/unknown/unknown_3/1.0.0") ~> routes(None) ~> check { + status shouldBe StatusCodes.NotFound + } + } + + it("find artifact") { + Get("/api/artifacts/org.typelevel/cats-core_3/2.6.1") ~> routes(None) ~> check { + status shouldBe StatusCodes.OK + val artifact = responseAs[ArtifactResponse] + val expected = ArtifactResponse(Cats.`core_3:2.6.1`) + artifact shouldBe expected + } + } +} diff --git a/modules/server/src/test/scala/scaladex/server/route/api/ArtifactApiTests.scala b/modules/server/src/test/scala/scaladex/server/route/api/ArtifactApiTests.scala deleted file mode 100644 index 9a216f46e..000000000 --- a/modules/server/src/test/scala/scaladex/server/route/api/ArtifactApiTests.scala +++ /dev/null @@ -1,168 +0,0 @@ -package scaladex.server.route.api - -import scala.concurrent.Await -import scala.concurrent.Future -import scala.concurrent.duration.Duration - -import cats.implicits.toTraverseOps -import org.apache.pekko.http.scaladsl.model.StatusCodes -import org.apache.pekko.http.scaladsl.server.Route -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 -import scaladex.core.model.Platform -import scaladex.core.model.search.Page -import scaladex.core.model.search.Pagination -import scaladex.core.test.Values.Cats -import scaladex.core.test.Values.now -import scaladex.server.route.ControllerBaseSuite -import scaladex.server.util.PlayJsonCodecs - -class ArtifactApiTests extends ControllerBaseSuite with BeforeAndAfterEach with PlayJsonSupport { - - val artifactRoute: Route = ArtifactApi(database).routes - - implicit val jsonPaginationReader: Reads[Pagination] = PlayJsonCodecs.paginationSchema.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) - - private def insertAllCatsArtifacts(): Future[Unit] = - Cats.allArtifacts.traverse(database.insertArtifact(_, Seq.empty, now)).map(_ => ()) - - describe("route") { - it("should return all inserted artifacts, given no language or platform") { - Get("/api/artifacts") ~> artifactRoute ~> check { - status shouldBe StatusCodes.OK - val response = responseAs[Page[ArtifactResponse]] - response match { - case Page(Pagination(_, _, numStoredArtifacts), artifacts) => - val distinctArtifacts = Cats.allArtifacts.distinctBy { artifact: Artifact => - (artifact.groupId.value, artifact.artifactId) - } - numStoredArtifacts shouldBe distinctArtifacts.size - artifacts.size shouldBe distinctArtifacts.size - distinctArtifacts.forall { storedArtifact => - artifacts.exists { case ArtifactResponse(_, artifactId) => storedArtifact.artifactId == artifactId } - } shouldBe true - } - } - } - - it("should be able to query artifacts by their language") { - val expectedResponse = Language - .fromLabel("2.13") - .map { targetLanguage => - Cats.allArtifacts.collect { - case artifact if artifact.language == targetLanguage => (artifact.groupId.value, artifact.artifactId) - } - } - .fold(Seq[(String, String)]())(identity) - Get("/api/artifacts?language=2.13") ~> artifactRoute ~> check { - status shouldBe StatusCodes.OK - val response = responseAs[Page[ArtifactResponse]] - response match { - case Page(_, artifacts) => - artifacts.size shouldBe 2 - artifacts.map { - case ArtifactResponse(groupId, artifactId) => (groupId, artifactId) - } should contain theSameElementsAs expectedResponse - } - } - } - - it("should be able to query artifacts by their platform") { - val expectedResponseArtifacts = Platform - .fromLabel("jvm") - .map { targetPlatform => - val distinctArtifacts = Cats.allArtifacts.distinctBy { artifact: Artifact => - (artifact.groupId.value, artifact.artifactId) - } - distinctArtifacts.collect { - case artifact if artifact.platform == targetPlatform => (artifact.groupId.value, artifact.artifactId) - } - } - .fold(Seq[(String, String)]())(identity) - Get("/api/artifacts?platform=jvm") ~> artifactRoute ~> check { - status shouldBe StatusCodes.OK - val response = responseAs[Page[ArtifactResponse]] - response match { - case Page(_, artifacts) => - artifacts.size shouldBe expectedResponseArtifacts.size - artifacts.map { - case ArtifactResponse(groupId, artifactId) => (groupId, artifactId) - } should contain theSameElementsAs expectedResponseArtifacts - } - } - } - - it("should be able to query artifacts by their language and platform") { - val expectedResponse = - Seq(("org.typelevel", "cats-core_sjs0.6_2.13"), ("org.typelevel", "cats-core_native0.4_2.13")) - Get("/api/artifacts?language=2.13&platform=sjs") ~> artifactRoute ~> check { - status shouldBe StatusCodes.OK - val response = responseAs[Page[ArtifactResponse]] - response match { - case Page(_, artifacts) => - artifacts.size shouldBe 2 - artifacts.map { - case ArtifactResponse(groupId, artifactId) => (groupId, artifactId) - } should contain theSameElementsAs expectedResponse - } - } - } - - 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 shouldBe Page.empty[ArtifactResponse] - } - } - } -} diff --git a/modules/server/src/test/scala/scaladex/server/route/api/DocumentationRoutesTests.scala b/modules/server/src/test/scala/scaladex/server/route/api/DocumentationRoutesTests.scala index e066665f5..d1fa879c7 100644 --- a/modules/server/src/test/scala/scaladex/server/route/api/DocumentationRoutesTests.scala +++ b/modules/server/src/test/scala/scaladex/server/route/api/DocumentationRoutesTests.scala @@ -5,10 +5,9 @@ import play.api.libs.json.JsValue import scaladex.server.route.ControllerBaseSuite class DocumentationRoutesTests extends ControllerBaseSuite with PlayJsonSupport { - describe("route") { it("should serve OpenAPI documentation") { - Get("/api/open-api.json") ~> DocumentationRoutes.routes ~> check { + Get("/api/open-api.json") ~> DocumentationRoute.route ~> check { status shouldBe StatusCodes.OK responseAs[JsValue] shouldNot be(null) } diff --git a/modules/server/src/test/scala/scaladex/server/route/api/SearchApiTests.scala b/modules/server/src/test/scala/scaladex/server/route/api/OldSearchApiTests.scala similarity index 100% rename from modules/server/src/test/scala/scaladex/server/route/api/SearchApiTests.scala rename to modules/server/src/test/scala/scaladex/server/route/api/OldSearchApiTests.scala diff --git a/modules/server/src/test/scala/scaladex/server/util/PlayJsonCodecs.scala b/modules/server/src/test/scala/scaladex/server/util/PlayJsonCodecs.scala deleted file mode 100644 index 567b65dfd..000000000 --- a/modules/server/src/test/scala/scaladex/server/util/PlayJsonCodecs.scala +++ /dev/null @@ -1,6 +0,0 @@ -package scaladex.server.util - -import scaladex.core.api.PaginationSchema -import scaladex.core.api.artifact.ArtifactEndpointSchema - -object PlayJsonCodecs extends PaginationSchema with ArtifactEndpointSchema with endpoints4s.playjson.JsonSchemas diff --git a/modules/webclient/src/main/scala/scaladex/client/Autocompletion.scala b/modules/webclient/src/main/scala/scaladex/client/Autocompletion.scala index ca83f2027..d8fb1349c 100644 --- a/modules/webclient/src/main/scala/scaladex/client/Autocompletion.scala +++ b/modules/webclient/src/main/scala/scaladex/client/Autocompletion.scala @@ -8,7 +8,7 @@ import org.scalajs.dom.HTMLUListElement import org.scalajs.dom.KeyboardEvent import org.scalajs.dom.Node import org.scalajs.dom.ext.KeyCode -import scaladex.client.rpc.RPC +import scaladex.client.RPC import scaladex.core.api.AutocompletionResponse import scalatags.JsDom.all._ diff --git a/modules/webclient/src/main/scala/scaladex/client/Dom.scala b/modules/webclient/src/main/scala/scaladex/client/Dom.scala index 3bc3c6e6b..8ab14e541 100644 --- a/modules/webclient/src/main/scala/scaladex/client/Dom.scala +++ b/modules/webclient/src/main/scala/scaladex/client/Dom.scala @@ -11,11 +11,11 @@ object Dom { for (query <- getSearchQuery) yield AutocompletionParams( query = query, - you = getById[HTMLInputElement]("you").map(_.value).contains("✓"), topics = getSearchFilter("topics"), languages = getSearchFilter("languages"), platforms = getSearchFilter("platforms"), - contributingSearch = getById[HTMLInputElement]("contributing-search").map(_.value).contains("true") + contributingSearch = getById[HTMLInputElement]("contributing-search").map(_.value).contains("true"), + you = getById[HTMLInputElement]("you").map(_.value).contains("✓"), ) def getSearchQuery: Option[String] = diff --git a/modules/webclient/src/main/scala/scaladex/client/RPC.scala b/modules/webclient/src/main/scala/scaladex/client/RPC.scala new file mode 100644 index 000000000..96cb818be --- /dev/null +++ b/modules/webclient/src/main/scala/scaladex/client/RPC.scala @@ -0,0 +1,8 @@ +package scaladex.client + +import endpoints4s.fetch +import scaladex.core.api.Endpoints + +object RPC extends Endpoints with fetch.future.Endpoints with fetch.JsonEntitiesFromSchemas { + override def settings: fetch.EndpointsSettings = fetch.EndpointsSettings() +} diff --git a/modules/webclient/src/main/scala/scaladex/client/rpc/RPC.scala b/modules/webclient/src/main/scala/scaladex/client/rpc/RPC.scala deleted file mode 100644 index 4b713af42..000000000 --- a/modules/webclient/src/main/scala/scaladex/client/rpc/RPC.scala +++ /dev/null @@ -1,8 +0,0 @@ -package scaladex.client.rpc - -import endpoints4s.fetch -import scaladex.core.api.SearchEndpoints - -object RPC extends SearchEndpoints with fetch.future.Endpoints with fetch.JsonEntitiesFromSchemas { - override def settings: fetch.EndpointsSettings = fetch.EndpointsSettings() -}