diff --git a/build.sbt b/build.sbt index 83bbc17..d260646 100644 --- a/build.sbt +++ b/build.sbt @@ -39,7 +39,9 @@ lazy val root = project.in(file(".")) Dependencies.circeParser, Dependencies.circeGeneric, // Scala (test only) - Dependencies.specs2 + Dependencies.specs2, + Dependencies.specs2Scalacheck, + Dependencies.scalacheck ) ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7d19fc0..9c094a2 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -20,6 +20,7 @@ object Dependencies { val circe = "0.13.0" // Scala (test only) val specs2 = "4.8.0" + val scalaCheck = "1.14.3" } val igluCore = "com.snowplowanalytics" %% "iglu-core-circe" % V.igluCore @@ -28,4 +29,6 @@ object Dependencies { val circeGeneric = "io.circe" %% "circe-generic" % V.circe // Scala (test only) val specs2 = "org.specs2" %% "specs2-core" % V.specs2 % Test + val specs2Scalacheck = "org.specs2" %% "specs2-scalacheck" % V.specs2 % Test + val scalacheck = "org.scalacheck" %% "scalacheck" % V.scalaCheck % Test } diff --git a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/Event.scala b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/Event.scala index 2319b3d..2ffbdae 100644 --- a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/Event.scala +++ b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/Event.scala @@ -15,6 +15,7 @@ package com.snowplowanalytics.snowplow.analytics.scalasdk // java import java.time.Instant import java.util.UUID +import java.time.format.DateTimeFormatter // circe import io.circe.{Encoder, Json, JsonObject, Decoder} @@ -28,8 +29,10 @@ import com.snowplowanalytics.iglu.core.circe.implicits._ // This library import com.snowplowanalytics.snowplow.analytics.scalasdk.decode.{Parser, DecodeResult} +import com.snowplowanalytics.snowplow.analytics.scalasdk.encode.TsvEncoder._ import com.snowplowanalytics.snowplow.analytics.scalasdk.SnowplowEvent.{Contexts, UnstructEvent} import com.snowplowanalytics.snowplow.analytics.scalasdk.SnowplowEvent._ +import com.snowplowanalytics.snowplow.analytics.scalasdk.encode.TsvEncoder /** * Case class representing a canonical Snowplow event. @@ -228,6 +231,9 @@ case class Event(app_id: Option[String], this.asJson } + /** Create the TSV representation of this event. */ + def toTSV: String = TsvEncoder.encode(this) + /** * This event as a map of keys to Circe JSON values */ diff --git a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/encode/TsvEncoder.scala b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/encode/TsvEncoder.scala new file mode 100644 index 0000000..c34a7be --- /dev/null +++ b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/encode/TsvEncoder.scala @@ -0,0 +1,89 @@ +package com.snowplowanalytics.snowplow.analytics.scalasdk.encode + +import java.time.format.DateTimeFormatter +import java.time.Instant +import java.util.UUID + +import io.circe.syntax._ + +import shapeless._ +import shapeless.labelled._ +import shapeless.ops.hlist._ +import shapeless.ops.record._ + +import com.snowplowanalytics.snowplow.analytics.scalasdk.SnowplowEvent._ + +trait TsvEncoder[A] { + def encode(value: A): List[String] +} + +object TsvEncoder { + def apply[A](implicit enc: TsvEncoder[A]): TsvEncoder[A] = enc + + def instance[A](func: A => List[String]): TsvEncoder[A] = new TsvEncoder[A] { + def encode(value: A): List[String] = + func(value) + } + + def createEncoder[A](func: A => List[String]): TsvEncoder[A] = + new TsvEncoder[A] { + def encode(value: A): List[String] = func(value) + } + + implicit val stringEncoder: TsvEncoder[String] = + createEncoder(s => List(s)) + implicit val optStringEncoder: TsvEncoder[Option[String]] = + createEncoder(optS => List(optS.getOrElse(""))) + implicit val instantEncoder: TsvEncoder[Instant] = + createEncoder(i => List(TsvEncoder.formatInstant(i))) + implicit val optInstantEncoder: TsvEncoder[Option[Instant]] = + createEncoder(optI => List(optI.map(TsvEncoder.formatInstant).getOrElse(""))) + implicit val uuidEncoder: TsvEncoder[UUID] = + createEncoder(u => List(u.toString())) + implicit val optIntEncoder: TsvEncoder[Option[Int]] = + createEncoder(optI => List(optI.map(_.toString).getOrElse(""))) + implicit val optDoubleEncoder: TsvEncoder[Option[Double]] = + createEncoder(optD => List(optD.map(_.toString).getOrElse(""))) + implicit val contextsEncoder: TsvEncoder[Contexts] = + createEncoder(c => List(c.asJson.noSpaces)) + implicit val unstructEncoder: TsvEncoder[UnstructEvent] = + createEncoder(u => List(u.asJson.noSpaces)) + implicit val optBooleanEncoder: TsvEncoder[Option[Boolean]] = + createEncoder(b => List(b.map(bb => if (bb) "1" else "0").getOrElse(""))) + implicit val hnilEncoder: TsvEncoder[HNil] = + createEncoder(hnil => Nil) + implicit def hlistEncoder[H, T <: HList]( + implicit hEncoder: TsvEncoder[H], + tEncoder: TsvEncoder[T] + ): TsvEncoder[H :: T] = + createEncoder { + case h :: t => hEncoder.encode(h) ++ tEncoder.encode(t) + } + + implicit def genericEncoder[A, R]( + implicit gen: Generic.Aux[A, R], + enc: TsvEncoder[R] + ): TsvEncoder[A] = + createEncoder(a => enc.encode(gen.to(a))) + + def encode[A, H <: HList, I <: HList, J <: HList](value: A)( + implicit enc: TsvEncoder[A], + gen: LabelledGeneric.Aux[A, H], + keys: Keys.Aux[H, I], + mapper: Mapper.Aux[Symboler.type, I, J], + toTraversable: ToTraversable.Aux[J, List, String] + ): String = + enc.encode(value).mkString("\t") + + /** Create timesteamp string with this format: '1978-03-13 20:57:30.661198953', like the output of enrich. */ + def formatInstant(instant: Instant): String = + DateTimeFormatter.ISO_INSTANT + .format(instant) + .replace("T", " ") + .dropRight(1) // remove trailing 'Z' +} + +object Symboler extends Poly1 { + implicit def default[A <: Symbol](implicit witness: Witness.Aux[A]) = + at[A](_ => witness.value.name) +} diff --git a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/EventGen.scala b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/EventGen.scala new file mode 100644 index 0000000..35b9827 --- /dev/null +++ b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/EventGen.scala @@ -0,0 +1,339 @@ +package com.snowplowanalytics.snowplow.analytics.scalasdk + +import org.scalacheck.{Arbitrary, Gen} + +import io.circe._ +import io.circe.syntax._ +import io.circe.{Encoder, Decoder, HCursor, Json} +import io.circe.parser._ + +import java.time.Instant + +object EventGen { + import SnowplowEvent._ + + def strGen(n: Int, gen: Gen[Char]): Gen[String] = + Gen.chooseNum(1, n).flatMap(len => Gen.listOfN(len, gen).map(_.mkString)) + + private val MaxTimestamp = 2871824840360L + + implicit val instantArbitrary: Arbitrary[Instant] = + Arbitrary { + for { + seconds <- Gen.chooseNum(0L, MaxTimestamp) + nanos <- Gen.chooseNum(Instant.MIN.getNano, Instant.MAX.getNano) + } yield Instant.ofEpochMilli(seconds).plusNanos(nanos.toLong) + } + + val instantGen: Gen[Instant] = + Arbitrary.arbitrary[Instant] + + val ipv4Address: Gen[String] = + for { + a <- Gen.chooseNum(0, 255) + b <- Gen.chooseNum(0, 255) + c <- Gen.chooseNum(0, 255) + d <- Gen.chooseNum(0, 255) + } yield s"$a.$b.$c.$d" + + val ipv6Address: Gen[String] = + for { + a <- Arbitrary.arbitrary[Short] + b <- Arbitrary.arbitrary[Short] + c <- Arbitrary.arbitrary[Short] + d <- Arbitrary.arbitrary[Short] + e <- Arbitrary.arbitrary[Short] + f <- Arbitrary.arbitrary[Short] + g <- Arbitrary.arbitrary[Short] + h <- Arbitrary.arbitrary[Short] + } yield f"$a%x:$b%x:$c%x:$d%x:$e%x:$f%x:$g%x:$h%x" + + val ipAddress: Gen[String] = + Gen.oneOf(ipv4Address, ipv6Address) + + val platform: Gen[String] = Gen.oneOf("web", "mob", "app") + + val eventType: Gen[String] = Gen.oneOf("page_view", "page_ping", "transaction", "unstruct") + + val contexts: Contexts = parse(EventSpec.contextsJson) + .flatMap(_.as[Contexts]) + .getOrElse(throw new UnsupportedOperationException("can't decode contexts")) + + val unstruct: UnstructEvent = parse(EventSpec.unstructJson) + .flatMap(_.as[UnstructEvent]) + .getOrElse(throw new UnsupportedOperationException("can't decode unstructured event")) + + val derived_contexts: Contexts = parse(EventSpec.derivedContextsJson) + .flatMap(_.as[Contexts]) + .getOrElse(throw new UnsupportedOperationException("can't decode derived contexts")) + + val event: Gen[Event] = + for { + app_id <- Gen.option(strGen(512, Gen.alphaNumChar)) + platform <- Gen.option(platform) + etl_tstamp <- Gen.option(instantGen) + collector_tstamp <- instantGen + dvce_created_tstamp <- Gen.option(instantGen) + event <- Gen.option(eventType) + event_id <- Gen.uuid + txn_id <- Gen.option(Gen.chooseNum(1, 10000)) + name_tracker <- Gen.option(strGen(256, Gen.alphaNumChar)) + v_tracker <- Gen.option(strGen(256, Gen.alphaNumChar)) + v_collector <- strGen(512, Gen.alphaNumChar) + v_etl <- strGen(512, Gen.alphaNumChar) + user_id <- Gen.option(Gen.uuid).map(_.map(_.toString())) + user_ipaddress <- Gen.option(ipAddress) + user_fingerprint <- Gen.option(strGen(512, Gen.alphaNumChar)) + domain_userid <- Gen.option(Gen.uuid).map(_.map(_.toString())) + domain_sessionidx <- Gen.option(Gen.chooseNum(1, 10000)) + network_userid <- Gen.option(Gen.uuid).map(_.map(_.toString())) + geo_country <- Gen.option(strGen(3, Gen.alphaUpperChar)) + geo_region <- Gen.option(strGen(100, Gen.alphaNumChar)) + geo_city <- Gen.option(strGen(512, Gen.alphaChar)) + geo_zipcode <- Gen.option(strGen(6, Gen.alphaNumChar)) + geo_latitude <- Gen.option(Arbitrary.arbitrary[Double]) + geo_longitude <- Gen.option(Arbitrary.arbitrary[Double]) + geo_region_name <- Gen.option(strGen(512, Gen.alphaChar)) + ip_isp <- Gen.option(strGen(512, Gen.alphaNumChar)) + ip_organization <- Gen.option(strGen(512, Gen.alphaNumChar)) + ip_domain <- Gen.option(strGen(512, Gen.alphaNumChar)) + ip_netspeed <- Gen.option(strGen(50, Gen.alphaNumChar)) + page_url <- Gen.option(strGen(512, Gen.alphaNumChar)) + page_title <- Gen.option(strGen(512, Gen.alphaNumChar)) + page_referrer <- Gen.option(strGen(512, Gen.alphaNumChar)) + page_urlscheme <- Gen.option(strGen(10, Gen.alphaNumChar)) + page_urlhost <- Gen.option(strGen(512, Gen.alphaNumChar)) + page_urlport <- Gen.option(Gen.chooseNum(1, 65000)) + page_urlpath <- Gen.option(strGen(512, Gen.alphaNumChar)) + page_urlquery <- Gen.option(strGen(512, Gen.alphaNumChar)) + page_urlfragment <- Gen.option(strGen(512, Gen.alphaNumChar)) + refr_urlscheme <- Gen.option(strGen(10, Gen.alphaNumChar)) + refr_urlhost <- Gen.option(strGen(512, Gen.alphaNumChar)) + refr_urlport <- Gen.option(Gen.chooseNum(1, 65000)) + refr_urlpath <- Gen.option(strGen(512, Gen.alphaNumChar)) + refr_urlquery <- Gen.option(strGen(512, Gen.alphaNumChar)) + refr_urlfragment <- Gen.option(strGen(512, Gen.alphaNumChar)) + refr_medium <- Gen.option(strGen(512, Gen.alphaNumChar)) + refr_source <- Gen.option(strGen(512, Gen.alphaNumChar)) + refr_term <- Gen.option(strGen(512, Gen.alphaNumChar)) + mkt_medium <- Gen.option(strGen(512, Gen.alphaNumChar)) + mkt_source <- Gen.option(strGen(512, Gen.alphaNumChar)) + mkt_term <- Gen.option(strGen(512, Gen.alphaNumChar)) + mkt_content <- Gen.option(strGen(512, Gen.alphaNumChar)) + mkt_campaign <- Gen.option(strGen(512, Gen.alphaNumChar)) + // contexts <- Gen.oneOf(contexts, Contexts(Nil)) // TODO + se_category <- Gen.option(strGen(512, Gen.alphaNumChar)) + se_action <- Gen.option(strGen(512, Gen.alphaNumChar)) + se_label <- Gen.option(strGen(512, Gen.alphaNumChar)) + se_property <- Gen.option(strGen(512, Gen.alphaNumChar)) + se_value <- Gen.option(Arbitrary.arbitrary[Double]) + //unstruct_event = event match { + // case Some("unstruct") => unstruct + // case _ => UnstructEvent(None) + //} + tr_orderid <- Gen.option(Gen.uuid).map(_.map(_.toString())) + tr_affiliation <- Gen.option(strGen(512, Gen.alphaNumChar)) + tr_total <- Gen.option(Arbitrary.arbitrary[Double]) + tr_tax <- Gen.option(Arbitrary.arbitrary[Double]) + tr_shipping <- Gen.option(Arbitrary.arbitrary[Double]) + tr_city <- Gen.option(strGen(512, Gen.alphaNumChar)) + tr_state <- Gen.option(strGen(512, Gen.alphaNumChar)) + tr_country <- Gen.option(strGen(512, Gen.alphaNumChar)) + ti_orderid <- Gen.option(Gen.uuid).map(_.map(_.toString())) + ti_sku <- Gen.option(strGen(512, Gen.alphaNumChar)) + ti_name <- Gen.option(strGen(512, Gen.alphaNumChar)) + ti_category <- Gen.option(strGen(512, Gen.alphaNumChar)) + ti_price <- Gen.option(Arbitrary.arbitrary[Double]) + ti_quantity <- Gen.option(Gen.chooseNum(1, 100)) + pp_xoffset_min <- Gen.option(Gen.chooseNum(1, 10000)) + pp_xoffset_max <- Gen.option(Gen.chooseNum(1, 10000)) + pp_yoffset_min <- Gen.option(Gen.chooseNum(1, 10000)) + pp_yoffset_max <- Gen.option(Gen.chooseNum(1, 10000)) + useragent <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_name <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_family <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_version <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_type <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_renderengine <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_lang <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_features_pdf <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_flash <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_java <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_director <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_quicktime <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_realplayer <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_windowsmedia <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_gears <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_features_silverlight <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_cookies <- Gen.option(Arbitrary.arbitrary[Boolean]) + br_colordepth <- Gen.option(strGen(512, Gen.alphaNumChar)) + br_viewwidth <- Gen.option(Gen.chooseNum(1, 10000)) + br_viewheight <- Gen.option(Gen.chooseNum(1, 10000)) + os_name <- Gen.option(strGen(512, Gen.alphaNumChar)) + os_family <- Gen.option(strGen(512, Gen.alphaNumChar)) + os_manufacturer <- Gen.option(strGen(512, Gen.alphaNumChar)) + os_timezone <- Gen.option(strGen(512, Gen.alphaNumChar)) + dvce_type <- Gen.option(strGen(512, Gen.alphaNumChar)) + dvce_ismobile <- Gen.option(Arbitrary.arbitrary[Boolean]) + dvce_screenwidth <- Gen.option(Gen.chooseNum(1, 10000)) + dvce_screenheight <- Gen.option(Gen.chooseNum(1, 10000)) + doc_charset <- Gen.option(strGen(512, Gen.alphaNumChar)) + doc_width <- Gen.option(Gen.chooseNum(1, 10000)) + doc_height <- Gen.option(Gen.chooseNum(1, 10000)) + tr_currency <- Gen.option(strGen(512, Gen.alphaNumChar)) + tr_total_base <- Gen.option(Arbitrary.arbitrary[Double]) + tr_tax_base <- Gen.option(Arbitrary.arbitrary[Double]) + tr_shipping_base <- Gen.option(Arbitrary.arbitrary[Double]) + ti_currency <- Gen.option(strGen(512, Gen.alphaNumChar)) + ti_price_base <- Gen.option(Arbitrary.arbitrary[Double]) + base_currency <- Gen.option(strGen(512, Gen.alphaNumChar)) + geo_timezone <- Gen.option(strGen(512, Gen.alphaNumChar)) + mkt_clickid <- Gen.option(Gen.uuid).map(_.map(_.toString())) + mkt_network <- Gen.option(strGen(512, Gen.alphaNumChar)) + etl_tags <- Gen.option(strGen(512, Gen.alphaNumChar)) + dvce_sent_tstamp <- Gen.option(instantGen) + refr_domain_userid <- Gen.option(Gen.uuid).map(_.map(_.toString())) + refr_dvce_tstamp <- Gen.option(instantGen) + // derived_contexts <- Gen.oneOf(derived_contexts, Contexts(Nil)) // TODO + domain_sessionid <- Gen.option(Gen.uuid).map(_.map(_.toString())) + derived_tstamp <- Gen.option(instantGen) + event_vendor <- Gen.option(Gen.identifier) + event_name <- Gen.option(Gen.identifier) + event_format <- Gen.option("jsonschema") + event_version <- Gen.option(strGen(10, Gen.alphaNumChar)) + event_fingerprint <- Gen.option(strGen(512, Gen.alphaNumChar)) + true_tstamp <- Gen.option(instantGen) + } yield Event( + app_id, + platform, + etl_tstamp, + collector_tstamp, + dvce_created_tstamp, + event, + event_id, + txn_id, + name_tracker, + v_tracker, + v_collector, + v_etl, + user_id, + user_ipaddress, + user_fingerprint, + domain_userid, + domain_sessionidx, + network_userid, + geo_country, + geo_region, + geo_city, + geo_zipcode, + geo_latitude, + geo_longitude, + geo_region_name, + ip_isp, + ip_organization, + ip_domain, + ip_netspeed, + page_url, + page_title, + page_referrer, + page_urlscheme, + page_urlhost, + page_urlport, + page_urlpath, + page_urlquery, + page_urlfragment, + refr_urlscheme, + refr_urlhost, + refr_urlport, + refr_urlpath, + refr_urlquery, + refr_urlfragment, + refr_medium, + refr_source, + refr_term, + mkt_medium, + mkt_source, + mkt_term, + mkt_content, + mkt_campaign, + contexts, + se_category, + se_action, + se_label, + se_property, + se_value, + unstruct, // unstruct_event TODO + tr_orderid, + tr_affiliation, + tr_total, + tr_tax, + tr_shipping, + tr_city, + tr_state, + tr_country, + ti_orderid, + ti_sku, + ti_name, + ti_category, + ti_price, + ti_quantity, + pp_xoffset_min, + pp_xoffset_max, + pp_yoffset_min, + pp_yoffset_max, + useragent, + br_name, + br_family, + br_version, + br_type, + br_renderengine, + br_lang, + br_features_pdf, + br_features_flash, + br_features_java, + br_features_director, + br_features_quicktime, + br_features_realplayer, + br_features_windowsmedia, + br_features_gears, + br_features_silverlight, + br_cookies, + br_colordepth, + br_viewwidth, + br_viewheight, + os_name, + os_family, + os_manufacturer, + os_timezone, + dvce_type, + dvce_ismobile, + dvce_screenwidth, + dvce_screenheight, + doc_charset, + doc_width, + doc_height, + tr_currency, + tr_total_base, + tr_tax_base, + tr_shipping_base, + ti_currency, + ti_price_base, + base_currency, + geo_timezone, + mkt_clickid, + mkt_network, + etl_tags, + dvce_sent_tstamp, + refr_domain_userid, + refr_dvce_tstamp, + derived_contexts, + domain_sessionid, + derived_tstamp, + event_vendor, + event_name, + event_format, + event_version, + event_fingerprint, + true_tstamp + ) +} diff --git a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/EventSpec.scala b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/EventSpec.scala index e1457f9..43d3b1f 100644 --- a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/EventSpec.scala +++ b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/EventSpec.scala @@ -34,6 +34,10 @@ import org.specs2.mutable.Specification // Iglu import com.snowplowanalytics.iglu.core.{SchemaKey, SchemaVer, SelfDescribingData} +// ScalaCheck +import org.specs2.ScalaCheck +import org.scalacheck.Prop.forAll + // This library import com.snowplowanalytics.snowplow.analytics.scalasdk.SnowplowEvent._ import com.snowplowanalytics.snowplow.analytics.scalasdk.ParsingError._ @@ -42,132 +46,8 @@ import com.snowplowanalytics.snowplow.analytics.scalasdk.ParsingError.RowDecodin /** * Tests Event case class */ -class EventSpec extends Specification { - - val unstructJson = - """{ - "schema": "iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0", - "data": { - "schema": "iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1", - "data": { - "targetUrl": "http://www.example.com", - "elementClasses": ["foreground"], - "elementId": "exampleLink" - } - } - }""" - - val contextsJson = - """{ - "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", - "data": [ - { - "schema": "iglu:org.schema/WebPage/jsonschema/1-0-0", - "data": { - "genre": "blog", - "inLanguage": "en-US", - "datePublished": "2014-11-06T00:00:00Z", - "author": "Fred Blundun", - "breadcrumb": [ - "blog", - "releases" - ], - "keywords": [ - "snowplow", - "javascript", - "tracker", - "event" - ] - } - }, - { - "schema": "iglu:org.w3/PerformanceTiming/jsonschema/1-0-0", - "data": { - "navigationStart": 1415358089861, - "unloadEventStart": 1415358090270, - "unloadEventEnd": 1415358090287, - "redirectStart": 0, - "redirectEnd": 0, - "fetchStart": 1415358089870, - "domainLookupStart": 1415358090102, - "domainLookupEnd": 1415358090102, - "connectStart": 1415358090103, - "connectEnd": 1415358090183, - "requestStart": 1415358090183, - "responseStart": 1415358090265, - "responseEnd": 1415358090265, - "domLoading": 1415358090270, - "domInteractive": 1415358090886, - "domContentLoadedEventStart": 1415358090968, - "domContentLoadedEventEnd": 1415358091309, - "domComplete": 0, - "loadEventStart": 0, - "loadEventEnd": 0 - } - } - ] - }""" - - val contextsWithDuplicate = """{ - "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", - "data": [ - { - "schema": "iglu:org.schema/WebPage/jsonschema/1-0-0", - "data": { - "genre": "blog", - "inLanguage": "en-US", - "datePublished": "2014-11-06T00:00:00Z", - "author": "Fred Blundun", - "breadcrumb": [ - "blog", - "releases" - ], - "keywords": [ - "snowplow", - "javascript", - "tracker", - "event" - ] - } - }, - { - "schema": "iglu:org.acme/context_one/jsonschema/1-0-0", - "data": { - "item": 1 - } - }, - { - "schema": "iglu:org.acme/context_one/jsonschema/1-0-1", - "data": { - "item": 2 - } - } - ] - }""" - - val derivedContextsJson = - """{ - "schema": "iglu:com.snowplowanalytics.snowplow\/contexts\/jsonschema\/1-0-1", - "data": [ - { - "schema": "iglu:com.snowplowanalytics.snowplow\/ua_parser_context\/jsonschema\/1-0-0", - "data": { - "useragentFamily": "IE", - "useragentMajor": "7", - "useragentMinor": "0", - "useragentPatch": null, - "useragentVersion": "IE 7.0", - "osFamily": "Windows XP", - "osMajor": null, - "osMinor": null, - "osPatch": null, - "osPatchMinor": null, - "osVersion": "Windows XP", - "deviceFamily": "Other" - } - } - ] - }""" +class EventSpec extends Specification with ScalaCheck { + import EventSpec._ "The Event parser" should { "successfully convert a tab-separated pageview event string to an Event instance and JSON" in { @@ -3059,4 +2939,138 @@ class EventSpec extends Specification { SnowplowEvent.transformSchema(Data.UnstructEvent, "com.snowplowanalytics.self-desc", "schema", 1) mustEqual "unstruct_event_com_snowplowanalytics_self_desc_schema_1" } } + + "Parsing the result of toTSV should produce the same event" in { + forAll(EventGen.event) { e => + println(s"% TSV % : [${e.toTSV}]") + Event.parse(e.toTSV) mustEqual(Valid(e)) + } + } +} + +object EventSpec { + val unstructJson = + """{ + "schema": "iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0", + "data": { + "schema": "iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1", + "data": { + "targetUrl": "http://www.example.com", + "elementClasses": ["foreground"], + "elementId": "exampleLink" + } + } + }""" + + val contextsJson = + """{ + "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", + "data": [ + { + "schema": "iglu:org.schema/WebPage/jsonschema/1-0-0", + "data": { + "genre": "blog", + "inLanguage": "en-US", + "datePublished": "2014-11-06T00:00:00Z", + "author": "Fred Blundun", + "breadcrumb": [ + "blog", + "releases" + ], + "keywords": [ + "snowplow", + "javascript", + "tracker", + "event" + ] + } + }, + { + "schema": "iglu:org.w3/PerformanceTiming/jsonschema/1-0-0", + "data": { + "navigationStart": 1415358089861, + "unloadEventStart": 1415358090270, + "unloadEventEnd": 1415358090287, + "redirectStart": 0, + "redirectEnd": 0, + "fetchStart": 1415358089870, + "domainLookupStart": 1415358090102, + "domainLookupEnd": 1415358090102, + "connectStart": 1415358090103, + "connectEnd": 1415358090183, + "requestStart": 1415358090183, + "responseStart": 1415358090265, + "responseEnd": 1415358090265, + "domLoading": 1415358090270, + "domInteractive": 1415358090886, + "domContentLoadedEventStart": 1415358090968, + "domContentLoadedEventEnd": 1415358091309, + "domComplete": 0, + "loadEventStart": 0, + "loadEventEnd": 0 + } + } + ] + }""" + + val contextsWithDuplicate = """{ + "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", + "data": [ + { + "schema": "iglu:org.schema/WebPage/jsonschema/1-0-0", + "data": { + "genre": "blog", + "inLanguage": "en-US", + "datePublished": "2014-11-06T00:00:00Z", + "author": "Fred Blundun", + "breadcrumb": [ + "blog", + "releases" + ], + "keywords": [ + "snowplow", + "javascript", + "tracker", + "event" + ] + } + }, + { + "schema": "iglu:org.acme/context_one/jsonschema/1-0-0", + "data": { + "item": 1 + } + }, + { + "schema": "iglu:org.acme/context_one/jsonschema/1-0-1", + "data": { + "item": 2 + } + } + ] + }""" + + val derivedContextsJson = + """{ + "schema": "iglu:com.snowplowanalytics.snowplow\/contexts\/jsonschema\/1-0-1", + "data": [ + { + "schema": "iglu:com.snowplowanalytics.snowplow\/ua_parser_context\/jsonschema\/1-0-0", + "data": { + "useragentFamily": "IE", + "useragentMajor": "7", + "useragentMinor": "0", + "useragentPatch": null, + "useragentVersion": "IE 7.0", + "osFamily": "Windows XP", + "osMajor": null, + "osMinor": null, + "osPatch": null, + "osPatchMinor": null, + "osVersion": "Windows XP", + "deviceFamily": "Other" + } + } + ] + }""" } diff --git a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingErrorSpec.scala b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingErrorSpec.scala index e47d11b..505e344 100644 --- a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingErrorSpec.scala +++ b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingErrorSpec.scala @@ -95,9 +95,9 @@ class ParsingErrorSpec extends Specification { def is = s2""" } private def parseJson(jsonStr: String): Json = - parse(jsonStr).right.getOrElse(throw new RuntimeException("Failed to parse expected JSON.")) + parse(jsonStr).getOrElse(throw new RuntimeException("Failed to parse expected JSON.")) private def decodeJson[A: Decoder](json: Json): A = { - json.as[A].right.getOrElse(throw new RuntimeException("Failed to decode to ParsingError.")) + json.as[A].getOrElse(throw new RuntimeException("Failed to decode to ParsingError.")) } }