From c07322367382534a0ca4270af8936e4ffad9015d Mon Sep 17 00:00:00 2001 From: Jakub Chrobasik Date: Wed, 21 Feb 2024 14:24:55 +0100 Subject: [PATCH] feat: type discriminator field in the search API response (#24) --- .../io/renku/search/api/HttpApplication.scala | 6 +- .../scala/io/renku/search/api/Project.scala | 64 -------------- .../scala/io/renku/search/api/SearchApi.scala | 2 +- .../io/renku/search/api/SearchApiImpl.scala | 3 +- .../io/renku/search/api/SearchEntity.scala | 85 +++++++++++++++++++ 5 files changed, 91 insertions(+), 69 deletions(-) delete mode 100644 modules/search-api/src/main/scala/io/renku/search/api/Project.scala create mode 100644 modules/search-api/src/main/scala/io/renku/search/api/SearchEntity.scala diff --git a/modules/search-api/src/main/scala/io/renku/search/api/HttpApplication.scala b/modules/search-api/src/main/scala/io/renku/search/api/HttpApplication.scala index e165431f..894c3d41 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/HttpApplication.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/HttpApplication.scala @@ -21,7 +21,6 @@ package io.renku.search.api import cats.effect.{Async, Resource} import cats.syntax.all.* import fs2.io.net.Network -import io.renku.search.api.Project.given import io.renku.search.http.borer.TapirBorerJson import io.renku.solr.client.SolrConfig import org.http4s.dsl.Http4sDsl @@ -60,13 +59,14 @@ class HttpApplication[F[_]: Async](searchApi: SearchApi[F]) searchEndpoint.serverLogic(searchApi.find) ) - private lazy val searchEndpoint: PublicEndpoint[String, String, List[Project], Any] = + private lazy val searchEndpoint + : PublicEndpoint[String, String, List[SearchEntity], Any] = val query = path[String].name("user query").description("User defined query e.g. renku~") endpoint.get .in(query) .errorOut(borerJsonBody[String]) - .out(borerJsonBody[List[Project]]) + .out(borerJsonBody[List[SearchEntity]]) .description("Search API for searching Renku entities") private lazy val swaggerEndpoints = diff --git a/modules/search-api/src/main/scala/io/renku/search/api/Project.scala b/modules/search-api/src/main/scala/io/renku/search/api/Project.scala deleted file mode 100644 index e9daed86..00000000 --- a/modules/search-api/src/main/scala/io/renku/search/api/Project.scala +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2024 Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.renku.search.api - -import io.bullet.borer.derivation.MapBasedCodecs.{deriveDecoder, deriveEncoder} -import io.bullet.borer.{Decoder, Encoder} -import io.renku.search.model.* -import sttp.tapir.Schema -import sttp.tapir.SchemaType.SDateTime - -final case class Project( - id: projects.Id, - name: projects.Name, - slug: projects.Slug, - repositories: Seq[projects.Repository], - visibility: projects.Visibility, - description: Option[projects.Description] = None, - createdBy: User, - creationDate: projects.CreationDate, - members: Seq[User] -) - -final case class User( - id: users.Id -) - -object Project: - given Encoder[User] = deriveEncoder - given Decoder[User] = deriveDecoder - given Encoder[Project] = deriveEncoder - given Decoder[Project] = deriveDecoder - - given Schema[User] = { - given Schema[users.Id] = Schema.string[users.Id] - Schema.derived[User] - } - given Schema[Project] = { - given Schema[projects.Id] = Schema.string[projects.Id] - given Schema[projects.Name] = Schema.string[projects.Name] - given Schema[projects.Slug] = Schema.string[projects.Slug] - given Schema[projects.Repository] = Schema.string[projects.Repository] - given Schema[projects.Visibility] = - Schema.derivedEnumeration[projects.Visibility].defaultStringBased - given Schema[projects.Description] = Schema.string[projects.Description] - given Schema[projects.CreationDate] = Schema(SDateTime()) - - Schema.derived[Project] - } diff --git a/modules/search-api/src/main/scala/io/renku/search/api/SearchApi.scala b/modules/search-api/src/main/scala/io/renku/search/api/SearchApi.scala index 569be6f4..525936ff 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/SearchApi.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/SearchApi.scala @@ -24,7 +24,7 @@ import io.renku.search.solr.client.SearchSolrClient import io.renku.solr.client.SolrConfig trait SearchApi[F[_]]: - def find(phrase: String): F[Either[String, List[Project]]] + def find(phrase: String): F[Either[String, List[SearchEntity]]] object SearchApi: def apply[F[_]: Async: Network]( diff --git a/modules/search-api/src/main/scala/io/renku/search/api/SearchApiImpl.scala b/modules/search-api/src/main/scala/io/renku/search/api/SearchApiImpl.scala index 4b4bf845..e79b5aa7 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/SearchApiImpl.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/SearchApiImpl.scala @@ -31,12 +31,13 @@ private class SearchApiImpl[F[_]: Async](solrClient: SearchSolrClient[F]) private given Scribe[F] = scribe.cats[F] - override def find(phrase: String): F[Either[String, List[Project]]] = + override def find(phrase: String): F[Either[String, List[SearchEntity]]] = solrClient .findProjects(phrase) .map(toApiModel) .map(_.asRight[String]) .handleErrorWith(errorResponse(phrase)) + .widen private def errorResponse( phrase: String diff --git a/modules/search-api/src/main/scala/io/renku/search/api/SearchEntity.scala b/modules/search-api/src/main/scala/io/renku/search/api/SearchEntity.scala new file mode 100644 index 00000000..62c6f2a6 --- /dev/null +++ b/modules/search-api/src/main/scala/io/renku/search/api/SearchEntity.scala @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.renku.search.api + +import io.bullet.borer.derivation.MapBasedCodecs.{deriveAllCodecs, deriveCodec} +import io.bullet.borer.{AdtEncodingStrategy, Codec, Decoder, Encoder} +import io.renku.search.model.* +import sttp.tapir.Schema.SName +import sttp.tapir.SchemaType.{SDateTime, SProductField} +import sttp.tapir.generic.Configuration +import sttp.tapir.{FieldName, Schema, SchemaType} + +sealed trait SearchEntity + +final case class Project( + id: projects.Id, + name: projects.Name, + slug: projects.Slug, + repositories: Seq[projects.Repository], + visibility: projects.Visibility, + description: Option[projects.Description] = None, + createdBy: User, + creationDate: projects.CreationDate, + members: Seq[User] +) extends SearchEntity + +object Project: + private given Schema[projects.Id] = Schema.string[projects.Id] + private given Schema[projects.Name] = Schema.string[projects.Name] + private given Schema[projects.Slug] = Schema.string[projects.Slug] + private given Schema[projects.Repository] = Schema.string[projects.Repository] + private given Schema[projects.Visibility] = + Schema.derivedEnumeration[projects.Visibility].defaultStringBased + private given Schema[projects.Description] = Schema.string[projects.Description] + private given Schema[projects.CreationDate] = Schema(SDateTime()) + given Schema[Project] = Schema.derived[Project] + +final case class User( + id: users.Id +) + +object User: + given Codec[User] = deriveCodec[User] + + private given Schema[users.Id] = Schema.string[users.Id] + given Schema[User] = Schema.derived[User] + +object SearchEntity: + + private val discriminatorField = "type" + given AdtEncodingStrategy = AdtEncodingStrategy.flat(discriminatorField) + given Codec[SearchEntity] = deriveAllCodecs[SearchEntity] + + given Schema[SearchEntity] = { + val derived = Schema.derived[SearchEntity] + derived.schemaType match { + case s: SchemaType.SCoproduct[_] => + derived.copy(schemaType = + s.addDiscriminatorField( + FieldName(discriminatorField), + Schema.string, + List( + implicitly[Schema[Project]].name.map(SchemaType.SRef(_)).map("Project" -> _) + ).flatten.toMap + ) + ) + case s => derived + } + }