From 89af6572151518b421cc0e80fce8f0b5a7e6c724 Mon Sep 17 00:00:00 2001 From: Enes Aldemir Date: Fri, 5 Jul 2019 14:50:46 +0300 Subject: [PATCH] Make errors type-safe --- .../ParsingError.scala | 36 +++++++++++++++++++ .../decode/Parser.scala | 22 ++++++------ .../decode/RowDecoder.scala | 7 ++-- .../decode/ValueDecoder.scala | 35 +++++++++--------- .../decode/package.scala | 5 +-- .../ExampleSpec.scala | 20 +++++++++++ .../decode/ValueDecoderSpec.scala | 18 ++++++++++ 7 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingError.scala create mode 100644 src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ExampleSpec.scala diff --git a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingError.scala b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingError.scala new file mode 100644 index 0000000..9088615 --- /dev/null +++ b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ParsingError.scala @@ -0,0 +1,36 @@ +package com.snowplowanalytics.snowplow.analytics.scalasdk + +/** + * Represents error during parsing TSV event + */ +sealed trait ParsingError + +object ParsingError { + + /** + * Represents error which given line is not a TSV + */ + case object NonTSVPayload extends ParsingError + + /** + * Represents error which number of given columns is not equal + * to number of expected columns + * @param expectedNum expected number of columns + * @param gotNum number of given columns + */ + case class ColumnNumberMismatch(expectedNum: Int, gotNum: Int) extends ParsingError + + /** + * Represents cases where value in a field is not valid + * e.g invalid timestamp, invalid UUID + * @param key key of field + * @param value value of the field + */ + case class InvalidValue(key: String, value: String) extends ParsingError + + /** + * Represents unexpected errors + * @param error error message + */ + case class UnexpectedError(error: String) extends ParsingError +} diff --git a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/Parser.scala b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/Parser.scala index 2b88f02..eb12207 100644 --- a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/Parser.scala +++ b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/Parser.scala @@ -15,8 +15,8 @@ package com.snowplowanalytics.snowplow.analytics.scalasdk.decode import shapeless._ import shapeless.ops.record._ import shapeless.ops.hlist._ - -import Parser._ +import cats.data.{NonEmptyList, Validated} +import com.snowplowanalytics.snowplow.analytics.scalasdk.ParsingError.{ColumnNumberMismatch, NonTSVPayload} private[scalasdk] trait Parser[A] extends Serializable { /** Heterogeneous TSV values */ @@ -33,9 +33,16 @@ private[scalasdk] trait Parser[A] extends Serializable { def parse(row: String): DecodeResult[A] = { val values = row.split("\t", -1) - val zipped = knownKeys.zipAll(values, UnknownKeyPlaceholder, ValueIsMissingPlaceholder) - val decoded = decoder(zipped) - decoded.map { decodedValue => generic.from(decodedValue) } + if (values.length == 1) { + Validated.Invalid(NonEmptyList.of(NonTSVPayload)) + } + else if (values.length != knownKeys.length) { + Validated.Invalid(NonEmptyList.of(ColumnNumberMismatch(knownKeys.length, values.length))) + } else { + val zipped = knownKeys.zip(values) + val decoded = decoder(zipped) + decoded.map { decodedValue => generic.from(decodedValue) } + } } } @@ -61,11 +68,6 @@ object Parser { } } - /** Key name that will be used if TSV has more columns than a class */ - val UnknownKeyPlaceholder = 'UnknownKey - /** Value that will be used if class has more fields than a TSV */ - val ValueIsMissingPlaceholder = "VALUE IS MISSING" - /** Derive a TSV parser for `A` */ private[scalasdk] def deriveFor[A]: DeriveParser[A] = new DeriveParser[A] {} diff --git a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/RowDecoder.scala b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/RowDecoder.scala index 91ddb45..91d46dd 100644 --- a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/RowDecoder.scala +++ b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/RowDecoder.scala @@ -16,6 +16,7 @@ import shapeless._ import cats.syntax.validated._ import cats.syntax.either._ import cats.syntax.apply._ +import com.snowplowanalytics.snowplow.analytics.scalasdk.ParsingError.UnexpectedError /** * Type class to decode List of keys-value pairs into HList @@ -41,15 +42,15 @@ private[scalasdk] object RowDecoder { row match { case h :: t => val hv: DecodeResult[H] = - ValueDecoder[H].parse(h).leftMap(_._2).toValidatedNel + ValueDecoder[H].parse(h).toValidatedNel val tv = RowDecoder[T].apply(t) (hv, tv).mapN { _ :: _ } - case Nil => "Not enough values, format is invalid".invalidNel + case Nil => UnexpectedError("Not enough values, format is invalid").invalidNel } implicit val hnilFromRow: RowDecoder[HNil] = fromFunc { case Nil => HNil.validNel - case rows => s"No more values expected, following provided: ${rows.map(_._2).mkString(", ")}".invalidNel + case rows => UnexpectedError(s"No more values expected, following provided: ${rows.map(_._2).mkString(", ")}").invalidNel } implicit def hconsFromRow[H: ValueDecoder, T <: HList: RowDecoder]: RowDecoder[H :: T] = diff --git a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoder.scala b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoder.scala index 4e05a50..6adca55 100644 --- a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoder.scala +++ b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoder.scala @@ -34,6 +34,7 @@ import io.circe.{Error, Json} // This library import com.snowplowanalytics.snowplow.analytics.scalasdk.Common.{ContextsCriterion, UnstructEventCriterion} import com.snowplowanalytics.snowplow.analytics.scalasdk.SnowplowEvent.{Contexts, UnstructEvent} +import com.snowplowanalytics.snowplow.analytics.scalasdk.ParsingError.InvalidValue private[decode] trait ValueDecoder[A] { def parse(column: (Key, String)): DecodedValue[A] @@ -50,7 +51,7 @@ private[decode] object ValueDecoder { implicit final val stringColumnDecoder: ValueDecoder[String] = fromFunc[String] { case (key, value) => - if (value.isEmpty) (key, s"Field $key cannot be empty").asLeft else value.asRight + if (value.isEmpty) InvalidValue(key.toString, s"Field $key cannot be empty").asLeft else value.asRight } implicit final val stringOptionColumnDecoder: ValueDecoder[Option[String]] = @@ -67,7 +68,7 @@ private[decode] object ValueDecoder { value.toInt.some.asRight } catch { case _: NumberFormatException => - (key, s"Cannot parse key $key with value $value into integer").asLeft + InvalidValue(key.toString, s"Cannot parse key $key with value $value into integer").asLeft } } @@ -75,13 +76,13 @@ private[decode] object ValueDecoder { fromFunc[UUID] { case (key, value) => if (value.isEmpty) - (key, s"Field $key cannot be empty").asLeft + InvalidValue(key.toString, s"Field $key cannot be empty").asLeft else try { - UUID.fromString(value).asRight[(Key, String)] + UUID.fromString(value).asRight[InvalidValue] } catch { case _: IllegalArgumentException => - (key, s"Cannot parse key $key with value $value into UUID").asLeft + InvalidValue(key.toString, s"Cannot parse key $key with value $value into UUID").asLeft } } @@ -92,7 +93,7 @@ private[decode] object ValueDecoder { case "0" => false.some.asRight case "1" => true.some.asRight case "" => none[Boolean].asRight - case _ => (key, s"Cannot parse key $key with value $value into boolean").asLeft + case _ => InvalidValue(key.toString, s"Cannot parse key $key with value $value into boolean").asLeft } } @@ -105,7 +106,7 @@ private[decode] object ValueDecoder { value.toDouble.some.asRight } catch { case _: NumberFormatException => - (key, s"Cannot parse key $key with value $value into double").asLeft + InvalidValue(key.toString, s"Cannot parse key $key with value $value into double").asLeft } } @@ -113,14 +114,14 @@ private[decode] object ValueDecoder { fromFunc[Instant] { case (key, value) => if (value.isEmpty) - (key, s"Field $key cannot be empty").asLeft + InvalidValue(key.toString, s"Field $key cannot be empty").asLeft else { val tstamp = reformatTstamp(value) try { Instant.parse(tstamp).asRight } catch { case _: DateTimeParseException => - (key, s"Cannot parse key $key with value $value into datetime").asLeft + InvalidValue(key.toString, s"Cannot parse key $key with value $value into datetime").asLeft } } } @@ -129,14 +130,14 @@ private[decode] object ValueDecoder { fromFunc[Option[Instant]] { case (key, value) => if (value.isEmpty) - none[Instant].asRight[(Key, String)] + none[Instant].asRight[InvalidValue] else { val tstamp = reformatTstamp(value) try { Instant.parse(tstamp).some.asRight } catch { case _: DateTimeParseException => - (key, s"Cannot parse key $key with value $value into datetime").asLeft + InvalidValue(key.toString, s"Cannot parse key $key with value $value into datetime").asLeft } } } @@ -144,9 +145,9 @@ private[decode] object ValueDecoder { implicit final val unstructuredJson: ValueDecoder[UnstructEvent] = fromFunc[UnstructEvent] { case (key, value) => - def asLeft(error: Error): (Key, String) = (key, error.show) + def asLeft(error: Error): InvalidValue = InvalidValue(key.toString(), error.show) if (value.isEmpty) - UnstructEvent(None).asRight[(Key, String)] + UnstructEvent(None).asRight[InvalidValue] else parseJson(value) .flatMap(_.as[SelfDescribingData[Json]]) @@ -154,7 +155,7 @@ private[decode] object ValueDecoder { case Right(SelfDescribingData(schema, data)) if UnstructEventCriterion.matches(schema) => data.as[SelfDescribingData[Json]].leftMap(asLeft).map(_.some).map(UnstructEvent.apply) case Right(SelfDescribingData(schema, _)) => - (key, s"Unknown payload: ${schema.toSchemaUri}").asLeft[UnstructEvent] + InvalidValue(key.toString, s"Unknown payload: ${schema.toSchemaUri}").asLeft[UnstructEvent] case Left(error) => error.asLeft[UnstructEvent] } } @@ -162,9 +163,9 @@ private[decode] object ValueDecoder { implicit final val contexts: ValueDecoder[Contexts] = fromFunc[Contexts] { case (key, value) => - def asLeft(error: Error): (Key, String) = (key, error.show) + def asLeft(error: Error): InvalidValue = InvalidValue(key.toString, error.show) if (value.isEmpty) - Contexts(List()).asRight[(Key, String)] + Contexts(List()).asRight[InvalidValue] else parseJson(value) .flatMap(_.as[SelfDescribingData[Json]]) @@ -172,7 +173,7 @@ private[decode] object ValueDecoder { case Right(SelfDescribingData(schema, data)) if ContextsCriterion.matches(schema) => data.as[List[SelfDescribingData[Json]]].leftMap(asLeft).map(Contexts.apply) case Right(SelfDescribingData(schema, _)) => - (key, s"Unknown payload: ${schema.toSchemaUri}").asLeft[Contexts] + InvalidValue(key.toString, s"Unknown payload: ${schema.toSchemaUri}").asLeft[Contexts] case Left(error) => error.asLeft[Contexts] } } diff --git a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/package.scala b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/package.scala index 7027446..99b91bd 100644 --- a/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/package.scala +++ b/src/main/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/package.scala @@ -13,14 +13,15 @@ package com.snowplowanalytics.snowplow.analytics.scalasdk import cats.data.ValidatedNel +import com.snowplowanalytics.snowplow.analytics.scalasdk.ParsingError.InvalidValue package object decode { /** Expected name of the field */ type Key = Symbol /** Result of single-value parsing */ - type DecodedValue[A] = Either[(Key, String), A] + type DecodedValue[A] = Either[InvalidValue, A] /** Result of TSV line parsing, which is either an event or non empty list of parse errors */ - type DecodeResult[A] = ValidatedNel[String, A] + type DecodeResult[A] = ValidatedNel[ParsingError, A] } diff --git a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ExampleSpec.scala b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ExampleSpec.scala new file mode 100644 index 0000000..8465f92 --- /dev/null +++ b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/ExampleSpec.scala @@ -0,0 +1,20 @@ +package com.snowplowanalytics.snowplow.analytics.scalasdk + +import org.specs2.Specification + +class ExampleSpec extends Specification { def is = s2""" + test test $e1 + """ + + def e1 = { + val nonTsv = Event.parse("""{ "hello": "hello" }""") + val correctEvent = Event.parse("""snowplowweb web 2018-12-18 15:07:17.970 2016-03-21 11:56:41.331 2016-03-21 11:56:41.816 page_ping 0b311241-6876-4b30-ba27-82fdfd85e5e8 snplow6 js-2.6.0 ssc-0.6.0-kinesis spark-1.16.0-common-0.35.0 50d9ee525d12b25618e2dbab1f25c39d 086271e520deb84db3259c0af2878ee5 262ac8a29684b99251deb81d2e246446 9506c862a9dbeea2f55fba6af4a0e7ec 10 05d09faa707a9ff092e49929c79630d1 http://snowplowanalytics.com/ Snowplow – Your digital nervous system http snowplowanalytics.com 80 / {"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":"3e4a5deb-1504-4cb2-8751-9d358c632328"}},{"schema":"iglu:org.w3/PerformanceTiming/jsonschema/1-0-0","data":{"navigationStart":1458561378194,"unloadEventStart":1458561379080,"unloadEventEnd":1458561379080,"redirectStart":0,"redirectEnd":0,"fetchStart":1458561378195,"domainLookupStart":1458561378211,"domainLookupEnd":1458561378435,"connectStart":1458561378435,"connectEnd":1458561378602,"secureConnectionStart":0,"requestStart":1458561378602,"responseStart":1458561379079,"responseEnd":1458561379080,"domLoading":1458561379095,"domInteractive":1458561380392,"domContentLoadedEventStart":1458561380392,"domContentLoadedEventEnd":1458561380395,"domComplete":1458561383683,"loadEventStart":1458561383683,"loadEventEnd":1458561383737,"chromeFirstPaint":1458561380397}}]} 0 0 2123 2123 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36 BMID/E6797F732B Chrome 49 Chrome 49.0.2623.87 Browser WEBKIT en-US 1 1 0 0 0 0 0 0 0 1 24 1591 270 Mac OS X Mac OS X Apple Inc. Europe/London Computer 0 1680 1050 UTF-8 1591 2856 2016-03-21 11:56:41.819 {"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":"Chrome","useragentMajor":"49","useragentMinor":"0","useragentPatch":"2623","useragentVersion":"Chrome 49.0.2623","osFamily":"Mac OS X","osMajor":"10","osMinor":"10","osPatch":"5","osPatchMinor":null,"osVersion":"Mac OS X 10.10.5","deviceFamily":"Other"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"05d09faa707a9ff092e49929c79630d1"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"b73833bf7e3af5a6d5a02e79be96b064"}}]} bfcfa2c3-9b9e-4023-a850-9873e52e0fd2 2016-03-21 11:56:41.328 com.snowplowanalytics.snowplow page_ping jsonschema 1-0-0 e0911984960264e489ab704435ee5888 """) + val eventWithEmptyUnstructData = Event.parse("""snowplowweb web 2018-12-18 15:07:17.970 2016-03-21 11:56:51.434 2016-03-21 11:56:49.605 unstruct 38927042-a195-4c32-9f48-5a8c7c6d427f snplow6 js-2.6.0 ssc-0.6.0-kinesis spark-1.16.0-common-0.35.0 50d9ee525d12b25618e2dbab1f25c39d 086271e520deb84db3259c0af2878ee5 262ac8a29684b99251deb81d2e246446 9506c862a9dbeea2f55fba6af4a0e7ec 10 05d09faa707a9ff092e49929c79630d1 http://snowplowanalytics.com/ http snowplowanalytics.com 80 / {"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":"3e4a5deb-1504-4cb2-8751-9d358c632328"}},{"schema":"iglu:org.w3/PerformanceTiming/jsonschema/1-0-0","data":{"navigationStart":1458561378194,"unloadEventStart":1458561379080,"unloadEventEnd":1458561379080,"redirectStart":0,"redirectEnd":0,"fetchStart":1458561378195,"domainLookupStart":1458561378211,"domainLookupEnd":1458561378435,"connectStart":1458561378435,"connectEnd":1458561378602,"secureConnectionStart":0,"requestStart":1458561378602,"responseStart":1458561379079,"responseEnd":1458561379080,"domLoading":1458561379095,"domInteractive":1458561380392,"domContentLoadedEventStart":1458561380392,"domContentLoadedEventEnd":1458561380395,"domComplete":1458561383683,"loadEventStart":1458561383683,"loadEventEnd":1458561383737,"chromeFirstPaint":1458561380397}}]} {"schema":"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0","data":{}} Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36 BMID/E6797F732B Chrome 49 Chrome 49.0.2623.87 Browser WEBKIT en-US 1 1 0 0 0 0 0 0 0 1 24 1591 551 Mac OS X Mac OS X Apple Inc. Europe/London Computer 0 1680 1050 UTF-8 1591 3074 2016-03-21 11:56:51.679 {"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":"Chrome","useragentMajor":"49","useragentMinor":"0","useragentPatch":"2623","useragentVersion":"Chrome 49.0.2623","osFamily":"Mac OS X","osMajor":"10","osMinor":"10","osPatch":"5","osPatchMinor":null,"osVersion":"Mac OS X 10.10.5","deviceFamily":"Other"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"05d09faa707a9ff092e49929c79630d1"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"b73833bf7e3af5a6d5a02e79be96b064"}}]} bfcfa2c3-9b9e-4023-a850-9873e52e0fd2 2016-03-21 11:56:49.360 com.snowplowanalytics.snowplow link_click jsonschema 1-0-1 15cc945a721f944dada154f8c77cdbb1 """) + val eventWithMissingField = Event.parse("""snowplowweb 2018-12-18 15:07:17.970 2016-03-21 11:56:51.434 2016-03-21 11:56:49.605 unstruct 38927042-a195-4c32-9f48-5a8c7c6d427f snplow6 js-2.6.0 ssc-0.6.0-kinesis spark-1.16.0-common-0.35.0 50d9ee525d12b25618e2dbab1f25c39d 086271e520deb84db3259c0af2878ee5 262ac8a29684b99251deb81d2e246446 9506c862a9dbeea2f55fba6af4a0e7ec 10 05d09faa707a9ff092e49929c79630d1 http://snowplowanalytics.com/ http snowplowanalytics.com 80 / {"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":"3e4a5deb-1504-4cb2-8751-9d358c632328"}},{"schema":"iglu:org.w3/PerformanceTiming/jsonschema/1-0-0","data":{"navigationStart":1458561378194,"unloadEventStart":1458561379080,"unloadEventEnd":1458561379080,"redirectStart":0,"redirectEnd":0,"fetchStart":1458561378195,"domainLookupStart":1458561378211,"domainLookupEnd":1458561378435,"connectStart":1458561378435,"connectEnd":1458561378602,"secureConnectionStart":0,"requestStart":1458561378602,"responseStart":1458561379079,"responseEnd":1458561379080,"domLoading":1458561379095,"domInteractive":1458561380392,"domContentLoadedEventStart":1458561380392,"domContentLoadedEventEnd":1458561380395,"domComplete":1458561383683,"loadEventStart":1458561383683,"loadEventEnd":1458561383737,"chromeFirstPaint":1458561380397}}]} {"schema":"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0","data":{}} Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36 BMID/E6797F732B Chrome 49 Chrome 49.0.2623.87 Browser WEBKIT en-US 1 1 0 0 0 0 0 0 0 1 24 1591 551 Mac OS X Mac OS X Apple Inc. Europe/London Computer 0 1680 1050 UTF-8 1591 3074 2016-03-21 11:56:51.679 {"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":"Chrome","useragentMajor":"49","useragentMinor":"0","useragentPatch":"2623","useragentVersion":"Chrome 49.0.2623","osFamily":"Mac OS X","osMajor":"10","osMinor":"10","osPatch":"5","osPatchMinor":null,"osVersion":"Mac OS X 10.10.5","deviceFamily":"Other"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"05d09faa707a9ff092e49929c79630d1"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"b73833bf7e3af5a6d5a02e79be96b064"}}]} bfcfa2c3-9b9e-4023-a850-9873e52e0fd2 2016-03-21 11:56:49.360 com.snowplowanalytics.snowplow link_click jsonschema 1-0-1 15cc945a721f944dada154f8c77cdbb1 """) + val eventWithMoreField = Event.parse("""snowplowweb snowplowweb web 2018-12-18 15:07:17.970 2016-03-21 11:56:51.434 2016-03-21 11:56:49.605 unstruct 38927042-a195-4c32-9f48-5a8c7c6d427f snplow6 js-2.6.0 ssc-0.6.0-kinesis spark-1.16.0-common-0.35.0 50d9ee525d12b25618e2dbab1f25c39d 086271e520deb84db3259c0af2878ee5 262ac8a29684b99251deb81d2e246446 9506c862a9dbeea2f55fba6af4a0e7ec 10 05d09faa707a9ff092e49929c79630d1 http://snowplowanalytics.com/ http snowplowanalytics.com 80 / {"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":"3e4a5deb-1504-4cb2-8751-9d358c632328"}},{"schema":"iglu:org.w3/PerformanceTiming/jsonschema/1-0-0","data":{"navigationStart":1458561378194,"unloadEventStart":1458561379080,"unloadEventEnd":1458561379080,"redirectStart":0,"redirectEnd":0,"fetchStart":1458561378195,"domainLookupStart":1458561378211,"domainLookupEnd":1458561378435,"connectStart":1458561378435,"connectEnd":1458561378602,"secureConnectionStart":0,"requestStart":1458561378602,"responseStart":1458561379079,"responseEnd":1458561379080,"domLoading":1458561379095,"domInteractive":1458561380392,"domContentLoadedEventStart":1458561380392,"domContentLoadedEventEnd":1458561380395,"domComplete":1458561383683,"loadEventStart":1458561383683,"loadEventEnd":1458561383737,"chromeFirstPaint":1458561380397}}]} {"schema":"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0","data":{}} Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36 BMID/E6797F732B Chrome 49 Chrome 49.0.2623.87 Browser WEBKIT en-US 1 1 0 0 0 0 0 0 0 1 24 1591 551 Mac OS X Mac OS X Apple Inc. Europe/London Computer 0 1680 1050 UTF-8 1591 3074 2016-03-21 11:56:51.679 {"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":"Chrome","useragentMajor":"49","useragentMinor":"0","useragentPatch":"2623","useragentVersion":"Chrome 49.0.2623","osFamily":"Mac OS X","osMajor":"10","osMinor":"10","osPatch":"5","osPatchMinor":null,"osVersion":"Mac OS X 10.10.5","deviceFamily":"Other"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"05d09faa707a9ff092e49929c79630d1"}},{"schema":"iglu:org.ietf/http_cookie/jsonschema/1-0-0","data":{"name":"sp","value":"b73833bf7e3af5a6d5a02e79be96b064"}}]} bfcfa2c3-9b9e-4023-a850-9873e52e0fd2 2016-03-21 11:56:49.360 com.snowplowanalytics.snowplow link_click jsonschema 1-0-1 15cc945a721f944dada154f8c77cdbb1 """) + val res4 = eventWithEmptyUnstructData.toEither.map(e => e.unstruct_event) + println(eventWithMoreField) + ok + } + +} diff --git a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoderSpec.scala b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoderSpec.scala index d62377a..6ed93be 100644 --- a/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoderSpec.scala +++ b/src/test/scala/com.snowplowanalytics.snowplow.analytics.scalasdk/decode/ValueDecoderSpec.scala @@ -201,5 +201,23 @@ class ValueDecoderSpec extends Specification { ).asRight ValueDecoder[UnstructEvent].parse(Symbol("key"), invalidPayloadUnstruct) mustEqual (Symbol("key"), "Unknown payload: iglu:invalid/schema/jsonschema/1-0-0").asLeft } + + "example" in { + val invalidPayloadUnstruct = + """{ + "schema": "iglu:invalid/schema/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 res = ValueDecoder[UnstructEvent].parse(Symbol("key"), invalidPayloadUnstruct) + println(res) + ok + } } }