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 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)) | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user