From 5f7ccafb23a178bd6a56ff730dca7b243167433a Mon Sep 17 00:00:00 2001 From: Faiaz Sanaulla Date: Sat, 26 May 2018 13:45:27 +0300 Subject: [PATCH] [MACROS][ISSUE-52]: added support for @timestamp annotation (#55) * [MACROS][ISSUE-52]: refactoring MacroImpl, added @timestamp annotations * [MACROS][ISSUE-52]: some experimenting with macro * [MACROS][ISSUE-52]: integration of timestamp functionality in macro readers * [MACROS][ISSUE-52]: added support for @timestamp annotation * [MACROS][ISSUE-52]: update documents * [MACROS][ISSUE-52]: setting version 0.2.3 --- docs/macros.md | 35 +- .../github/fsanaulla/macros/MacrosImpl.scala | 423 ++++++++---------- .../macros/annotations/timestamp.scala | 7 + .../fsanaulla/macros/MacroFormatterSpec.scala | 23 +- .../fsanaulla/macros/MacroReaderSpec.scala | 24 +- .../fsanaulla/macros/MacroWriterSpec.scala | 12 +- version.sbt | 2 +- 7 files changed, 276 insertions(+), 250 deletions(-) create mode 100644 macros/src/main/scala/com/github/fsanaulla/macros/annotations/timestamp.scala diff --git a/docs/macros.md b/docs/macros.md index a6bf7f54..db46712c 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -6,11 +6,31 @@ To use it, add to your `build.sbt`: ``` libraryDependencies += "com.github.fsanaulla" %% "chronicler-macros" % ``` -## Usage +## Tag +All [tag](https://docs.influxdata.com/influxdb/v1.5/concepts/glossary/#tag)'s field must be marked with `@tag`. It's can be used for optional fields. + +Supported types: **`String`**, **`Option[String]`** + +## Field +All [field](https://docs.influxdata.com/influxdb/v1.5/concepts/glossary/#field)'s must be market with `@field` annotation. + +Supported types: **`Int`**, **`Long`**, **`Double`**, **`Boolean`**, **`String`**. + +## Timestamp +You can specify which field will be used as a influx [timestamp](https://docs.influxdata.com/influxdb/v1.5/concepts/glossary/#timestamp) in process serialization/deserialization by marking with `@timestamp` annotation. +It's optional field. If it's not specified, time will be generated on database level. Otherwise will be setted from entity. + +**Remember**: InfluxDB use nano precision. + +Supported type: **Long**. + +# Usage Feel the power of Macros. Let's start from reader example: -``` -case class Entity(@tag name: String, @field age: Int) +```scala +case class Entity(@tag name: String, + @field age: Int, + @timestamp time: Long) // that's all, after compilation, at this place will apper valid InfluxReader[T] implicit val rd: InfluxReader[Entity] = Macros.reader[Entity] @@ -18,7 +38,7 @@ implicit val rd: InfluxReader[Entity] = Macros.reader[Entity] // it's required for using type method, such ``` For writer it's little bit differ, because you need specify [tag](https://docs.influxdata.com/influxdb/v1.5/concepts/key_concepts/#tag-key) and [field](https://docs.influxdata.com/influxdb/v1.5/concepts/key_concepts/#field-value). It can be done by simply annotating: -``` +```scala case class Entity(@tag name: String, @field age: Int) // that's all, after compilation, at this place will apper valid InfluxWriter[T] @@ -28,12 +48,13 @@ implicit val wr: InfluxWriter[Entity] = Macros.writer[Entity] meas.write[Entity](Entity("Martin", 54) ``` You can add both of them in the scope by using: -``` -implicit val fmt = Macros.format[Entity] +```scala +implicit val fmt: InfluxFormatter[Entity] = Macros.format[Entity] meas.write[Entity](Entity("Martin", 54) db.read[Entity]("SELECT * FROM some_meas") ``` + In short it's look like: -1. Mark tags(`@tag`) and fields(`@field`). +1. Mark tags(`@tag`) and fields(`@field`), and optional (`@timestamp`). 2. Generate reader/writer/formatter. diff --git a/macros/src/main/scala/com/github/fsanaulla/macros/MacrosImpl.scala b/macros/src/main/scala/com/github/fsanaulla/macros/MacrosImpl.scala index 83bae7c3..bf2188dd 100644 --- a/macros/src/main/scala/com/github/fsanaulla/macros/MacrosImpl.scala +++ b/macros/src/main/scala/com/github/fsanaulla/macros/MacrosImpl.scala @@ -1,7 +1,6 @@ package com.github.fsanaulla.macros -import com.github.fsanaulla.core.model.DeserializationException -import com.github.fsanaulla.macros.annotations.{field, tag} +import com.github.fsanaulla.macros.annotations.{field, tag, timestamp} import scala.language.experimental.macros import scala.reflect.macros.blackbox @@ -11,115 +10,34 @@ import scala.reflect.macros.blackbox * Author: fayaz.sanaulla@gmail.com * Date: 13.02.18 */ -private[macros] object MacrosImpl { +private[macros] class MacrosImpl(val c: blackbox.Context) { + import c.universe._ - /*** - * Generate AST for current type at compile time. - * @tparam T - Type parameter for whom will be generated AST - */ - def writer_impl[T: c.WeakTypeTag](c: blackbox.Context): c.universe.Tree = { - import c.universe._ - - def tpdls[A: TypeTag]: c.universe.Type = typeOf[A].dealias - - val SUPPORTED_TAGS_TYPES = Seq(tpdls[Option[String]], tpdls[String]) - val SUPPORTED_FIELD_TYPES = Seq(tpdls[Boolean], tpdls[Int], tpdls[Long], tpdls[Double], tpdls[String]) - - /** Is it Option container*/ - def isOption(tpe: c.universe.Type): Boolean = - tpe.typeConstructor =:= typeOf[Option[_]].typeConstructor - - def isSupportedTagType(tpe: c.universe.Type): Boolean = - SUPPORTED_TAGS_TYPES.exists(t => t =:= tpe) - - def isSupportedFieldType(tpe: c.universe.Type): Boolean = - SUPPORTED_FIELD_TYPES.exists(t => t =:= tpe) - - - /** Predicate for finding fields of instance marked with '@tag' annotation */ - def isTag(m: MethodSymbol): Boolean = { - if (m.annotations.exists(_.tree.tpe =:= typeOf[tag])) { - if (isSupportedTagType(m.returnType)) true - else c.abort(c.enclosingPosition, s"@tag ${m.name} has unsupported type ${m.returnType}. Tag must have String or Optional[String]") - } else false - } - - /** Predicate for finding fields of instance marked with '@field' annotation */ - def isField(m: MethodSymbol): Boolean = { - if (m.annotations.exists(_.tree.tpe =:= typeOf[field])) { - if (isSupportedFieldType(m.returnType)) true - else c.abort(c.enclosingPosition, s"Unsupported type for @field ${m.name}: ${m.returnType}") - } else false - } - - val tpe = c.weakTypeOf[T] - - val methods: List[MethodSymbol] = tpe.decls.toList collect { - case m: MethodSymbol if m.isCaseAccessor => m - } - - // If `methods` comes up empty we raise a compilation error: - if (methods.lengthCompare(1) < 0) { - c.abort(c.enclosingPosition, "Type parameter must be a case class with more then 1 fields") - } - - val optTags: List[c.universe.Tree] = methods collect { - case m: MethodSymbol if isTag(m) && isOption(m.returnType) => - q"${m.name.decodedName.toString} -> obj.${m.name}" - } + final val TIMESTAMP_TYPE = tpdls[Long] - val nonOptTags: List[c.universe.Tree] = methods collect { - case m: MethodSymbol if isTag(m) && !isOption(m.returnType) => - q"${m.name.decodedName.toString} -> obj.${m.name}" - } - - val fields = methods collect { - case m: MethodSymbol if isField(m) => - q"${m.name.decodedName.toString} -> obj.${m.name}" - } + final val SUPPORTED_TAGS_TYPES = + Seq(tpdls[Option[String]], tpdls[String]) - q""" - new InfluxWriter[$tpe] { - def write(obj: $tpe): String = { - val fieldsMap: Map[String, Any] = Map(..$fields) - val fields = fieldsMap map { case (k, v) => k + "=" + v } mkString(" ") + final val SUPPORTED_FIELD_TYPES = + Seq(tpdls[Boolean], tpdls[Int], tpdls[Double], tpdls[String], TIMESTAMP_TYPE) - val nonOptTagsMap: Map[String, String] = Map(..$nonOptTags) - val nonOptTags: String = nonOptTagsMap map { - case (k: String, v: String) => k + "=" + v - } mkString(",") + /** return type dealias */ + def tpdls[A: TypeTag]: c.universe.Type = typeOf[A].dealias - val optTagsMap: Map[String, Option[String]] = Map(..$optTags) - val optTags: String = optTagsMap collect { - case (k: String, v: Option[String]) if v.isDefined => k + "=" + v.get - } mkString(",") - - val combTags: String = if (optTags.isEmpty) nonOptTags else nonOptTags + "," + optTags - - combTags + " " + fields trim - } - }""" + /** Check if this method valid timestamp */ + def isTimestamp(m: MethodSymbol): Boolean = { + if (m.annotations.exists(_.tree.tpe =:= typeOf[timestamp])) { + if (m.returnType =:= TIMESTAMP_TYPE) true + else c.abort(c.enclosingPosition, s"@timestamp ${m.name} has unsupported type ${m.returnType}. Timestamp must be Long") + } else false } - /*** - * Generate AST for current type at compile time. - * @tparam T - Type parameter for whom will be generated AST + /** + * Generate read method for specified type + * @param tpe - for which type + * @return - AST that will be expanded to read method */ - def reader_impl[T: c.WeakTypeTag](c: blackbox.Context): c.universe.Tree = { - import c.universe._ - - def tpdls[A: TypeTag]: c.universe.Type = typeOf[A].dealias - - val tpe = c.weakTypeOf[T] - - val methods = tpe.decls.toList collect { - case m: MethodSymbol if m.isCaseAccessor => - m.name.decodedName.toString -> m.returnType.dealias - } - - if (methods.lengthCompare(1) < 0) { - c.abort(c.enclosingPosition, "Type parameter must be a case class with more then 1 fields") - } + def createReadMethod(tpe: c.universe.Type): c.universe.Tree = { val bool = tpdls[Boolean] val int = tpdls[Int] @@ -128,7 +46,19 @@ private[macros] object MacrosImpl { val string = tpdls[String] val optString = tpdls[Option[String]] - val params = methods + val (timeField, othFields) = tpe.decls.toList + .collect { case m: MethodSymbol if m.isCaseAccessor => m } + .partition(isTimestamp) + + if (timeField.size > 1) + c.abort(c.enclosingPosition, "Only one field can be marked as @timestamp.") + + if (othFields.lengthCompare(1) < 0) + c.abort(c.enclosingPosition, "Type parameter must be a case class with more then 1 fields.") + + val fields = othFields.map(m => m.name.decodedName.toString -> m.returnType.dealias) + + val constructorParams = fields .sortBy(_._1) .map { case (k, v) => TermName(k) -> v } .map { @@ -141,113 +71,154 @@ private[macros] object MacrosImpl { case (_, other) => c.abort(c.enclosingPosition, s"Unsupported type $other") } - val paramss = methods + val patternParams: List[Tree] = fields .map(_._1) .sorted // influx return results in alphabetical order .map(k => TermName(k)) .map(k => pq"$k: JValue") - // success case clause component - val successPat = pq"Array(..$paramss)" - val successBody = q"new $tpe(..$params)" - val successCase = cq"$successPat => $successBody" + val readMethodDefinition: Tree = if (timeField.nonEmpty) { - // failure case clause component - val failurePat = pq"_" - val failureMsg = s"Can't deserialize $tpe object" - val failureBody = q"throw new DeserializationException($failureMsg)" - val failureCase = cq"$failurePat => $failureBody" + val timestamp = TermName(timeField.head.name.decodedName.toString) - val cases = successCase :: failureCase :: Nil + val constructorTime: c.universe.Tree = q"$timestamp = toNanoLong($timestamp.asString)" - q""" - new InfluxReader[$tpe] { - import jawn.ast.{JValue, JArray} - import com.github.fsanaulla.core.model.DeserializationException + val patternTime: c.universe.Tree = pq"$timestamp: JValue" - def read(js: JArray): $tpe = js.vs.tail match { case ..$cases } - } - """ + val patterns: List[Tree] = patternTime :: patternParams + val constructor: List[Tree] = constructorTime :: constructorParams + + // success case clause component + val successPat = pq"Array(..$patterns)" + val successBody = q"new $tpe(..$constructor)" + val successCase = cq"$successPat => $successBody" + + // failure case clause component + val failurePat = pq"_" + val failureMsg = s"Can't deserialize $tpe object." + val failureBody = q"throw new DeserializationException($failureMsg)" + val failureCase = cq"$failurePat => $failureBody" + + val cases = successCase :: failureCase :: Nil + + q"js.vs match { case ..$cases }" + + } else { + + // success case clause component + val successPat = pq"Array(..$patternParams)" + val successBody = q"new $tpe(..$constructorParams)" + val successCase = cq"$successPat => $successBody" + + // failure case clause component + val failurePat = pq"_" + val failureMsg = s"Can't deserialize $tpe object." + val failureBody = q"throw new DeserializationException($failureMsg)" + val failureCase = cq"$failurePat => $failureBody" + + val cases = successCase :: failureCase :: Nil + + q"js.vs.tail match { case ..$cases }" + } + + q"""def read(js: JArray): $tpe = $readMethodDefinition""" } - /*** - * Generate AST for current type at compile time. - * @tparam T - Type parameter for whom will be generated AST + /** + * Create write method for specified type + * @param tpe - specified type + * @return - AST that will be expanded to write method */ - def format_impl[T: c.WeakTypeTag](c: blackbox.Context): c.universe.Tree = { - import c.universe._ - - def tpdls[A: TypeTag]: c.universe.Type = typeOf[A].dealias + def createWriteMethod(tpe: c.Type): c.universe.Tree = { - val SUPPORTED_TAGS_TYPES = Seq(tpdls[Option[String]], tpdls[String]) - val SUPPORTED_FIELD_TYPES = Seq(tpdls[Boolean], tpdls[Int], tpdls[Long], tpdls[Double], tpdls[String]) + /** Is it Option container*/ + def isOption(tpe: c.universe.Type): Boolean = + tpe.typeConstructor =:= typeOf[Option[_]].typeConstructor - val tpe = c.weakTypeOf[T] + /** Is it valid tag type */ + def isSupportedTagType(tpe: c.universe.Type): Boolean = + SUPPORTED_TAGS_TYPES.exists(t => t =:= tpe) - val methods = tpe.decls.toList + /** Is it valid field type */ + def isSupportedFieldType(tpe: c.universe.Type): Boolean = + SUPPORTED_FIELD_TYPES.exists(t => t =:= tpe) - if (methods.lengthCompare(1) < 0) { - c.abort(c.enclosingPosition, "Type parameter must be a case class with more then 1 fields") + /** Predicate for finding fields of instance marked with '@tag' annotation */ + def isTag(m: MethodSymbol): Boolean = { + if (m.annotations.exists(_.tree.tpe =:= typeOf[tag])) { + if (isSupportedTagType(m.returnType)) true + else c.abort(c.enclosingPosition, s"@tag ${m.name} has unsupported type ${m.returnType}. Tag must have String or Optional[String]") + } else false } - def createWriteMethod(methods: List[c.universe.Symbol]): c.universe.Tree = { + /** Predicate for finding fields of instance marked with '@field' annotation */ + def isField(m: MethodSymbol): Boolean = { + if (m.annotations.exists(_.tree.tpe =:= typeOf[field])) { + if (isSupportedFieldType(m.returnType)) true + else c.abort(c.enclosingPosition, s"Unsupported type for @field ${m.name}: ${m.returnType}") + } else false + } - val writeMethods: List[MethodSymbol] = methods collect { - case m: MethodSymbol if m.isCaseAccessor => m - } + /** Check method for one of @tag, @field annotaions */ + def isMarked(m: MethodSymbol): Boolean = isTag(m) || isField(m) - /** Is it Option container*/ - def isOption(tpe: c.universe.Type): Boolean = - tpe.typeConstructor =:= typeOf[Option[_]].typeConstructor + val (timeField, othField) = tpe.decls.toList + .collect { case m: MethodSymbol if m.isCaseAccessor => m } + .partition(isTimestamp) - def isSupportedTagType(tpe: c.universe.Type): Boolean = - SUPPORTED_TAGS_TYPES.exists(t => t =:= tpe) + if (timeField.size > 1) + c.abort(c.enclosingPosition, "Only one field can be marked as @timestamp.") - def isSupportedFieldType(tpe: c.universe.Type): Boolean = - SUPPORTED_FIELD_TYPES.exists(t => t =:= tpe) + if (othField.lengthCompare(1) < 0) + c.abort(c.enclosingPosition, "Type parameter must be a case class with more then 1 fields") - /** Predicate for finding fields of instance marked with '@tag' annotation */ - def isTag(m: MethodSymbol): Boolean = { - if (m.annotations.exists(_.tree.tpe =:= typeOf[tag])) { - if (isSupportedTagType(m.returnType)) true - else c.abort(c.enclosingPosition, s"@tag ${m.name} has unsupported type ${m.returnType}. Tag must have String or Optional[String]") - } else false + val (tagsMethods, fieldsMethods) = othField + .filter(isMarked) + .span { + case m: MethodSymbol if isTag(m) => true + case _ => false } - /** Predicate for finding fields of instance marked with '@field' annotation */ - def isField(m: MethodSymbol): Boolean = { - if (m.annotations.exists(_.tree.tpe =:= typeOf[field])) { - if (isSupportedFieldType(m.returnType)) true - else c.abort(c.enclosingPosition, s"Unsupported type for @field ${m.name}: ${m.returnType}") - } else false - } + val optTags: List[c.universe.Tree] = tagsMethods collect { + case m: MethodSymbol if isOption(m.returnType) => + q"${m.name.decodedName.toString} -> obj.${m.name}" + } - def isMarked(m: MethodSymbol): Boolean = isTag(m) || isField(m) + val nonOptTags: List[c.universe.Tree] = tagsMethods collect { + case m: MethodSymbol if !isOption(m.returnType) => + q"${m.name.decodedName.toString} -> obj.${m.name}" + } - val (tagsMethods, fieldsMethods) = writeMethods - .filter(isMarked) - .span { - case m: MethodSymbol if isTag(m) => true - case _ => false - } + val fields = fieldsMethods map { + m: MethodSymbol => + q"${m.name.decodedName.toString} -> obj.${m.name}" + } - val optTags: List[c.universe.Tree] = tagsMethods collect { - case m: MethodSymbol if isOption(m.returnType) => - q"${m.name.decodedName.toString} -> obj.${m.name}" - } + // creation of 4 map in serializing, not the best case + timeField + .headOption + .map(m => q"obj.${m.name}") match { + case None => + q"""def write(obj: $tpe): String = { + val fieldsMap: Map[String, Any] = Map(..$fields) + val fields = fieldsMap map { case (k, v) => k + "=" + v } mkString(" ") - val nonOptTags: List[c.universe.Tree] = tagsMethods collect { - case m: MethodSymbol if !isOption(m.returnType) => - q"${m.name.decodedName.toString} -> obj.${m.name}" - } + val nonOptTagsMap: Map[String, String] = Map(..$nonOptTags) + val nonOptTags: String = nonOptTagsMap map { + case (k: String, v: String) => k + "=" + v + } mkString(",") - val fields = fieldsMethods map { - m: MethodSymbol => - q"${m.name.decodedName.toString} -> obj.${m.name}" - } + val optTagsMap: Map[String, Option[String]] = Map(..$optTags) + val optTags: String = optTagsMap collect { + case (k, Some(v)) => k + "=" + v + } mkString(",") + val combTags: String = if (optTags.isEmpty) nonOptTags else nonOptTags + "," + optTags - q"""def write(obj: $tpe): String = { + combTags + " " + fields trim + }""" + case Some(t) => + q"""def write(obj: $tpe): String = { val fieldsMap: Map[String, Any] = Map(..$fields) val fields = fieldsMap map { case (k, v) => k + "=" + v } mkString(" ") @@ -258,73 +229,67 @@ private[macros] object MacrosImpl { val optTagsMap: Map[String, Option[String]] = Map(..$optTags) val optTags: String = optTagsMap collect { - case (k: String, v: Option[String]) if v.isDefined => k + "=" + v.get + case (k, Some(v)) => k + "=" + v } mkString(",") val combTags: String = if (optTags.isEmpty) nonOptTags else nonOptTags + "," + optTags + val time: Long = $t - combTags + " " + fields trim + combTags + " " + fields + " " + time trim }""" - } - - def createReadMethod(methods: List[c.universe.Symbol]) = { - - val readMethods = methods collect { - case m: MethodSymbol if m.isCaseAccessor => - m.name.decodedName.toString -> m.returnType.dealias } + } - val bool = tpdls[Boolean] - val int = tpdls[Int] - val long = tpdls[Long] - val double = tpdls[Double] - val string = tpdls[String] - val optString = tpdls[Option[String]] - - val params = readMethods - .sortBy(_._1) - .map { case (k, v) => TermName(k) -> v } - .map { - case (k, `bool`) => q"$k = $k.asBoolean" - case (k, `string`) => q"$k = $k.asString" - case (k, `int`) => q"$k = $k.asInt" - case (k, `long`) => q"$k = $k.asLong" - case (k, `double`) => q"$k = $k.asDouble" - case (k, `optString`) => q"$k = if ($k.isNull) None else $k.getString" - case (_, other) => c.abort(c.enclosingPosition, s"Unsupported type $other") - } - - val paramss = readMethods - .map(_._1) - .sorted // influx return results in alphabetical order - .map(k => TermName(k)) - .map(k => pq"$k: JValue") + /*** + * Generate AST for current type at compile time. + * @tparam T - Type parameter for whom will be generated AST + */ + def writer_impl[T: c.WeakTypeTag]: c.universe.Tree = { + val tpe = c.weakTypeOf[T] + q"""new InfluxWriter[$tpe] {${createWriteMethod(tpe)}} """ + } - // success case clause component - val successPat = pq"Array(..$paramss)" - val successBody = q"new $tpe(..$params)" - val successCase = cq"$successPat => $successBody" + /*** + * Generate AST for current type at compile time. + * @tparam T - Type parameter for whom will be generated AST + */ + def reader_impl[T: c.WeakTypeTag]: c.universe.Tree = { + val tpe = c.weakTypeOf[T] - // failure case clause component - val failurePat = pq"_" - val failureMsg = s"Can't deserialize $tpe object" - val failureBody = q"throw new DeserializationException($failureMsg)" - val failureCase = cq"$failurePat => $failureBody" + q"""new InfluxReader[$tpe] { + import jawn.ast.{JValue, JArray} + import java.time.Instant + import com.github.fsanaulla.core.model.DeserializationException - val cases = successCase :: failureCase :: Nil + def toNanoLong(str: String): Long = { + val i = Instant.parse(str) + i.getEpochSecond * 1000000000 + i.getNano + } - q""" - def read(js: JArray): $tpe = js.vs.tail match { case ..$cases } - """ - } + ${createReadMethod(tpe)} + }""" + } + + /*** + * Generate AST for current type at compile time. + * @tparam T - Type parameter for whom will be generated AST + */ + def format_impl[T: c.WeakTypeTag]: c.universe.Tree = { + val tpe = c.weakTypeOf[T] q""" new InfluxFormatter[$tpe] { import jawn.ast.{JValue, JArray} + import java.time.Instant import com.github.fsanaulla.core.model.DeserializationException - ${createWriteMethod(methods)} - ${createReadMethod(methods)} + def toNanoLong(str: String): Long = { + val i = Instant.parse(str) + i.getEpochSecond * 1000000000 + i.getNano + } + + ${createWriteMethod(tpe)} + ${createReadMethod(tpe)} }""" } } diff --git a/macros/src/main/scala/com/github/fsanaulla/macros/annotations/timestamp.scala b/macros/src/main/scala/com/github/fsanaulla/macros/annotations/timestamp.scala new file mode 100644 index 00000000..d5f8cffd --- /dev/null +++ b/macros/src/main/scala/com/github/fsanaulla/macros/annotations/timestamp.scala @@ -0,0 +1,7 @@ +package com.github.fsanaulla.macros.annotations + +/** Mark field that will be used as timestamp in InfluxDB. + * It can be optional, in this case will be used default + * InfluxDB time, that equal to now.*/ +@scala.annotation.meta.getter +final class timestamp extends scala.annotation.StaticAnnotation diff --git a/macros/src/test/scala/com/github/fsanaulla/macros/MacroFormatterSpec.scala b/macros/src/test/scala/com/github/fsanaulla/macros/MacroFormatterSpec.scala index 0a0067e8..d2919d1c 100644 --- a/macros/src/test/scala/com/github/fsanaulla/macros/MacroFormatterSpec.scala +++ b/macros/src/test/scala/com/github/fsanaulla/macros/MacroFormatterSpec.scala @@ -2,28 +2,39 @@ package com.github.fsanaulla.macros import com.github.fsanaulla.core.model.InfluxFormatter import com.github.fsanaulla.core.test.FlatSpecWithMatchers -import com.github.fsanaulla.macros.annotations.{field, tag} +import com.github.fsanaulla.macros.annotations.{field, tag, timestamp} import jawn.ast.{JArray, JNull, JNum, JString} class MacroFormatterSpec extends FlatSpecWithMatchers { - case class Test(@tag name: String, @tag surname: Option[String], @field age: Int) + case class Test(@tag name: String, + @tag surname: Option[String], + @field age: Int, + @timestamp time: Long) val fm: InfluxFormatter[Test] = Macros.format[Test] "Macros.format" should "read with None" in { - fm.read(JArray(Array(JNum(234324), JNum(4), JString("Fz"), JNull))) shouldEqual Test("Fz", None, 4) + fm + .read(JArray(Array(JString("2015-08-04T19:05:14.318570484Z"), JNum(4), JString("Fz"), JNull))) + .shouldEqual(Test("Fz", None, 4, 1438715114318570484L)) } it should "read with Some" in { - fm.read(JArray(Array(JNum(234324), JNum(4), JString("Fz"), JString("Sz")))) shouldEqual Test("Fz", Some("Sz"), 4) + fm + .read(JArray(Array(JString("2015-08-04T19:05:14.318570484Z"), JNum(4), JString("Fz"), JString("Sz")))) + .shouldEqual(Test("Fz", Some("Sz"), 4, 1438715114318570484L)) } it should "write with None" in { - fm.write(Test("tName", None, 65)) shouldEqual "name=tName age=65" + fm + .write(Test("tName", None, 65, 1438715114318570484L)) + .shouldEqual("name=tName age=65 1438715114318570484") } it should "write with Some" in { - fm.write(Test("tName", Some("Sz"), 65)) shouldEqual "name=tName,surname=Sz age=65" + fm + .write(Test("tName", Some("Sz"), 65, 1438715114318570484L)) + .shouldEqual("name=tName,surname=Sz age=65 1438715114318570484") } } diff --git a/macros/src/test/scala/com/github/fsanaulla/macros/MacroReaderSpec.scala b/macros/src/test/scala/com/github/fsanaulla/macros/MacroReaderSpec.scala index 9fa5498a..92bd9797 100644 --- a/macros/src/test/scala/com/github/fsanaulla/macros/MacroReaderSpec.scala +++ b/macros/src/test/scala/com/github/fsanaulla/macros/MacroReaderSpec.scala @@ -2,20 +2,32 @@ package com.github.fsanaulla.macros import com.github.fsanaulla.core.model.InfluxReader import com.github.fsanaulla.core.test.FlatSpecWithMatchers -import com.github.fsanaulla.macros.annotations.{field, tag} +import com.github.fsanaulla.macros.annotations.{field, tag, timestamp} import jawn.ast._ class MacroReaderSpec extends FlatSpecWithMatchers { - case class Test(@tag name: String, @tag surname: Option[String], @field age: Int) + case class Test(@tag name: String, + @tag surname: Option[String], + @field age: Int) + + case class Test1(@tag name: String, + @tag surname: Option[String], + @field age: Int, + @timestamp time: Long) val rd: InfluxReader[Test] = Macros.reader[Test] + val rd1: InfluxReader[Test1] = Macros.reader[Test1] + + "Macros.reader" should "read with None and ignore time" in { + rd.read(JArray(Array(JString("2015-08-04T19:05:14.318570484Z"), JNum(4), JString("Fz"), JNull))) shouldEqual Test("Fz", None, 4) + } - "Macros.reader" should "read with None" in { - rd.read(JArray(Array(JNum(234324), JNum(4), JString("Fz"), JNull))) shouldEqual Test("Fz", None, 4) + it should "read with Some and ignore time" in { + rd.read(JArray(Array(JString("2015-08-04T19:05:14Z"), JNum(4), JString("Fz"), JString("Sr")))) shouldEqual Test("Fz", Some("Sr"), 4) } - it should "read with Some" in { - rd.read(JArray(Array(JNum(234324), JNum(4), JString("Fz"), JString("Sr")))) shouldEqual Test("Fz", Some("Sr"), 4) + it should "read with timestamp" in { + rd1.read(JArray(Array(JString("2015-08-04T19:05:14.318570484Z"), JNum(4), JString("Fz"), JNull))) shouldEqual Test1("Fz", None, 4, 1438715114318570484L) } } diff --git a/macros/src/test/scala/com/github/fsanaulla/macros/MacroWriterSpec.scala b/macros/src/test/scala/com/github/fsanaulla/macros/MacroWriterSpec.scala index 5484a8b7..823d7251 100644 --- a/macros/src/test/scala/com/github/fsanaulla/macros/MacroWriterSpec.scala +++ b/macros/src/test/scala/com/github/fsanaulla/macros/MacroWriterSpec.scala @@ -2,7 +2,7 @@ package com.github.fsanaulla.macros import com.github.fsanaulla.core.model.InfluxWriter import com.github.fsanaulla.core.test.FlatSpecWithMatchers -import com.github.fsanaulla.macros.annotations.{field, tag} +import com.github.fsanaulla.macros.annotations.{field, tag, timestamp} class MacroWriterSpec extends FlatSpecWithMatchers { @@ -10,7 +10,13 @@ class MacroWriterSpec extends FlatSpecWithMatchers { @tag surname: Option[String], @field age: Int) + case class Test1(@tag name: String, + @tag surname: Option[String], + @field age: Int, + @timestamp time: Long) + val wr: InfluxWriter[Test] = Macros.writer[Test] + val wr1: InfluxWriter[Test1] = Macros.writer[Test1] "Macros.writer" should "write with None" in { wr.write(Test("nm", None, 65)) shouldEqual "name=nm age=65" @@ -19,4 +25,8 @@ class MacroWriterSpec extends FlatSpecWithMatchers { it should "write with Some" in { wr.write(Test("nm", Some("sn"), 65)) shouldEqual "name=nm,surname=sn age=65" } + + it should "write with timestamp" in { + wr1.write(Test1("nm", Some("sn"), 65, 1438715114318570484L)) shouldEqual "name=nm,surname=sn age=65 1438715114318570484" + } } diff --git a/version.sbt b/version.sbt index 105b3319..46265fff 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "0.2.2" \ No newline at end of file +version in ThisBuild := "0.2.3" \ No newline at end of file