mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-07 06:39:32 +00:00
Generate a query string given an expression
Initialize share record and improve tests.
This commit is contained in:
parent
d065abf7ac
commit
de1baf725f
modules
query/shared/src
main/scala/docspell/query
test/scala/docspell/query
store/src/main/scala/docspell/store
@ -123,9 +123,11 @@ object ItemQuery {
|
||||
final case class ChecksumMatch(checksum: String) extends Expr
|
||||
final case class AttachId(id: String) extends Expr
|
||||
|
||||
final case object ValidItemStates extends Expr
|
||||
final case object Trashed extends Expr
|
||||
final case object ValidItemsOrTrashed extends Expr
|
||||
/** A "private" expression is only visible in code, but cannot be parsed. */
|
||||
sealed trait PrivateExpr extends Expr
|
||||
final case object ValidItemStates extends PrivateExpr
|
||||
final case object Trashed extends PrivateExpr
|
||||
final case object ValidItemsOrTrashed extends PrivateExpr
|
||||
|
||||
// things that can be expressed with terms above
|
||||
sealed trait MacroExpr extends Expr {
|
||||
|
@ -8,12 +8,23 @@ package docspell.query
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.query.internal.ExprParser
|
||||
import docspell.query.internal.ExprUtil
|
||||
import docspell.query.internal.{ExprParser, ExprString, ExprUtil}
|
||||
|
||||
object ItemQueryParser {
|
||||
|
||||
val PrivateExprError = ExprString.PrivateExprError
|
||||
type PrivateExprError = ExprString.PrivateExprError
|
||||
|
||||
def parse(input: String): Either[ParseFailure, ItemQuery] =
|
||||
parse0(input, expandMacros = true)
|
||||
|
||||
def parseKeepMacros(input: String): Either[ParseFailure, ItemQuery] =
|
||||
parse0(input, expandMacros = false)
|
||||
|
||||
private def parse0(
|
||||
input: String,
|
||||
expandMacros: Boolean
|
||||
): Either[ParseFailure, ItemQuery] =
|
||||
if (input.isEmpty)
|
||||
Left(
|
||||
ParseFailure("", 0, NonEmptyList.of(ParseFailure.SimpleMessage(0, "No input.")))
|
||||
@ -24,9 +35,16 @@ object ItemQueryParser {
|
||||
.parseQuery(in)
|
||||
.left
|
||||
.map(ParseFailure.fromError(in))
|
||||
.map(q => q.copy(expr = ExprUtil.reduce(q.expr)))
|
||||
.map(q => q.copy(expr = ExprUtil.reduce(expandMacros)(q.expr)))
|
||||
}
|
||||
|
||||
def parseUnsafe(input: String): ItemQuery =
|
||||
parse(input).fold(m => sys.error(m.render), identity)
|
||||
|
||||
def asString(q: ItemQuery.Expr): Either[PrivateExprError, String] =
|
||||
ExprString(q)
|
||||
|
||||
def unsafeAsString(q: ItemQuery.Expr): String =
|
||||
asString(q).fold(f => sys.error(s"Cannot expose private query part: $f"), identity)
|
||||
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ object BasicParser {
|
||||
)
|
||||
|
||||
private[this] val identChars: Set[Char] =
|
||||
(('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.").toSet
|
||||
(('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.@").toSet
|
||||
|
||||
val parenAnd: P[Unit] =
|
||||
P.stringIn(List("(&", "(and")).void <* ws0
|
||||
|
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.query.internal
|
||||
|
||||
import java.time.Period
|
||||
|
||||
import docspell.query.Date
|
||||
import docspell.query.Date.DateLiteral
|
||||
import docspell.query.ItemQuery.Attr._
|
||||
import docspell.query.ItemQuery.Expr._
|
||||
import docspell.query.ItemQuery._
|
||||
import docspell.query.internal.{Constants => C}
|
||||
|
||||
/** Creates the string representation for a given expression. The returned string can be
|
||||
* parsed back to the expression using `ExprParser`. Note that expressions obtained from
|
||||
* the `ItemQueryParser` have macros already expanded.
|
||||
*
|
||||
* It may fail when the expression contains non-public parts. Every expression that has
|
||||
* been created by parsing a string, can be transformed back to a string. But an
|
||||
* expression created via code may contain parts that cannot be transformed to a string.
|
||||
*/
|
||||
object ExprString {
|
||||
|
||||
final case class PrivateExprError(expr: Expr.PrivateExpr)
|
||||
type Result = Either[PrivateExprError, String]
|
||||
|
||||
def apply(expr: Expr): Result =
|
||||
expr match {
|
||||
case AndExpr(inner) =>
|
||||
val es = inner.traverse(ExprString.apply)
|
||||
es.map(_.toList.mkString(" ")).map(els => s"(& $els )")
|
||||
|
||||
case OrExpr(inner) =>
|
||||
val es = inner.traverse(ExprString.apply)
|
||||
es.map(_.toList.mkString(" ")).map(els => s"(| $els )")
|
||||
|
||||
case NotExpr(inner) =>
|
||||
inner match {
|
||||
case NotExpr(inner2) =>
|
||||
apply(inner2)
|
||||
case _ =>
|
||||
apply(inner).map(n => s"!$n")
|
||||
}
|
||||
|
||||
case m: MacroExpr =>
|
||||
Right(macroStr(m))
|
||||
|
||||
case DirectionExpr(v) =>
|
||||
Right(s"${C.incoming}${C.like}${v}")
|
||||
|
||||
case InboxExpr(v) =>
|
||||
Right(s"${C.inbox}${C.like}${v}")
|
||||
|
||||
case InExpr(attr, values) =>
|
||||
val els = values.map(quote).toList.mkString(",")
|
||||
Right(s"${attrStr(attr)}${C.in}$els")
|
||||
|
||||
case InDateExpr(attr, values) =>
|
||||
val els = values.map(dateStr).toList.mkString(",")
|
||||
Right(s"${attrStr(attr)}${C.in}$els")
|
||||
|
||||
case TagsMatch(op, values) =>
|
||||
val els = values.map(quote).toList.mkString(",")
|
||||
Right(s"${C.tag}${tagOpStr(op)}$els")
|
||||
|
||||
case TagIdsMatch(op, values) =>
|
||||
val els = values.map(quote).toList.mkString(",")
|
||||
Right(s"${C.tagId}${tagOpStr(op)}$els")
|
||||
|
||||
case Exists(field) =>
|
||||
Right(s"${C.exist}${C.like}${attrStr(field)}")
|
||||
|
||||
case Fulltext(v) =>
|
||||
Right(s"${C.content}${C.like}${quote(v)}")
|
||||
|
||||
case SimpleExpr(op, prop) =>
|
||||
prop match {
|
||||
case Property.StringProperty(attr, value) =>
|
||||
Right(s"${stringAttr(attr)}${opStr(op)}${quote(value)}")
|
||||
case Property.DateProperty(attr, value) =>
|
||||
Right(s"${dateAttr(attr)}${opStr(op)}${dateStr(value)}")
|
||||
case Property.IntProperty(attr, value) =>
|
||||
Right(s"${attrStr(attr)}${opStr(op)}$value")
|
||||
}
|
||||
|
||||
case TagCategoryMatch(op, values) =>
|
||||
val els = values.map(quote).toList.mkString(",")
|
||||
Right(s"${C.cat}${tagOpStr(op)}$els")
|
||||
|
||||
case CustomFieldMatch(name, op, value) =>
|
||||
Right(s"${C.customField}:$name${opStr(op)}${quote(value)}")
|
||||
|
||||
case CustomFieldIdMatch(id, op, value) =>
|
||||
Right(s"${C.customFieldId}:$id${opStr(op)}${quote(value)}")
|
||||
|
||||
case ChecksumMatch(cs) =>
|
||||
Right(s"${C.checksum}${C.like}$cs")
|
||||
|
||||
case AttachId(aid) =>
|
||||
Right(s"${C.attachId}${C.eqs}$aid")
|
||||
|
||||
case pe: PrivateExpr =>
|
||||
// There is no parser equivalent for this
|
||||
Left(PrivateExprError(pe))
|
||||
}
|
||||
|
||||
private[internal] def macroStr(expr: Expr.MacroExpr): String =
|
||||
expr match {
|
||||
case Expr.NamesMacro(name) =>
|
||||
s"${C.names}:${quote(name)}"
|
||||
case Expr.YearMacro(_, year) =>
|
||||
s"${C.year}:$year" //currently, only for Attr.Date
|
||||
case Expr.ConcMacro(term) =>
|
||||
s"${C.conc}:${quote(term)}"
|
||||
case Expr.CorrMacro(term) =>
|
||||
s"${C.corr}:${quote(term)}"
|
||||
case Expr.DateRangeMacro(attr, left, right) =>
|
||||
val name = attr match {
|
||||
case Attr.CreatedDate =>
|
||||
C.createdIn
|
||||
case Attr.Date =>
|
||||
C.dateIn
|
||||
case Attr.DueDate =>
|
||||
C.dueIn
|
||||
}
|
||||
(left, right) match {
|
||||
case (_: Date.DateLiteral, Date.Calc(date, calc, period)) =>
|
||||
s"$name:${dateStr(date)};${calcStr(calc)}${periodStr(period)}"
|
||||
|
||||
case (Date.Calc(date, calc, period), _: DateLiteral) =>
|
||||
s"$name:${dateStr(date)};${calcStr(calc)}${periodStr(period)}"
|
||||
|
||||
case (Date.Calc(d1, _, p1), Date.Calc(_, _, _)) =>
|
||||
s"$name:${dateStr(d1)};/${periodStr(p1)}"
|
||||
|
||||
case (_: DateLiteral, _: DateLiteral) =>
|
||||
sys.error("Invalid date range")
|
||||
}
|
||||
}
|
||||
|
||||
private[internal] def dateStr(date: Date): String =
|
||||
date match {
|
||||
case Date.Today =>
|
||||
"today"
|
||||
case Date.Local(ld) =>
|
||||
f"${ld.getYear}-${ld.getMonthValue}%02d-${ld.getDayOfMonth}%02d"
|
||||
|
||||
case Date.Millis(ms) =>
|
||||
s"ms$ms"
|
||||
|
||||
case Date.Calc(date, calc, period) =>
|
||||
val ds = dateStr(date)
|
||||
s"$ds;${calcStr(calc)}${periodStr(period)}"
|
||||
}
|
||||
|
||||
private[internal] def calcStr(c: Date.CalcDirection): String =
|
||||
c match {
|
||||
case Date.CalcDirection.Plus => "+"
|
||||
case Date.CalcDirection.Minus => "-"
|
||||
}
|
||||
|
||||
private[internal] def periodStr(p: Period): String =
|
||||
if (p.toTotalMonths == 0) s"${p.getDays}d"
|
||||
else s"${p.toTotalMonths}m"
|
||||
|
||||
private[internal] def attrStr(attr: Attr): String =
|
||||
attr match {
|
||||
case a: StringAttr => stringAttr(a)
|
||||
case a: DateAttr => dateAttr(a)
|
||||
case a: IntAttr => intAttr(a)
|
||||
}
|
||||
|
||||
private[internal] def intAttr(attr: IntAttr): String =
|
||||
attr match {
|
||||
case AttachCount =>
|
||||
Constants.attachCount
|
||||
}
|
||||
|
||||
private[internal] def dateAttr(attr: DateAttr): String =
|
||||
attr match {
|
||||
case Attr.Date =>
|
||||
Constants.date
|
||||
case DueDate =>
|
||||
Constants.due
|
||||
case CreatedDate =>
|
||||
Constants.created
|
||||
}
|
||||
|
||||
private[internal] def stringAttr(attr: StringAttr): String =
|
||||
attr match {
|
||||
case Attr.ItemName =>
|
||||
Constants.name
|
||||
case Attr.ItemId =>
|
||||
Constants.id
|
||||
case Attr.ItemSource =>
|
||||
Constants.source
|
||||
case Attr.ItemNotes =>
|
||||
Constants.notes
|
||||
case Correspondent.OrgId =>
|
||||
Constants.corrOrgId
|
||||
case Correspondent.OrgName =>
|
||||
Constants.corrOrgName
|
||||
case Correspondent.PersonId =>
|
||||
Constants.corrPersId
|
||||
case Correspondent.PersonName =>
|
||||
Constants.corrPersName
|
||||
case Concerning.EquipId =>
|
||||
Constants.concEquipId
|
||||
case Concerning.EquipName =>
|
||||
Constants.concEquipName
|
||||
case Concerning.PersonId =>
|
||||
Constants.concPersId
|
||||
case Concerning.PersonName =>
|
||||
Constants.concPersName
|
||||
case Folder.FolderName =>
|
||||
Constants.folder
|
||||
case Folder.FolderId =>
|
||||
Constants.folderId
|
||||
}
|
||||
|
||||
private[internal] def opStr(op: Operator): String =
|
||||
op match {
|
||||
case Operator.Like => Constants.like.toString
|
||||
case Operator.Gte => Constants.gte
|
||||
case Operator.Lte => Constants.lte
|
||||
case Operator.Eq => Constants.eqs.toString
|
||||
case Operator.Lt => Constants.lt.toString
|
||||
case Operator.Gt => Constants.gt.toString
|
||||
case Operator.Neq => Constants.neq
|
||||
}
|
||||
|
||||
private[internal] def tagOpStr(op: TagOperator): String =
|
||||
op match {
|
||||
case TagOperator.AllMatch => C.eqs.toString
|
||||
case TagOperator.AnyMatch => C.like.toString
|
||||
}
|
||||
|
||||
private def quote(s: String): String =
|
||||
s"\"$s\""
|
||||
}
|
@ -13,35 +13,42 @@ import docspell.query.ItemQuery._
|
||||
|
||||
object ExprUtil {
|
||||
|
||||
def reduce(expr: Expr): Expr =
|
||||
reduce(expandMacros = true)(expr)
|
||||
|
||||
/** Does some basic transformation, like unfolding nested and trees containing one value
|
||||
* etc.
|
||||
*/
|
||||
def reduce(expr: Expr): Expr =
|
||||
def reduce(expandMacros: Boolean)(expr: Expr): Expr =
|
||||
expr match {
|
||||
case AndExpr(inner) =>
|
||||
val nodes = spliceAnd(inner)
|
||||
if (nodes.tail.isEmpty) reduce(nodes.head)
|
||||
else AndExpr(nodes.map(reduce))
|
||||
if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head)
|
||||
else AndExpr(nodes.map(reduce(expandMacros)))
|
||||
|
||||
case OrExpr(inner) =>
|
||||
val nodes = spliceOr(inner)
|
||||
if (nodes.tail.isEmpty) reduce(nodes.head)
|
||||
else OrExpr(nodes.map(reduce))
|
||||
if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head)
|
||||
else OrExpr(nodes.map(reduce(expandMacros)))
|
||||
|
||||
case NotExpr(inner) =>
|
||||
inner match {
|
||||
case NotExpr(inner2) =>
|
||||
reduce(inner2)
|
||||
reduce(expandMacros)(inner2)
|
||||
case InboxExpr(flag) =>
|
||||
InboxExpr(!flag)
|
||||
case DirectionExpr(flag) =>
|
||||
DirectionExpr(!flag)
|
||||
case _ =>
|
||||
NotExpr(reduce(inner))
|
||||
NotExpr(reduce(expandMacros)(inner))
|
||||
}
|
||||
|
||||
case m: MacroExpr =>
|
||||
reduce(m.body)
|
||||
if (expandMacros) {
|
||||
reduce(expandMacros)(m.body)
|
||||
} else {
|
||||
m
|
||||
}
|
||||
|
||||
case DirectionExpr(_) =>
|
||||
expr
|
||||
|
@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.query
|
||||
|
||||
import java.time.{Instant, Period, ZoneOffset}
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.query.ItemQuery.Expr.TagIdsMatch
|
||||
import docspell.query.ItemQuery._
|
||||
|
||||
import org.scalacheck.Gen
|
||||
|
||||
/** Generator for syntactically valid queries. */
|
||||
object ItemQueryGen {
|
||||
|
||||
def exprGen: Gen[Expr] =
|
||||
Gen.oneOf(
|
||||
simpleExprGen,
|
||||
existsExprGen,
|
||||
inExprGen,
|
||||
inDateExprGen,
|
||||
inboxExprGen,
|
||||
directionExprGen,
|
||||
tagIdsMatchExprGen,
|
||||
tagMatchExprGen,
|
||||
tagCatMatchExpr,
|
||||
customFieldMatchExprGen,
|
||||
customFieldIdMatchExprGen,
|
||||
fulltextExprGen,
|
||||
checksumMatchExprGen,
|
||||
attachIdExprGen,
|
||||
namesMacroGen,
|
||||
corrMacroGen,
|
||||
concMacroGen,
|
||||
yearMacroGen,
|
||||
dateRangeMacro,
|
||||
Gen.lzy(andExprGen(exprGen)),
|
||||
Gen.lzy(orExprGen(exprGen)),
|
||||
Gen.lzy(notExprGen(exprGen))
|
||||
)
|
||||
|
||||
def andExprGen(g: Gen[Expr]): Gen[Expr.AndExpr] =
|
||||
nelGen(g).map(Expr.AndExpr)
|
||||
|
||||
def orExprGen(g: Gen[Expr]): Gen[Expr.OrExpr] =
|
||||
nelGen(g).map(Expr.OrExpr)
|
||||
|
||||
// avoid generating nested not expressions, they are already flattened by the parser
|
||||
// and only occur artificially
|
||||
def notExprGen(g: Gen[Expr]): Gen[Expr] =
|
||||
g.map {
|
||||
case Expr.NotExpr(inner) => inner
|
||||
case e => Expr.NotExpr(e)
|
||||
}
|
||||
|
||||
val opGen: Gen[Operator] =
|
||||
Gen.oneOf(
|
||||
Operator.Like,
|
||||
Operator.Gte,
|
||||
Operator.Lt,
|
||||
Operator.Gt,
|
||||
Operator.Lte,
|
||||
Operator.Eq,
|
||||
Operator.Neq
|
||||
)
|
||||
|
||||
val tagOpGen: Gen[TagOperator] =
|
||||
Gen.oneOf(TagOperator.AllMatch, TagOperator.AnyMatch)
|
||||
|
||||
val stringAttrGen: Gen[Attr.StringAttr] =
|
||||
Gen.oneOf(
|
||||
Attr.Concerning.EquipName,
|
||||
Attr.Concerning.EquipId,
|
||||
Attr.Concerning.PersonName,
|
||||
Attr.Concerning.PersonId,
|
||||
Attr.Correspondent.OrgName,
|
||||
Attr.Correspondent.OrgId,
|
||||
Attr.Correspondent.PersonName,
|
||||
Attr.Correspondent.PersonId,
|
||||
Attr.ItemId,
|
||||
Attr.ItemName,
|
||||
Attr.ItemSource,
|
||||
Attr.ItemNotes,
|
||||
Attr.Folder.FolderId,
|
||||
Attr.Folder.FolderName
|
||||
)
|
||||
|
||||
val dateAttrGen: Gen[Attr.DateAttr] =
|
||||
Gen.oneOf(Attr.Date, Attr.DueDate, Attr.CreatedDate)
|
||||
|
||||
val intAttrGen: Gen[Attr.IntAttr] =
|
||||
Gen.const(Attr.AttachCount)
|
||||
|
||||
val attrGen: Gen[Attr] =
|
||||
Gen.oneOf(stringAttrGen, dateAttrGen, intAttrGen)
|
||||
|
||||
private val valueChars =
|
||||
Gen.oneOf(Gen.alphaNumChar, Gen.oneOf(" /{}*?-:@#$~+%…_[]^!ß"))
|
||||
|
||||
private val stringValueGen: Gen[String] =
|
||||
Gen.choose(1, 20).flatMap(n => Gen.stringOfN(n, valueChars))
|
||||
|
||||
private val intValueGen: Gen[Int] =
|
||||
Gen.choose(1900, 9999)
|
||||
|
||||
private val identGen: Gen[String] =
|
||||
Gen
|
||||
.choose(3, 12)
|
||||
.flatMap(n =>
|
||||
Gen.stringOfN(
|
||||
n,
|
||||
Gen.oneOf((('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.@").toSet)
|
||||
)
|
||||
)
|
||||
|
||||
private def nelGen[T](gen: Gen[T]): Gen[NonEmptyList[T]] =
|
||||
for {
|
||||
head <- gen
|
||||
tail <- Gen.choose(0, 9).flatMap(n => Gen.listOfN(n, gen))
|
||||
} yield NonEmptyList(head, tail)
|
||||
|
||||
private val dateMillisGen: Gen[Long] =
|
||||
Gen.choose(0, Instant.parse("2100-12-24T20:00:00Z").toEpochMilli)
|
||||
|
||||
val localDateGen: Gen[Date.Local] =
|
||||
dateMillisGen
|
||||
.map(ms => Instant.ofEpochMilli(ms).atOffset(ZoneOffset.UTC).toLocalDate)
|
||||
.map(Date.Local)
|
||||
|
||||
val millisDateGen: Gen[Date.Millis] =
|
||||
dateMillisGen.map(Date.Millis)
|
||||
|
||||
val dateLiteralGen: Gen[Date.DateLiteral] =
|
||||
Gen.oneOf(
|
||||
localDateGen,
|
||||
millisDateGen,
|
||||
Gen.const(Date.Today)
|
||||
)
|
||||
|
||||
val periodGen: Gen[Period] =
|
||||
for {
|
||||
mOrD <- Gen.oneOf(a => Period.ofDays(a), a => Period.ofMonths(a))
|
||||
num <- Gen.choose(1, 30)
|
||||
} yield mOrD(num)
|
||||
|
||||
val calcGen: Gen[Date.CalcDirection] =
|
||||
Gen.oneOf(Date.CalcDirection.Plus, Date.CalcDirection.Minus)
|
||||
|
||||
val dateCalcGen: Gen[Date.Calc] =
|
||||
for {
|
||||
dl <- dateLiteralGen
|
||||
calc <- calcGen
|
||||
period <- periodGen
|
||||
} yield Date.Calc(dl, calc, period)
|
||||
|
||||
val dateValueGen: Gen[Date] =
|
||||
Gen.oneOf(dateLiteralGen, dateCalcGen)
|
||||
|
||||
val stringPropGen: Gen[Property.StringProperty] =
|
||||
for {
|
||||
attr <- stringAttrGen
|
||||
sval <- stringValueGen
|
||||
} yield Property.StringProperty(attr, sval)
|
||||
|
||||
val intPropGen: Gen[Property.IntProperty] =
|
||||
for {
|
||||
attr <- intAttrGen
|
||||
ival <- intValueGen
|
||||
} yield Property.IntProperty(attr, ival)
|
||||
|
||||
val datePropGen: Gen[Property.DateProperty] =
|
||||
for {
|
||||
attr <- dateAttrGen
|
||||
dv <- dateValueGen
|
||||
} yield Property.DateProperty(attr, dv)
|
||||
|
||||
val propertyGen: Gen[Property] =
|
||||
Gen.oneOf(stringPropGen, datePropGen, intPropGen)
|
||||
|
||||
val simpleExprGen: Gen[Expr.SimpleExpr] =
|
||||
for {
|
||||
op <- opGen
|
||||
prop <- propertyGen
|
||||
} yield Expr.SimpleExpr(op, prop)
|
||||
|
||||
val existsExprGen: Gen[Expr.Exists] =
|
||||
attrGen.map(Expr.Exists)
|
||||
|
||||
val inExprGen: Gen[Expr.InExpr] =
|
||||
for {
|
||||
attr <- stringAttrGen
|
||||
vals <- nelGen(stringValueGen)
|
||||
} yield Expr.InExpr(attr, vals)
|
||||
|
||||
val inDateExprGen: Gen[Expr.InDateExpr] =
|
||||
for {
|
||||
attr <- dateAttrGen
|
||||
vals <- nelGen(dateValueGen)
|
||||
} yield Expr.InDateExpr(attr, vals)
|
||||
|
||||
val inboxExprGen: Gen[Expr.InboxExpr] =
|
||||
Gen.oneOf(true, false).map(Expr.InboxExpr)
|
||||
|
||||
val directionExprGen: Gen[Expr.DirectionExpr] =
|
||||
Gen.oneOf(true, false).map(Expr.DirectionExpr)
|
||||
|
||||
val tagIdsMatchExprGen: Gen[Expr.TagIdsMatch] =
|
||||
for {
|
||||
op <- tagOpGen
|
||||
vals <- nelGen(stringValueGen)
|
||||
} yield TagIdsMatch(op, vals)
|
||||
|
||||
val tagMatchExprGen: Gen[Expr.TagsMatch] =
|
||||
for {
|
||||
op <- tagOpGen
|
||||
vals <- nelGen(stringValueGen)
|
||||
} yield Expr.TagsMatch(op, vals)
|
||||
|
||||
val tagCatMatchExpr: Gen[Expr.TagCategoryMatch] =
|
||||
for {
|
||||
op <- tagOpGen
|
||||
vals <- nelGen(stringValueGen)
|
||||
} yield Expr.TagCategoryMatch(op, vals)
|
||||
|
||||
val customFieldMatchExprGen: Gen[Expr.CustomFieldMatch] =
|
||||
for {
|
||||
name <- identGen
|
||||
op <- opGen
|
||||
value <- stringValueGen
|
||||
} yield Expr.CustomFieldMatch(name, op, value)
|
||||
|
||||
val customFieldIdMatchExprGen: Gen[Expr.CustomFieldIdMatch] =
|
||||
for {
|
||||
name <- identGen
|
||||
op <- opGen
|
||||
value <- identGen
|
||||
} yield Expr.CustomFieldIdMatch(name, op, value)
|
||||
|
||||
val fulltextExprGen: Gen[Expr.Fulltext] =
|
||||
Gen
|
||||
.choose(3, 20)
|
||||
.flatMap(n => Gen.stringOfN(n, valueChars))
|
||||
.map(Expr.Fulltext)
|
||||
|
||||
val checksumMatchExprGen: Gen[Expr.ChecksumMatch] =
|
||||
Gen.stringOfN(64, Gen.hexChar).map(Expr.ChecksumMatch)
|
||||
|
||||
val attachIdExprGen: Gen[Expr.AttachId] =
|
||||
identGen.map(Expr.AttachId)
|
||||
|
||||
val namesMacroGen: Gen[Expr.NamesMacro] =
|
||||
stringValueGen.map(Expr.NamesMacro)
|
||||
|
||||
val concMacroGen: Gen[Expr.ConcMacro] =
|
||||
stringValueGen.map(Expr.ConcMacro)
|
||||
|
||||
val corrMacroGen: Gen[Expr.CorrMacro] =
|
||||
stringValueGen.map(Expr.CorrMacro)
|
||||
|
||||
val yearMacroGen: Gen[Expr.YearMacro] =
|
||||
Gen.choose(1900, 9999).map(Expr.YearMacro(Attr.Date, _))
|
||||
|
||||
val dateRangeMacro: Gen[Expr.DateRangeMacro] =
|
||||
for {
|
||||
attr <- dateAttrGen
|
||||
dl <- dateLiteralGen
|
||||
p <- periodGen
|
||||
calc <- Gen.option(calcGen)
|
||||
range = calc match {
|
||||
case Some(c @ Date.CalcDirection.Plus) =>
|
||||
Expr.DateRangeMacro(attr, dl, Date.Calc(dl, c, p))
|
||||
case Some(c @ Date.CalcDirection.Minus) =>
|
||||
Expr.DateRangeMacro(attr, Date.Calc(dl, c, p), dl)
|
||||
case None =>
|
||||
Expr.DateRangeMacro(
|
||||
attr,
|
||||
Date.Calc(dl, Date.CalcDirection.Minus, p),
|
||||
Date.Calc(dl, Date.CalcDirection.Plus, p)
|
||||
)
|
||||
}
|
||||
} yield range
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.query.internal
|
||||
|
||||
import java.time.{LocalDate, Period}
|
||||
|
||||
import docspell.query.ItemQuery._
|
||||
import docspell.query.{Date, ItemQueryGen, ParseFailure}
|
||||
|
||||
import munit.{FunSuite, ScalaCheckSuite}
|
||||
import org.scalacheck.Prop.forAll
|
||||
|
||||
class ExprStringTest extends FunSuite with ScalaCheckSuite {
|
||||
|
||||
// parses the query without reducing and expanding macros
|
||||
def singleParse(s: String): Expr =
|
||||
ExprParser
|
||||
.parseQuery(s)
|
||||
.left
|
||||
.map(ParseFailure.fromError(s))
|
||||
.fold(f => sys.error(f.render), _.expr)
|
||||
|
||||
def exprString(expr: Expr): String =
|
||||
ExprString(expr).fold(f => sys.error(f.toString), identity)
|
||||
|
||||
test("macro: name") {
|
||||
val str = exprString(Expr.NamesMacro("test"))
|
||||
val q = singleParse(str)
|
||||
assertEquals(str, "names:\"test\"")
|
||||
assertEquals(q, Expr.NamesMacro("test"))
|
||||
}
|
||||
|
||||
test("macro: year") {
|
||||
val str = exprString(Expr.YearMacro(Attr.Date, 1990))
|
||||
val q = singleParse(str)
|
||||
assertEquals(str, "year:1990")
|
||||
assertEquals(q, Expr.YearMacro(Attr.Date, 1990))
|
||||
}
|
||||
|
||||
test("macro: daterange") {
|
||||
val range = Expr.DateRangeMacro(
|
||||
attr = Attr.Date,
|
||||
left = Date.Calc(
|
||||
date = Date.Local(
|
||||
date = LocalDate.of(2076, 12, 9)
|
||||
),
|
||||
calc = Date.CalcDirection.Minus,
|
||||
period = Period.ofMonths(27)
|
||||
),
|
||||
right = Date.Local(LocalDate.of(2076, 12, 9))
|
||||
)
|
||||
val str = exprString(range)
|
||||
val q = singleParse(str)
|
||||
assertEquals(str, "dateIn:2076-12-09;-27m")
|
||||
assertEquals(q, range)
|
||||
}
|
||||
|
||||
property("generate expr and parse it") {
|
||||
forAll(ItemQueryGen.exprGen) { expr =>
|
||||
val str = exprString(expr)
|
||||
val q = singleParse(str)
|
||||
assertEquals(q, expr)
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ package docspell.query.internal
|
||||
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.query.ItemQueryParser
|
||||
import docspell.query.{ItemQuery, ItemQueryParser}
|
||||
|
||||
import munit._
|
||||
|
||||
@ -64,4 +64,14 @@ class ItemQueryParserTest extends FunSuite {
|
||||
ItemQueryParser.parseUnsafe("(| name:hello date:2021-02 name:world name:hello )")
|
||||
assertEquals(expect.copy(raw = raw.some), q)
|
||||
}
|
||||
|
||||
test("f.id:name=value") {
|
||||
val raw = "f.id:QsuGW@=\"dAHBstXJd0\""
|
||||
val q = ItemQueryParser.parseUnsafe(raw)
|
||||
val expect =
|
||||
ItemQuery.Expr.CustomFieldIdMatch("QsuGW@", ItemQuery.Operator.Eq, "dAHBstXJd0")
|
||||
|
||||
assertEquals(q.expr, expect)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import java.time.{Instant, LocalDate}
|
||||
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.query.{ItemQuery, ItemQueryParser}
|
||||
import docspell.totp.Key
|
||||
|
||||
import com.github.eikek.calev.CalEvent
|
||||
@ -142,6 +143,11 @@ trait DoobieMeta extends EmilDoobieMeta {
|
||||
|
||||
implicit val metaByteSize: Meta[ByteSize] =
|
||||
Meta[Long].timap(ByteSize.apply)(_.bytes)
|
||||
|
||||
implicit val metaItemQuery: Meta[ItemQuery] =
|
||||
Meta[String].timap(s => ItemQueryParser.parseUnsafe(s))(q =>
|
||||
q.raw.getOrElse(ItemQueryParser.unsafeAsString(q.expr))
|
||||
)
|
||||
}
|
||||
|
||||
object DoobieMeta extends DoobieMeta {
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.query.ItemQuery
|
||||
import docspell.store.qb._
|
||||
|
||||
final case class RShare(
|
||||
id: Ident,
|
||||
cid: Ident,
|
||||
query: ItemQuery,
|
||||
enabled: Boolean,
|
||||
password: Option[Password],
|
||||
publishedAt: Timestamp,
|
||||
publishedUntil: Timestamp,
|
||||
views: Int,
|
||||
lastAccess: Option[Timestamp]
|
||||
) {}
|
||||
|
||||
object RShare {
|
||||
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "item_share";
|
||||
|
||||
val id = Column[Ident]("id", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val query = Column[ItemQuery]("query", this)
|
||||
val enabled = Column[Boolean]("enabled", this)
|
||||
val password = Column[Password]("password", this)
|
||||
val publishedAt = Column[Timestamp]("published_at", this)
|
||||
val publishedUntil = Column[Timestamp]("published_until", this)
|
||||
val views = Column[Int]("views", this)
|
||||
val lastAccess = Column[Timestamp]("last_access", this)
|
||||
|
||||
val all: NonEmptyList[Column[_]] =
|
||||
NonEmptyList.of(
|
||||
id,
|
||||
cid,
|
||||
query,
|
||||
enabled,
|
||||
password,
|
||||
publishedAt,
|
||||
publishedUntil,
|
||||
views,
|
||||
lastAccess
|
||||
)
|
||||
}
|
||||
|
||||
val T: Table = Table(None)
|
||||
def as(alias: String): Table = Table(Some(alias))
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user