mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-07 15:45:59 +00:00
Generate a query string given an expression
Initialize share record and improve tests.
This commit is contained in:
parent
d065abf7ac
commit
de1baf725f
@ -123,9 +123,11 @@ object ItemQuery {
|
|||||||
final case class ChecksumMatch(checksum: String) extends Expr
|
final case class ChecksumMatch(checksum: String) extends Expr
|
||||||
final case class AttachId(id: String) extends Expr
|
final case class AttachId(id: String) extends Expr
|
||||||
|
|
||||||
final case object ValidItemStates extends Expr
|
/** A "private" expression is only visible in code, but cannot be parsed. */
|
||||||
final case object Trashed extends Expr
|
sealed trait PrivateExpr extends Expr
|
||||||
final case object ValidItemsOrTrashed 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
|
// things that can be expressed with terms above
|
||||||
sealed trait MacroExpr extends Expr {
|
sealed trait MacroExpr extends Expr {
|
||||||
|
@ -8,12 +8,23 @@ package docspell.query
|
|||||||
|
|
||||||
import cats.data.NonEmptyList
|
import cats.data.NonEmptyList
|
||||||
|
|
||||||
import docspell.query.internal.ExprParser
|
import docspell.query.internal.{ExprParser, ExprString, ExprUtil}
|
||||||
import docspell.query.internal.ExprUtil
|
|
||||||
|
|
||||||
object ItemQueryParser {
|
object ItemQueryParser {
|
||||||
|
|
||||||
|
val PrivateExprError = ExprString.PrivateExprError
|
||||||
|
type PrivateExprError = ExprString.PrivateExprError
|
||||||
|
|
||||||
def parse(input: String): Either[ParseFailure, ItemQuery] =
|
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)
|
if (input.isEmpty)
|
||||||
Left(
|
Left(
|
||||||
ParseFailure("", 0, NonEmptyList.of(ParseFailure.SimpleMessage(0, "No input.")))
|
ParseFailure("", 0, NonEmptyList.of(ParseFailure.SimpleMessage(0, "No input.")))
|
||||||
@ -24,9 +35,16 @@ object ItemQueryParser {
|
|||||||
.parseQuery(in)
|
.parseQuery(in)
|
||||||
.left
|
.left
|
||||||
.map(ParseFailure.fromError(in))
|
.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 =
|
def parseUnsafe(input: String): ItemQuery =
|
||||||
parse(input).fold(m => sys.error(m.render), identity)
|
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] =
|
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] =
|
val parenAnd: P[Unit] =
|
||||||
P.stringIn(List("(&", "(and")).void <* ws0
|
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 {
|
object ExprUtil {
|
||||||
|
|
||||||
|
def reduce(expr: Expr): Expr =
|
||||||
|
reduce(expandMacros = true)(expr)
|
||||||
|
|
||||||
/** Does some basic transformation, like unfolding nested and trees containing one value
|
/** Does some basic transformation, like unfolding nested and trees containing one value
|
||||||
* etc.
|
* etc.
|
||||||
*/
|
*/
|
||||||
def reduce(expr: Expr): Expr =
|
def reduce(expandMacros: Boolean)(expr: Expr): Expr =
|
||||||
expr match {
|
expr match {
|
||||||
case AndExpr(inner) =>
|
case AndExpr(inner) =>
|
||||||
val nodes = spliceAnd(inner)
|
val nodes = spliceAnd(inner)
|
||||||
if (nodes.tail.isEmpty) reduce(nodes.head)
|
if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head)
|
||||||
else AndExpr(nodes.map(reduce))
|
else AndExpr(nodes.map(reduce(expandMacros)))
|
||||||
|
|
||||||
case OrExpr(inner) =>
|
case OrExpr(inner) =>
|
||||||
val nodes = spliceOr(inner)
|
val nodes = spliceOr(inner)
|
||||||
if (nodes.tail.isEmpty) reduce(nodes.head)
|
if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head)
|
||||||
else OrExpr(nodes.map(reduce))
|
else OrExpr(nodes.map(reduce(expandMacros)))
|
||||||
|
|
||||||
case NotExpr(inner) =>
|
case NotExpr(inner) =>
|
||||||
inner match {
|
inner match {
|
||||||
case NotExpr(inner2) =>
|
case NotExpr(inner2) =>
|
||||||
reduce(inner2)
|
reduce(expandMacros)(inner2)
|
||||||
case InboxExpr(flag) =>
|
case InboxExpr(flag) =>
|
||||||
InboxExpr(!flag)
|
InboxExpr(!flag)
|
||||||
case DirectionExpr(flag) =>
|
case DirectionExpr(flag) =>
|
||||||
DirectionExpr(!flag)
|
DirectionExpr(!flag)
|
||||||
case _ =>
|
case _ =>
|
||||||
NotExpr(reduce(inner))
|
NotExpr(reduce(expandMacros)(inner))
|
||||||
}
|
}
|
||||||
|
|
||||||
case m: MacroExpr =>
|
case m: MacroExpr =>
|
||||||
reduce(m.body)
|
if (expandMacros) {
|
||||||
|
reduce(expandMacros)(m.body)
|
||||||
|
} else {
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
case DirectionExpr(_) =>
|
case DirectionExpr(_) =>
|
||||||
expr
|
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 cats.implicits._
|
||||||
|
|
||||||
import docspell.query.ItemQueryParser
|
import docspell.query.{ItemQuery, ItemQueryParser}
|
||||||
|
|
||||||
import munit._
|
import munit._
|
||||||
|
|
||||||
@ -64,4 +64,14 @@ class ItemQueryParserTest extends FunSuite {
|
|||||||
ItemQueryParser.parseUnsafe("(| name:hello date:2021-02 name:world name:hello )")
|
ItemQueryParser.parseUnsafe("(| name:hello date:2021-02 name:world name:hello )")
|
||||||
assertEquals(expect.copy(raw = raw.some), q)
|
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._
|
||||||
import docspell.common.syntax.all._
|
import docspell.common.syntax.all._
|
||||||
|
import docspell.query.{ItemQuery, ItemQueryParser}
|
||||||
import docspell.totp.Key
|
import docspell.totp.Key
|
||||||
|
|
||||||
import com.github.eikek.calev.CalEvent
|
import com.github.eikek.calev.CalEvent
|
||||||
@ -142,6 +143,11 @@ trait DoobieMeta extends EmilDoobieMeta {
|
|||||||
|
|
||||||
implicit val metaByteSize: Meta[ByteSize] =
|
implicit val metaByteSize: Meta[ByteSize] =
|
||||||
Meta[Long].timap(ByteSize.apply)(_.bytes)
|
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 {
|
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