diff --git a/README.md b/README.md index a5101a8..b14e743 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ MongoQuery supports several MongoDB specific literal types. - ObjectIds. `mq"""{ clientId : ObjectId("01234567890abcdef1234") }"""` - Booleans. `mq"{ expired : false }"` - Regular expressions (since 0.5). `mq"{ name : /^joe/i }"` + - ISODates. `mq"""{ createdOn : ISODate("2018-07-06T20:45:00.000") }"""` ### mqt interpolator ### diff --git a/core/src/main/scala/com/github/limansky/mongoquery/core/bsonparser/Parser.scala b/core/src/main/scala/com/github/limansky/mongoquery/core/bsonparser/Parser.scala index 4af634c..30af62e 100644 --- a/core/src/main/scala/com/github/limansky/mongoquery/core/bsonparser/Parser.scala +++ b/core/src/main/scala/com/github/limansky/mongoquery/core/bsonparser/Parser.scala @@ -16,7 +16,10 @@ package com.github.limansky.mongoquery.core.bsonparser +import java.time.{LocalDateTime, ZoneOffset} + import com.github.limansky.mongoquery.core.BSON.Member + import scala.util.parsing.combinator.syntactical.StdTokenParsers import scala.util.parsing.input.CharArrayReader @@ -62,14 +65,17 @@ class Parser extends StdTokenParsers { override val lexical = new Lexical lexical.delimiters ++= List("[", "]", "{", "}", ":", ",", "(", ")") - lexical.reserved ++= List("ObjectId", "true", "false", "null") + lexical.reserved ++= List("ObjectId", "true", "false", "null", "ISODate") lexical.operators ++= queryOperators ++ updateOperators ++ aggregationOperators + // Regular expression for ISO date (2018:07:12T10:27:00.000Z) + val isoDateRegex = """[0-9]{4}-[0-9]{2}-[0-9]{2}[T][0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}[Z]""".r + val hexDigits = Set[Char]() ++ "0123456789abcdefABCDEF".toArray import lexical._ - def value: Parser[Any] = id | regexLit | int | double | boolean | nullLit | array | variable | obj | stringLit + def value: Parser[Any] = id | dateTime | regexLit | int | double | boolean | nullLit | array | variable | obj | stringLit def operator: Parser[Operator] = elem("operator", _.isInstanceOf[OperatorLit]) ^^ (o => Operator(o.chars)) @@ -91,6 +97,10 @@ class Parser extends StdTokenParsers { def id: Parser[Id] = keyword("ObjectId") ~> "(" ~> objectIdValue <~ ")" ^^ Id + def ISODateValue= acceptIf(t => t.isInstanceOf[StringLit] && isoDateRegex.findFirstIn(t.chars).nonEmpty)(t => "Invalid ISODate: " + t.chars) ^^ (v => parseISODate(v.chars)) + + def dateTime: Parser[DateTime] = keyword("ISODate") ~> "(" ~> ISODateValue <~ ")" + def regexLit: Parser[Regex] = elem("regex", _.isInstanceOf[RegexLit]) ^^ { case RegexLit(e, o) => Regex(e, o) } def fields: Parser[Member] = elem("fields", _.isInstanceOf[FieldLit]) ^^ { case FieldLit(p) => Member(p) } @@ -116,4 +126,10 @@ class Parser extends StdTokenParsers { val rs = parts.map(p => new CharArrayReader(p.toCharArray)) phrase(obj)(new lexical.Scanner(rs)) } + + def parseISODate(stringDate: String): DateTime = { + val date = stringDate.replaceAll("Z", "") // Java ISODate format + val milli = LocalDateTime.parse(date).toInstant(ZoneOffset.UTC).toEpochMilli + DateTime(milli) + } } diff --git a/reactivemongo/src/main/scala/com/github/limansky/mongoquery/reactive/BSONParser.scala b/reactivemongo/src/main/scala/com/github/limansky/mongoquery/reactive/BSONParser.scala index 7fc5367..3f9bbfc 100644 --- a/reactivemongo/src/main/scala/com/github/limansky/mongoquery/reactive/BSONParser.scala +++ b/reactivemongo/src/main/scala/com/github/limansky/mongoquery/reactive/BSONParser.scala @@ -37,6 +37,7 @@ object BSONParser { case BSON.Object(m) => wrapObject(m) case BSON.Id(id) => BSONObjectID.parse(id).getOrElse(throw new IllegalArgumentException(s"Invalid ObjectId $id")) case BSON.Regex(r, opt) => BSONRegex(r, opt) + case BSON.DateTime(v) => BSONDateTime(v) case list: List[_] => BSONArray(list.map(wrapValue)) case s: String => BSONString(s) case n: Double => BSONDouble(n) diff --git a/reactivemongo/src/test/scala/com/github/limansky/mongoquery/reactive/BSONParserTest.scala b/reactivemongo/src/test/scala/com/github/limansky/mongoquery/reactive/BSONParserTest.scala index 3cfb5c9..1b2bf7e 100644 --- a/reactivemongo/src/test/scala/com/github/limansky/mongoquery/reactive/BSONParserTest.scala +++ b/reactivemongo/src/test/scala/com/github/limansky/mongoquery/reactive/BSONParserTest.scala @@ -1,7 +1,7 @@ package com.github.limansky.mongoquery.reactive -import org.scalatest.{ FlatSpec, Matchers } -import reactivemongo.bson.{ BSONDocument, BSONNull, BSONObjectID, BSONRegex } +import org.scalatest.{FlatSpec, Matchers} +import reactivemongo.bson.{BSONDateTime, BSONDocument, BSONNull, BSONObjectID, BSONRegex} class BSONParserTest extends FlatSpec with Matchers { "ReactiveMongo parser" should "parse valid BSON" in { @@ -30,6 +30,12 @@ class BSONParserTest extends FlatSpec with Matchers { )) } + it should "support ISODate" in { + BSONParser.parse("""{ date : ISODate("2018-07-06T20:45:00.000Z") }""") should equal(BSONDocument( + "date" -> BSONDateTime(1530909900000L) + )) + } + it should "support null" in { BSONParser.parse("{ n : null }") should equal(BSONDocument("n" -> BSONNull)) }