mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-30 21:40:12 +00:00 
			
		
		
		
	Generate a query string given an expression
Initialize share record and improve tests.
This commit is contained in:
		| @@ -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)) | ||||||
|  |  | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user