diff --git a/modules/solr-client/src/main/scala/io/renku/solr/client/QueryResponse.scala b/modules/solr-client/src/main/scala/io/renku/solr/client/QueryResponse.scala index d735b8d0..6ea476be 100644 --- a/modules/solr-client/src/main/scala/io/renku/solr/client/QueryResponse.scala +++ b/modules/solr-client/src/main/scala/io/renku/solr/client/QueryResponse.scala @@ -19,16 +19,19 @@ package io.renku.solr.client import io.bullet.borer.Decoder -import io.bullet.borer.derivation.MapBasedCodecs.deriveDecoder +import io.bullet.borer.NullOptions.given +import io.bullet.borer.derivation.MapBasedCodecs import io.bullet.borer.derivation.key +import io.renku.solr.client.facet.FacetResponse final case class QueryResponse[A]( responseHeader: ResponseHeader, - @key("response") responseBody: ResponseBody[A] + @key("response") responseBody: ResponseBody[A], + @key("facets") facetResponse: Option[FacetResponse] = None ): def map[B](f: A => B): QueryResponse[B] = copy(responseBody = responseBody.map(f)) object QueryResponse: given [A](using Decoder[A]): Decoder[QueryResponse[A]] = - deriveDecoder + MapBasedCodecs.deriveDecoder diff --git a/modules/solr-client/src/main/scala/io/renku/solr/client/facet/FacetResponse.scala b/modules/solr-client/src/main/scala/io/renku/solr/client/facet/FacetResponse.scala new file mode 100644 index 00000000..01cf26aa --- /dev/null +++ b/modules/solr-client/src/main/scala/io/renku/solr/client/facet/FacetResponse.scala @@ -0,0 +1,59 @@ +/* + * 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.solr.client.facet + +import io.renku.solr.client.schema.FieldName +import io.bullet.borer.derivation.key +import io.bullet.borer.Decoder +import io.bullet.borer.derivation.MapBasedCodecs +import io.bullet.borer.Reader + +import FacetResponse.Values + +final case class FacetResponse( + count: Int, + buckets: Map[FieldName, Values] +): + def isEmpty: Boolean = count == 0 && buckets.isEmpty + + private def withBuckets(field: FieldName, values: Values): FacetResponse = + copy(buckets = buckets.updated(field, values)) + +object FacetResponse: + val empty: FacetResponse = FacetResponse(0, Map.empty) + + final case class Bucket(@key("val") value: String, count: Int) + object Bucket: + given Decoder[Bucket] = MapBasedCodecs.deriveDecoder + + final case class Values(buckets: Buckets) + object Values: + given Decoder[Values] = MapBasedCodecs.deriveDecoder + + type Buckets = Seq[Bucket] + + given Decoder[FacetResponse] = new Decoder[FacetResponse] { + def read(r: Reader): FacetResponse = + r.readMapStart() + r.readUntilBreak(empty) { fr => + val nextKey = r.readString() + if (nextKey == "count") fr.copy(count = r.readInt()) + else fr.withBuckets(FieldName(nextKey), r.read[Values]()) + } + } 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 e8b85d43..bf68a458 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 @@ -29,6 +29,8 @@ import munit.CatsEffectSuite import munit.ScalaCheckEffectSuite import org.scalacheck.effect.PropF import io.bullet.borer.Reader +import org.scalacheck.Gen +import io.renku.solr.client.facet.{Facet, Facets} class SolrClientSpec extends CatsEffectSuite @@ -45,17 +47,22 @@ class SolrClientSpec SchemaCommand.Add(Field(FieldName("roomSeats"), TypeName("roomInt"))) ) withSolrClient().use { client => + val rooms = Seq(Room("meeting room", "room for meetings", 56)) for { + _ <- truncateAll(client)( + List("roomName", "roomDescription", "roomSeats").map(FieldName.apply), + List("roomText", "roomInt").map(TypeName.apply) + ) _ <- client.modifySchema(cmds) _ <- client - .insert[Room](Seq(Room("meeting room", "room for meetings", 56))) + .insert[Room](rooms) r <- client.query[Room](QueryData(QueryString("_type:Room"))) - _ <- IO.println(r) + _ = assertEquals(r.responseBody.docs, rooms) } yield () } test("correct facet queries"): - given Decoder[Unit] = new Decoder { + val decoder: Decoder[Unit] = new Decoder { def read(r: Reader): Unit = r.skipElement() () @@ -63,12 +70,39 @@ class SolrClientSpec PropF.forAllF(SolrClientGenerator.facets) { facets => val q = QueryData(QueryString("*:*")).withFacet(facets) withSolrClient().use { client => - client.query[Unit](q).void + client.query(q)(using decoder).void } } + test("decoding facet response"): + val rooms = Gen.listOfN(15, Room.gen).sample.get + val facets = + Facets(Facet.Terms(FieldName("by_name"), FieldName("roomName"), limit = Some(6))) + withSolrClient().use { client => + for { + _ <- client.delete(QueryString("*:*")) + _ <- client.insert(rooms) + r <- client.query[Room](QueryData(QueryString("*:*")).withFacet(facets)) + _ = assert(r.facetResponse.nonEmpty) + _ = assertEquals(r.facetResponse.get.count, 15) + _ = assertEquals( + r.facetResponse.get.buckets(FieldName("by_name")).buckets.size, + 6 + ) + } yield () + } + object SolrClientSpec: case class Room(roomName: String, roomDescription: String, roomSeats: Int) object Room: + val gen: Gen[Room] = for { + name <- Gen + .choose(4, 12) + .flatMap(n => Gen.listOfN(n, Gen.alphaChar)) + .map(_.mkString) + descr = s"Room description for $name" + seats <- Gen.choose(15, 350) + } yield Room(name, descr, seats) + given Decoder[Room] = deriveDecoder given Encoder[Room] = EncoderSupport.deriveWithDiscriminator[Room]