├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── fluent │ ├── TransformError.scala │ ├── internal │ ├── DeepHLister.scala │ └── Transformer.scala │ └── package.scala └── test └── scala └── fluent └── FluentSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: scala 3 | jdk: openjdk8 4 | scala: 5 | - 2.12.4 6 | 7 | script: 8 | - sbt ++$TRAVIS_SCALA_VERSION clean coverage test coverageReport 9 | - test $TRAVIS_PULL_REQUEST = false && sbt updateImpactSubmit || true 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Beyond the lines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://api.travis-ci.org/btlines/fluent.svg?branch=master)](https://travis-ci.org/btlines/fluent) 2 | [![codecov](https://codecov.io/gh/btlines/fluent/branch/master/graph/badge.svg)](https://codecov.io/gh/btlines/fluent) 3 | [![Dependencies](https://app.updateimpact.com/badge/852442212779298816/fluent.svg?config=compile)](https://app.updateimpact.com/latest/852442212779298816/fluent) 4 | [![License](https://img.shields.io/:license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | [![Download](https://api.bintray.com/packages/beyondthelines/maven/fluent/images/download.svg) ](https://bintray.com/beyondthelines/maven/fluent/_latestVersion) 6 | 7 | # Fluent 8 | The seamless translation layer 9 | 10 | ## Context 11 | 12 | In DDD (Domain Driven Design) it is recommended to introduce a translation layer (aka anticorruption layer) between different domains. It allows to isolate the domain from each other making sure the can evolve independently. 13 | 14 | However writing code for the translation layer isn't really exciting as quite often the domain objects share some similarities. Translating from one domain to the other introduces lots of boilerplate code and doesn't add much business value. 15 | 16 | Fluent aims at reducing this boilerplate code. It leverages the similarities between the domains in order to convert case classes from one domain into the other. Under the hood it relies on Shapeless and implicit type class instances. 17 | 18 | ## Setup 19 | 20 | In order to use Fluent you need to add the following lines to your `build.sbt`: 21 | 22 | ```scala 23 | resolvers += Resolver.bintrayRepo("beyondthelines", "maven") 24 | 25 | libraryDependencies += "beyondthelines" %% "fluent" % "0.0.7" 26 | ``` 27 | 28 | ## Dependencies 29 | 30 | Fluent has only 2 dependencies: Shapeless and Cats. 31 | 32 | ## Usage 33 | 34 | In order to use Fluent you need to import the following: 35 | 36 | ```scala 37 | import cats.intances.all._ 38 | import fluent._ 39 | ``` 40 | 41 | The cats import is only needed if your case classes contains functors like `List`, `Option`, ... Of course you can be more specific and import only the instances you need. 42 | 43 | Importing fluent._ adds a new method to your case classes `transformTo`. You only need to call this method to transform a case class into another one. 44 | 45 | ### Basic Example 46 | 47 | Let's take a basic example to illustrate the usage: 48 | 49 | ```scala 50 | object External { 51 | case class Circle(x: Double, y: Double, radius: Double, color: Option[String]) 52 | } 53 | 54 | object Internal { 55 | case class Point(x: Double, y: Double) 56 | sealed trait Color 57 | object Color { 58 | case object Blue extends Color 59 | case object Red extends Color 60 | case object Yellow extends Color 61 | } 62 | case class Circle(center: Point, radius: Double, color: Option[Color]) 63 | } 64 | ``` 65 | 66 | Let's create an instance of external circle: 67 | 68 | ```scala 69 | val externalCircle = External.Circle( 70 | x = 1.0, 71 | y = 2.0, 72 | radius = 3.0, 73 | color = Some("Red") 74 | ) 75 | ``` 76 | 77 | and turn it into an internal circle 78 | 79 | ```scala 80 | val internalCircle = externalCircle.transformTo[Internal.Circle] 81 | ``` 82 | 83 | In this case it's easy to figure out what Fluent does: 84 | `External.Circle` contains a `x` and a `y` of type `Double` so Fluent can create a `Point` from an `External.Circle`. The `radius` can be taken as it is and `color` needs to be turned into the correct type (Note that if the colour doesn't exist in the expected values it would fail at runtime). 85 | 86 | ### Using user defined functions 87 | 88 | Sometimes Fluent cannot figure out how to convert from one case class to the other. In such cases it's possible to define implicit functions that Fluent can use to achieve the conversion. 89 | 90 | Let's take another example: 91 | 92 | ```scala 93 | import java.time.Instant 94 | 95 | object External { 96 | case class Post(author: String, body: String, timestamp: Long) 97 | } 98 | object Internal { 99 | case class Author(name: String) 100 | case class Post(author: Author, body: String, tags: List[String], timestamp: Instant) 101 | } 102 | ``` 103 | 104 | Let's create an `External.Post` instance: 105 | 106 | ```scala 107 | val externalPost = External.Post( 108 | author = "Misty", 109 | body = "#Fluent is a cool library to implement your #DDD #translationLayer seamlessly", 110 | timestamp = 1491823712002L 111 | ) 112 | ``` 113 | 114 | In this case it's not possible to transform the `External.Post` into an `Internal.Post` because Fluent can't figure out how to extract the tags from the post and how to convert the `timestamp` of type `Long` into a `timestamp` of type `Instant`. 115 | 116 | We can define 2 implicit functions to perform these transformations: 117 | 118 | ```scala 119 | implicit def tagsExtractor(post: External.Post): List[String] = 120 | post.body.split("\\s").toList.filter(_.startsWith("#")) 121 | 122 | implicit def toInstant(timestamp: Long): Instant = 123 | Instant.ofEpochMilli(timestamp) 124 | ``` 125 | 126 | With these 2 functions in scope we can now transform the `External.Post` into an `Internal.Post` 127 | 128 | ```scala 129 | val internalPost = externalPost.transformTo[Internal.Post] 130 | ``` 131 | 132 | ### More information 133 | 134 | More implementation details can be found on this blog post: http://www.beyondthelines.net/programing/fluent-a-deep-dive-into-shapeless-and-implicit-resolution/ 135 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "fluent" 2 | 3 | version := "0.0.7" 4 | 5 | scalaVersion := "2.12.4" 6 | crossScalaVersions := Seq("2.11.12", "2.12.4") 7 | 8 | libraryDependencies ++= Seq( 9 | "org.typelevel" %% "cats-core" % "1.0.1", 10 | "com.chuusai" %% "shapeless" % "2.3.3", 11 | "org.scalatest" %% "scalatest" % "3.0.4" % Test 12 | ) 13 | 14 | organization := "beyondthelines" 15 | 16 | licenses := ("MIT", url("http://opensource.org/licenses/MIT")) :: Nil 17 | 18 | bintrayOrganization := Some("beyondthelines") 19 | 20 | bintrayPackageLabels := Seq("scala") -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.0.4 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.2") 2 | 3 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 4 | 5 | addSbtPlugin("com.updateimpact" % "updateimpact-sbt-plugin" % "2.1.3") 6 | -------------------------------------------------------------------------------- /src/main/scala/fluent/TransformError.scala: -------------------------------------------------------------------------------- 1 | package fluent 2 | 3 | trait TransformError { 4 | def message: String 5 | } 6 | 7 | object TransformError { 8 | def apply(error: String): TransformError = TransformErrorMessage(error) 9 | def apply(error: Throwable): TransformError = TransformErrorThrowable(error) 10 | 11 | case class TransformErrorMessage(message: String) extends TransformError 12 | 13 | case class TransformErrorThrowable(throwable: Throwable) extends TransformError { 14 | override def message: String = throwable.getMessage 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/fluent/internal/DeepHLister.scala: -------------------------------------------------------------------------------- 1 | package fluent.internal 2 | 3 | import shapeless.labelled.FieldType 4 | import shapeless.ops.hlist.Prepend 5 | import shapeless.{ ::, DepFn1, HList, HNil, LabelledGeneric, Lazy } 6 | 7 | /** 8 | * DeepHLister is inspired by https://github.com/milessabin/shapeless/blob/master/examples/src/main/scala/shapeless/examples/deephlister.scala 9 | * It has been adapted to works with Labelled fields and flatten out the final HList 10 | */ 11 | trait DeepHLister[R <: HList] extends DepFn1[R] { type Out <: HList } 12 | 13 | trait LowPriorityDeepHLister { 14 | type Aux[R <: HList, Out0 <: HList] = DeepHLister[R] { type Out = Out0 } 15 | 16 | implicit def headNotCaseClassDeepHLister[H, T <: HList]( 17 | implicit dht: Lazy[DeepHLister[T]] 18 | ): Aux[H :: T, H :: dht.value.Out] = new DeepHLister[H :: T] { 19 | type Out = H :: dht.value.Out 20 | def apply(r: H :: T): Out = r.head :: dht.value(r.tail) 21 | } 22 | } 23 | 24 | object DeepHLister extends LowPriorityDeepHLister { 25 | def apply[R <: HList](implicit dh: DeepHLister[R]): Aux[R, dh.Out] = dh 26 | 27 | implicit object hnilDeepHLister extends DeepHLister[HNil] { 28 | type Out = HNil 29 | def apply(r: HNil): Out = HNil 30 | } 31 | 32 | implicit def headCaseClassDeepHLister[K, V, R <: HList, T <: HList, OutR <: HList, OutT <: HList, Out0 <: HList]( 33 | implicit 34 | gen: LabelledGeneric.Aux[V, R], 35 | dhh: Lazy[DeepHLister.Aux[R, OutR]], 36 | dht: Lazy[DeepHLister.Aux[T, OutT]], 37 | prepend: Prepend.Aux[OutR, OutT, Out0] 38 | ): Aux[FieldType[K, V] :: T, Out0] = new DeepHLister[FieldType[K, V] :: T] { 39 | type Out = Out0 40 | def apply(r: FieldType[K, V] :: T): Out = prepend(dhh.value(gen.to(r.head.asInstanceOf[V])), dht.value(r.tail)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/fluent/internal/Transformer.scala: -------------------------------------------------------------------------------- 1 | package fluent.internal 2 | 3 | import cats.{ Applicative, Monoid, Traverse } 4 | import fluent.TransformError 5 | import shapeless.labelled.{FieldType, field} 6 | import shapeless.ops.record.Selector 7 | import shapeless.{ :+:, ::, CNil, Coproduct, Generic, HList, HNil, Inl, Inr, LabelledGeneric, Lazy } 8 | import cats.syntax.either._ 9 | 10 | import scala.util.{Failure, Success, Try} 11 | 12 | trait Transformer[A, B] { 13 | def apply(a: A): Either[TransformError, B] 14 | } 15 | 16 | trait ImplicitTransformersPriority1 { 17 | type TransformResult[T] = Either[TransformError, T] 18 | 19 | def instance[A, B](f: A => Either[TransformError, B]): Transformer[A, B] = 20 | new Transformer[A, B] { 21 | override def apply(a: A): Either[TransformError, B] = f(a) 22 | } 23 | 24 | implicit def hlistGlobalTransformer[A, K, V, T <: HList](implicit 25 | transformHead: Transformer[A, V], 26 | transformTail: Transformer[A, T] 27 | ): Transformer[A, FieldType[K, V] :: T] = instance { a: A => 28 | for { 29 | h <- transformHead(a) 30 | t <- transformTail(a) 31 | } yield field[K](h) :: t 32 | } 33 | 34 | implicit def emptyMonoidTransformer[F[_], A, B](implicit monoid: Monoid[F[B]]): Transformer[A, F[B]] = 35 | instance { _: A => 36 | Right(monoid.empty) 37 | } 38 | } 39 | 40 | trait ImplicitTransformersPriority2 extends ImplicitTransformersPriority1 { 41 | implicit def deepHListTransformer[A, ARepr <: HList, R <: HList, F, K, V, T <: HList](implicit 42 | generic: LabelledGeneric.Aux[A, ARepr], 43 | deepHLister: DeepHLister.Aux[ARepr, R], 44 | selector: Selector.Aux[R, K, F], 45 | transformHead: Transformer[F, V], 46 | transformTail: Transformer[A, T] 47 | ): Transformer[A, FieldType[K, V] :: T] = instance { a: A => 48 | val r = deepHLister(generic.to(a)) 49 | for { 50 | h <- transformHead(selector(r)) 51 | t <- transformTail(a) 52 | } yield field[K](h) :: t 53 | } 54 | } 55 | 56 | trait ImplicitTransformersPriority3 extends ImplicitTransformersPriority2 { 57 | implicit def hlistTransformer[A, ARepr <: HList, F, K, V, T <: HList](implicit 58 | generic: LabelledGeneric.Aux[A, ARepr], 59 | selector: Selector.Aux[ARepr, K, F], 60 | transformHead: Transformer[F, V], 61 | transformTail: Transformer[A, T] 62 | ): Transformer[A, FieldType[K, V] :: T] = instance { a: A => 63 | for { 64 | h <- transformHead(selector(generic.to(a))) 65 | t <- transformTail(a) 66 | } yield field[K](h) :: t 67 | } 68 | 69 | implicit def fromSealedTransformer[A, Repr <: Coproduct, B](implicit 70 | generic: Generic.Aux[A, Repr], 71 | transform: Transformer[Repr, B] 72 | ): Transformer[A, B] = instance { a: A => 73 | transform(generic.to(a)) 74 | } 75 | 76 | implicit def fromCoprodTransformer[H, T <: Coproduct, B](implicit 77 | transformHead: Transformer[H, B], 78 | transformTail: Transformer[T, B] 79 | ): Transformer[H :+: T, B] = instance { (a: H :+: T) => 80 | a match { 81 | case Inl(h) => transformHead(h) 82 | case Inr(t) => transformTail(t) 83 | } 84 | } 85 | 86 | implicit def fromCnilTransformer[A]: Transformer[CNil, A] = instance { _ => 87 | // there is no CNil instance, so this is never executed 88 | Left(TransformError("Can't transform CNil")) 89 | } 90 | 91 | implicit def toSealedTransformer[A, Repr <: Coproduct, B](implicit 92 | generic: Generic.Aux[B, Repr], 93 | transform: Transformer[A, Repr] 94 | ): Transformer[A, B] = instance { a: A => 95 | transform(a) map generic.from 96 | } 97 | 98 | implicit def toCoprodTransformer[A, H, T <: Coproduct](implicit 99 | transformHead: Transformer[A, H], 100 | transformTail: Transformer[A, T] 101 | ): Transformer[A, H :+: T] = instance { a: A => 102 | transformHead(a).map(Inl.apply) match { 103 | case h@Right(_) => h 104 | case _ => transformTail(a).map(Inr.apply) 105 | } 106 | } 107 | 108 | implicit def toCnilTransformer[A]: Transformer[A, CNil] = instance { a: A => 109 | // There is no instance of CNil, so this won't be used 110 | Left(TransformError("Can't transform into CNil")) 111 | } 112 | 113 | implicit def optionExtractorTransformer[A, B](implicit 114 | transform: Transformer[A, B] 115 | ): Transformer[Option[A], B] = instance { a: Option[A] => 116 | a.map(transform.apply) getOrElse Left(TransformError("Missing required field")) 117 | } 118 | } 119 | 120 | trait ImplicitTransformersPriority4 extends ImplicitTransformersPriority3 { 121 | def fromTry[A](t: Try[A]): Either[Throwable, A] = 122 | t match { 123 | case Failure(e) => Either.left(e) 124 | case Success(v) => Either.right(v) 125 | } 126 | 127 | implicit def customTransformer[A, B](implicit f: A => B): Transformer[A, B] = 128 | instance { a: A => 129 | fromTry(Try(f(a))).left.map(TransformError.apply) 130 | } 131 | implicit def hlistCustomTransformer[A, K, V, T <: HList](implicit 132 | f: A => V, 133 | transformTail: Transformer[A, T] 134 | ): Transformer[A, FieldType[K, V] :: T] = instance { a: A => 135 | for { 136 | h <- fromTry(Try(f(a))).left.map(TransformError.apply) 137 | t <- transformTail(a) 138 | } yield field[K](h) :: t 139 | } 140 | 141 | implicit def genericTransformer[A, B, BRepr <: HList](implicit 142 | generic: LabelledGeneric.Aux[B, BRepr], 143 | transform: Lazy[Transformer[A, BRepr]] 144 | ): Transformer[A, B] = instance { a: A => 145 | transform.value(a) map generic.from 146 | } 147 | 148 | implicit def headTransformer[A, ARepr <: HList, F, K, V](implicit 149 | generic: LabelledGeneric.Aux[A, ARepr], 150 | selector: Selector.Aux[ARepr, K, F], 151 | transform: Transformer[F, V] 152 | ): Transformer[A, V] = instance { a: A => 153 | transform(selector(generic.to(a))) 154 | } 155 | 156 | implicit def hnilTransformer[A]: Transformer[A, HNil] = instance { 157 | _: A => Right(HNil) 158 | } 159 | 160 | implicit def identityTransformer[A]: Transformer[A, A] = instance { 161 | a: A => Right(a) 162 | } 163 | 164 | implicit def functorTransformer[M[_], A, B](implicit 165 | transform: Transformer[A, B], 166 | traverse: Traverse[M], 167 | applicative: Applicative[TransformResult] 168 | ): Transformer[M[A], M[B]] = instance { a: M[A] => 169 | traverse.traverse[TransformResult, A, B](a)(transform.apply) 170 | } 171 | 172 | implicit def applicativeTransformer[F[_], A, B](implicit 173 | applicative: Applicative[F], 174 | transform: Transformer[A, B] 175 | ): Transformer[A, F[B]] = instance { a: A => 176 | transform(a).map(applicative.pure) 177 | } 178 | 179 | implicit def emptyToStringTransformer[A](implicit 180 | generic: Generic.Aux[A, HNil] 181 | ): Transformer[A, String] = instance { a: A => 182 | val b = a.toString 183 | val i = b.indexOf('(') 184 | if (i >= 0) Right(b.substring(0, i)) else Right(b) 185 | } 186 | 187 | implicit def stringToEmptyTransformer[A](implicit 188 | generic: Generic.Aux[A, HNil], 189 | transform: Transformer[A, String] 190 | ): Transformer[String, A] = instance { a: String => 191 | val b = generic.from(HNil) 192 | transform(b) match { 193 | case Right(`a`) => Right(b) 194 | case Right(_) => Left(TransformError(s"Can't transform '$a' into $b")) 195 | case Left(error) => Left(error) 196 | } 197 | } 198 | } 199 | 200 | trait ImplicitTransformersPriority5 extends ImplicitTransformersPriority4 { 201 | implicit def extractorTransformer[A, B](implicit 202 | generic: Generic.Aux[A, B :: HNil] 203 | ): Transformer[A, B] = instance { a: A => 204 | Right(generic.to(a).head) 205 | } 206 | } 207 | 208 | object Transformer extends ImplicitTransformersPriority5 209 | -------------------------------------------------------------------------------- /src/main/scala/fluent/package.scala: -------------------------------------------------------------------------------- 1 | import fluent.internal.Transformer 2 | 3 | package object fluent { 4 | implicit class TransformerOps[A](a: A) { 5 | def transformTo[B](implicit transform: Transformer[A, B]): B = transform(a) match { 6 | case Right(b) => b 7 | case Left(TransformError.TransformErrorThrowable(error)) => throw error 8 | case Left(error) => throw new IllegalArgumentException(s"Can't transform $a: ${error.message}") 9 | } 10 | def changeTo[B](implicit transformer: Transformer[A, B]): Either[TransformError, B] = transformer(a) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/scala/fluent/FluentSpec.scala: -------------------------------------------------------------------------------- 1 | package fluent 2 | 3 | import java.time.Instant 4 | 5 | import org.scalatest.{ Matchers, WordSpecLike } 6 | 7 | object External { 8 | case class Circle(x: Double, y: Double, radius: Double, color: Option[String]) 9 | case class Post(author: String, body: String, timestamp: Long) 10 | case class PostWithOptionalFields(author: Option[String], body: Option[String], timestamp: Option[Long]) 11 | } 12 | 13 | object Internal { 14 | case class Point(x: Double, y: Double) 15 | sealed trait Color 16 | object Color { 17 | case object Blue extends Color 18 | case object Red extends Color 19 | case object Yellow extends Color 20 | } 21 | case class Circle(center: Point, radius: Double, color: Option[Color]) 22 | case class Cylinder(origin: Point, radius: Double, height: Double, color: Color) 23 | 24 | case class Author(author: String) 25 | case class Post(author: Author, body: String, tags: List[String], timestamp: Instant) 26 | case class Published(post: Post) 27 | } 28 | 29 | class FluentSpec extends WordSpecLike with Matchers { 30 | 31 | // Needed to support transformation on Option and List 32 | import cats.instances.option._ 33 | import cats.instances.list._ 34 | 35 | "Fluent" should { 36 | val externalCircle = External.Circle( 37 | x = 1.0, 38 | y = 2.0, 39 | radius = 3.0, 40 | color = Some("Red") 41 | ) 42 | val internalCircle = Internal.Circle( 43 | center = Internal.Point(1.0, 2.0), 44 | radius = 3.0, 45 | color = Some(Internal.Color.Red) 46 | ) 47 | "tranform External.Circle into Internal.Circle" in { 48 | externalCircle.changeTo[Internal.Circle] shouldBe Right(internalCircle) 49 | } 50 | "transform Internal.Circle into External.Circle" in { 51 | internalCircle.changeTo[External.Circle] shouldBe Right(externalCircle) 52 | } 53 | "transform Option[Internal.Circle] into Option[External.Circle]" in { 54 | import cats.instances.either._ 55 | import cats.instances.option._ 56 | Option(internalCircle).changeTo[Option[External.Circle]] shouldBe Right(Some(externalCircle)) 57 | (None: Option[Internal.Circle]).changeTo[Option[External.Circle]] shouldBe Right(None) 58 | } 59 | val externalPost = External.Post( 60 | author = "Misty", 61 | body = "#Fluent is a cool library to implement your #DDD #translationLayer seamlessly", 62 | timestamp = 1491823712002L 63 | ) 64 | val externalPostWithOptionalFields = External.PostWithOptionalFields( 65 | author = Some("Misty"), 66 | body = Some("#Fluent is a cool library to implement your #DDD #translationLayer seamlessly"), 67 | timestamp = Some(1491823712002L) 68 | ) 69 | val internalPost = Internal.Post( 70 | author = Internal.Author("Misty"), 71 | body = "#Fluent is a cool library to implement your #DDD #translationLayer seamlessly", 72 | timestamp = Instant.ofEpochMilli(1491823712002L), 73 | tags = List("#Fluent", "#DDD", "#translationLayer") 74 | ) 75 | "transform External.Post to Internal.Post without extracting tags" in { 76 | implicit def toInstant(timestamp: Long): Instant = Instant.ofEpochMilli(timestamp) 77 | externalPost.transformTo[Internal.Post] shouldBe internalPost.copy(tags = Nil) 78 | } 79 | "transform External.PostWithOptionalFields to Internal.Post without extracting tags" in { 80 | implicit def toInstant(timestamp: Long): Instant = Instant.ofEpochMilli(timestamp) 81 | externalPostWithOptionalFields.transformTo[Internal.Post] shouldBe internalPost.copy(tags = Nil) 82 | } 83 | "transform External.Post to Internal.Post using user defined functions" in { 84 | implicit def tagsExtractor(post: External.Post): List[String] = 85 | post.body.split("\\s").toList.filter(_.startsWith("#")) 86 | implicit def toInstant(timestamp: Long): Instant = Instant.ofEpochMilli(timestamp) 87 | externalPost.transformTo[Internal.Post] shouldBe internalPost 88 | } 89 | "transform Internal.Post to External.Post using user defined functions" in { 90 | implicit def toTimestamp(instant: Instant): Long = instant.toEpochMilli 91 | internalPost.transformTo[External.Post] shouldBe externalPost 92 | } 93 | "transform Internal.Post to External.PostWithOptionalFields using user defined functions" in { 94 | implicit def toTimestamp(instant: Instant): Long = instant.toEpochMilli 95 | internalPost.transformTo[External.PostWithOptionalFields] shouldBe externalPostWithOptionalFields 96 | } 97 | "transform Internal.Published event to External.PostWithOptionalFields" in { 98 | implicit def toTimestamp(instant: Instant): Long = instant.toEpochMilli 99 | Internal.Published(internalPost).transformTo[External.PostWithOptionalFields] shouldBe externalPostWithOptionalFields 100 | } 101 | "transform Internal.Cylinder transformTo External.Circle" in { 102 | val cylinder = Internal.Cylinder( 103 | origin = Internal.Point(1.0, 2.0), 104 | radius = 3.0, 105 | height = 4.0, 106 | color = Internal.Color.Red 107 | ) 108 | cylinder.transformTo[External.Circle] shouldBe externalCircle 109 | } 110 | "failed to transform when missing required field" in { 111 | case class Optional(value: Option[String]) 112 | case class Required(value: String) 113 | Optional(None).changeTo[Required] shouldBe Left(TransformError("Missing required field")) 114 | an [IllegalArgumentException] should be thrownBy Optional(None).transformTo[Required] 115 | } 116 | "failed to transform string into case object if name doesn't match" in { 117 | case object SomethingElse 118 | "Something".changeTo[SomethingElse.type] shouldBe Left(TransformError("Can't transform 'Something' into SomethingElse")) 119 | an [IllegalArgumentException] should be thrownBy "Something".transformTo[SomethingElse.type] 120 | } 121 | } 122 | 123 | } 124 | --------------------------------------------------------------------------------