Skip to content

Commit

Permalink
feat: ProjectCreated event to be reflected in Solr and the API
Browse files Browse the repository at this point in the history
  • Loading branch information
jachro committed Feb 14, 2024
1 parent 721ae07 commit 72ecad4
Show file tree
Hide file tree
Showing 28 changed files with 509 additions and 76 deletions.
10 changes: 5 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ lazy val commons = project
.settings(
name := "commons",
libraryDependencies ++=
Dependencies.catsCore ++
Dependencies.borer ++
Dependencies.catsCore ++
Dependencies.catsEffect ++
Dependencies.fs2Core ++
Dependencies.scodecBits ++
Expand Down Expand Up @@ -173,7 +174,7 @@ lazy val searchSolrClient = project
.dependsOn(
avroCodec % "compile->compile;test->test",
solrClient % "compile->compile;test->test",
commons % "test->test"
commons % "compile->compile;test->test"
)

lazy val avroCodec = project
Expand Down Expand Up @@ -296,9 +297,8 @@ lazy val commonSettings = Seq(
),
Compile / console / scalacOptions := (Compile / scalacOptions).value.filterNot(_ == "-Xfatal-warnings"),
Test / console / scalacOptions := (Compile / console / scalacOptions).value,
libraryDependencies ++= (
Dependencies.scribe
),
libraryDependencies ++=
Dependencies.scribe,
libraryDependencies ++= (
Dependencies.catsEffectMunit ++
Dependencies.scalacheckEffectMunit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.borer.codecs

import cats.syntax.all.*
import io.bullet.borer.Decoder
import io.bullet.borer.Decoder.*

import java.time.Instant
import java.time.format.DateTimeParseException

trait DateTimeDecoders:
given Decoder[Instant] = DateTimeDecoders.forInstant

object DateTimeDecoders:

val forInstant: Decoder[Instant] =
Decoder.forString.mapEither { v =>
Either
.catchOnly[DateTimeParseException](Instant.parse(v))
.leftMap(_.getMessage)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.borer.codecs

import io.bullet.borer.Encoder

import java.time.Instant

trait DateTimeEncoders:
given Encoder[Instant] = DateTimeEncoders.forInstant

object DateTimeEncoders:
val forInstant: Encoder[Instant] = Encoder.forString.contramap[Instant](_.toString)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.borer.codecs

trait all extends DateTimeEncoders, DateTimeDecoders

object all extends all
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.model

import io.bullet.borer.derivation.MapBasedCodecs.*
import io.bullet.borer.{Codec, Decoder, Encoder}
import io.renku.search.borer.codecs.all.given

import java.time.Instant

object projects:

opaque type Id = String
object Id:
def apply(v: String): Id = v
extension (self: Id) def value: String = self
given Codec[Id] = Codec.of[String]

opaque type Name = String
object Name:
def apply(v: String): Name = v
extension (self: Name) def value: String = self
given Codec[Name] = Codec.of[String]

opaque type Slug = String
object Slug:
def apply(v: String): Slug = v
extension (self: Slug) def value: String = self
given Codec[Slug] = Codec.of[String]

opaque type Repository = String
object Repository:
def apply(v: String): Repository = v
extension (self: Repository) def value: String = self
given Codec[Repository] = Codec.of[String]

opaque type Description = String
object Description:
def apply(v: String): Description = v
def from(v: Option[String]): Option[Description] =
v.flatMap {
_.trim match {
case "" => Option.empty[Description]
case o => Option(o)
}
}
extension (self: Description) def value: String = self
given Codec[Description] = Codec.of[String]

opaque type CreationDate = Instant
object CreationDate:
def apply(v: Instant): CreationDate = v
extension (self: CreationDate) def value: Instant = self
given Codec[CreationDate] = Codec.of[Instant]

enum Visibility derives Codec:
lazy val name: String = productPrefix
case Public, Private

object Visibility:
def fromCaseInsensitive(v: String): Visibility =
valueOf(v.toLowerCase.capitalize)
29 changes: 29 additions & 0 deletions modules/commons/src/main/scala/io/renku/search/model/users.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.model

import io.bullet.borer.Codec

object users:

opaque type Id = String
object Id:
def apply(v: String): Id = v
extension (self: Id) def value: String = self
given Codec[Id] = Codec.bimap[String, Id](_.value, Id.apply)
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.UUID

class SerializeDeserializeTest extends FunSuite {
class SerializeDeserializeSpec extends FunSuite {

test("serialize and deserialize ProjectCreated") {
val data = ProjectCreated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import cats.data.EitherT
import cats.effect.*
import cats.syntax.all.*
import fs2.Chunk

import io.bullet.borer.*
import org.http4s.*
import org.http4s.headers.*

object BorerEntities:

def decodeEntityJson[F[_]: Async, A: Decoder]: EntityDecoder[F, A] =
EntityDecoder.decodeBy(MediaType.application.json)(decodeJson)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import org.http4s.dsl.Http4sDsl
import org.http4s.server.Router
import org.http4s.{HttpApp, HttpRoutes, Response}
import sttp.tapir.*
import sttp.tapir.docs.openapi.OpenAPIDocsOptions
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.http4s.Http4sServerInterpreter
import sttp.tapir.swagger.SwaggerUIOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,42 @@ 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.generic.auto.schemaForCaseClass
import sttp.tapir.Schema.SName
import sttp.tapir.SchemaType.SDateTime

final case class Project(id: String, name: String, description: String)
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[Project] = Schema.derivedSchema[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[User] = {
given Schema[users.Id] = Schema.string[users.Id]
Schema.derived[User]
}
given Schema[Project] = Schema.derived[Project]
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ package io.renku.search.api
import cats.effect.Async
import cats.syntax.all.*
import io.renku.search.solr.client.SearchSolrClient
import io.renku.search.solr.documents.Project as SolrProject
import io.renku.search.solr.documents.{Project as SolrProject, User as SolrUser}
import org.http4s.dsl.Http4sDsl
import scribe.Scribe

Expand Down Expand Up @@ -49,4 +49,17 @@ private class SearchApiImpl[F[_]: Async](solrClient: SearchSolrClient[F])
.map(_.asLeft[List[Project]])

private def toApiModel(entities: List[SolrProject]): List[Project] =
entities.map(p => Project(p.id, p.name, p.description))
entities.map { p =>
def toUser(user: SolrUser): User = User(user.id)
Project(
p.id,
p.name,
p.slug,
p.repositories,
p.visibility,
p.description,
toUser(p.createdBy),
p.creationDate,
p.members.map(toUser)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ package io.renku.search.api
import cats.effect.IO
import io.renku.search.solr.client.SearchSolrClientGenerators.*
import io.renku.search.solr.client.SearchSolrSpec
import io.renku.search.solr.documents.Project as SolrProject
import io.renku.search.solr.documents.{Project as SolrProject, User as SolrUser}
import munit.CatsEffectSuite
import scribe.Scribe

Expand All @@ -42,5 +42,16 @@ class SearchApiSpec extends CatsEffectSuite with SearchSolrSpec:
} yield assert(results contains toApiProject(project1))
}

private def toApiProject(project: SolrProject) =
Project(project.id, project.name, project.description)
private def toApiProject(p: SolrProject) =
def toUser(user: SolrUser): User = User(user.id)
Project(
p.id,
p.name,
p.slug,
p.repositories,
p.visibility,
p.description,
toUser(p.createdBy),
p.creationDate,
p.members.map(toUser)
)
Loading

0 comments on commit 72ecad4

Please sign in to comment.