Skip to content

Commit

Permalink
[WIP] Add toTSV method (close #97)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjben committed Oct 20, 2020
1 parent bcd6025 commit 759f000
Show file tree
Hide file tree
Showing 7 changed files with 582 additions and 129 deletions.
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)

Expand Down
3 changes: 3 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 759f000

Please sign in to comment.