Skip to content

Commit

Permalink
Add role filter
Browse files Browse the repository at this point in the history
  • Loading branch information
eikek committed Mar 19, 2024
1 parent 2634dd9 commit 38f998e
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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
Expand All @@ -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}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 != ',')

Expand All @@ -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) =>
Expand All @@ -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) }
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)

Expand All @@ -151,7 +161,8 @@ object QueryGenerators:
slugTerm,
createdByTerm,
visibilityTerm,
createdTerm
createdTerm,
roleTerm
)

val freeText: Gen[String] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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] =
Expand All @@ -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] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down

0 comments on commit 38f998e

Please sign in to comment.