diff --git a/build.sbt b/build.sbt index 4584918b..a7dbb8ca 100644 --- a/build.sbt +++ b/build.sbt @@ -57,6 +57,7 @@ lazy val root = project } ) .aggregate( + json, commons, jwt, openidKeycloak, @@ -72,6 +73,18 @@ lazy val root = project searchCli ) +lazy val json = project + .in(file("modules/json")) + .settings(commonSettings) + .settings( + name := "json", + // please don't add more dependencies here + libraryDependencies ++= Dependencies.borer, + description := "Utilities around working with borer" + ) + .enablePlugins(AutomateHeaderPlugin) + .disablePlugins(DbTestPlugin, RevolverPlugin) + lazy val commons = project .in(file("modules/commons")) .settings(commonSettings) @@ -81,7 +94,6 @@ lazy val commons = project Dependencies.borer ++ Dependencies.catsCore ++ Dependencies.catsEffect ++ - Dependencies.ducktape ++ Dependencies.fs2Core ++ Dependencies.scodecBits ++ Dependencies.scribe, @@ -102,6 +114,7 @@ lazy val commons = project buildInfoKeys := Seq(name, version, gitHeadCommit, gitDescribedVersion), buildInfoPackage := "io.renku.search" ) + .dependsOn(json % "compile->compile;test->test") .enablePlugins(AutomateHeaderPlugin, BuildInfoPlugin) .disablePlugins(DbTestPlugin, RevolverPlugin) @@ -255,6 +268,7 @@ lazy val solrClient = project ) .dependsOn( httpClient % "compile->compile;test->test", + json % "compile->compile;test->test", commons % "test->test" ) diff --git a/flake.nix b/flake.nix index 1e51f713..f763a0fe 100644 --- a/flake.nix +++ b/flake.nix @@ -84,7 +84,9 @@ RS_CONTAINER = "rsdev"; RS_LOG_LEVEL = "3"; RS_SEARCH_HTTP_SERVER_PORT = "8080"; + RS_SEARCH_HTTP_SHUTDOWN_TIMEOUT = "0ms"; RS_PROVISION_HTTP_SERVER_PORT = "8082"; + RS_PROVISION_HTTP_SHUTDOWN_TIMEOUT = "0ms"; RS_METRICS_UPDATE_INTERVAL = "0s"; RS_SOLR_CREATE_CORE_CMD = "cnt-solr-create-core %s"; RS_SOLR_DELETE_CORE_CMD = "cnt-solr-delete-core %s"; @@ -94,6 +96,7 @@ NO_SOLR = "true"; NO_REDIS = "true"; DEV_CONTAINER = "rsdev-cnt"; + SBT_OPTS = "-Xmx2G"; buildInputs = commonPackages @@ -110,7 +113,9 @@ VM_SSH_PORT = "10022"; RS_LOG_LEVEL = "3"; RS_SEARCH_HTTP_SERVER_PORT = "8080"; + RS_SEARCH_HTTP_SHUTDOWN_TIMEOUT = "0ms"; RS_PROVISION_HTTP_SERVER_PORT = "8082"; + RS_PROVISION_HTTP_SHUTDOWN_TIMEOUT = "0ms"; RS_METRICS_UPDATE_INTERVAL = "0s"; RS_SOLR_CREATE_CORE_CMD = "vm-solr-create-core %s"; RS_SOLR_DELETE_CORE_CMD = "vm-solr-delete-core %s"; @@ -119,8 +124,8 @@ #don't start docker container for dbTests NO_SOLR = "true"; NO_REDIS = "true"; - DEV_VM = "rsdev-vm"; + SBT_OPTS = "-Xmx2G"; buildInputs = commonPackages diff --git a/modules/commons/src/main/scala/io/renku/search/model/CreationDate.scala b/modules/commons/src/main/scala/io/renku/search/model/CreationDate.scala new file mode 100644 index 00000000..36097f2e --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/CreationDate.scala @@ -0,0 +1,33 @@ +/* + * 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 java.time.Instant + +import cats.kernel.Order + +import io.bullet.borer.Codec +import io.renku.json.codecs.all.given + +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] + given Order[CreationDate] = Order.fromComparable[Instant] diff --git a/modules/commons/src/main/scala/io/renku/search/model/groups.scala b/modules/commons/src/main/scala/io/renku/search/model/Description.scala similarity index 62% rename from modules/commons/src/main/scala/io/renku/search/model/groups.scala rename to modules/commons/src/main/scala/io/renku/search/model/Description.scala index c84bb9a1..126ae917 100644 --- a/modules/commons/src/main/scala/io/renku/search/model/groups.scala +++ b/modules/commons/src/main/scala/io/renku/search/model/Description.scala @@ -19,19 +19,16 @@ package io.renku.search.model import io.bullet.borer.Codec -import io.github.arainko.ducktape.Transformer -object groups: - 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) - } +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 Transformer[String, Description] = apply - given Codec[Description] = Codec.of[String] + } + extension (self: Description) def value: String = self + given Codec[Description] = Codec.of[String] diff --git a/modules/commons/src/main/scala/io/renku/search/model/Email.scala b/modules/commons/src/main/scala/io/renku/search/model/Email.scala new file mode 100644 index 00000000..a7ecd8be --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/Email.scala @@ -0,0 +1,27 @@ +/* + * 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 + +opaque type Email = String +object Email: + def apply(v: String): Email = v + extension (self: Email) def value: String = self + given Codec[Email] = Codec.bimap[String, Email](_.value, Email.apply) diff --git a/modules/commons/src/main/scala/io/renku/search/model/Firstname.scala b/modules/commons/src/main/scala/io/renku/search/model/Firstname.scala new file mode 100644 index 00000000..1221c750 --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/Firstname.scala @@ -0,0 +1,27 @@ +/* + * 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 + +opaque type FirstName = String +object FirstName: + def apply(v: String): FirstName = v + extension (self: FirstName) def value: String = self + given Codec[FirstName] = Codec.bimap[String, FirstName](_.value, FirstName.apply) diff --git a/modules/commons/src/main/scala/io/renku/search/model/Id.scala b/modules/commons/src/main/scala/io/renku/search/model/Id.scala index 12b10eb3..5dc5dec3 100644 --- a/modules/commons/src/main/scala/io/renku/search/model/Id.scala +++ b/modules/commons/src/main/scala/io/renku/search/model/Id.scala @@ -21,12 +21,10 @@ package io.renku.search.model import cats.Show import io.bullet.borer.Codec -import io.github.arainko.ducktape.Transformer opaque type Id = String object Id: def apply(v: String): Id = v extension (self: Id) def value: String = self - given Transformer[String, Id] = apply given Codec[Id] = Codec.of[String] given Show[Id] = Show.show(_.value) diff --git a/modules/commons/src/main/scala/io/renku/search/model/LastName.scala b/modules/commons/src/main/scala/io/renku/search/model/LastName.scala new file mode 100644 index 00000000..e3eabb79 --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/LastName.scala @@ -0,0 +1,27 @@ +/* + * 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 + +opaque type LastName = String +object LastName: + def apply(v: String): LastName = v + extension (self: LastName) def value: String = self + given Codec[LastName] = Codec.bimap[String, LastName](_.value, LastName.apply) diff --git a/modules/commons/src/main/scala/io/renku/search/model/Name.scala b/modules/commons/src/main/scala/io/renku/search/model/Name.scala index 894e3198..c94af2d0 100644 --- a/modules/commons/src/main/scala/io/renku/search/model/Name.scala +++ b/modules/commons/src/main/scala/io/renku/search/model/Name.scala @@ -19,11 +19,9 @@ package io.renku.search.model import io.bullet.borer.Codec -import io.github.arainko.ducktape.Transformer opaque type Name = String object Name: def apply(v: String): Name = v extension (self: Name) def value: String = self - given Transformer[String, Name] = apply given Codec[Name] = Codec.of[String] diff --git a/modules/commons/src/main/scala/io/renku/search/model/Repository.scala b/modules/commons/src/main/scala/io/renku/search/model/Repository.scala new file mode 100644 index 00000000..8b161308 --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/Repository.scala @@ -0,0 +1,30 @@ +/* + * 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 cats.kernel.Order + +import io.bullet.borer.Codec + +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] + given Order[Repository] = Order.fromComparable[String] diff --git a/modules/commons/src/main/scala/io/renku/search/model/Slug.scala b/modules/commons/src/main/scala/io/renku/search/model/Slug.scala new file mode 100644 index 00000000..5ec1df3a --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/Slug.scala @@ -0,0 +1,30 @@ +/* + * 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 cats.kernel.Order + +import io.bullet.borer.Codec + +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] + given Order[Slug] = Order.fromComparable[String] diff --git a/modules/commons/src/main/scala/io/renku/search/model/Username.scala b/modules/commons/src/main/scala/io/renku/search/model/Username.scala new file mode 100644 index 00000000..90399cd6 --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/Username.scala @@ -0,0 +1,27 @@ +/* + * 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 + +opaque type Username = String +object Username: + def apply(v: String): Username = v + extension (self: Username) def value: String = self + given Codec[Username] = Codec.bimap[String, Username](_.value, Username.apply) diff --git a/modules/commons/src/main/scala/io/renku/search/model/Visibility.scala b/modules/commons/src/main/scala/io/renku/search/model/Visibility.scala new file mode 100644 index 00000000..c1d6651e --- /dev/null +++ b/modules/commons/src/main/scala/io/renku/search/model/Visibility.scala @@ -0,0 +1,41 @@ +/* + * 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 cats.kernel.Order + +import io.bullet.borer.derivation.MapBasedCodecs.* +import io.bullet.borer.{Decoder, Encoder} + +enum Visibility: + lazy val name: String = productPrefix.toLowerCase + case Public, Private + +object Visibility: + given Order[Visibility] = Order.by(_.ordinal) + given Encoder[Visibility] = Encoder.forString.contramap(_.name) + given Decoder[Visibility] = Decoder.forString.mapEither(Visibility.fromString) + + def fromString(v: String): Either[String, Visibility] = + Visibility.values + .find(_.name.equalsIgnoreCase(v)) + .toRight(s"Invalid visibility: $v") + + def unsafeFromString(v: String): Visibility = + fromString(v).fold(sys.error, identity) diff --git a/modules/commons/src/main/scala/io/renku/search/model/projects.scala b/modules/commons/src/main/scala/io/renku/search/model/projects.scala deleted file mode 100644 index b7a4370d..00000000 --- a/modules/commons/src/main/scala/io/renku/search/model/projects.scala +++ /dev/null @@ -1,81 +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.model - -import java.time.Instant - -import cats.kernel.Order - -import io.bullet.borer.derivation.MapBasedCodecs.* -import io.bullet.borer.{Codec, Decoder, Encoder} -import io.github.arainko.ducktape.* -import io.renku.search.borer.codecs.all.given - -object projects: - opaque type Slug = String - object Slug: - def apply(v: String): Slug = v - extension (self: Slug) def value: String = self - given Transformer[String, Slug] = apply - 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 Transformer[String, Repository] = apply - 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 Transformer[String, Description] = apply - 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 Transformer[Instant, CreationDate] = apply - given Codec[CreationDate] = Codec.of[Instant] - - enum Visibility: - lazy val name: String = productPrefix.toLowerCase - case Public, Private - - object Visibility: - given Order[Visibility] = Order.by(_.ordinal) - given Encoder[Visibility] = Encoder.forString.contramap(_.name) - given Decoder[Visibility] = Decoder.forString.mapEither(Visibility.fromString) - - def fromString(v: String): Either[String, Visibility] = - Visibility.values - .find(_.name.equalsIgnoreCase(v)) - .toRight(s"Invalid visibility: $v") - - def unsafeFromString(v: String): Visibility = - fromString(v).fold(sys.error, identity) diff --git a/modules/commons/src/main/scala/io/renku/search/model/users.scala b/modules/commons/src/main/scala/io/renku/search/model/users.scala deleted file mode 100644 index 788fb685..00000000 --- a/modules/commons/src/main/scala/io/renku/search/model/users.scala +++ /dev/null @@ -1,52 +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.model - -import io.bullet.borer.Codec -import io.github.arainko.ducktape.Transformer - -object users: - - opaque type FirstName = String - object FirstName: - def apply(v: String): FirstName = v - extension (self: FirstName) def value: String = self - given Transformer[String, FirstName] = apply - given Codec[FirstName] = Codec.bimap[String, FirstName](_.value, FirstName.apply) - - opaque type LastName = String - object LastName: - def apply(v: String): LastName = v - extension (self: LastName) def value: String = self - given Transformer[String, LastName] = apply - given Codec[LastName] = Codec.bimap[String, LastName](_.value, LastName.apply) - - opaque type Email = String - object Email: - def apply(v: String): Email = v - extension (self: Email) def value: String = self - given Transformer[String, Email] = apply - given Codec[Email] = Codec.bimap[String, Email](_.value, Email.apply) - - opaque type Username = String - object Username: - def apply(v: String): Username = v - extension (self: Username) def value: String = self - given Transformer[String, Username] = apply - given Codec[Username] = Codec.bimap[String, Username](_.value, Username.apply) diff --git a/modules/commons/src/test/scala/io/renku/search/model/ModelGenerators.scala b/modules/commons/src/test/scala/io/renku/search/model/ModelGenerators.scala index 87ebf4e3..8f2347a4 100644 --- a/modules/commons/src/test/scala/io/renku/search/model/ModelGenerators.scala +++ b/modules/commons/src/test/scala/io/renku/search/model/ModelGenerators.scala @@ -30,8 +30,8 @@ object ModelGenerators: val idGen: Gen[Id] = Gen.uuid.map(uuid => Id(uuid.toString)) val nameGen: Gen[Name] = alphaStringGen(max = 10).map(Name.apply) - val projectDescGen: Gen[projects.Description] = - alphaStringGen(max = 30).map(projects.Description.apply) + val projectDescGen: Gen[Description] = + alphaStringGen(max = 30).map(Description.apply) val timestampGen: Gen[Timestamp] = Gen @@ -47,10 +47,10 @@ object ModelGenerators: val memberRoleGen: Gen[MemberRole] = Gen.oneOf(MemberRole.valuesV2) - val projectVisibilityGen: Gen[projects.Visibility] = - Gen.oneOf(projects.Visibility.values.toList) - val projectCreationDateGen: Gen[projects.CreationDate] = - instantGen().map(projects.CreationDate.apply) + val visibilityGen: Gen[Visibility] = + Gen.oneOf(Visibility.values.toList) + val creationDateGen: Gen[CreationDate] = + instantGen().map(CreationDate.apply) val keywordGen: Gen[Keyword] = Gen @@ -73,25 +73,25 @@ object ModelGenerators: .chooseNum(min.toEpochMilli, max.toEpochMilli) .map(Instant.ofEpochMilli(_).truncatedTo(ChronoUnit.MILLIS)) - val userFirstNameGen: Gen[users.FirstName] = Gen + val userFirstNameGen: Gen[FirstName] = Gen .oneOf("Eike", "Kuba", "Ralf", "Lorenzo", "Jean-Pierre", "Alfonso") - .map(users.FirstName.apply) - val userLastNameGen: Gen[users.LastName] = Gen + .map(FirstName.apply) + val userLastNameGen: Gen[LastName] = Gen .oneOf("Kowalski", "Doe", "Tourist", "Milkman", "Da Silva", "Bar") - .map(users.LastName.apply) - def userEmailGen(first: users.FirstName, last: users.LastName): Gen[users.Email] = Gen + .map(LastName.apply) + def userEmailGen(first: FirstName, last: LastName): Gen[Email] = Gen .oneOf("mail.com", "hotmail.com", "epfl.ch", "ethz.ch") - .map(v => users.Email(s"$first.$last@$v")) - val userEmailGen: Gen[users.Email] = + .map(v => Email(s"$first.$last@$v")) + val userEmailGen: Gen[Email] = ( Gen.oneOf("mail.com", "hotmail.com", "epfl.ch", "ethz.ch"), userFirstNameGen, userLastNameGen - ).mapN((f, l, p) => users.Email(s"$f.$l@$p")) + ).mapN((f, l, p) => Email(s"$f.$l@$p")) val groupNameGen: Gen[Name] = Gen.oneOf( List("sdsc", "renku", "datascience", "rocket-science").map(Name.apply) ) - val groupDescGen: Gen[groups.Description] = - alphaStringGen(max = 5).map(groups.Description.apply) + val groupDescGen: Gen[Description] = + alphaStringGen(max = 5).map(Description.apply) diff --git a/modules/config-values/src/main/scala/io/renku/search/config/ConfigValues.scala b/modules/config-values/src/main/scala/io/renku/search/config/ConfigValues.scala index c906ae5d..4a6c7d6c 100644 --- a/modules/config-values/src/main/scala/io/renku/search/config/ConfigValues.scala +++ b/modules/config-values/src/main/scala/io/renku/search/config/ConfigValues.scala @@ -86,7 +86,8 @@ object ConfigValues extends ConfigDecoders: renv(s"${prefix}_HTTP_SERVER_BIND_ADDRESS").default("0.0.0.0").as[Ipv4Address] val port = renv(s"${prefix}_HTTP_SERVER_PORT").default(defaultPort.value.toString).as[Port] - (bindAddress, port).mapN(HttpServerConfig.apply) + val shutdownTimeout = renv(s"${prefix}_HTTP_SHUTDOWN_TIMEOUT").default("30s").as[Duration] + (bindAddress, port, shutdownTimeout).mapN(HttpServerConfig.apply) val jwtVerifyConfig: ConfigValue[Effect, JwtVerifyConfig] = { val defaults = JwtVerifyConfig.default diff --git a/modules/events/src/main/scala/io/renku/search/events/GroupAdded.scala b/modules/events/src/main/scala/io/renku/search/events/GroupAdded.scala index 10ed9b4f..2b4b788d 100644 --- a/modules/events/src/main/scala/io/renku/search/events/GroupAdded.scala +++ b/modules/events/src/main/scala/io/renku/search/events/GroupAdded.scala @@ -25,7 +25,6 @@ import io.renku.avro.codec.AvroEncoder import io.renku.avro.codec.all.given import io.renku.events.v2 import io.renku.search.model.* -import io.renku.search.model.Id import org.apache.avro.Schema sealed trait GroupAdded extends RenkuEventPayload: @@ -41,7 +40,7 @@ object GroupAdded: id: Id, name: Name, namespace: Namespace, - description: Option[groups.Description] + description: Option[Description] ): GroupAdded = GroupAdded.V2( v2.GroupAdded(id.value, name.value, description.map(_.value), namespace.value) diff --git a/modules/events/src/main/scala/io/renku/search/events/GroupUpdated.scala b/modules/events/src/main/scala/io/renku/search/events/GroupUpdated.scala index 4c9e2463..c6680d21 100644 --- a/modules/events/src/main/scala/io/renku/search/events/GroupUpdated.scala +++ b/modules/events/src/main/scala/io/renku/search/events/GroupUpdated.scala @@ -41,7 +41,7 @@ object GroupUpdated: id: Id, name: Name, namespace: Namespace, - description: Option[groups.Description] + description: Option[Description] ): GroupUpdated = GroupUpdated.V2( v2.GroupUpdated(id.value, name.value, description.map(_.value), namespace.value) diff --git a/modules/events/src/main/scala/io/renku/search/events/ProjectCreated.scala b/modules/events/src/main/scala/io/renku/search/events/ProjectCreated.scala index 7166200d..5bcb02c6 100644 --- a/modules/events/src/main/scala/io/renku/search/events/ProjectCreated.scala +++ b/modules/events/src/main/scala/io/renku/search/events/ProjectCreated.scala @@ -26,7 +26,6 @@ import io.renku.avro.codec.AvroEncoder import io.renku.avro.codec.all.given import io.renku.events.{v1, v2} import io.renku.search.model.* -import io.renku.search.model.projects.Visibility import org.apache.avro.Schema sealed trait ProjectCreated extends RenkuEventPayload: @@ -44,12 +43,12 @@ object ProjectCreated: id: Id, name: Name, namespace: Namespace, - slug: projects.Slug, - visibility: projects.Visibility, + slug: Slug, + visibility: Visibility, createdBy: Id, creationDate: Timestamp, - repositories: Seq[projects.Repository] = Seq(), - description: Option[projects.Description] = None, + repositories: Seq[Repository] = Seq(), + description: Option[Description] = None, keywords: Seq[Keyword] = Seq() ): ProjectCreated = V2( v2.ProjectCreated( diff --git a/modules/events/src/main/scala/io/renku/search/events/ProjectUpdated.scala b/modules/events/src/main/scala/io/renku/search/events/ProjectUpdated.scala index baaa8e58..029cc773 100644 --- a/modules/events/src/main/scala/io/renku/search/events/ProjectUpdated.scala +++ b/modules/events/src/main/scala/io/renku/search/events/ProjectUpdated.scala @@ -25,7 +25,6 @@ import io.renku.avro.codec.AvroEncoder import io.renku.avro.codec.all.given import io.renku.events.{v1, v2} import io.renku.search.model.* -import io.renku.search.model.projects.Visibility import org.apache.avro.Schema sealed trait ProjectUpdated extends RenkuEventPayload: @@ -43,10 +42,10 @@ object ProjectUpdated: id: Id, name: Name, namespace: Namespace, - slug: projects.Slug, - visibility: projects.Visibility, - repositories: Seq[projects.Repository] = Seq(), - description: Option[projects.Description] = None, + slug: Slug, + visibility: Visibility, + repositories: Seq[Repository] = Seq(), + description: Option[Description] = None, keywords: Seq[Keyword] = Seq() ): ProjectUpdated = V2( v2.ProjectUpdated( diff --git a/modules/events/src/main/scala/io/renku/search/events/UserAdded.scala b/modules/events/src/main/scala/io/renku/search/events/UserAdded.scala index 9355ebb8..dc5d18b1 100644 --- a/modules/events/src/main/scala/io/renku/search/events/UserAdded.scala +++ b/modules/events/src/main/scala/io/renku/search/events/UserAdded.scala @@ -24,7 +24,7 @@ import cats.data.NonEmptyList import io.renku.avro.codec.AvroEncoder import io.renku.avro.codec.all.given import io.renku.events.{v1, v2} -import io.renku.search.model.{Id, Namespace, users} +import io.renku.search.model.* import org.apache.avro.Schema sealed trait UserAdded extends RenkuEventPayload: @@ -39,9 +39,9 @@ object UserAdded: def apply( id: Id, namespace: Namespace, - firstName: Option[users.FirstName], - lastName: Option[users.LastName], - email: Option[users.Email] + firstName: Option[FirstName], + lastName: Option[LastName], + email: Option[Email] ): UserAdded = V2( v2.UserAdded( diff --git a/modules/events/src/main/scala/io/renku/search/events/UserUpdated.scala b/modules/events/src/main/scala/io/renku/search/events/UserUpdated.scala index 8a0c746f..e0da9c0e 100644 --- a/modules/events/src/main/scala/io/renku/search/events/UserUpdated.scala +++ b/modules/events/src/main/scala/io/renku/search/events/UserUpdated.scala @@ -24,7 +24,7 @@ import cats.data.NonEmptyList import io.renku.avro.codec.AvroEncoder import io.renku.avro.codec.all.given import io.renku.events.{v1, v2} -import io.renku.search.model.{Id, Namespace, users} +import io.renku.search.model.* import org.apache.avro.Schema sealed trait UserUpdated extends RenkuEventPayload: @@ -39,9 +39,9 @@ object UserUpdated: def apply( id: Id, namespace: Namespace, - firstName: Option[users.FirstName], - lastName: Option[users.LastName], - email: Option[users.Email] + firstName: Option[FirstName], + lastName: Option[LastName], + email: Option[Email] ): UserUpdated = V2( v2.UserUpdated( diff --git a/modules/events/src/main/scala/io/renku/search/events/syntax.scala b/modules/events/src/main/scala/io/renku/search/events/syntax.scala index 1d9ea80a..ae045635 100644 --- a/modules/events/src/main/scala/io/renku/search/events/syntax.scala +++ b/modules/events/src/main/scala/io/renku/search/events/syntax.scala @@ -22,8 +22,6 @@ import java.time.Instant import io.renku.events.{v1, v2} import io.renku.search.model.* -import io.renku.search.model.projects.* -import io.renku.search.model.users.{FirstName, LastName} trait syntax: extension (self: v1.Visibility) @@ -52,8 +50,6 @@ trait syntax: def toFirstName: FirstName = FirstName(self) def toLastName: LastName = LastName(self) def toKeyword: Keyword = Keyword(self) - def toGroupName: Name = toName - def toGroupDescription: groups.Description = groups.Description(self) extension (self: Instant) def toCreationDate: CreationDate = CreationDate(self) diff --git a/modules/events/src/test/scala/io/renku/events/EventsGenerators.scala b/modules/events/src/test/scala/io/renku/events/EventsGenerators.scala index 74b7c0e3..6d8f974e 100644 --- a/modules/events/src/test/scala/io/renku/events/EventsGenerators.scala +++ b/modules/events/src/test/scala/io/renku/events/EventsGenerators.scala @@ -24,10 +24,7 @@ import java.time.temporal.ChronoUnit import io.renku.events.v1.ProjectAuthorizationAdded import io.renku.search.GeneratorSyntax.* import io.renku.search.events.* -import io.renku.search.model.Id -import io.renku.search.model.MemberRole -import io.renku.search.model.ModelGenerators -import io.renku.search.model.users.FirstName +import io.renku.search.model.* import org.apache.avro.Schema import org.scalacheck.Gen import org.scalacheck.Gen.{alphaChar, alphaNumChar} diff --git a/modules/http-client/src/main/scala/io/renku/search/http/ClientBuilder.scala b/modules/http-client/src/main/scala/io/renku/search/http/ClientBuilder.scala index 355108c6..3f9200bd 100644 --- a/modules/http-client/src/main/scala/io/renku/search/http/ClientBuilder.scala +++ b/modules/http-client/src/main/scala/io/renku/search/http/ClientBuilder.scala @@ -61,6 +61,9 @@ final class ClientBuilder[F[_]: Async]( new ClientBuilder[F](delegate.withLogger(LoggerProxy(logger)), mw :: middlewares) } + def withIdleConnectionTime(t: Duration) = + forward(_.withIdleConnectionTime(t)) + private def forward( f: EmberClientBuilder[F] => EmberClientBuilder[F] ): ClientBuilder[F] = diff --git a/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServer.scala b/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServer.scala index 19c26ed1..99218733 100644 --- a/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServer.scala +++ b/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServer.scala @@ -34,5 +34,6 @@ object HttpServer: .default[F] .withHost(config.bindAddress) .withPort(config.port) + .withShutdownTimeout(config.shutdownTimeout) .withHttpApp(routes.orNotFound) .build diff --git a/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServerConfig.scala b/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServerConfig.scala index e7637624..60f4ef3c 100644 --- a/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServerConfig.scala +++ b/modules/http4s-commons/src/main/scala/io/renku/search/http/HttpServerConfig.scala @@ -19,5 +19,11 @@ package io.renku.search.http import com.comcast.ip4s.{Ipv4Address, Port} +import scala.concurrent.duration.Duration -final case class HttpServerConfig(bindAddress: Ipv4Address, port: Port) +final case class HttpServerConfig( + bindAddress: Ipv4Address, + port: Port, + shutdownTimeout: Duration +): + override def toString = s"Http server @ ${bindAddress}:$port" diff --git a/modules/solr-client/src/main/scala/io/renku/solr/client/EncoderSupport.scala b/modules/json/src/main/scala/io/renku/json/EncoderSupport.scala similarity index 99% rename from modules/solr-client/src/main/scala/io/renku/solr/client/EncoderSupport.scala rename to modules/json/src/main/scala/io/renku/json/EncoderSupport.scala index 249a22d4..76687ffd 100644 --- a/modules/solr-client/src/main/scala/io/renku/solr/client/EncoderSupport.scala +++ b/modules/json/src/main/scala/io/renku/json/EncoderSupport.scala @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.renku.solr.client +package io.renku.json import scala.compiletime.* import scala.deriving.* diff --git a/modules/solr-client/src/main/scala/io/renku/solr/client/LabelsMacro.scala b/modules/json/src/main/scala/io/renku/json/LabelsMacro.scala similarity index 98% rename from modules/solr-client/src/main/scala/io/renku/solr/client/LabelsMacro.scala rename to modules/json/src/main/scala/io/renku/json/LabelsMacro.scala index 0b18af65..42c41f4a 100644 --- a/modules/solr-client/src/main/scala/io/renku/solr/client/LabelsMacro.scala +++ b/modules/json/src/main/scala/io/renku/json/LabelsMacro.scala @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.renku.solr.client +package io.renku.json import scala.quoted.* diff --git a/modules/commons/src/main/scala/io/renku/search/borer/codecs/DateTimeDecoders.scala b/modules/json/src/main/scala/io/renku/json/codecs/DateTimeDecoders.scala similarity index 97% rename from modules/commons/src/main/scala/io/renku/search/borer/codecs/DateTimeDecoders.scala rename to modules/json/src/main/scala/io/renku/json/codecs/DateTimeDecoders.scala index f0d11915..ef63e38a 100644 --- a/modules/commons/src/main/scala/io/renku/search/borer/codecs/DateTimeDecoders.scala +++ b/modules/json/src/main/scala/io/renku/json/codecs/DateTimeDecoders.scala @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.renku.search.borer.codecs +package io.renku.json.codecs import java.time.Instant import java.time.format.DateTimeParseException diff --git a/modules/commons/src/main/scala/io/renku/search/borer/codecs/DateTimeEncoders.scala b/modules/json/src/main/scala/io/renku/json/codecs/DateTimeEncoders.scala similarity index 96% rename from modules/commons/src/main/scala/io/renku/search/borer/codecs/DateTimeEncoders.scala rename to modules/json/src/main/scala/io/renku/json/codecs/DateTimeEncoders.scala index fddda8d3..b61906d3 100644 --- a/modules/commons/src/main/scala/io/renku/search/borer/codecs/DateTimeEncoders.scala +++ b/modules/json/src/main/scala/io/renku/json/codecs/DateTimeEncoders.scala @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.renku.search.borer.codecs +package io.renku.json.codecs import java.time.Instant diff --git a/modules/commons/src/main/scala/io/renku/search/borer/codecs/all.scala b/modules/json/src/main/scala/io/renku/json/codecs/all.scala similarity index 95% rename from modules/commons/src/main/scala/io/renku/search/borer/codecs/all.scala rename to modules/json/src/main/scala/io/renku/json/codecs/all.scala index ae13cf7c..d64a531e 100644 --- a/modules/commons/src/main/scala/io/renku/search/borer/codecs/all.scala +++ b/modules/json/src/main/scala/io/renku/json/codecs/all.scala @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.renku.search.borer.codecs +package io.renku.json.codecs trait all extends DateTimeEncoders, DateTimeDecoders diff --git a/modules/solr-client/src/test/scala/io/renku/solr/client/JsonEncodingSpec.scala b/modules/json/src/test/scala/io/renku/json/JsonEncodingSpec.scala similarity index 96% rename from modules/solr-client/src/test/scala/io/renku/solr/client/JsonEncodingSpec.scala rename to modules/json/src/test/scala/io/renku/json/JsonEncodingSpec.scala index d1026969..21be4fc5 100644 --- a/modules/solr-client/src/test/scala/io/renku/solr/client/JsonEncodingSpec.scala +++ b/modules/json/src/test/scala/io/renku/json/JsonEncodingSpec.scala @@ -16,13 +16,13 @@ * limitations under the License. */ -package io.renku.solr.client +package io.renku.json import io.bullet.borer.* import io.bullet.borer.derivation.MapBasedCodecs import io.bullet.borer.derivation.MapBasedCodecs.deriveDecoder import io.bullet.borer.derivation.key -import io.renku.solr.client.JsonEncodingSpec.{Animal, Room} +import io.renku.json.JsonEncodingSpec.{Animal, Room} import munit.FunSuite class JsonEncodingSpec extends FunSuite { diff --git a/modules/search-api/src/main/scala/io/renku/search/api/Microservice.scala b/modules/search-api/src/main/scala/io/renku/search/api/Microservice.scala index 4c9a099e..5612b745 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/Microservice.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/Microservice.scala @@ -24,7 +24,7 @@ import io.renku.logging.LoggingSetup import io.renku.search.http.HttpServer object Microservice extends IOApp: - + private val logger = scribe.cats.io private val loadConfig = SearchApiConfig.config.load[IO] override def run(args: List[String]): IO[ExitCode] = @@ -33,5 +33,9 @@ object Microservice extends IOApp: _ <- IO(LoggingSetup.doConfigure(config.verbosity)) _ <- Routes[IO](config.solrConfig, config.jwtVerifyConfig).makeRoutes .flatMap(HttpServer.build(_, config.httpServerConfig)) - .use(_ => IO.never) + .use { _ => + logger.info( + s"Search microservice running: ${config.httpServerConfig}" + ) >> IO.never + } } yield ExitCode.Success 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 40b2da60..56d4e796 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 @@ -21,10 +21,8 @@ package io.renku.search.api import cats.effect.Async import cats.syntax.all.* -import io.github.arainko.ducktape.* import io.renku.search.api.data.* import io.renku.search.model.EntityType -import io.renku.search.model.Id import io.renku.search.solr.client.SearchSolrClient import io.renku.search.solr.documents.EntityDocument import io.renku.search.solr.schema.EntityDocumentSchema.Fields @@ -80,10 +78,6 @@ private class SearchApiImpl[F[_]: Async](solrClient: SearchSolrClient[F]) FacetData(all) } .getOrElse(FacetData.empty) - val items = solrResult.responseBody.docs.map(toApiEntity) + val items = solrResult.responseBody.docs.map(EntityConverter.apply) if (hasMore) SearchResult(items.init, facets, pageInfo) else SearchResult(items, facets, pageInfo) - - private lazy val toApiEntity: EntityDocument => SearchEntity = - given Transformer[Id, UserId] = (id: Id) => UserId(id) - _.to[SearchEntity] diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala new file mode 100644 index 00000000..8313eb5c --- /dev/null +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala @@ -0,0 +1,69 @@ +/* + * 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.data + +import io.renku.search.solr.documents.{ + Group as GroupDocument, + Project as ProjectDocument, + User as UserDocument, + * +} + +trait EntityConverter: + def project(p: ProjectDocument): SearchEntity.Project = + SearchEntity.Project( + id = p.id, + name = p.name, + slug = p.slug, + namespace = p.namespace, + repositories = p.repositories, + visibility = p.visibility, + description = p.description, + createdBy = SearchEntity.UserId(p.createdBy), + creationDate = p.creationDate, + keywords = p.keywords, + score = p.score + ) + + def user(u: UserDocument): SearchEntity.User = + SearchEntity.User( + id = u.id, + namespace = u.namespace, + firstName = u.firstName, + lastName = u.lastName, + score = u.score + ) + + def group(g: GroupDocument): SearchEntity.Group = + SearchEntity.Group( + id = g.id, + name = g.name, + namespace = g.namespace, + description = g.description, + score = g.score + ) + + def entity(e: EntityDocument): SearchEntity = e match + case p: ProjectDocument => project(p) + case u: UserDocument => user(u) + case g: GroupDocument => group(g) +end EntityConverter + +object EntityConverter extends EntityConverter: + def apply(e: EntityDocument): SearchEntity = entity(e) diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala index 70e0c0f8..781f665c 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala @@ -18,143 +18,50 @@ package io.renku.search.api.data -import java.time.Instant - import io.bullet.borer.* import io.bullet.borer.NullOptions.given import io.bullet.borer.derivation.MapBasedCodecs.{deriveAllCodecs, deriveCodec} -import io.renku.search.api.tapir.SchemaSyntax.* import io.renku.search.model.* -import sttp.tapir.Schema.SName -import sttp.tapir.SchemaType.* -import sttp.tapir.generic.Configuration -import sttp.tapir.{FieldName, Schema, SchemaType} - -sealed trait SearchEntity - -final case class Project( - id: Id, - name: Name, - slug: projects.Slug, - namespace: Option[Namespace], - repositories: Seq[projects.Repository], - visibility: projects.Visibility, - description: Option[projects.Description] = None, - createdBy: UserId, - creationDate: projects.CreationDate, - keywords: List[Keyword] = Nil, - score: Option[Double] = None -) extends SearchEntity - -object Project: - private given Schema[Id] = Schema.string[Id] - private given Schema[Name] = Schema.string[Name] - private given Schema[Namespace] = Schema.string[Namespace] - 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()) - private given Schema[Keyword] = Schema.string[Keyword] - given Schema[Project] = Schema - .derived[Project] - .jsonExample( - Project( - Id("01HRA7AZ2Q234CDQWGA052F8MK"), - Name("renku"), - projects.Slug("renku"), - Some(Namespace("renku/renku")), - Seq(projects.Repository("https://github.com/renku")), - projects.Visibility.Public, - Some(projects.Description("Renku project")), - UserId(Id("1CAF4C73F50D4514A041C9EDDB025A36")), - projects.CreationDate(Instant.now), - List(Keyword("data"), Keyword("science")), - Some(1.0) - ): SearchEntity - ) - -final case class UserId(id: Id) -object UserId: - given Codec[UserId] = deriveCodec[UserId] - - private given Schema[Id] = Schema.string[Id] - given Schema[UserId] = Schema - .derived[UserId] - .jsonExample(UserId(Id("01HRA7AZ2Q234CDQWGA052F8MK"))) - -final case class User( - id: Id, - namespace: Option[Namespace] = None, - firstName: Option[users.FirstName] = None, - lastName: Option[users.LastName] = None, - score: Option[Double] = None -) extends SearchEntity -object User: - private given Schema[Id] = Schema.string[Id] - private given Schema[users.FirstName] = Schema.string[users.FirstName] - private given Schema[users.LastName] = Schema.string[users.LastName] - private given Schema[users.Email] = Schema.string[users.Email] - private given Schema[Namespace] = Schema.string[Namespace] - given Schema[User] = Schema - .derived[User] - .jsonExample( - User( - Id("1CAF4C73F50D4514A041C9EDDB025A36"), - Some(Namespace("renku/renku")), - Some(users.FirstName("Albert")), - Some(users.LastName("Einstein")), - Some(2.1) - ): SearchEntity - ) - -final case class Group( - id: Id, - name: Name, - namespace: Namespace, - description: Option[groups.Description] = None, - score: Option[Double] = None -) extends SearchEntity - -object Group: - private given Schema[Id] = Schema.string[Id] - private given Schema[Name] = Schema.string[Name] - private given Schema[Namespace] = Schema.string[Namespace] - private given Schema[groups.Description] = Schema.string[groups.Description] - given Schema[Group] = Schema - .derived[Group] - .jsonExample( - Group( - Id("2CAF4C73F50D4514A041C9EDDB025A36"), - Name("SDSC"), - Namespace("SDSC"), - Some(groups.Description("SDSC group")), - Some(1.1) - ): SearchEntity - ) +sealed trait SearchEntity: + def id: Id + def score: Option[Double] object SearchEntity: - - private val discriminatorField = "type" + private[api] 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: SCoproduct[_] => - derived.copy(schemaType = - s.addDiscriminatorField( - FieldName(discriminatorField), - Schema.string, - List( - summon[Schema[Project]].name.map(SRef(_)).map("Project" -> _), - summon[Schema[User]].name.map(SRef(_)).map("User" -> _) - ).flatten.toMap - ) - ) - case s => derived - } - } + final case class Project( + id: Id, + name: Name, + slug: Slug, + namespace: Option[Namespace], + repositories: Seq[Repository], + visibility: Visibility, + description: Option[Description] = None, + createdBy: UserId, + creationDate: CreationDate, + keywords: List[Keyword] = Nil, + score: Option[Double] = None + ) extends SearchEntity + + final case class UserId(id: Id) + object UserId: + given Codec[UserId] = deriveCodec[UserId] + + final case class User( + id: Id, + namespace: Option[Namespace] = None, + firstName: Option[FirstName] = None, + lastName: Option[LastName] = None, + score: Option[Double] = None + ) extends SearchEntity + + final case class Group( + id: Id, + name: Name, + namespace: Namespace, + description: Option[Description] = None, + score: Option[Double] = None + ) extends SearchEntity diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchResult.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchResult.scala index c3733270..2b92cc40 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchResult.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchResult.scala @@ -21,7 +21,6 @@ package io.renku.search.api.data import io.bullet.borer.Decoder import io.bullet.borer.Encoder import io.bullet.borer.derivation.MapBasedCodecs -import sttp.tapir.Schema final case class SearchResult( items: Seq[SearchEntity], @@ -32,4 +31,3 @@ final case class SearchResult( object SearchResult: given Encoder[SearchResult] = MapBasedCodecs.deriveEncoder given Decoder[SearchResult] = MapBasedCodecs.deriveDecoder - given Schema[SearchResult] = Schema.derived diff --git a/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala b/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala new file mode 100644 index 00000000..f1086966 --- /dev/null +++ b/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala @@ -0,0 +1,122 @@ +/* + * 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.tapir + +import java.time.Instant + +import io.renku.search.api.data.* +import io.renku.search.api.data.SearchEntity.* +import io.renku.search.api.tapir.SchemaSyntax.* +import io.renku.search.model.* +import sttp.tapir.Schema.SName +import sttp.tapir.SchemaType.* +import sttp.tapir.generic.Configuration +import sttp.tapir.{FieldName, Schema, SchemaType} + +/** tapir schema definitions for the search api data structures */ +trait ApiSchema extends ApiSchema.Primitives: + given Schema[User] = Schema + .derived[User] + .jsonExample(ApiSchema.exampleUser) + + given Schema[Group] = Schema + .derived[Group] + .jsonExample(ApiSchema.exampleGroup) + + given Schema[Project] = Schema + .derived[Project] + .jsonExample(ApiSchema.exampleProject) + + given Schema[UserId] = Schema + .derived[UserId] + .jsonExample(UserId(Id("01HRA7AZ2Q234CDQWGA052F8MK"))) + + given (using + projectSchema: Schema[Project], + userSchema: Schema[User], + groupSchema: Schema[Group] + ): Schema[SearchEntity] = { + val derived = Schema.derived[SearchEntity] + derived.schemaType match { + case s: SCoproduct[_] => + derived.copy(schemaType = + s.addDiscriminatorField( + FieldName(SearchEntity.discriminatorField), + Schema.string, + List( + projectSchema.name.map(SRef(_)).map("Project" -> _), + userSchema.name.map(SRef(_)).map("User" -> _), + groupSchema.name.map(SRef(_)).map("Group" -> _) + ).flatten.toMap + ) + ) + case s => derived + } + } + + given Schema[SearchResult] = Schema.derived +end ApiSchema + +object ApiSchema: + trait Primitives: + given Schema[Id] = Schema.string[Id] + given Schema[Name] = Schema.string[Name] + given Schema[Namespace] = Schema.string[Namespace] + given Schema[Slug] = Schema.string[Slug] + given Schema[Repository] = Schema.string[Repository] + given Schema[Visibility] = + Schema.derivedEnumeration[Visibility].defaultStringBased + given Schema[Description] = Schema.string[Description] + given Schema[CreationDate] = Schema(SDateTime()) + given Schema[Keyword] = Schema.string[Keyword] + given Schema[FirstName] = Schema.string[FirstName] + given Schema[LastName] = Schema.string[LastName] + given Schema[Email] = Schema.string[Email] + end Primitives + + val exampleUser: SearchEntity = User( + Id("1CAF4C73F50D4514A041C9EDDB025A36"), + Some(Namespace("renku/renku")), + Some(FirstName("Albert")), + Some(LastName("Einstein")), + Some(2.1) + ) + + val exampleGroup: SearchEntity = Group( + Id("2CAF4C73F50D4514A041C9EDDB025A36"), + Name("SDSC"), + Namespace("SDSC"), + Some(Description("SDSC group")), + Some(1.1) + ) + + val exampleProject: SearchEntity = Project( + Id("01HRA7AZ2Q234CDQWGA052F8MK"), + Name("renku"), + Slug("renku"), + Some(Namespace("renku/renku")), + Seq(Repository("https://github.com/renku")), + Visibility.Public, + Some(Description("Renku project")), + UserId(Id("bla")), + CreationDate(Instant.now), + List(Keyword("data"), Keyword("science")), + Some(1.0) + ) +end ApiSchema diff --git a/modules/search-api/src/main/scala/io/renku/search/api/tapir/Params.scala b/modules/search-api/src/main/scala/io/renku/search/api/tapir/Params.scala index 0dda5ef6..ea8a8d5f 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/tapir/Params.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/tapir/Params.scala @@ -25,7 +25,7 @@ import io.renku.search.http.borer.TapirBorerJson import io.renku.search.query.Query import sttp.tapir.{query as queryParam, *} -object Params extends TapirCodecs with TapirBorerJson { +object Params extends TapirCodecs with TapirBorerJson with ApiSchema { val query: EndpointInput[Query] = queryParam[Query]("q").description("User defined search query").default(Query.empty) diff --git a/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala b/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala index 66ddb12b..ecd85269 100644 --- a/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala +++ b/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala @@ -21,12 +21,9 @@ package io.renku.search.api import cats.effect.IO import cats.syntax.all.* -import io.github.arainko.ducktape.* import io.renku.search.GeneratorSyntax.* import io.renku.search.api.data.* -import io.renku.search.model.Id -import io.renku.search.model.projects.Visibility -import io.renku.search.model.users.FirstName +import io.renku.search.model.* import io.renku.search.query.Query import io.renku.search.solr.client.SearchSolrSuite import io.renku.search.solr.client.SolrDocumentGenerators.* @@ -92,15 +89,11 @@ class SearchApiSpec extends CatsEffectSuite with SearchSolrSuite: ) private def scoreToNone(e: SearchEntity): SearchEntity = e match - case e: Project => e.copy(score = None) - case e: User => e.copy(score = None) - case e: Group => e.copy(score = None) + case e: SearchEntity.Project => e.copy(score = None) + case e: SearchEntity.User => e.copy(score = None) + case e: SearchEntity.Group => e.copy(score = None) private def mkQuery(phrase: String): QueryInput = QueryInput.pageOne(Query.parse(s"Fields $phrase").fold(sys.error, identity)) - private def toApiEntities(e: EntityDocument*) = e.map(toApiEntity) - - private def toApiEntity(e: EntityDocument) = - given Transformer[Id, UserId] = (id: Id) => UserId(id) - e.to[SearchEntity] + private def toApiEntities(e: EntityDocument*) = e.map(EntityConverter.apply) diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/CommonOpts.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/CommonOpts.scala index f0c2801d..830be96e 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/CommonOpts.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/CommonOpts.scala @@ -23,7 +23,6 @@ import cats.syntax.all.* import com.monovore.decline.Argument import com.monovore.decline.Opts import io.renku.search.model.* -import io.renku.search.model.projects.{Description as ProjectDescription, *} trait CommonOpts: @@ -52,20 +51,20 @@ trait CommonOpts: given Argument[Repository] = Argument.readString.map(Repository(_)) - given Argument[ProjectDescription] = - Argument.readString.map(ProjectDescription(_)) + given Argument[Description] = + Argument.readString.map(Description(_)) given Argument[Keyword] = Argument.readString.map(Keyword(_)) - given Argument[users.FirstName] = - Argument.readString.map(users.FirstName(_)) + given Argument[FirstName] = + Argument.readString.map(FirstName(_)) - given Argument[users.LastName] = - Argument.readString.map(users.LastName(_)) + given Argument[LastName] = + Argument.readString.map(LastName(_)) - given Argument[users.Email] = - Argument.readString.map(users.Email(_)) + given Argument[Email] = + Argument.readString.map(Email(_)) val nameOpt: Opts[Name] = Opts.option[Name]("name", "The name of the entity") @@ -111,16 +110,16 @@ trait CommonOpts: .map(_.toList) .withDefault(Nil) - val projectDescription: Opts[Option[ProjectDescription]] = - Opts.option[ProjectDescription]("description", "The project description").orNone + val projectDescription: Opts[Option[Description]] = + Opts.option[Description]("description", "The project description").orNone - val firstName: Opts[Option[users.FirstName]] = - Opts.option[users.FirstName]("first-name", "The first name").orNone + val firstName: Opts[Option[FirstName]] = + Opts.option[FirstName]("first-name", "The first name").orNone - val lastName: Opts[Option[users.LastName]] = - Opts.option[users.LastName]("last-name", "The last name").orNone + val lastName: Opts[Option[LastName]] = + Opts.option[LastName]("last-name", "The last name").orNone - val email: Opts[Option[users.Email]] = - Opts.option[users.Email]("email", "The email address").orNone + val email: Opts[Option[Email]] = + Opts.option[Email]("email", "The email address").orNone object CommonOpts extends CommonOpts diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabDocsCreator.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabDocsCreator.scala index c96ad90f..984ed5c4 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabDocsCreator.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabDocsCreator.scala @@ -24,11 +24,10 @@ import fs2.Stream import fs2.io.net.Network import io.bullet.borer.Decoder -import io.github.arainko.ducktape.* +import io.renku.search.events.syntax.* import io.renku.search.http.HttpClientDsl import io.renku.search.http.borer.BorerEntityJsonCodec.given import io.renku.search.model.* -import io.renku.search.model.Namespace import io.renku.search.solr.documents.{Project, User} import io.renku.solr.client.DocVersion import org.http4s.* @@ -63,7 +62,7 @@ private class GitLabDocsCreator[F[_]: Async: ModelTypesGenerators]( .takeWhile(_.nonEmpty) .flatMap(Stream.emits) .evalMap(gp => findProjectUsers(gp.id).compile.toList.map(_.distinct).tupleLeft(gp)) - .evalMap(toProject) + .map(toProject) .unNone private def getProjects(page: Int) = @@ -75,34 +74,26 @@ private class GitLabDocsCreator[F[_]: Async: ModelTypesGenerators]( ) client.expect[List[GitLabProject]](req) - private lazy val toProject - : ((GitLabProject, List[User])) => F[Option[(Project, List[User])]] = { - case (glProj, all @ user :: users) => - (glProj - .into[Project] - .transform( - Field.default(_.namespace), - Field.computed(_.keywords, _.tagsAndTopics.map(Keyword.apply)), - Field.default(_.version), - Field.computed(_.id, s => Id(s"gl_proj_${s.id}")), - Field.computed(_.slug, s => projects.Slug(s.path_with_namespace)), - Field - .computed(_.repositories, s => Seq(projects.Repository(s.http_url_to_repo))), - Field.computed(_.visibility, s => s.visibility), - Field.computed(_.createdBy, s => user.id), - Field.computed(_.creationDate, s => projects.CreationDate(s.created_at)), - Field.default(_.owners), - Field.default(_.editors), - Field.default(_.viewers), - Field.default(_.groupOwners), - Field.default(_.groupEditors), - Field.default(_.groupViewers), - Field.default(_.members), - Field.default(_.score) - ) -> all).some.pure[F] - case (name, Nil) => - Option.empty.pure[F] - } + private def toProject( + glProj: GitLabProject, + users: List[User] + ): Option[(Project, List[User])] = + users match + case Nil => None + case creator :: all => + val p = Project( + id = Id(s"gl_proj_${glProj.id}"), + name = glProj.name.toName, + slug = glProj.path_with_namespace.toSlug, + repositories = Seq(glProj.http_url_to_repo.toRepository), + visibility = glProj.visibility, + description = glProj.description.map(_.toDescription), + createdBy = creator.id, + creationDate = glProj.created_at.toCreationDate, + keywords = glProj.tagsAndTopics.map(_.toKeyword), + namespace = glProj.namespace.toNamespace.some + ) + Some(p -> users) private def findProjectUsers(projectId: Int) = Stream @@ -121,32 +112,20 @@ private class GitLabDocsCreator[F[_]: Async: ModelTypesGenerators]( ) client.expect[List[GitLabProjectUser]](req) - private def toUser(glUser: GitLabProjectUser): User = - val firstAndLast = toFirstAndLast(glUser.name.trim) - glUser - .into[User] - .transform( - Field.computed(_.namespace, u => Namespace(u.username).some), - Field.default(_.version), - Field.computed(_.id, s => Id(s"gl_user_${s.id}")), - Field.computed( - _.firstName, - s => - firstAndLast.map(_._1).flatMap { - case v if v.value.trim.isBlank => None - case v => v.some - } - ), - Field.computed( - _.lastName, - s => - firstAndLast.map(_._2).flatMap { - case v if v.value.trim.isBlank => None - case v => v.some - } - ), - Field.default(_.score) - ) + private def toUser(u: GitLabProjectUser): User = + val firstAndLast = toFirstAndLast(u.name.trim) + User( + id = Id(s"gl_user_${u.id}"), + namespace = u.username.toNamespace.some, + firstName = firstAndLast.map(_._1).flatMap { + case v if v.value.trim.isBlank => None + case v => v.some + }, + lastName = firstAndLast.map(_._2).flatMap { + case v if v.value.trim.isBlank => None + case v => v.some + } + ) private lazy val apiV4 = gitLabUri / "api" / "v4" @@ -154,8 +133,8 @@ private class GitLabDocsCreator[F[_]: Async: ModelTypesGenerators]( GET(uri) .putHeaders(Accept(application.json)) - private def toFirstAndLast(v: String): Option[(users.FirstName, users.LastName)] = + private def toFirstAndLast(v: String): Option[(FirstName, LastName)] = v.trim.split(' ').toList match { - case f :: r => Some(users.FirstName(f) -> users.LastName(r.mkString(" "))) + case f :: r => Some(FirstName(f) -> LastName(r.mkString(" "))) case _ => None } diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabEntities.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabEntities.scala index 94c24eac..a94f2bb5 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabEntities.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/GitLabEntities.scala @@ -26,8 +26,8 @@ import io.bullet.borer.Decoder import io.bullet.borer.NullOptions.given import io.bullet.borer.derivation.MapBasedCodecs.deriveDecoder import io.bullet.borer.derivation.key -import io.renku.search.borer.codecs.DateTimeDecoders -import io.renku.search.model.projects +import io.renku.json.codecs.DateTimeDecoders +import io.renku.search.model.Visibility final private case class GitLabProject( id: Int, @@ -39,11 +39,16 @@ final private case class GitLabProject( @key("tag_list") tagList: List[String], topics: List[String] ): - val visibility: projects.Visibility = projects.Visibility.Public + val visibility: Visibility = Visibility.Public lazy val tagsAndTopics: List[String] = (tagList ::: topics).distinct + lazy val namespace: String = + path_with_namespace.lastIndexOf('/') match + case n if n > 0 => path_with_namespace.drop(n) + case _ => path_with_namespace + private object GitLabProject extends DateTimeDecoders: given Decoder[GitLabProject] = deriveDecoder diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/ModelTypesGenerators.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/ModelTypesGenerators.scala index d318ebfa..9c6bff49 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/ModelTypesGenerators.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/ModelTypesGenerators.scala @@ -28,8 +28,7 @@ import cats.effect.std.{Random, UUIDGen} import cats.syntax.all.* import io.renku.search.events.* -import io.renku.search.model.MemberRole -import io.renku.search.model.{Id, projects} +import io.renku.search.model.* private object ModelTypesGenerators: @@ -49,16 +48,16 @@ private trait ModelTypesGenerators[F[_]: Monad: Random: UUIDGen]: def generateId: F[Id] = UUIDGen.randomString[F].map(_.replace("-", "").toUpperCase).map(Id(_)) - def generateCreationDate: F[projects.CreationDate] = + def generateCreationDate: F[CreationDate] = Random[F] .betweenLong( Instant.now().minus(5 * 365, DAYS).toEpochMilli, Instant.now().toEpochMilli ) .map(Instant.ofEpochMilli) - .map(projects.CreationDate.apply) - def generateVisibility: F[projects.Visibility] = - Random[F].shuffleList(projects.Visibility.values.toList).map(_.head) + .map(CreationDate.apply) + def generateVisibility: F[Visibility] = + Random[F].shuffleList(Visibility.values.toList).map(_.head) def generateRole: F[MemberRole] = Random[F].shuffleList(MemberRole.values.toList).map(_.head) diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala index 79e6ad9a..78ba9b35 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala @@ -68,11 +68,10 @@ private class RandommerIoDocsCreator[F[_]: Async: ModelTypesGenerators]( ) client.expect[List[String]](req).map(_.flatMap(toFirstAndLast)) - private lazy val toUser: ((users.FirstName, users.LastName)) => F[User] = { - case (first, last) => - gens.generateId.map(id => - User(id, DocVersion.NotExists, first.some, last.some, Name(s"$first $last").some) - ) + private lazy val toUser: ((FirstName, LastName)) => F[User] = { case (first, last) => + gens.generateId.map(id => + User(id, DocVersion.NotExists, first.some, last.some, Name(s"$first $last").some) + ) } override def findProject: Stream[F, (Project, List[User])] = @@ -86,7 +85,7 @@ private class RandommerIoDocsCreator[F[_]: Async: ModelTypesGenerators]( private def toProject( name: Name, - desc: projects.Description, + desc: Description, keywords: List[Keyword], user: User ): F[(Project, List[User])] = @@ -98,7 +97,7 @@ private class RandommerIoDocsCreator[F[_]: Async: ModelTypesGenerators]( name = name, slug = slug, repositories = Seq(createRepo(slug)), - visibility = projects.Visibility.Public, + visibility = Visibility.Public, description = Some(desc), keywords = keywords, createdBy = user.id, @@ -107,14 +106,14 @@ private class RandommerIoDocsCreator[F[_]: Async: ModelTypesGenerators]( } private def createSlug(name: Name, user: User) = - projects.Slug { + Slug { val nameConditioned = name.value.replace(" ", "_") val namespace = user.name.map(_.value.replace(" ", "_")).getOrElse(nameConditioned) s"$namespace/$nameConditioned".toLowerCase } - private def createRepo(slug: projects.Slug) = - projects.Repository(s"https://github.com/$slug") + private def createRepo(slug: Slug) = + Repository(s"https://github.com/$slug") private lazy val findName: Stream[F, Name] = Stream.evals(getNameSuggestions).map(Name.apply) ++ findName @@ -126,12 +125,12 @@ private class RandommerIoDocsCreator[F[_]: Async: ModelTypesGenerators]( ) client.expect[List[String]](req) - private lazy val findDescription: Stream[F, projects.Description] = + private lazy val findDescription: Stream[F, Description] = Stream .evals(getReviews) .zip(Stream.evals(getBusinessNames)) .flatMap { case (l, r) => Stream(l, r) } - .map(projects.Description.apply) ++ findDescription + .map(Description.apply) ++ findDescription private lazy val getReviews: F[List[String]] = val req = post( @@ -168,8 +167,8 @@ private class RandommerIoDocsCreator[F[_]: Async: ModelTypesGenerators]( .putHeaders(Accept(application.json)) .putHeaders(Header.Raw(ci"X-Api-Key", apiKey)) - private def toFirstAndLast(v: String): Option[(users.FirstName, users.LastName)] = + private def toFirstAndLast(v: String): Option[(FirstName, LastName)] = v.split(' ').toList match { - case f :: r => Some(users.FirstName(f) -> users.LastName(r.mkString(" "))) + case f :: r => Some(FirstName(f) -> LastName(r.mkString(" "))) case _ => None } diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/projects/CreateCmd.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/projects/CreateCmd.scala index b74ac6df..aa9dcbfa 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/projects/CreateCmd.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/projects/CreateCmd.scala @@ -33,12 +33,12 @@ object CreateCmd extends CommonOpts: id: Id, name: Name, namespace: Namespace, - slug: projects.Slug, - visibility: projects.Visibility, + slug: Slug, + visibility: Visibility, createdBy: Id, creationDate: Timestamp, - repositories: Seq[projects.Repository], - description: Option[projects.Description], + repositories: Seq[Repository], + description: Option[Description], keywords: Seq[Keyword] ): def asPayload: ProjectCreated = ProjectCreated( diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/projects/UpdateCmd.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/projects/UpdateCmd.scala index 49ec6582..39b8ce21 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/projects/UpdateCmd.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/projects/UpdateCmd.scala @@ -33,10 +33,10 @@ object UpdateCmd extends CommonOpts: id: Id, name: Name, namespace: Namespace, - slug: projects.Slug, - visibility: projects.Visibility, - repositories: Seq[projects.Repository], - description: Option[projects.Description], + slug: Slug, + visibility: Visibility, + repositories: Seq[Repository], + description: Option[Description], keywords: Seq[Keyword] ): def asPayload: ProjectUpdated = ProjectUpdated( diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/users/AddCmd.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/users/AddCmd.scala index 34597645..f0bf4519 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/users/AddCmd.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/users/AddCmd.scala @@ -26,7 +26,6 @@ import io.renku.search.cli.{CommonOpts, Services} import io.renku.search.config.QueuesConfig import io.renku.search.events.UserAdded import io.renku.search.model.* -import io.renku.search.model.users.* object AddCmd: diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/users/UpdateCmd.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/users/UpdateCmd.scala index 03c9c084..a69e79a2 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/users/UpdateCmd.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/users/UpdateCmd.scala @@ -26,7 +26,6 @@ import io.renku.search.cli.{CommonOpts, Services} import io.renku.search.config.QueuesConfig import io.renku.search.events.UserUpdated import io.renku.search.model.* -import io.renku.search.model.users.* object UpdateCmd: diff --git a/modules/search-provision/src/main/scala/io/renku/search/provision/events/Groups.scala b/modules/search-provision/src/main/scala/io/renku/search/provision/events/Groups.scala index 3106c486..fd92dec2 100644 --- a/modules/search-provision/src/main/scala/io/renku/search/provision/events/Groups.scala +++ b/modules/search-provision/src/main/scala/io/renku/search/provision/events/Groups.scala @@ -31,9 +31,9 @@ trait Groups: GroupDocument( id = ga.id.toId, version = version, - name = ga.name.toGroupName, + name = ga.name.toName, namespace = ga.namespace.toNamespace, - description = ga.description.map(_.toGroupDescription) + description = ga.description.map(_.toDescription) ) def fromGroupUpdated( @@ -43,17 +43,17 @@ trait Groups: PartialEntityDocument.Group( id = ga.id.toId, version = version, - name = Some(ga.name.toGroupName), + name = Some(ga.name.toName), namespace = Some(ga.namespace.toNamespace), - description = ga.description.map(_.toGroupDescription) + description = ga.description.map(_.toDescription) ) def fromGroupUpdated(gu: v2.GroupUpdated, orig: GroupDocument): GroupDocument = orig.copy( id = gu.id.toId, - name = gu.name.toGroupName, + name = gu.name.toName, namespace = gu.namespace.toNamespace, - description = gu.description.map(_.toGroupDescription) + description = gu.description.map(_.toDescription) ) def fromGroupUpdated( @@ -62,9 +62,9 @@ trait Groups: ): PartialEntityDocument.Group = orig.copy( id = gu.id.toId, - name = Some(gu.name.toGroupName), + name = Some(gu.name.toName), namespace = Some(gu.namespace.toNamespace), - description = gu.description.map(_.toGroupDescription) + description = gu.description.map(_.toDescription) ) def fromGroupMemberAdded( diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala index a65922a3..a575a975 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala @@ -24,8 +24,7 @@ import cats.syntax.all.* import io.renku.events.EventsGenerators import io.renku.search.GeneratorSyntax.* import io.renku.search.events.GroupRemoved -import io.renku.search.model.EntityType -import io.renku.search.model.Id +import io.renku.search.model.{EntityType, Id} import io.renku.search.provision.ProvisioningSuite import io.renku.search.solr.client.SearchSolrClient import io.renku.search.solr.client.SolrDocumentGenerators diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala index f192460d..7302efac 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala @@ -24,7 +24,6 @@ import io.renku.events.EventsGenerators import io.renku.search.GeneratorSyntax.* import io.renku.search.events.ProjectMemberAdded import io.renku.search.model.* -import io.renku.search.model.MemberRole import io.renku.search.provision.project.AuthorizationAddedProvisioningSpec.testCases import io.renku.search.provision.{BackgroundCollector, ProvisioningSuite} import io.renku.search.solr.client.{SearchSolrClient, SolrDocumentGenerators} diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserAddedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserAddedProvisioningSpec.scala index bbb20b01..9feeac24 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserAddedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserAddedProvisioningSpec.scala @@ -25,9 +25,7 @@ import cats.syntax.all.* import io.renku.events.EventsGenerators import io.renku.search.GeneratorSyntax.* import io.renku.search.events.UserAdded -import io.renku.search.model.Id -import io.renku.search.model.ModelGenerators -import io.renku.search.model.users.FirstName +import io.renku.search.model.* import io.renku.search.provision.ProvisioningSuite import io.renku.search.provision.events.syntax.* import io.renku.search.solr.documents.{CompoundId, EntityDocument, User as UserDocument} diff --git a/modules/search-query-docs/docs/manual.md b/modules/search-query-docs/docs/manual.md index 6f0e9c70..aaeebb4f 100644 --- a/modules/search-query-docs/docs/manual.md +++ b/modules/search-query-docs/docs/manual.md @@ -85,7 +85,7 @@ Possbile values are: ```scala mdoc:passthrough println( - projects.Visibility.values.map(e => s"`${e.name}`").mkString("- ", "\n- ", "") + Visibility.values.map(e => s"`${e.name}`").mkString("- ", "\n- ", "") ) ``` diff --git a/modules/search-query/src/main/scala/io/renku/search/query/FieldTerm.scala b/modules/search-query/src/main/scala/io/renku/search/query/FieldTerm.scala index 2820821d..f77fead7 100644 --- a/modules/search-query/src/main/scala/io/renku/search/query/FieldTerm.scala +++ b/modules/search-query/src/main/scala/io/renku/search/query/FieldTerm.scala @@ -20,9 +20,7 @@ package io.renku.search.query import cats.data.NonEmptyList -import io.renku.search.model.Namespace -import io.renku.search.model.projects.Visibility -import io.renku.search.model.{EntityType, Keyword, MemberRole} +import io.renku.search.model.* enum FieldTerm(val field: Field, val cmp: Comparison): case TypeIs(values: NonEmptyList[EntityType]) diff --git a/modules/search-query/src/main/scala/io/renku/search/query/Query.scala b/modules/search-query/src/main/scala/io/renku/search/query/Query.scala index d095b45a..c52ef03d 100644 --- a/modules/search-query/src/main/scala/io/renku/search/query/Query.scala +++ b/modules/search-query/src/main/scala/io/renku/search/query/Query.scala @@ -22,8 +22,7 @@ import cats.data.NonEmptyList import cats.kernel.Monoid import cats.syntax.all.* -import io.renku.search.model.EntityType -import io.renku.search.model.projects.Visibility +import io.renku.search.model.* import io.renku.search.query.FieldTerm.Created import io.renku.search.query.Query.Segment import io.renku.search.query.parse.{QueryParser, QueryUtil} diff --git a/modules/search-query/src/main/scala/io/renku/search/query/parse/QueryParser.scala b/modules/search-query/src/main/scala/io/renku/search/query/parse/QueryParser.scala index 6f4b2089..d44d850d 100644 --- a/modules/search-query/src/main/scala/io/renku/search/query/parse/QueryParser.scala +++ b/modules/search-query/src/main/scala/io/renku/search/query/parse/QueryParser.scala @@ -22,7 +22,6 @@ import cats.data.NonEmptyList import cats.parse.{Parser as P, Parser0 as P0} import io.renku.search.model.* -import io.renku.search.model.projects.Visibility import io.renku.search.query.* private[query] object QueryParser { diff --git a/modules/search-query/src/test/scala/io/renku/search/query/QueryGenerators.scala b/modules/search-query/src/test/scala/io/renku/search/query/QueryGenerators.scala index 01ddbc63..5fea9536 100644 --- a/modules/search-query/src/test/scala/io/renku/search/query/QueryGenerators.scala +++ b/modules/search-query/src/test/scala/io/renku/search/query/QueryGenerators.scala @@ -26,7 +26,6 @@ import cats.syntax.all.* import io.renku.search.common.CommonGenerators import io.renku.search.model.* -import io.renku.search.model.projects.Visibility import io.renku.search.query.parse.QueryUtil import org.scalacheck.Gen import org.scalacheck.cats.implicits.* @@ -134,8 +133,8 @@ object QueryGenerators: val visibilityTerm: Gen[FieldTerm] = Gen .frequency( - 10 -> ModelGenerators.projectVisibilityGen.map(NonEmptyList.one), - 1 -> CommonGenerators.nelOfN(2, ModelGenerators.projectVisibilityGen) + 10 -> ModelGenerators.visibilityGen.map(NonEmptyList.one), + 1 -> CommonGenerators.nelOfN(2, ModelGenerators.visibilityGen) ) .map(vs => FieldTerm.VisibilityIs(vs.distinct)) diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/DocumentKind.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/DocumentKind.scala index 196fe907..cf666711 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/DocumentKind.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/DocumentKind.scala @@ -20,8 +20,8 @@ package io.renku.search.solr.documents import io.bullet.borer.Decoder import io.bullet.borer.Encoder +import io.renku.json.EncoderSupport import io.renku.search.solr.schema.EntityDocumentSchema.Fields -import io.renku.solr.client.EncoderSupport enum DocumentKind: case FullEntity diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala index 858292a0..5e1872ba 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala @@ -21,12 +21,11 @@ package io.renku.search.solr.documents import io.bullet.borer.* import io.bullet.borer.NullOptions.given import io.bullet.borer.derivation.{MapBasedCodecs, key} +import io.renku.json.EncoderSupport import io.renku.search.model.* import io.renku.search.model.MemberRole.* -import io.renku.search.model.projects.Visibility import io.renku.search.solr.schema.EntityDocumentSchema.Fields -import io.renku.solr.client.EncoderSupport.* -import io.renku.solr.client.{DocVersion, EncoderSupport} +import io.renku.solr.client.DocVersion sealed trait EntityDocument extends SolrDocument: val score: Option[Double] @@ -45,12 +44,12 @@ final case class Project( id: Id, @key("_version_") version: DocVersion = DocVersion.Off, name: Name, - slug: projects.Slug, - repositories: Seq[projects.Repository] = Seq.empty, - visibility: projects.Visibility, - description: Option[projects.Description] = None, + slug: Slug, + repositories: Seq[Repository] = Seq.empty, + visibility: Visibility, + description: Option[Description] = None, createdBy: Id, - creationDate: projects.CreationDate, + creationDate: CreationDate, owners: Set[Id] = Set.empty, editors: Set[Id] = Set.empty, viewers: Set[Id] = Set.empty, @@ -97,8 +96,8 @@ object Project: final case class User( id: Id, @key("_version_") version: DocVersion = DocVersion.Off, - firstName: Option[users.FirstName] = None, - lastName: Option[users.LastName] = None, + firstName: Option[FirstName] = None, + lastName: Option[LastName] = None, name: Option[Name] = None, namespace: Option[Namespace] = None, score: Option[Double] = None @@ -123,8 +122,8 @@ object User: def of( id: Id, namespace: Option[Namespace] = None, - firstName: Option[users.FirstName] = None, - lastName: Option[users.LastName] = None, + firstName: Option[FirstName] = None, + lastName: Option[LastName] = None, score: Option[Double] = None ): User = User( @@ -142,7 +141,7 @@ final case class Group( @key("_version_") version: DocVersion = DocVersion.Off, name: Name, namespace: Namespace, - description: Option[groups.Description] = None, + description: Option[Description] = None, owners: Set[Id] = Set.empty, editors: Set[Id] = Set.empty, viewers: Set[Id] = Set.empty, @@ -180,7 +179,7 @@ object Group: owners: Set[Id] = Set.empty, editors: Set[Id] = Set.empty, viewers: Set[Id] = Set.empty, - description: Option[groups.Description] = None, + description: Option[Description] = None, score: Option[Double] = None ): Group = Group( diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/PartialEntityDocument.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/PartialEntityDocument.scala index f4ba84ce..02a48557 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/PartialEntityDocument.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/PartialEntityDocument.scala @@ -21,11 +21,11 @@ package io.renku.search.solr.documents import io.bullet.borer.* import io.bullet.borer.NullOptions.given import io.bullet.borer.derivation.{MapBasedCodecs, key} +import io.renku.json.EncoderSupport import io.renku.search.model.* -import io.renku.search.model.projects.* import io.renku.search.solr.documents.{Group as GroupDocument, Project as ProjectDocument} import io.renku.search.solr.schema.EntityDocumentSchema.Fields as SolrField -import io.renku.solr.client.{DocVersion, EncoderSupport} +import io.renku.solr.client.DocVersion sealed trait PartialEntityDocument extends SolrDocument: def applyTo(e: EntityDocument): EntityDocument @@ -113,7 +113,7 @@ object PartialEntityDocument: @key("_version_") version: DocVersion = DocVersion.Off, name: Option[Name] = None, namespace: Option[Namespace] = None, - description: Option[groups.Description] = None, + description: Option[Description] = None, owners: Set[Id] = Set.empty, editors: Set[Id] = Set.empty, viewers: Set[Id] = Set.empty diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/SolrToken.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/SolrToken.scala index 5cde85e3..db4d5357 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/SolrToken.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/SolrToken.scala @@ -25,7 +25,6 @@ import cats.data.NonEmptyList import cats.syntax.all.* import io.renku.search.model.* -import io.renku.search.model.projects.Visibility import io.renku.search.query.Comparison import io.renku.search.solr.documents.DocumentKind import io.renku.search.solr.schema.EntityDocumentSchema.Fields as SolrField diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala index dd6c143b..583d5c48 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala @@ -24,10 +24,7 @@ import cats.syntax.all.* import io.bullet.borer.Decoder import io.bullet.borer.derivation.MapBasedCodecs.deriveDecoder import io.renku.search.GeneratorSyntax.* -import io.renku.search.model.Id -import io.renku.search.model.ModelGenerators -import io.renku.search.model.projects.Visibility -import io.renku.search.model.users +import io.renku.search.model.* import io.renku.search.query.Query import io.renku.search.solr.SearchRole import io.renku.search.solr.client.SolrDocumentGenerators.* @@ -70,7 +67,7 @@ class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: } yield () test("be able to insert and fetch a User document"): - val firstName = users.FirstName("Johnny") + val firstName = FirstName("Johnny") val user = userDocumentGen.generateOne.copy(firstName = firstName.some) for { client <- IO(searchSolrClient()) @@ -97,7 +94,7 @@ class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: } yield () test("be able to find by the given query"): - val firstName = users.FirstName("Ian") + val firstName = FirstName("Ian") val user = userDocumentGen.generateOne.copy(firstName = firstName.some) case class UserId(id: String) given Decoder[UserId] = deriveDecoder[UserId] diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala index eda7bfae..839a7c9b 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala @@ -23,7 +23,7 @@ import cats.syntax.all.* import io.renku.search.GeneratorSyntax.* import io.renku.search.model.* import io.renku.search.model.ModelGenerators.* -import io.renku.search.model.projects.Visibility +import io.renku.search.model.Visibility import io.renku.search.solr.documents.* import io.renku.solr.client.DocVersion import org.scalacheck.Gen @@ -53,18 +53,18 @@ trait SolrDocumentGenerators: def projectDocumentGen( name: String, desc: String, - visibilityGen: Gen[Visibility] = projectVisibilityGen + visibilityGen: Gen[Visibility] = visibilityGen ): Gen[Project] = - (idGen, idGen, visibilityGen, projectCreationDateGen) + (idGen, idGen, visibilityGen, creationDateGen) .mapN((projectId, creatorId, visibility, creationDate) => Project( projectId, DocVersion.NotExists, Name(name), - projects.Slug(name), - Seq(projects.Repository(s"http://github.com/$name")), + Slug(name), + Seq(Repository(s"http://github.com/$name")), visibility, - Option(projects.Description(desc)), + Option(Description(desc)), creatorId, creationDate ) diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala index 94a73e15..f791cc53 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala @@ -22,8 +22,7 @@ import cats.effect.IO import cats.syntax.all.* import io.renku.search.GeneratorSyntax.* -import io.renku.search.model.projects.Visibility -import io.renku.search.model.{Id, MemberRole} +import io.renku.search.model.{Id, MemberRole, Visibility} import io.renku.search.query.Query import io.renku.search.solr.client.SolrDocumentGenerators import io.renku.search.solr.documents.* diff --git a/modules/solr-client/src/test/scala/io/renku/solr/client/SolrClientSpec.scala b/modules/solr-client/src/test/scala/io/renku/solr/client/SolrClientSpec.scala index 35dfc703..8f5fbd26 100644 --- a/modules/solr-client/src/test/scala/io/renku/solr/client/SolrClientSpec.scala +++ b/modules/solr-client/src/test/scala/io/renku/solr/client/SolrClientSpec.scala @@ -28,6 +28,7 @@ import cats.effect.IO import io.bullet.borer.derivation.MapBasedCodecs import io.bullet.borer.derivation.key import io.bullet.borer.{Decoder, Encoder, Reader} +import io.renku.json.EncoderSupport import io.renku.search.GeneratorSyntax.* import io.renku.solr.client.SolrClientSpec.CourseMember import io.renku.solr.client.SolrClientSpec.{Course, Room} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f8b8760a..f4436bbd 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -14,7 +14,6 @@ object Dependencies { val catsScalaCheck = "0.3.2" val ciris = "3.6.0" val decline = "2.4.1" - val ducktape = "0.2.2" val fs2 = "3.10.2" val http4s = "0.23.27" val http4sPrometheusMetrics = "0.24.6" @@ -91,10 +90,6 @@ object Dependencies { "com.monovore" %% "decline-effect" % V.decline ) - val ducktape = Seq( - "io.github.arainko" %% "ducktape" % V.ducktape - ) - val fs2Core = Seq( "co.fs2" %% "fs2-core" % V.fs2 )