Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Query classes #26

Merged
merged 6 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions core/src/main/scala/pink/cozydev/lucille/Op.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,17 @@ object Op {
case Nil =>
op match {
// no more ops to pair
case OR => NonEmptyList.of(Query.OrQ(acc))
case AND => NonEmptyList.of(Query.AndQ(acc))
case OR => NonEmptyList.of(Query.Or(acc))
case AND => NonEmptyList.of(Query.And(acc))
}
case (nextOp, q) :: tailOpP =>
(op, nextOp) match {
case (OR, OR) => go(acc.append(q), nextOp, tailOpP)
case (AND, AND) => go(acc.append(q), nextOp, tailOpP)
case (AND, OR) =>
go(NonEmptyList.of(q), nextOp, tailOpP).prepend(Query.AndQ(acc))
go(NonEmptyList.of(q), nextOp, tailOpP).prepend(Query.And(acc))
case (OR, AND) =>
// TODO we only get away with not wrapping the `allButLast` in an OrQ
// TODO we only get away with not wrapping the `allButLast` in an Or
// because `OR` is the default query type. This should be configurable
val allButLast = NonEmptyList(acc.head, acc.tail.dropRight(1))
allButLast.concatNel(go(NonEmptyList.of(acc.last, q), nextOp, tailOpP))
Expand Down
36 changes: 18 additions & 18 deletions core/src/main/scala/pink/cozydev/lucille/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,40 +41,40 @@ object Parser {
// Term query
// e.g. 'cat', 'catch22'
val term: P[String] = P.not(P.stringIn(reserved)).with1 *> allowed.rep.string
val termQ: P[TermQ] = term.map(TermQ.apply)
val termQ: P[Term] = term.map(Term.apply)

// Phrase query
// e.g. 'the cat jumped'
val phrase: P[String] = (term ~ sp.?).rep.string.surroundedBy(dquote)
val phraseQ: P[PhraseQ] = phrase.map(PhraseQ.apply)
val phraseQ: P[Phrase] = phrase.map(Phrase.apply)

// Proximity query
// e.g. '"cat jumped"~3', '"one two three"~2'
val proxSoft: P[String] = phrase.soft <* pchar('~')
val proximityQ: P[ProximityQ] = (proxSoft ~ int).map { case (p, n) =>
ProximityQ(p, n)
val proximityQ: P[Proximity] = (proxSoft ~ int).map { case (p, n) =>
Proximity(p, n)
}

// Fuzzy term
// e.g. 'cat~', 'cat~2'
val fuzzySoft: P[String] = term.soft <* pchar('~')
val fuzzyT: P[FuzzyTerm] = (fuzzySoft ~ int.?).map { case (q, n) =>
FuzzyTerm(q, n)
val fuzzyT: P[Fuzzy] = (fuzzySoft ~ int.?).map { case (q, n) =>
Fuzzy(q, n)
}

// Prefix term
// e.g. 'jump*'
val prefixT: P[PrefixTerm] =
val prefixT: P[Prefix] =
(term.soft <* P.char('*'))
.map(PrefixTerm.apply)
.map(Prefix.apply)

// Regex query
// TermRegex query
// e.g. '/.ump(s|ing)/'
private val regex: P[String] = {
val notEscape = P.charIn(baseRange - '\\' - '/')
notEscape.orElse(P.string("\\/")).rep.string.surroundedBy(pchar('/'))
}
val regexQ: P[Regex] = regex.map(Regex.apply)
val regexQ: P[TermRegex] = regex.map(TermRegex.apply)

val or = (P.string("OR") | P.string("||")).as(Op.OR)
val and = (P.string("AND") | P.string("&&")).as(Op.AND)
Expand Down Expand Up @@ -104,14 +104,14 @@ object Parser {
// Not query
// e.g. 'animals NOT (cats AND dogs)'
def notQ(query: P[Query]): P[Query] =
((P.string("NOT").soft ~ maybeSpace) *> query).map(NotQ.apply)
((P.string("NOT").soft ~ maybeSpace) *> query).map(Not.apply)

// Minimum match query
// e.g. '(one two three)@2'
def minimumMatchQ(query: P[Query]): P[MinimumMatchQ] = {
def minimumMatchQ(query: P[Query]): P[MinimumMatch] = {
val matchNum = P.char('@') *> int
val grouped = nonGrouped(query).between(P.char('('), P.char(')'))
(grouped.soft ~ matchNum).map { case (qs, n) => MinimumMatchQ(qs, n) }
(grouped.soft ~ matchNum).map { case (qs, n) => MinimumMatch(qs, n) }
}

// Group query
Expand All @@ -126,8 +126,8 @@ object Parser {
// Field query
// e.g. 'title:cats', 'author:"Silly Goose"', 'title:(cats AND dogs)'
val fieldValueSoft: P[String] = term.soft <* pchar(':')
def fieldQuery(query: P[Query]): P[FieldQ] =
(fieldValueSoft ~ query).map { case (f, q) => FieldQ(f, q) }
def fieldQuery(query: P[Query]): P[Field] =
(fieldValueSoft ~ query).map { case (f, q) => Field(f, q) }

// Unary Plus query
// e.g. '+cat', '+(cats AND dogs)'
Expand All @@ -139,9 +139,9 @@ object Parser {
def unaryMinus(query: P[Query]): P[UnaryMinus] =
P.char('-') *> query.map(UnaryMinus.apply)

// Range query
// TermRange query
// e.g. '{cats TO dogs}', '[1 TO *}'
def rangeQuery: P[RangeQ] = {
def rangeQuery: P[TermRange] = {
val inclLower = P.charIn('{', '[').map(lowerBound => lowerBound == '[') <* maybeSpace
val inclUpper = maybeSpace *> P.charIn('}', ']').map(upperBound => upperBound == ']')
val boundValue =
Expand All @@ -152,7 +152,7 @@ object Parser {
val to = spaces *> P.string("TO") <* spaces
(inclLower ~ boundValue ~ to ~ boundValue ~ inclUpper)
.map { case ((((il, l), _), u), iu) =>
RangeQ(l, u, il, iu)
TermRange(l, u, il, iu)
}
}

Expand Down
47 changes: 32 additions & 15 deletions core/src/main/scala/pink/cozydev/lucille/Query.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,41 @@ import cats.data.NonEmptyList
sealed trait Query extends Product with Serializable

object Query {
final case class TermQ(q: String) extends Query
final case class PhraseQ(q: String) extends Query
final case class FieldQ(field: String, q: Query) extends Query
final case class ProximityQ(q: String, num: Int) extends Query
final case class PrefixTerm(q: String) extends Query
final case class Regex(r: String) extends Query
final case class FuzzyTerm(q: String, num: Option[Int]) extends Query
final case class OrQ(qs: NonEmptyList[Query]) extends Query
final case class AndQ(qs: NonEmptyList[Query]) extends Query
final case class NotQ(q: Query) extends Query
final case class Group(qs: NonEmptyList[Query]) extends Query
final case class UnaryPlus(q: Query) extends Query
final case class UnaryMinus(q: Query) extends Query
final case class RangeQ(
final case class Term(str: String) extends Query
final case class Phrase(str: String) extends Query
final case class Prefix(str: String) extends Query
final case class Proximity(str: String, num: Int) extends Query
final case class Fuzzy(str: String, num: Option[Int]) extends Query
final case class TermRegex(str: String) extends Query
final case class TermRange(
lower: Option[String],
upper: Option[String],
lowerInc: Boolean,
upperInc: Boolean,
) extends Query
final case class MinimumMatchQ(qs: NonEmptyList[Query], num: Int) extends Query

final case class Or(qs: NonEmptyList[Query]) extends Query
object Or {
def apply(head: Query, tail: Query*): Or =
Or(NonEmptyList(head, tail.toList))
}

final case class And(qs: NonEmptyList[Query]) extends Query
object And {
def apply(head: Query, tail: Query*): And =
And(NonEmptyList(head, tail.toList))
}

final case class Not(q: Query) extends Query

final case class Group(qs: NonEmptyList[Query]) extends Query
object Group {
def apply(head: Query, tail: Query*): Group =
Group(NonEmptyList(head, tail.toList))
}

final case class UnaryPlus(q: Query) extends Query
final case class UnaryMinus(q: Query) extends Query
final case class MinimumMatch(qs: NonEmptyList[Query], num: Int) extends Query
final case class Field(field: String, q: Query) extends Query
}
68 changes: 34 additions & 34 deletions core/src/test/scala/pink/cozydev/lucille/OpSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,99 +22,99 @@ import Op._
class AssociateOpsSuite extends munit.FunSuite {

test("associates ORs") {
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((OR, TermQ("dog")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((OR, Term("dog")))
val result = associateOps(leftQs, opQs)
val expected = NonEmptyList.of(TermQ("the"), OrQ(NonEmptyList.of(TermQ("cat"), TermQ("dog"))))
val expected = NonEmptyList.of(Term("the"), Or(Term("cat"), Term("dog")))
assertEquals(result, expected)
}

test("associates ANDs") {
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((AND, TermQ("dog")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((AND, Term("dog")))
val result = associateOps(leftQs, opQs)
val expected = NonEmptyList.of(TermQ("the"), AndQ(NonEmptyList.of(TermQ("cat"), TermQ("dog"))))
val expected = NonEmptyList.of(Term("the"), And(Term("cat"), Term("dog")))
assertEquals(result, expected)
}

test("associates multiple ORs") {
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((OR, TermQ("dog")), (OR, TermQ("fish")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((OR, Term("dog")), (OR, Term("fish")))
val result = associateOps(leftQs, opQs)
val expected =
NonEmptyList.of(TermQ("the"), OrQ(NonEmptyList.of(TermQ("cat"), TermQ("dog"), TermQ("fish"))))
NonEmptyList.of(Term("the"), Or(Term("cat"), Term("dog"), Term("fish")))
assertEquals(result, expected)
}

test("associates multiple ANDs") {
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((AND, TermQ("dog")), (AND, TermQ("fish")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((AND, Term("dog")), (AND, Term("fish")))
val result = associateOps(leftQs, opQs)
val expected =
NonEmptyList.of(
TermQ("the"),
AndQ(NonEmptyList.of(TermQ("cat"), TermQ("dog"), TermQ("fish"))),
Term("the"),
And(Term("cat"), Term("dog"), Term("fish")),
)
assertEquals(result, expected)
}

test("associates with OR and then AND") {
// the cat OR ocean AND fish
// default:the default:cat +default:ocean +default:fish
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((OR, TermQ("ocean")), (AND, TermQ("fish")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((OR, Term("ocean")), (AND, Term("fish")))
val result = associateOps(leftQs, opQs)
val expected =
NonEmptyList.of(
TermQ("the"),
TermQ("cat"),
AndQ(NonEmptyList.of(TermQ("ocean"), TermQ("fish"))),
Term("the"),
Term("cat"),
And(Term("ocean"), Term("fish")),
)
assertEquals(result, expected)
}

test("associates with AND and then OR") {
// the cat AND ocean OR fish
// default:the +default:cat +default:ocean default:fish
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((AND, TermQ("ocean")), (OR, TermQ("fish")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((AND, Term("ocean")), (OR, Term("fish")))
val result = associateOps(leftQs, opQs)
val expected =
NonEmptyList.of(
TermQ("the"),
AndQ(NonEmptyList.of(TermQ("cat"), TermQ("ocean"))),
OrQ(NonEmptyList.of(TermQ("fish"))),
Term("the"),
And(Term("cat"), Term("ocean")),
Or(Term("fish")),
)
assertEquals(result, expected)
}

test("associates with two ORs and then AND") {
// the cat OR ocean OR ocean2 AND fish
// default:the default:cat default:ocean +default:ocean2 +default:fish
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((OR, TermQ("ocean")), (OR, TermQ("ocean2")), (AND, TermQ("fish")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((OR, Term("ocean")), (OR, Term("ocean2")), (AND, Term("fish")))
val result = associateOps(leftQs, opQs)
val expected =
NonEmptyList.of(
TermQ("the"),
TermQ("cat"),
TermQ("ocean"),
AndQ(NonEmptyList.of(TermQ("ocean2"), TermQ("fish"))),
Term("the"),
Term("cat"),
Term("ocean"),
And(Term("ocean2"), Term("fish")),
)
assertEquals(result, expected)
}

test("associates with two ANDs and then OR") {
// the cat AND ocean AND ocean2 OR fish
// default:the +default:cat +default:ocean +default:ocean2 default:fish
val leftQs = NonEmptyList.of(TermQ("the"), TermQ("cat"))
val opQs = List((AND, TermQ("ocean")), (AND, TermQ("ocean2")), (OR, TermQ("fish")))
val leftQs = NonEmptyList.of(Term("the"), Term("cat"))
val opQs = List((AND, Term("ocean")), (AND, Term("ocean2")), (OR, Term("fish")))
val result = associateOps(leftQs, opQs)
val expected =
NonEmptyList.of(
TermQ("the"),
AndQ(NonEmptyList.of(TermQ("cat"), TermQ("ocean"), TermQ("ocean2"))),
OrQ(NonEmptyList.of(TermQ("fish"))),
Term("the"),
And(Term("cat"), Term("ocean"), Term("ocean2")),
Or(Term("fish")),
)
assertEquals(result, expected)
}
Expand Down
Loading