diff --git a/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsPartialTransformerImplicits.scala b/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsPartialTransformerImplicits.scala index 177846415..5c6ea4e96 100644 --- a/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsPartialTransformerImplicits.scala +++ b/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsPartialTransformerImplicits.scala @@ -51,17 +51,16 @@ trait CatsPartialTransformerImplicits { override def tailRecM[A, B](a: A)( f: A => PartialTransformer[Source, Either[A, B]] - ): PartialTransformer[Source, B] = - (src, failFast) => { - def run(a1: A) = partial.Result.fromCatching(f(a1).transform(src, failFast)).flatMap(identity) - @scala.annotation.tailrec - def loop(a1: A): partial.Result[B] = run(a1) match { - case partial.Result.Value(Left(a2)) => loop(a2) - case partial.Result.Value(Right(b)) => partial.Result.Value(b) - case errors => errors.asInstanceOf[partial.Result[B]] - } - loop(a) + ): PartialTransformer[Source, B] = { (src, failFast) => + def run(a1: A) = partial.Result.fromCatching(f(a1).transform(src, failFast)).flatMap(identity) + @scala.annotation.tailrec + def loop(a1: A): partial.Result[B] = run(a1) match { + case partial.Result.Value(Left(a2)) => loop(a2) + case partial.Result.Value(Right(b)) => partial.Result.Value(b) + case errors => errors.asInstanceOf[partial.Result[B]] } + loop(a) + } override def raiseError[A](e: partial.Result.Errors): PartialTransformer[Source, A] = (_, _) => e diff --git a/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsTotalTransformerImplicits.scala b/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsTotalTransformerImplicits.scala index b2b6eb4f3..78a534647 100644 --- a/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsTotalTransformerImplicits.scala +++ b/chimney-cats/src/main/scala/io/scalaland/chimney/cats/CatsTotalTransformerImplicits.scala @@ -39,15 +39,14 @@ trait CatsTotalTransformerImplicits { override def tailRecM[A, B](a: A)( f: A => Transformer[Source, Either[A, B]] - ): Transformer[Source, B] = - src => { - @scala.annotation.tailrec - def loop(a1: A): B = f(a1).transform(src) match { - case Left(a2) => loop(a2) - case Right(b) => b - } - loop(a) + ): Transformer[Source, B] = { src => + @scala.annotation.tailrec + def loop(a1: A): B = f(a1).transform(src) match { + case Left(a2) => loop(a2) + case Right(b) => b } + loop(a) + } override def coflatMap[A, B]( fa: Transformer[Source, A] @@ -60,6 +59,6 @@ trait CatsTotalTransformerImplicits { implicit final def catsContravariantForTransformer[Target]: Contravariant[Transformer[*, Target]] = new Contravariant[Transformer[*, Target]] { def contramap[A, B](fa: Transformer[A, Target])(f: B => A): Transformer[B, Target] = - b => fa.transform(f(b)) + fa.contramap(f) } } diff --git a/chimney/src/main/scala/io/scalaland/chimney/PartialTransformer.scala b/chimney/src/main/scala/io/scalaland/chimney/PartialTransformer.scala index ab6605a74..9bfbfe938 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/PartialTransformer.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/PartialTransformer.scala @@ -19,7 +19,7 @@ import io.scalaland.chimney.internal.runtime.{TransformerFlags, TransformerOverr * @since 0.7.0 */ @FunctionalInterface -trait PartialTransformer[From, To] extends PartialTransformer.AutoDerived[From, To] { +trait PartialTransformer[From, To] extends PartialTransformer.AutoDerived[From, To] { self => /** Run transformation using provided value as a source. * @@ -58,6 +58,7 @@ trait PartialTransformer[From, To] extends PartialTransformer.AutoDerived[From, */ final def transformFailFast(src: From): partial.Result[To] = transform(src, failFast = true) + } /** Companion of [[io.scalaland.chimney.PartialTransformer]]. @@ -163,7 +164,7 @@ object PartialTransformer extends PartialTransformerCompanionPlatform { * @since 0.8.0 */ @FunctionalInterface - trait AutoDerived[From, To] { + trait AutoDerived[From, To] extends PartialTransformerOps[From, To] { def transform(src: From, failFast: Boolean): partial.Result[To] } @@ -173,6 +174,62 @@ object PartialTransformer extends PartialTransformerCompanionPlatform { implicit def liftTotal[From, To](implicit total: Transformer[From, To]): AutoDerived[From, To] = (src: From, failFast: Boolean) => partial.Result.fromCatching(total.transform(src)) } + + trait PartialTransformerOps[From, To] { self => + + def transform(src: From, failFast: Boolean): partial.Result[To] + + /** Creates a new [[io.scalaland.chimney.PartialTransformer PartialTransformer]] by applying a pure function to a + * source of type `A` before transforming it to `To`. See an example: + * {{{ + * val stringTransformer: PartialTransformer[String, Int] = + * PartialTransformer.fromFunction(_.length) + * + * case class Id(id: String) + * + * implicit val idTransformer: PartialTransformer[Id, Int] = + * stringTransformer.contramap(_.id) + * }}} + * + * @param f + * a pure function that maps a value of `A` to `From` + * @return + * new [[io.scalaland.chimney.PartialTransformer PartialTransformer]] + * + * @since 1.5.0 + */ + final def contramap[A](f: A => From): PartialTransformer[A, To] = new PartialTransformer[A, To] { + override def transform(src: A, failFast: Boolean): partial.Result[To] = + self.transform(f(src), failFast) + } + + /** Creates a new [[io.scalaland.chimney.PartialTransformer PartialTransformer]] by applying a pure function to a + * [[io.scalaland.chimney.partial.Result Result]] of transforming `From` to `To`. See an example: + * {{{ + * val stringTransformer: PartialTransformer[String, Int] = + * PartialTransformer.fromFunction(_.length) + * + * case class Length(length: Int) + * + * implicit val toLengthTransformer: PartialTransformer[String, Length] = + * stringTransformer.map(id => Length(id)) + * }}} + * + * @param f + * a pure function that maps a value of `To` to `A` + * @return + * new [[io.scalaland.chimney.PartialTransformer PartialTransformer]] + * + * @since 1.5.0 + */ + final def map[A](f: To => A): PartialTransformer[From, A] = new PartialTransformer[From, A] { + override def transform(src: From, failFast: Boolean): partial.Result[A] = + self.transform(src, failFast) match { + case partial.Result.Value(to) => partial.Result.Value(f(to)) + case errs: partial.Result.Errors => errs.asInstanceOf[partial.Result[A]] + } + } + } } // extended by PartialTransformerCompanionPlatform private[chimney] trait PartialTransformerLowPriorityImplicits1 { this: PartialTransformer.type => diff --git a/chimney/src/main/scala/io/scalaland/chimney/Transformer.scala b/chimney/src/main/scala/io/scalaland/chimney/Transformer.scala index d6863c8cb..70592ae1a 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/Transformer.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/Transformer.scala @@ -18,7 +18,7 @@ import io.scalaland.chimney.internal.runtime.{TransformerFlags, TransformerOverr * @since 0.1.0 */ @FunctionalInterface -trait Transformer[From, To] extends Transformer.AutoDerived[From, To] { +trait Transformer[From, To] extends Transformer.AutoDerived[From, To] { self => /** Run transformation using provided value as a source. * @@ -30,6 +30,7 @@ trait Transformer[From, To] extends Transformer.AutoDerived[From, To] { * @since 0.1.0 */ def transform(src: From): To + } /** Companion of [[io.scalaland.chimney.Transformer]]. @@ -96,12 +97,61 @@ object Transformer extends TransformerCompanionPlatform { * @since 0.8.0 */ @FunctionalInterface - trait AutoDerived[From, To] { + trait AutoDerived[From, To] extends TransformerOps[From, To] { def transform(src: From): To } /** @since 0.8.0 */ object AutoDerived extends TransformerAutoDerivedCompanionPlatform + + trait TransformerOps[From, To] { self => + + def transform(src: From): To + + /** Creates a new [[io.scalaland.chimney.Transformer Transformer]] by applying a pure function to a source of type + * `A` before transforming it to `To`. See an example: + * {{{ + * val stringTransformer: Transformer[String, Int] = _.length + * + * case class Id(id: String) + * + * implicit val idTransformer: Transformer[Id, Int] = + * stringTransformer.contramap(_.id) + * }}} + * + * @param f + * a pure function that maps a value of `A` to `From` + * @return + * new [[io.scalaland.chimney.Transformer Transformer]] + * + * @since 1.5.0 + */ + final def contramap[A](f: A => From): Transformer[A, To] = new Transformer[A, To] { + override def transform(src: A): To = self.transform(f(src)) + } + + /** Creates a new [[io.scalaland.chimney.Transformer Transformer]] by applying a pure function to a result of + * transforming `From` to `To`. See an example: + * {{{ + * val stringTransformer: Transformer[String, Int] = _.length + * + * case class Length(length: Int) + * + * implicit val toLengthTransformer: Transformer[String, Length] = + * stringTransformer.map(id => Length(id)) + * }}} + * + * @param f + * a pure function that maps a value of `To` to `A` + * @return + * new [[io.scalaland.chimney.Transformer Transformer]] + * + * @since 1.5.0 + */ + final def map[A](f: To => A): Transformer[From, A] = new Transformer[From, A] { + override def transform(src: From): A = f(self.transform(src)) + } + } } // extended by TransformerCompanionPlatform private[chimney] trait TransformerLowPriorityImplicits1 extends TransformerLowPriorityImplicits2 { diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSpec.scala index 93a5a7a79..b7fa486a3 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSpec.scala @@ -1,5 +1,7 @@ package io.scalaland.chimney +import io.scalaland.chimney.dsl.* + class PartialTransformerSpec extends ChimneySpec { private val pt1 = PartialTransformer[String, Int](str => partial.Result.fromValue(str.toInt)) @@ -73,4 +75,73 @@ class PartialTransformerSpec extends ChimneySpec { // no second error due to fail fast mode ) } + + test("map") { + case class Length(length: Int) + + trait Prefix { + def code: Int + } + + object Prefix { + + def from(i: Int): Prefix = i match { + case 1 => FooPrefix + case 2 => BarPrefix + case _ => NanPrefix + } + + case object NanPrefix extends Prefix { + override def code: Int = 0 + } + + case object FooPrefix extends Prefix { + override def code: Int = 1 + } + + case object BarPrefix extends Prefix { + override def code: Int = 2 + } + } + + implicit val toLengthTransformer: PartialTransformer[String, Length] = + pt1.map(Length.apply) + + implicit val toPrefixTransformer: PartialTransformer[String, Prefix] = + pt1.map(Prefix.from) + + val id = "2" + id.intoPartial[Length].transform ==> partial.Result.fromValue(Length(id.toInt)) + id.intoPartial[Prefix].transform ==> partial.Result.fromValue(Prefix.BarPrefix) + } + + test("contramap") { + case class Id(id: String) + + trait Prefix { + def value: String + } + + object Prefix { + case object FooPrefix extends Prefix { + override def value: String = "1" + } + + case object BarPrefix extends Prefix { + override def value: String = "2" + } + } + + implicit val idTransformer: PartialTransformer[Id, Int] = + pt1.contramap(_.id) + + implicit val prefixTransformer: PartialTransformer[Prefix, Int] = + pt1.contramap(_.value) + + val id = "1" + Id(id).intoPartial[Int].transform ==> partial.Result.fromValue(id.toInt) + + val prefix: Prefix = Prefix.FooPrefix + prefix.intoPartial[Int].transform ==> partial.Result.fromValue(id.toInt) + } } diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSpec.scala new file mode 100644 index 000000000..a23a1efc0 --- /dev/null +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSpec.scala @@ -0,0 +1,85 @@ +package io.scalaland.chimney + +import io.scalaland.chimney.dsl.* + +class TotalTransformerSpec extends ChimneySpec { + + test("map") { + case class Length(length: Int) + + trait Prefix { + def code: Int + } + + object Prefix { + + def from(i: Int): Prefix = i match { + case 1 => FooPrefix + case 2 => BarPrefix + case _ => NanPrefix + } + + case object NanPrefix extends Prefix { + override def code: Int = 0 + } + + case object FooPrefix extends Prefix { + override def code: Int = 1 + } + + case object BarPrefix extends Prefix { + override def code: Int = 2 + } + } + + val stringTransformer = new Transformer[String, Int] { + override def transform(src: String): Int = src.length + } + + implicit val toLengthTransformer: Transformer[String, Length] = + stringTransformer.map(Length.apply) + + implicit val toPrefixTransformer: Transformer[String, Prefix] = + stringTransformer.map(Prefix.from) + + val id = "1" + id.into[Length].transform ==> Length(id.length) + id.into[Prefix].transform ==> Prefix.FooPrefix + } + + test("contramap") { + case class Id(id: String) + + case class Length(length: Int) + + trait Prefix { + def value: String + } + + object Prefix { + case object FooPrefix extends Prefix { + override def value: String = "Foo" + } + + case object BarPrefix extends Prefix { + override def value: String = "Bar" + } + } + + val stringTransformer = new Transformer[String, Length] { + override def transform(src: String): Length = Length(src.length) + } + + implicit val idTransformer: Transformer[Id, Length] = + stringTransformer.contramap(_.id) + + implicit val prefixTransformer: Transformer[Prefix, Length] = + stringTransformer.contramap(_.value) + + val id = "id" + Id(id).into[Length].transform ==> Length(id.length) + + val prefix: Prefix = Prefix.FooPrefix + prefix.into[Length].transform ==> Length(prefix.value.length) + } +}