From f06af67892a9f37b29114457637c215b2e003169 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 19 Mar 2024 06:56:22 +0100 Subject: [PATCH 1/5] Remove side-effect message header creation --- .../io/renku/queue/client/MessageHeader.scala | 27 ++++++++++--------- .../io/renku/queue/client/Generators.scala | 11 ++++++++ .../provision/handler/PushToRedis.scala | 17 +++++------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/modules/renku-redis-client/src/main/scala/io/renku/queue/client/MessageHeader.scala b/modules/renku-redis-client/src/main/scala/io/renku/queue/client/MessageHeader.scala index 9d344bb6..5da81da5 100644 --- a/modules/renku-redis-client/src/main/scala/io/renku/queue/client/MessageHeader.scala +++ b/modules/renku-redis-client/src/main/scala/io/renku/queue/client/MessageHeader.scala @@ -46,21 +46,25 @@ final case class MessageHeader( ) object MessageHeader: - def apply( + def apply[F[_]: Clock: Functor]( source: MessageSource, payloadSchema: Schema, dataContentType: DataContentType, schemaVersion: SchemaVersion, requestId: RequestId - ): MessageHeader = - MessageHeader( - source, - payloadSchema, - dataContentType, - schemaVersion, - CreationTime.now, - requestId - ) + ): F[MessageHeader] = + CreationTime + .now[F] + .map(now => + MessageHeader( + source, + payloadSchema, + dataContentType, + schemaVersion, + now, + requestId + ) + ) opaque type MessageSource = String object MessageSource: @@ -76,8 +80,7 @@ object SchemaVersion: opaque type CreationTime = Instant object CreationTime: def apply(v: Instant): CreationTime = v - def now: CreationTime = Instant.now().truncatedTo(ChronoUnit.MILLIS) - def nowF[F[_]: Clock: Functor]: F[CreationTime] = + def now[F[_]: Clock: Functor]: F[CreationTime] = Clock[F].realTimeInstant.map(_.truncatedTo(ChronoUnit.MILLIS)) extension (self: CreationTime) def value: Instant = self diff --git a/modules/renku-redis-client/src/test/scala/io/renku/queue/client/Generators.scala b/modules/renku-redis-client/src/test/scala/io/renku/queue/client/Generators.scala index 357ba236..026c0635 100644 --- a/modules/renku-redis-client/src/test/scala/io/renku/queue/client/Generators.scala +++ b/modules/renku-redis-client/src/test/scala/io/renku/queue/client/Generators.scala @@ -20,11 +20,20 @@ package io.renku.queue.client import org.apache.avro.Schema import org.scalacheck.Gen +import java.time.Instant object Generators: val requestIdGen: Gen[RequestId] = Gen.uuid.map(_.toString).map(RequestId(_)) + val creationTimeGen: Gen[CreationTime] = + Gen + .choose( + Instant.parse("2020-01-01T01:00:00Z").toEpochMilli(), + Instant.now().toEpochMilli() + ) + .map(millis => CreationTime(Instant.ofEpochMilli(millis))) + def messageHeaderGen(schema: Schema, contentType: DataContentType): Gen[MessageHeader] = messageHeaderGen(schema, Gen.const(contentType)) @@ -36,10 +45,12 @@ object Generators: contentType <- ctGen schemaVersion <- Gen.choose(1, 100).map(v => SchemaVersion(s"v$v")) requestId <- requestIdGen + creationTime <- creationTimeGen yield MessageHeader( MessageSource("test"), schema, contentType, schemaVersion, + creationTime, requestId ) diff --git a/modules/search-provision/src/main/scala/io/renku/search/provision/handler/PushToRedis.scala b/modules/search-provision/src/main/scala/io/renku/search/provision/handler/PushToRedis.scala index 814fcdeb..d7692a8a 100644 --- a/modules/search-provision/src/main/scala/io/renku/search/provision/handler/PushToRedis.scala +++ b/modules/search-provision/src/main/scala/io/renku/search/provision/handler/PushToRedis.scala @@ -65,14 +65,11 @@ object PushToRedis: ) def createHeader(requestId: RequestId): F[MessageHeader] = - CreationTime.nowF.map { now => - MessageHeader( - MessageSource(clientId.value), - ProjectAuthorizationRemoved.SCHEMA$, - DataContentType.Binary, - SchemaVersion.V1, - now, - requestId - ) - } + MessageHeader[F]( + MessageSource(clientId.value), + ProjectAuthorizationRemoved.SCHEMA$, + DataContentType.Binary, + SchemaVersion.V1, + requestId + ) } From cb765e5fdac965c03627b9b605860a9ed782b838 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 19 Mar 2024 07:01:52 +0100 Subject: [PATCH 2/5] Remove scribe from common dependencies --- build.sbt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index f9880909..7c76fa57 100644 --- a/build.sbt +++ b/build.sbt @@ -78,8 +78,7 @@ lazy val commons = project Dependencies.catsEffect ++ Dependencies.ducktape ++ Dependencies.fs2Core ++ - Dependencies.scodecBits ++ - Dependencies.scribe, + Dependencies.scodecBits, Test / sourceGenerators += Def.task { val sourceDir = (LocalRootProject / baseDirectory).value / "project" From 5d714fc11750ddebff2afb26c336bcad28e4c684 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 19 Mar 2024 07:05:15 +0100 Subject: [PATCH 3/5] Remove QueryJsonCodec --- .../io/renku/search/api/data/QueryInput.scala | 6 - .../scala/io/renku/search/query/Query.scala | 5 - .../search/query/json/QueryJsonCodec.scala | 181 ------------------ .../search/query/json/QueryJsonSpec.scala | 35 ---- 4 files changed, 227 deletions(-) delete mode 100644 modules/search-query/src/main/scala/io/renku/search/query/json/QueryJsonCodec.scala delete mode 100644 modules/search-query/src/test/scala/io/renku/search/query/json/QueryJsonSpec.scala diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/QueryInput.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/QueryInput.scala index 29ca0a15..cf10eaee 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/QueryInput.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/QueryInput.scala @@ -19,9 +19,6 @@ package io.renku.search.api.data import io.renku.search.query.Query -import io.bullet.borer.Encoder -import io.bullet.borer.derivation.MapBasedCodecs.{deriveDecoder, deriveEncoder} -import io.bullet.borer.Decoder import cats.Show final case class QueryInput( @@ -30,9 +27,6 @@ final case class QueryInput( ) object QueryInput: - given Encoder[QueryInput] = deriveEncoder - given Decoder[QueryInput] = deriveDecoder - given Show[QueryInput] = Show.show(i => s"(${i.query.render}, ${i.page})") def pageOne(query: Query): QueryInput = 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 41ddfbaa..81973547 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 @@ -21,12 +21,10 @@ package io.renku.search.query import cats.data.NonEmptyList import cats.kernel.Monoid import cats.syntax.all.* -import io.bullet.borer.{Decoder, Encoder} import io.renku.search.model.EntityType import io.renku.search.model.projects.Visibility import io.renku.search.query.FieldTerm.Created import io.renku.search.query.Query.Segment -import io.renku.search.query.json.QueryJsonCodec import io.renku.search.query.parse.{QueryParser, QueryUtil} final case class Query( @@ -44,9 +42,6 @@ final case class Query( def isEmpty: Boolean = segments.isEmpty object Query: - given Encoder[Query] = QueryJsonCodec.encoder.contramap(_.segments) - given Decoder[Query] = QueryJsonCodec.decoder.map(Query.apply) - def parse(str: String): Either[String, Query] = val trimmed = str.trim if (trimmed.isEmpty) Right(empty) diff --git a/modules/search-query/src/main/scala/io/renku/search/query/json/QueryJsonCodec.scala b/modules/search-query/src/main/scala/io/renku/search/query/json/QueryJsonCodec.scala deleted file mode 100644 index bce857d3..00000000 --- a/modules/search-query/src/main/scala/io/renku/search/query/json/QueryJsonCodec.scala +++ /dev/null @@ -1,181 +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.query.json - -import cats.data.NonEmptyList -import io.bullet.borer.compat.cats.* -import io.bullet.borer.{Decoder, Encoder, Reader, Writer} -import io.renku.search.model.EntityType -import io.renku.search.model.projects.Visibility -import io.renku.search.query.* -import io.renku.search.query.FieldTerm.* -import io.renku.search.query.Query.Segment - -import scala.collection.mutable.ListBuffer - -/** Use these json encoding to have it more convenient json than the derived version with - * nested objects or discriminator field. - * - * {{{ - * [ - * { - * "id": ["p1", "p2"], - * "id": "p2", - * "name": "test", - * "_text": "some phrase", - * "creationDate": ["<", "2024-01-29T12:00"] - * } - * ] - * }}} - */ -private[query] object QueryJsonCodec: - private[this] val freeTextField = "_text" - private[this] val sortTextField = "_sort" - - enum Name: - case FieldName(v: Field) - case SortName - case TextName - - private given Decoder[Name] = - new Decoder[Name]: - def read(r: Reader): Name = - if (r.tryReadString(freeTextField)) Name.TextName - else if (r.tryReadString(sortTextField)) Name.SortName - else Decoder[Field].map(Name.FieldName.apply).read(r) - - private def writeNelValue[T: Encoder](w: Writer, ts: NonEmptyList[T]): w.type = - if (ts.tail.isEmpty) w.write(ts.head) - else w.writeLinearSeq(ts.toList) - - private def writeFieldTermValue(w: Writer, term: FieldTerm): Writer = - term match - case FieldTerm.TypeIs(values) => - writeNelValue(w, values) - - case FieldTerm.IdIs(values) => - writeNelValue(w, values) - - case FieldTerm.NameIs(values) => - writeNelValue(w, values) - - case FieldTerm.SlugIs(values) => - writeNelValue(w, values) - - case FieldTerm.VisibilityIs(values) => - writeNelValue(w, values) - - case FieldTerm.CreatedByIs(values) => - writeNelValue(w, values) - - case FieldTerm.Created(cmp, date) => - Encoder.forTuple[(Comparison, List[DateTimeRef])].write(w, (cmp, date.toList)) - - def encoder: Encoder[List[Segment]] = - new Encoder[List[Segment]] { - def write(w: Writer, values: List[Segment]): w.type = - w.writeMapOpen(values.size) - values.foreach { - case Segment.Text(v) => - w.writeMapMember(freeTextField, v) - case Segment.Field(v) => - w.write(v.field) - writeFieldTermValue(w, v) - case Segment.Sort(v) => - w.write(sortTextField) - writeNelValue(w, v.fields) - } - w.writeMapClose() - } - - private def readNel[T: Decoder](r: Reader): NonEmptyList[T] = - if (r.hasString) NonEmptyList.of(r.read[T]()) - else Decoder[NonEmptyList[T]].read(r) - - private def readTermValue(r: Reader, name: Name): Segment = - name match - case Name.TextName => - Segment.Text(r.readString()) - - case Name.FieldName(Field.Type) => - val values = readNel[EntityType](r) - Segment.Field(TypeIs(values)) - - case Name.FieldName(Field.Id) => - val values = readNel[String](r) - Segment.Field(IdIs(values)) - - case Name.FieldName(Field.Name) => - val values = readNel[String](r) - Segment.Field(NameIs(values)) - - case Name.FieldName(Field.Visibility) => - val values = readNel[Visibility](r) - Segment.Field(VisibilityIs(values)) - - case Name.FieldName(Field.Slug) => - val values = readNel[String](r) - Segment.Field(SlugIs(values)) - - case Name.FieldName(Field.CreatedBy) => - val values = readNel[String](r) - Segment.Field(CreatedByIs(values)) - - case Name.FieldName(Field.Created) => - val (cmp, date) = - Decoder.forTuple[(Comparison, NonEmptyList[DateTimeRef])].read(r) - Segment.Field(Created(cmp, date)) - - case Name.SortName => - val values = readNel[Order.OrderedBy](r) - Segment.Sort(Order(values)) - - val decoder: Decoder[List[Segment]] = - new Decoder[List[Segment]] { - def read(r: Reader) = { - val buffer = ListBuffer.newBuilder[Segment] - if (r.hasMapHeader) { - val size = r.readMapHeader() - @annotation.tailrec - def loop(remain: Long): Unit = - if (remain > 0) { - val key = r[Name] - val value = readTermValue(r, key) - buffer.addOne(value) - loop(remain - 1) - } - loop(size) - - } else if (r.hasMapStart) { - r.readMapStart() - @annotation.tailrec - def loop(): Unit = - if (r.tryReadBreak()) () - else { - val key = r[Name] - val value = readTermValue(r, key) - buffer.addOne(value) - loop() - } - loop() - } else r.unexpectedDataItem(expected = "Map") - - buffer.result().result() - } - } diff --git a/modules/search-query/src/test/scala/io/renku/search/query/json/QueryJsonSpec.scala b/modules/search-query/src/test/scala/io/renku/search/query/json/QueryJsonSpec.scala deleted file mode 100644 index 2e93f15c..00000000 --- a/modules/search-query/src/test/scala/io/renku/search/query/json/QueryJsonSpec.scala +++ /dev/null @@ -1,35 +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.query.json - -import io.bullet.borer.Json -import io.renku.search.query.{Query, QueryGenerators} -import munit.{FunSuite, ScalaCheckSuite} -import org.scalacheck.Prop - -class QueryJsonSpec extends ScalaCheckSuite { - - property("query json encode/decode") { - Prop.forAll(QueryGenerators.query) { q => - val jsonStr = Json.encode(q).toUtf8String - val decoded = Json.decode(jsonStr.getBytes).to[Query].value - assertEquals(decoded, q) - } - } -} From 2634dd9155dbf0b83a38b74c82063c105ff64600 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 19 Mar 2024 07:24:04 +0100 Subject: [PATCH 4/5] Fix query encoder to move role into context This main reason for `Context` is to carry all the inputs to creating the query. --- .../scala/io/renku/search/query/Field.scala | 1 + .../solr/client/SearchSolrClientImpl.scala | 2 +- .../io/renku/search/solr/query/Context.scala | 30 +++++++---- .../solr/query/LuceneQueryEncoders.scala | 24 ++++----- .../solr/query/LuceneQueryInterpreter.scala | 8 +-- .../search/solr/query/QueryInterpreter.scala | 7 ++- .../renku/search/solr/query/SolrToken.scala | 50 ++++++++----------- .../solr/query/LuceneQueryEncoderSpec.scala | 26 ++++++---- .../query/LuceneQueryInterpreterSpec.scala | 4 +- .../search/solr/query/SolrTokenSpec.scala | 13 ++--- 10 files changed, 87 insertions(+), 78 deletions(-) diff --git a/modules/search-query/src/main/scala/io/renku/search/query/Field.scala b/modules/search-query/src/main/scala/io/renku/search/query/Field.scala index 4999e8ed..c7b2fb42 100644 --- a/modules/search-query/src/main/scala/io/renku/search/query/Field.scala +++ b/modules/search-query/src/main/scala/io/renku/search/query/Field.scala @@ -28,6 +28,7 @@ enum Field: case Created case CreatedBy case Type + case Role val name: String = Strings.lowerFirst(productPrefix) diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala index 6891d4ad..7da64b2d 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala @@ -60,7 +60,7 @@ private class SearchSolrClientImpl[F[_]: Async](solrClient: SolrClient[F]) offset: Int ): F[QueryResponse[EntityDocument]] = for { - solrQuery <- interpreter.run(role, query) + solrQuery <- interpreter(role).run(query) _ <- logger.debug(s"Query: ${query.render} ->Solr: $solrQuery") res <- solrClient .query[EntityDocument]( diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/Context.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/Context.scala index c242da55..d3bdb60c 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/Context.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/Context.scala @@ -23,23 +23,35 @@ import cats.effect.{Clock, Sync} import cats.syntax.all.* import java.time.ZoneId import cats.Applicative +import io.renku.search.solr.SearchRole trait Context[F[_]]: def currentTime: F[Instant] def zoneId: F[ZoneId] + def role: SearchRole object Context: - def forSync[F[_]: Sync]: Context[F] = + def forSync[F[_]: Sync](searchRole: SearchRole): Context[F] = new Context[F]: - def currentTime: F[Instant] = Clock[F].realTimeInstant - def zoneId: F[ZoneId] = Sync[F].delay(ZoneId.systemDefault()) + val currentTime: F[Instant] = Clock[F].realTimeInstant + val zoneId: F[ZoneId] = Sync[F].delay(ZoneId.systemDefault()) + val role = searchRole - def fixed[F[_]: Applicative](time: Instant, zone: ZoneId): Context[F] = + def fixed[F[_]: Applicative]( + time: Instant, + zone: ZoneId, + searchRole: SearchRole + ): Context[F] = new Context[F]: - def currentTime = time.pure[F] - def zoneId = zone.pure[F] + val currentTime = time.pure[F] + val zoneId = zone.pure[F] + val role = searchRole - def fixedZone[F[_]: Applicative: Clock](zone: ZoneId): Context[F] = + def fixedZone[F[_]: Applicative: Clock]( + zone: ZoneId, + searchRole: SearchRole + ): Context[F] = new Context[F]: - def currentTime = Clock[F].realTimeInstant - def zoneId = zone.pure[F] + val currentTime = Clock[F].realTimeInstant + val zoneId = zone.pure[F] + val role = searchRole diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala index 1e2002aa..862bab45 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala @@ -21,8 +21,8 @@ package io.renku.search.solr.query import cats.syntax.all.* import io.renku.search.query.Query.Segment import io.renku.search.query.FieldTerm -import io.renku.search.query.Field import io.renku.search.query.Query +import io.renku.search.solr.schema.EntityDocumentSchema.Fields as SolrField import cats.Monad import cats.Applicative @@ -32,38 +32,38 @@ trait LuceneQueryEncoders: given projectIdIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.IdIs] = SolrTokenEncoder.basic { case FieldTerm.IdIs(ids) => - SolrQuery(SolrToken.orFieldIs(Field.Id, ids.map(SolrToken.fromString))) + SolrQuery(SolrToken.orFieldIs(SolrField.id, ids.map(SolrToken.fromString))) } given nameIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.NameIs] = SolrTokenEncoder.basic { case FieldTerm.NameIs(names) => - SolrQuery(SolrToken.orFieldIs(Field.Name, names.map(SolrToken.fromString))) + SolrQuery(SolrToken.orFieldIs(SolrField.name, names.map(SolrToken.fromString))) } given typeIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.TypeIs] = SolrTokenEncoder.basic { case FieldTerm.TypeIs(values) => - SolrQuery(SolrToken.orFieldIs(Field.Type, values.map(SolrToken.fromEntityType))) + SolrQuery(SolrToken.orFieldIs(SolrField.entityType, values.map(SolrToken.fromEntityType))) } given slugIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.SlugIs] = SolrTokenEncoder.basic { case FieldTerm.SlugIs(names) => - SolrQuery(SolrToken.orFieldIs(Field.Slug, names.map(SolrToken.fromString))) + SolrQuery(SolrToken.orFieldIs(SolrField.slug, names.map(SolrToken.fromString))) } given createdByIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.CreatedByIs] = SolrTokenEncoder.basic { case FieldTerm.CreatedByIs(names) => - SolrQuery(SolrToken.orFieldIs(Field.CreatedBy, names.map(SolrToken.fromString))) + SolrQuery(SolrToken.orFieldIs(SolrField.createdBy, names.map(SolrToken.fromString))) } given visibilityIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.VisibilityIs] = SolrTokenEncoder.basic { case FieldTerm.VisibilityIs(values) => SolrQuery( - SolrToken.orFieldIs(Field.Visibility, values.map(SolrToken.fromVisibility)) + SolrToken.orFieldIs(SolrField.visibility, values.map(SolrToken.fromVisibility)) ) } given created[F[_]: Monad]: SolrTokenEncoder[F, FieldTerm.Created] = - val created = SolrToken.fromField(Field.Created) + val createdIs = SolrToken.fieldIs(SolrField.creationDate, _) SolrTokenEncoder.create[F, FieldTerm.Created] { case (ctx, FieldTerm.Created(Comparison.Is, values)) => (ctx.currentTime, ctx.zoneId).mapN { (ref, zone) => @@ -72,8 +72,8 @@ trait LuceneQueryEncoders: .map(_.resolve(ref, zone)) .map { case (min, maxOpt) => maxOpt - .map(max => created === SolrToken.fromDateRange(min, max)) - .getOrElse(created === SolrToken.fromInstant(min)) + .map(max => createdIs(SolrToken.fromDateRange(min, max))) + .getOrElse(createdIs(SolrToken.fromInstant(min))) } .toList .foldOr @@ -86,7 +86,7 @@ trait LuceneQueryEncoders: values .map(_.resolve(ref, zone)) .map { case (min, maxOpt) => - SolrToken.dateGt(Field.Created, maxOpt.getOrElse(min)) + SolrToken.createdDateGt(maxOpt.getOrElse(min)) } .toList .foldOr @@ -99,7 +99,7 @@ trait LuceneQueryEncoders: values .map(_.resolve(ref, zone)) .map { case (min, _) => - SolrToken.dateLt(Field.Created, min) + SolrToken.createdDateLt(min) } .toList .foldOr diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryInterpreter.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryInterpreter.scala index 992f7a4e..6708a024 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryInterpreter.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryInterpreter.scala @@ -33,8 +33,8 @@ final class LuceneQueryInterpreter[F[_]: Monad] with LuceneQueryEncoders: private val encoder = SolrTokenEncoder[F, Query] - def run(ctx: Context[F], role: SearchRole, query: Query): F[SolrQuery] = - amendUserId(role) { + def run(ctx: Context[F], query: Query): F[SolrQuery] = + amendUserId(ctx.role) { if (query.isEmpty) SolrQuery(SolrToken.allTypes).pure[F] else encoder.encode(ctx, query) } @@ -48,5 +48,5 @@ final class LuceneQueryInterpreter[F[_]: Monad] } object LuceneQueryInterpreter: - def forSync[F[_]: Sync]: QueryInterpreter.WithContext[F] = - QueryInterpreter.withContext(LuceneQueryInterpreter[F], Context.forSync[F]) + def forSync[F[_]: Sync](role: SearchRole): QueryInterpreter.WithContext[F] = + QueryInterpreter.withContext(LuceneQueryInterpreter[F], Context.forSync[F](role)) diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/QueryInterpreter.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/QueryInterpreter.scala index 536a955b..02ad7787 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/QueryInterpreter.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/QueryInterpreter.scala @@ -19,15 +19,14 @@ package io.renku.search.solr.query import io.renku.search.query.Query -import io.renku.search.solr.SearchRole trait QueryInterpreter[F[_]]: - def run(ctx: Context[F], role: SearchRole, q: Query): F[SolrQuery] + def run(ctx: Context[F], q: Query): F[SolrQuery] object QueryInterpreter: trait WithContext[F[_]]: - def run(role: SearchRole, q: Query): F[SolrQuery] + def run(q: Query): F[SolrQuery] def withContext[F[_]](qi: QueryInterpreter[F], ctx: Context[F]): WithContext[F] = new WithContext[F]: - def run(role: SearchRole, q: Query) = qi.run(ctx, role, q) + def run(q: Query) = qi.run(ctx, q) 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 3b95417f..3e1cca33 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 @@ -26,10 +26,11 @@ import cats.syntax.all.* import io.renku.search.model.{EntityType, Id} import io.renku.search.model.projects.Visibility -import io.renku.search.query.{Comparison, Field} +import io.renku.search.query.Comparison import io.renku.search.solr.documents.{Project as SolrProject, User as SolrUser} import io.renku.search.solr.schema.EntityDocumentSchema.Fields as SolrField import io.renku.solr.client.schema.FieldName +import io.renku.search.model.projects.MemberRole opaque type SolrToken = String @@ -43,17 +44,6 @@ object SolrToken: case EntityType.Project => SolrProject.entityType case EntityType.User => SolrUser.entityType - def fromField(field: Field): SolrToken = - (field match - case Field.Id => SolrField.id - case Field.Name => SolrField.name - case Field.Slug => SolrField.slug - case Field.Visibility => SolrField.visibility - case Field.CreatedBy => SolrField.createdBy - case Field.Created => SolrField.creationDate - case Field.Type => SolrField.entityType - ).name - def fromInstant(ts: Instant): SolrToken = StringEscape.escape(ts.toString, ":") def fromDateRange(min: Instant, max: Instant): SolrToken = s"[$min TO $max]" @@ -66,33 +56,33 @@ object SolrToken: def contentAll(text: String): SolrToken = s"${SolrField.contentAll.name}:${StringEscape.queryChars(text)}" - def orFieldIs(field: Field, values: NonEmptyList[SolrToken]): SolrToken = + def orFieldIs(field: FieldName, values: NonEmptyList[SolrToken]): SolrToken = values.map(fieldIs(field, _)).toList.foldOr - def dateIs(field: Field, date: Instant): SolrToken = fieldIs(field, fromInstant(date)) - def dateGt(field: Field, date: Instant): SolrToken = - fieldIs(field, s"[${fromInstant(date)} TO *]") - def dateLt(field: Field, date: Instant): SolrToken = - fieldIs(field, s"[* TO ${fromInstant(date)}]") + def createdDateIs(date: Instant): SolrToken = + fieldIs(SolrField.creationDate, fromInstant(date)) + def createdDateGt(date: Instant): SolrToken = + fieldIs(SolrField.creationDate, s"[${fromInstant(date)} TO *]") + def createdDateLt(date: Instant): SolrToken = + fieldIs(SolrField.creationDate, s"[* TO ${fromInstant(date)}]") - val allTypes: SolrToken = fieldIs(Field.Type, "*") + val allTypes: SolrToken = fieldIs(SolrField.entityType, "*") val publicOnly: SolrToken = - fieldIs(Field.Visibility, fromVisibility(Visibility.Public)) + fieldIs(SolrField.visibility, fromVisibility(Visibility.Public)) - def ownerIs(id: Id): SolrToken = SolrField.owners.name === fromString(id.value) - def memberIs(id: Id): SolrToken = SolrField.members.name === fromString(id.value) + def ownerIs(id: Id): SolrToken = SolrField.owners.name === fromId(id) + def memberIs(id: Id): SolrToken = SolrField.members.name === fromId(id) - def forUser(id: Id): SolrToken = - Seq(publicOnly, ownerIs(id), memberIs(id)).foldOr + def roleIs(id: Id, role: MemberRole): SolrToken = role match + case MemberRole.Owner => fieldIs(SolrField.owners, fromId(id)) + case MemberRole.Member => fieldIs(SolrField.members, fromId(id)) - private def fieldOp(field: Field, op: Comparison, value: SolrToken): SolrToken = - val cmp = fromComparison(op) - val f = fromField(field) - f ~ cmp ~ value + def roleIn(id: Id, roles: NonEmptyList[MemberRole]): SolrToken = + roles.toList.distinct.map(roleIs(id, _)).foldOr - def fieldIs(field: Field, value: SolrToken): SolrToken = - fieldOp(field, Comparison.Is, value) + def forUser(id: Id): SolrToken = + Seq(publicOnly, ownerIs(id), memberIs(id)).foldOr def fieldIs(field: FieldName, value: SolrToken): SolrToken = s"${field.name}:$value" diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala index c820287e..1eb34d55 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala @@ -18,19 +18,25 @@ package io.renku.search.solr.query -import munit.FunSuite +import java.time.{Instant, ZoneId} + import cats.Id import cats.data.NonEmptyList as Nel -import io.renku.search.query.{Comparison, FieldTerm} -import java.time.{Instant, ZoneId} + +import io.renku.search.model +import io.renku.search.model.projects.MemberRole import io.renku.search.query.* +import io.renku.search.query.{Comparison, FieldTerm} +import io.renku.search.solr.SearchRole +import io.renku.search.solr.schema.EntityDocumentSchema.Fields as SolrField +import munit.FunSuite class LuceneQueryEncoderSpec extends FunSuite with LuceneQueryEncoders: val refDate: Instant = Instant.parse("2024-02-27T15:34:55Z") val utc: ZoneId = ZoneId.of("UTC") - val ctx: Context[Id] = Context.fixed(refDate, utc) + val ctx: Context[Id] = Context.fixed(refDate, utc, SearchRole.Admin) val createdEncoder = SolrTokenEncoder[Id, FieldTerm.Created] test("use date-max for greater-than"): @@ -39,7 +45,7 @@ class LuceneQueryEncoderSpec extends FunSuite with LuceneQueryEncoders: FieldTerm.Created(Comparison.GreaterThan, Nel.of(DateTimeRef(pd))) assertEquals( createdEncoder.encode(ctx, date), - SolrQuery(SolrToken.dateGt(Field.Created, pd.instantMax(utc))) + SolrQuery(SolrToken.createdDateGt(pd.instantMax(utc))) ) test("use date-min for lower-than"): @@ -48,7 +54,7 @@ class LuceneQueryEncoderSpec extends FunSuite with LuceneQueryEncoders: FieldTerm.Created(Comparison.LowerThan, Nel.of(DateTimeRef(pd))) assertEquals( createdEncoder.encode(ctx, date), - SolrQuery(SolrToken.dateLt(Field.Created, pd.instantMin(utc))) + SolrQuery(SolrToken.createdDateLt(pd.instantMin(utc))) ) test("created comparison is"): @@ -56,7 +62,7 @@ class LuceneQueryEncoderSpec extends FunSuite with LuceneQueryEncoders: FieldTerm.Created(Comparison.Is, Nel.of(DateTimeRef(RelativeDate.Today))) assertEquals( createdEncoder.encode(ctx, cToday), - SolrQuery(SolrToken.dateIs(Field.Created, refDate)) + SolrQuery(SolrToken.createdDateIs(refDate)) ) test("single range"): @@ -67,7 +73,7 @@ class LuceneQueryEncoderSpec extends FunSuite with LuceneQueryEncoders: createdEncoder.encode(ctx, date), SolrQuery( SolrToken.fieldIs( - Field.Created, + SolrField.creationDate, SolrToken.fromDateRange(pd.instantMin(utc), pd.instantMax(utc)) ) ) @@ -83,11 +89,11 @@ class LuceneQueryEncoderSpec extends FunSuite with LuceneQueryEncoders: SolrQuery( List( SolrToken.fieldIs( - Field.Created, + SolrField.creationDate, SolrToken.fromDateRange(pd1.instantMin(utc), pd1.instantMax(utc)) ), SolrToken.fieldIs( - Field.Created, + SolrField.creationDate, SolrToken.fromDateRange(pd2.instantMin(utc), pd2.instantMax(utc)) ) ).foldOr diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala index 579acea9..95177a95 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala @@ -64,8 +64,8 @@ class LuceneQueryInterpreterSpec case str: String => Query.parse(str).fold(sys.error, identity) case qq: Query => qq - val ctx = Context.fixed[Id](Instant.EPOCH, ZoneId.of("UTC")) - val q = LuceneQueryInterpreter[Id].run(ctx, role, userQuery) + val ctx = Context.fixed[Id](Instant.EPOCH, ZoneId.of("UTC"), role) + val q = LuceneQueryInterpreter[Id].run(ctx, userQuery) QueryData(QueryString(q.query.value, 10, 0)).withSort(q.sort) def withSolr = diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/SolrTokenSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/SolrTokenSpec.scala index f280da79..5b3649dd 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/SolrTokenSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/SolrTokenSpec.scala @@ -18,24 +18,25 @@ package io.renku.search.solr.query -import munit.FunSuite -import io.renku.search.query.Field import java.time.Instant +import io.renku.search.solr.schema.EntityDocumentSchema.Fields as SolrField +import munit.FunSuite + class SolrTokenSpec extends FunSuite: test("fold with parens"): assertEquals( List( - SolrToken.fieldIs(Field.Name, SolrToken.fromString("john")), - SolrToken.fieldIs(Field.Id, SolrToken.fromString("1")) + SolrToken.fieldIs(SolrField.name, SolrToken.fromString("john")), + SolrToken.fieldIs(SolrField.id, SolrToken.fromString("1")) ).foldAnd, SolrToken.unsafeFromString("(name:john AND id:1)") ) assertEquals( List( - SolrToken.fieldIs(Field.Name, SolrToken.fromString("john")), - SolrToken.fieldIs(Field.Id, SolrToken.fromString("1")) + SolrToken.fieldIs(SolrField.name, SolrToken.fromString("john")), + SolrToken.fieldIs(SolrField.id, SolrToken.fromString("1")) ).foldOr, SolrToken.unsafeFromString("(name:john OR id:1)") ) From 38f998e0a3ff9798fe1f53bdf967b67ed125002f Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 19 Mar 2024 07:40:14 +0100 Subject: [PATCH 5/5] Add role filter --- .../io/renku/search/query/FieldTerm.scala | 4 ++ .../search/query/parse/QueryParser.scala | 54 ++++++++++--------- .../renku/search/query/QueryGenerators.scala | 21 ++++++-- .../solr/query/LuceneQueryEncoders.scala | 15 +++++- .../renku/search/solr/query/SolrToken.scala | 2 +- .../solr/query/LuceneQueryEncoderSpec.scala | 53 ++++++++++++++++++ .../query/LuceneQueryInterpreterSpec.scala | 2 +- 7 files changed, 118 insertions(+), 33 deletions(-) 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 bbc726d4..516ad202 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 @@ -21,6 +21,7 @@ package io.renku.search.query import cats.data.NonEmptyList import io.renku.search.model.EntityType import io.renku.search.model.projects.Visibility +import io.renku.search.model.projects.MemberRole enum FieldTerm(val field: Field, val cmp: Comparison): case TypeIs(values: NonEmptyList[EntityType]) @@ -34,6 +35,8 @@ enum FieldTerm(val field: Field, val cmp: Comparison): extends FieldTerm(Field.Created, cmp) case CreatedByIs(values: NonEmptyList[String]) extends FieldTerm(Field.CreatedBy, Comparison.Is) + case RoleIs(values: NonEmptyList[MemberRole]) + extends FieldTerm(Field.Role, Comparison.Is) private[query] def asString = val value = this match @@ -48,6 +51,7 @@ enum FieldTerm(val field: Field, val cmp: Comparison): vis.mkString(",") case Created(_, values) => FieldTerm.nelToString(values.map(_.asString)) case CreatedByIs(values) => FieldTerm.nelToString(values) + case RoleIs(values) => FieldTerm.nelToString(values.map(_.name)) s"${field.name}${cmp.asString}${value}" 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 19ca7856..c4204c12 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 @@ -23,8 +23,12 @@ import cats.parse.{Parser as P, Parser0 as P0} import io.renku.search.model.EntityType import io.renku.search.model.projects.Visibility import io.renku.search.query.* +import io.renku.search.model.projects.MemberRole private[query] object QueryParser { + private def candidates(names: Set[String]): Set[String] = + names ++ names.map(_.toLowerCase()) + val basicString = P.charsWhile(c => c > ' ' && !c.isWhitespace && c != '"' && c != '\\' && c != ',') @@ -37,24 +41,18 @@ private[query] object QueryParser { val commaSep = comma.surroundedBy(sp0).backtrack def mkFieldNames(fs: Set[Field]) = - fs.map(_.name) ++ fs.map(_.name.toLowerCase) + candidates(fs.map(_.name)) - def fieldNameFrom(candidates: Set[Field]) = - P.stringIn(mkFieldNames(candidates)).map(Field.unsafeFromString) + def fieldNameFrom(fields: Set[Field]) = + P.stringIn(candidates(fields.map(_.name))).map(Field.unsafeFromString) val sortableField: P[SortableField] = - P.stringIn( - SortableField.values - .map(_.name) - .toSeq ++ SortableField.values.map(_.name.toLowerCase).toSeq - ).map(SortableField.unsafeFromString) + P.stringIn(candidates(SortableField.values.map(_.name).toSet)) + .map(SortableField.unsafeFromString) val sortDirection: P[Order.Direction] = - P.stringIn( - Order.Direction.values - .map(_.name) - .toSeq ++ Order.Direction.values.map(_.name.toLowerCase).toSeq - ).map(Order.Direction.unsafeFromString) + P.stringIn(candidates(Order.Direction.values.map(_.name).toSet)) + .map(Order.Direction.unsafeFromString) val orderedBy: P[Order.OrderedBy] = (sortableField ~ (P.string("-") *> sortDirection)).map { case (f, s) => @@ -75,11 +73,8 @@ private[query] object QueryParser { (P.string("sort").with1 *> (is *> orderedByNel)).map(Order.apply) val visibility: P[Visibility] = - P.stringIn( - Visibility.values - .map(_.name.toLowerCase) - .toSet ++ Visibility.values.map(_.name).toSet - ).map(Visibility.unsafeFromString) + P.stringIn(candidates(Visibility.values.map(_.name).toSet)) + .map(Visibility.unsafeFromString) def nelOf[A](p: P[A], sep: P[Unit]) = (p ~ (sep *> p).rep0).map { case (h, t) => NonEmptyList(h, t) } @@ -91,18 +86,22 @@ private[query] object QueryParser { nelOf(visibility, commaSep) val entityType: P[EntityType] = - P.stringIn( - EntityType.values - .map(_.name.toLowerCase) - .toSet ++ EntityType.values.map(_.name).toSet - ).map(EntityType.unsafeFromString) + P.stringIn(candidates(EntityType.values.map(_.name).toSet)) + .map(EntityType.unsafeFromString) val entityTypes: P[NonEmptyList[EntityType]] = nelOf(entityType, commaSep) + val memberRole: P[MemberRole] = + P.stringIn(candidates(MemberRole.values.map(_.name).toSet)) + .map(MemberRole.unsafeFromString) + + val memberRoles: P[NonEmptyList[MemberRole]] = + nelOf(memberRole, commaSep) + val termIs: P[FieldTerm] = { val field = fieldNameFrom( - Field.values.toSet - Field.Created - Field.Visibility - Field.Type + Field.values.toSet - Field.Created - Field.Visibility - Field.Type - Field.Role ) ((field <* is) ~ values).map { case (f, v) => f match @@ -132,7 +131,12 @@ private[query] object QueryParser { } } - val fieldTerm: P[FieldTerm] = termIs | visibilityIs | typeIs | created + val roleIs: P[FieldTerm] = { + val field = fieldNameFrom(Set(Field.Role)) + ((field *> is).void *> memberRoles).map(v => FieldTerm.RoleIs(v)) + } + + val fieldTerm: P[FieldTerm] = termIs | visibilityIs | typeIs | created | roleIs val freeText: P[String] = P.charsWhile(c => !c.isWhitespace) 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 e5c433f2..44402ddd 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 @@ -18,17 +18,19 @@ package io.renku.search.query +import java.time._ + import cats.data.NonEmptyList -import cats.Order as CatsOrder import cats.syntax.all.* -import io.renku.search.model.{CommonGenerators, ModelGenerators} +import cats.Order as CatsOrder + +import io.renku.search.model.projects.MemberRole import io.renku.search.model.projects.Visibility +import io.renku.search.model.{CommonGenerators, ModelGenerators} import io.renku.search.query.parse.QueryUtil import org.scalacheck.Gen import org.scalacheck.cats.implicits.* -import java.time.{Period, YearMonth, ZoneId, ZoneOffset} - object QueryGenerators: val utc: Gen[Option[ZoneId]] = Gen.frequency(2 -> Gen.const(Some(ZoneOffset.UTC)), 1 -> Gen.const(None)) @@ -134,6 +136,14 @@ object QueryGenerators: ) .map(vs => FieldTerm.VisibilityIs(vs.distinct)) + val roleTerm: Gen[FieldTerm] = + Gen + .frequency( + 10 -> ModelGenerators.projectMemberRoleGen.map(NonEmptyList.one), + 1 -> CommonGenerators.nelOfN(2, ModelGenerators.projectMemberRoleGen) + ) + .map(vs => FieldTerm.RoleIs(vs)) + private val comparison: Gen[Comparison] = Gen.oneOf(Comparison.values.toSeq) @@ -151,7 +161,8 @@ object QueryGenerators: slugTerm, createdByTerm, visibilityTerm, - createdTerm + createdTerm, + roleTerm ) val freeText: Gen[String] = diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala index 862bab45..03fdd4dd 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/query/LuceneQueryEncoders.scala @@ -27,6 +27,7 @@ import io.renku.search.solr.schema.EntityDocumentSchema.Fields as SolrField import cats.Monad import cats.Applicative import io.renku.search.query.Comparison +import io.renku.search.solr.SearchRole trait LuceneQueryEncoders: @@ -42,7 +43,9 @@ trait LuceneQueryEncoders: given typeIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.TypeIs] = SolrTokenEncoder.basic { case FieldTerm.TypeIs(values) => - SolrQuery(SolrToken.orFieldIs(SolrField.entityType, values.map(SolrToken.fromEntityType))) + SolrQuery( + SolrToken.orFieldIs(SolrField.entityType, values.map(SolrToken.fromEntityType)) + ) } given slugIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.SlugIs] = @@ -62,6 +65,16 @@ trait LuceneQueryEncoders: ) } + given roleIs[F[_]: Applicative]: SolrTokenEncoder[F, FieldTerm.RoleIs] = + SolrTokenEncoder.create[F, FieldTerm.RoleIs] { case (ctx, FieldTerm.RoleIs(values)) => + SolrQuery { + ctx.role match + case SearchRole.Admin => SolrToken.empty + case SearchRole.Anonymous => SolrToken.publicOnly + case SearchRole.User(id) => SolrToken.roleIn(id, values) + }.pure[F] + } + given created[F[_]: Monad]: SolrTokenEncoder[F, FieldTerm.Created] = val createdIs = SolrToken.fieldIs(SolrField.creationDate, _) SolrTokenEncoder.create[F, FieldTerm.Created] { 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 3e1cca33..47225171 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 @@ -75,7 +75,7 @@ object SolrToken: def memberIs(id: Id): SolrToken = SolrField.members.name === fromId(id) def roleIs(id: Id, role: MemberRole): SolrToken = role match - case MemberRole.Owner => fieldIs(SolrField.owners, fromId(id)) + case MemberRole.Owner => fieldIs(SolrField.owners, fromId(id)) case MemberRole.Member => fieldIs(SolrField.members, fromId(id)) def roleIn(id: Id, roles: NonEmptyList[MemberRole]): SolrToken = diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala index 1eb34d55..e6b57585 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryEncoderSpec.scala @@ -99,3 +99,56 @@ class LuceneQueryEncoderSpec extends FunSuite with LuceneQueryEncoders: ).foldOr ) ) + + List(SearchRole.Admin, SearchRole.Anonymous, SearchRole.User(model.Id("5"))).foreach { + role => + val encoder = SolrTokenEncoder[Id, FieldTerm.RoleIs] + val ctx = Context.fixed[Id](refDate, utc, role) + val encode = encoder.encode(ctx, _) + + test(s"role filter: $role"): + val memberQuery: FieldTerm.RoleIs = FieldTerm.RoleIs(Nel.of(MemberRole.Member)) + val ownerQuery: FieldTerm.RoleIs = FieldTerm.RoleIs(Nel.of(MemberRole.Owner)) + val allQuery: FieldTerm.RoleIs = + FieldTerm.RoleIs(Nel.of(MemberRole.Member, MemberRole.Owner, MemberRole.Member)) + role match + case SearchRole.Admin => + assertEquals( + encode(memberQuery), + SolrQuery(SolrToken.empty) + ) + assertEquals( + encode(ownerQuery), + SolrQuery(SolrToken.empty) + ) + assertEquals( + encode(allQuery), + SolrQuery(SolrToken.empty) + ) + case SearchRole.Anonymous => + assertEquals( + encode(memberQuery), + SolrQuery(SolrToken.publicOnly) + ) + assertEquals( + encode(ownerQuery), + SolrQuery(SolrToken.publicOnly) + ) + assertEquals( + encode(allQuery), + SolrQuery(SolrToken.publicOnly) + ) + case SearchRole.User(id) => + assertEquals( + encode(memberQuery), + SolrQuery(SolrToken.roleIs(id, MemberRole.Member)) + ) + assertEquals( + encode(ownerQuery), + SolrQuery(SolrToken.roleIs(id, MemberRole.Owner)) + ) + assertEquals( + encode(allQuery), + SolrQuery(SolrToken.roleIn(id, Nel.of(MemberRole.Member, MemberRole.Owner))) + ) + } diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala index 95177a95..b14c7249 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/LuceneQueryInterpreterSpec.scala @@ -95,7 +95,7 @@ class LuceneQueryInterpreterSpec test("valid content_all query"): withSolr.use { client => - List("hello world", "role:test") + List("hello world", "bla:test") .map(query(_)) .traverse_(client.query[Unit]) }