diff --git a/build.sbt b/build.sbt index c9ee152f..f4133f6f 100644 --- a/build.sbt +++ b/build.sbt @@ -14,7 +14,7 @@ ThisBuild / developers := List( url("https://github.com/arainko") ) ) -ThisBuild / scalaVersion := "3.2.1" +ThisBuild / scalaVersion := "3.2.2" ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0" ThisBuild / versionPolicyIntention := Compatibility.BinaryCompatible @@ -30,7 +30,9 @@ lazy val previousArtifacts = Set( "io.github.arainko" %% "ducktape" % "0.1.0", "io.github.arainko" %% "ducktape" % "0.1.1", - "io.github.arainko" %% "ducktape" % "0.1.2" + "io.github.arainko" %% "ducktape" % "0.1.2", + "io.github.arainko" %% "ducktape" % "0.1.3", + "io.github.arainko" %% "ducktape" % "0.1.4", ) lazy val root = diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala index d2d764d4..a2456545 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala @@ -23,6 +23,103 @@ object Transformer { inline def showCode[A](inline value: A): A = DebugMacros.code(value) } + given identity[Source, Dest >: Source]: Identity[Source, Dest] = Identity[Source, Dest] + + given betweenNonOptionOption[Source, Dest](using Transformer[Source, Dest]): Transformer[Source, Option[Dest]] = + from => Transformer[Source, Dest].transform.andThen(Some.apply)(from) + + given betweenOptions[Source, Dest](using + Transformer[Source, Dest] + ): Transformer[Option[Source], Option[Dest]] = + from => from.map(Transformer[Source, Dest].transform) + + given betweenCollections[Source, Dest, SourceCollection[elem] <: Iterable[elem], DestCollection[elem] <: Iterable[elem]](using + transformer: Transformer[Source, Dest], + factory: Factory[Dest, DestCollection[Dest]] + ): Transformer[SourceCollection[Source], DestCollection[Dest]] = from => from.map(transformer.transform).to(factory) + + given betweenEithers[Source1, Source2, Dest1, Dest2](using + Transformer[Source1, Source2], + Transformer[Dest1, Dest2] + ): Transformer[Either[Source1, Dest1], Either[Source2, Dest2]] = { + case Right(value) => Right(Transformer[Dest1, Dest2].transform(value)) + case Left(value) => Left(Transformer[Source1, Source2].transform(value)) + } + + inline given betweenProducts[Source, Dest](using + Mirror.ProductOf[Source], + Mirror.ProductOf[Dest] + ): Transformer.ForProduct[Source, Dest] = + Transformer.ForProduct.make(DerivedTransformers.product[Source, Dest]) + + inline given betweenCoproducts[Source, Dest](using + Mirror.SumOf[Source], + Mirror.SumOf[Dest] + ): Transformer.ForCoproduct[Source, Dest] = + Transformer.ForCoproduct.make(DerivedTransformers.coproduct[Source, Dest]) + + inline given betweenUnwrappedWrapped[Source, Dest](using + Dest <:< AnyVal, + Dest <:< Product + ): Transformer.ToAnyVal[Source, Dest] = + Transformer.ToAnyVal.make(DerivedTransformers.toAnyVal[Source, Dest]) + + inline given betweenWrappedUnwrapped[Source, Dest](using + Source <:< AnyVal, + Source <:< Product + ): Transformer.FromAnyVal[Source, Dest] = + Transformer.FromAnyVal.make(DerivedTransformers.fromAnyVal[Source, Dest]) + + @deprecated(message = "Use 'Transformer.identity' instead", since = "0.1.5") + final def given_Identity_Source_Dest[Source, Dest >: Source]: Identity[Source, Dest] = identity[Source, Dest] + + @deprecated(message = "Use 'Transformer.betweenProducts' instead", since = "0.1.5") + inline def forProducts[Source, Dest](using Mirror.ProductOf[Source], Mirror.ProductOf[Dest]): ForProduct[Source, Dest] = + ForProduct.make(DerivedTransformers.product[Source, Dest]) + + @deprecated(message = "Use 'Transformer.betweenCoproducts' instead", since = "0.1.5") + inline def forCoproducts[Source, Dest](using Mirror.SumOf[Source], Mirror.SumOf[Dest]): ForCoproduct[Source, Dest] = + ForCoproduct.make(DerivedTransformers.coproduct[Source, Dest]) + + @deprecated(message = "Use 'Transformer.betweenNonOptionOption' instead", since = "0.1.5") + final def given_Transformer_Source_Option[Source, Dest](using Transformer[Source, Dest]): Transformer[Source, Option[Dest]] = + betweenNonOptionOption[Source, Dest] + + @deprecated(message = "Use 'Transformer.betweenOptions' instead", since = "0.1.5") + final def given_Transformer_Option_Option[Source, Dest](using + Transformer[Source, Dest] + ): Transformer[Option[Source], Option[Dest]] = + from => from.map(Transformer[Source, Dest].transform) + + @deprecated(message = "Use 'Transformer.betweenEithers' instead", since = "0.1.5") + final def given_Transformer_Either_Either[A1, A2, B1, B2](using + Transformer[A1, A2], + Transformer[B1, B2] + ): Transformer[Either[A1, B1], Either[A2, B2]] = { + case Right(value) => Right(Transformer[B1, B2].transform(value)) + case Left(value) => Left(Transformer[A1, A2].transform(value)) + } + + @deprecated(message = "Use 'Transformer.betweenCollections' instead", since = "0.1.5") + final def given_Transformer_SourceCollection_DestCollection[ + Source, + Dest, + SourceCollection[elem] <: Iterable[elem], + DestCollection[elem] <: Iterable[elem] + ](using + trans: Transformer[Source, Dest], + factory: Factory[Dest, DestCollection[Dest]] + ): Transformer[SourceCollection[Source], DestCollection[Dest]] = + betweenCollections[Source, Dest, SourceCollection, DestCollection] + + @deprecated(message = "Use 'Transformer.betweenUnwrappedWrapped' instead", since = "0.1.5") + inline def fromAnyVal[Source <: AnyVal, Dest]: FromAnyVal[Source, Dest] = + FromAnyVal.make(DerivedTransformers.fromAnyVal[Source, Dest]) + + @deprecated(message = "Use 'Transformer.betweenWrappedUnwrapped' instead", since = "0.1.5") + inline def toAnyVal[Source, Dest <: AnyVal]: ToAnyVal[Source, Dest] = + ToAnyVal.make(DerivedTransformers.toAnyVal[Source, Dest]) + final class Identity[Source, Dest >: Source] private[Transformer] extends Transformer[Source, Dest] { def transform(from: Source): Dest = from } @@ -57,7 +154,7 @@ object Transformer { } } - sealed trait FromAnyVal[Source <: AnyVal, Dest] extends Transformer[Source, Dest] + sealed trait FromAnyVal[Source, Dest] extends Transformer[Source, Dest] object FromAnyVal { @deprecated(message = "Use the variant with a Transformer instead", since = "0.1.1") @@ -66,13 +163,13 @@ object Transformer { def transform(from: Source): Dest = f(from) } - private[ducktape] def make[Source <: AnyVal, Dest](transformer: Transformer[Source, Dest]): FromAnyVal[Source, Dest] = + private[ducktape] def make[Source, Dest](transformer: Transformer[Source, Dest]): FromAnyVal[Source, Dest] = new { def transform(from: Source): Dest = transformer.transform(from) } } - sealed trait ToAnyVal[Source, Dest <: AnyVal] extends Transformer[Source, Dest] + sealed trait ToAnyVal[Source, Dest] extends Transformer[Source, Dest] object ToAnyVal { @deprecated(message = "Use the variant with a Transformer instead", since = "0.1.1") @@ -81,39 +178,9 @@ object Transformer { def transform(from: Source): Dest = f(from) } - private[ducktape] def make[Source, Dest <: AnyVal](transformer: Transformer[Source, Dest]): ToAnyVal[Source, Dest] = + private[ducktape] def make[Source, Dest](transformer: Transformer[Source, Dest]): ToAnyVal[Source, Dest] = new { def transform(from: Source): Dest = transformer.transform(from) } } - - given [Source, Dest >: Source]: Identity[Source, Dest] = Identity[Source, Dest] - - inline given forProducts[Source, Dest](using Mirror.ProductOf[Source], Mirror.ProductOf[Dest]): ForProduct[Source, Dest] = - ForProduct.make(DerivedTransformers.product[Source, Dest]) - - inline given forCoproducts[Source, Dest](using Mirror.SumOf[Source], Mirror.SumOf[Dest]): ForCoproduct[Source, Dest] = - ForCoproduct.make(DerivedTransformers.coproduct[Source, Dest]) - - given [Source, Dest](using Transformer[Source, Dest]): Transformer[Source, Option[Dest]] = - from => Transformer[Source, Dest].transform.andThen(Some.apply)(from) - - given [Source, Dest](using Transformer[Source, Dest]): Transformer[Option[Source], Option[Dest]] = - from => from.map(Transformer[Source, Dest].transform) - - given [A1, A2, B1, B2](using Transformer[A1, A2], Transformer[B1, B2]): Transformer[Either[A1, B1], Either[A2, B2]] = { - case Right(value) => Right(Transformer[B1, B2].transform(value)) - case Left(value) => Left(Transformer[A1, A2].transform(value)) - } - - given [Source, Dest, SourceCollection[elem] <: Iterable[elem], DestCollection[elem] <: Iterable[elem]](using - trans: Transformer[Source, Dest], - factory: Factory[Dest, DestCollection[Dest]] - ): Transformer[SourceCollection[Source], DestCollection[Dest]] = from => from.map(trans.transform).to(factory) - - inline given fromAnyVal[Source <: AnyVal, Dest]: FromAnyVal[Source, Dest] = - FromAnyVal.make(DerivedTransformers.fromAnyVal[Source, Dest]) - - inline given toAnyVal[Source, Dest <: AnyVal]: ToAnyVal[Source, Dest] = - ToAnyVal.make(DerivedTransformers.toAnyVal[Source, Dest]) } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/DerivedTransformers.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/DerivedTransformers.scala index 54c91c41..4e7b7a7f 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/DerivedTransformers.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/DerivedTransformers.scala @@ -30,15 +30,15 @@ private[ducktape] object DerivedTransformers { )(using Quotes): Expr[Transformer[Source, Dest]] = '{ source => ${ CoproductTransformations.transform[Source, Dest]('source, Source, Dest) } } - inline def toAnyVal[Source, Dest <: AnyVal]: Transformer[Source, Dest] = + inline def toAnyVal[Source, Dest]: Transformer[Source, Dest] = ${ deriveToAnyValTransformerMacro[Source, Dest] } - def deriveToAnyValTransformerMacro[Source: Type, Dest <: AnyVal: Type](using Quotes): Expr[Transformer[Source, Dest]] = + def deriveToAnyValTransformerMacro[Source: Type, Dest: Type](using Quotes): Expr[Transformer[Source, Dest]] = '{ source => ${ ProductTransformations.transformToAnyVal('source) } } - inline def fromAnyVal[Source <: AnyVal, Dest] = + inline def fromAnyVal[Source, Dest] = ${ deriveFromAnyValTransformerMacro[Source, Dest] } - def deriveFromAnyValTransformerMacro[Source <: AnyVal: Type, Dest: Type](using Quotes): Expr[Transformer[Source, Dest]] = + def deriveFromAnyValTransformerMacro[Source: Type, Dest: Type](using Quotes): Expr[Transformer[Source, Dest]] = '{ source => ${ ProductTransformations.transformFromAnyVal('source) } } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/LiftTransformation.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/LiftTransformation.scala index a704a2a9..672b9c03 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/LiftTransformation.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/LiftTransformation.scala @@ -36,12 +36,12 @@ private[ducktape] object LiftTransformation { appliedTo: Expr[A] )(using Quotes): Option[Expr[B]] = PartialFunction.condOpt(transformer) { - case '{ Transformer.given_Transformer_Source_Option[source, dest](using $transformer) } => + case '{ Transformer.betweenNonOptionOption[source, dest](using $transformer) } => val field = appliedTo.asExprOf[source] val lifted = liftTransformation(transformer, field) '{ Some($lifted) }.asExprOf[B] - case '{ Transformer.given_Transformer_Option_Option[source, dest](using $transformer) } => + case '{ Transformer.betweenOptions[source, dest](using $transformer) } => val field = appliedTo.asExprOf[Option[source]] '{ $field.map(src => ${ liftTransformation(transformer, 'src) }) }.asExprOf[B] @@ -50,7 +50,7 @@ private[ducktape] object LiftTransformation { // https://github.com/lampepfl/dotty/discussions/12446 // Because of that we need to do some more shenanigans to get the exact collection type we transform into case '{ - Transformer.given_Transformer_SourceCollection_DestCollection[ + Transformer.betweenCollections[ source, dest, Iterable, diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/ProductTransformations.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/ProductTransformations.scala index a3a0e6ca..6ecfe308 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/ProductTransformations.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/ProductTransformations.scala @@ -106,7 +106,7 @@ private[ducktape] object ProductTransformations { .asExprOf[Dest] } - def transformFromAnyVal[Source <: AnyVal: Type, Dest: Type]( + def transformFromAnyVal[Source: Type, Dest: Type]( sourceValue: Expr[Source] )(using Quotes): Expr[Dest] = { import quotes.reflect.* @@ -119,7 +119,7 @@ private[ducktape] object ProductTransformations { accessField(sourceValue, fieldSymbol.name).asExprOf[Dest] } - def transformToAnyVal[Source: Type, Dest <: AnyVal: Type]( + def transformToAnyVal[Source: Type, Dest: Type]( sourceValue: Expr[Source] )(using Quotes): Expr[Dest] = { import quotes.reflect.* diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue37Spec.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue37Spec.scala new file mode 100644 index 00000000..b5dc23a8 --- /dev/null +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue37Spec.scala @@ -0,0 +1,18 @@ +package io.github.arainko.ducktape.issues + +import io.github.arainko.ducktape.* + +// https://github.com/arainko/ducktape/issues/37 +class Issue37Spec extends DucktapeSuite { + final case class Rec[A](value: A, rec: Option[Rec[A]]) + + given rec[A, B](using Transformer[A, B]): Transformer[Rec[A], Rec[B]] = Transformer.define[Rec[A], Rec[B]].build() + + test("value class transformers don't interfere with primitives") { + val actual = rec[Int, Option[Int]].transform(Rec(1, Some(Rec(2, None)))) + val expected: Rec[Option[Int]] = Rec(Some(1), Some(Rec(Some(2), None))) + + assertEquals(actual, expected) + } + +} diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue41Spec.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue41Spec.scala index 9dc39cc9..189fae5d 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue41Spec.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue41Spec.scala @@ -14,7 +14,7 @@ class Issue41Spec extends DucktapeSuite { final case class Inside2(str: String) val roughAstCount = AstInstanceCounter.roughlyCount[Transformer[?, ?]](summon[Transformer[TestClass, TestClass2]]) - assert(roughAstCount == 10) + assert(clue(roughAstCount) == 10) } test("Nested transformers are optimized away when case class' companion has vals inside") { @@ -45,6 +45,6 @@ class Issue41Spec extends DucktapeSuite { val roughAstCount = AstInstanceCounter.roughlyCount[Transformer[?, ?]](summon[Transformer[TestClass, TestClass2]]) - assert(roughAstCount == 10) + assert(clue(roughAstCount) == 10) } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/macros/MakeTransformerSpec.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/macros/MakeTransformerSpec.scala index 9c318b02..e2a96668 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/macros/MakeTransformerSpec.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/macros/MakeTransformerSpec.scala @@ -11,15 +11,15 @@ import scala.quoted.* class MakeTransformerSpec extends DucktapeSuite { test("should match ForProduct.make") { - MakeTransformerChecker.check(Transformer.forProducts[ComplexPerson, PrimitivePerson]) + MakeTransformerChecker.check(Transformer.betweenProducts[ComplexPerson, PrimitivePerson]) } test("should match FromAnyVal.make") { - MakeTransformerChecker.check(Transformer.fromAnyVal[Hobby, String]) + MakeTransformerChecker.check(Transformer.betweenWrappedUnwrapped[Hobby, String]) } test("should match ToAnyVal.make") { - MakeTransformerChecker.check(Transformer.toAnyVal[String, Hobby]) + MakeTransformerChecker.check(Transformer.betweenUnwrappedWrapped[String, Hobby]) } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/macros/TransformerLambdaSpec.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/macros/TransformerLambdaSpec.scala index 50ce6fb5..11f12c34 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/macros/TransformerLambdaSpec.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/macros/TransformerLambdaSpec.scala @@ -6,15 +6,15 @@ import io.github.arainko.ducktape.model.* class TransformerLambdaSpec extends DucktapeSuite { test("should match ForProduct") { - TransformerLambdaChecker.check[Transformer.ForProduct.type](Transformer.forProducts[ComplexPerson, PrimitivePerson]) + TransformerLambdaChecker.check[Transformer.ForProduct.type](Transformer.betweenProducts[ComplexPerson, PrimitivePerson]) } test("should match FromAnyVal") { TransformerLambdaChecker - .check[Transformer.FromAnyVal.type](Transformer.fromAnyVal[Hobby, String]) + .check[Transformer.FromAnyVal.type](Transformer.betweenWrappedUnwrapped[Hobby, String]) } test("should match ToAnyVal") { - TransformerLambdaChecker.check[Transformer.ToAnyVal.type](Transformer.toAnyVal[String, Hobby]) + TransformerLambdaChecker.check[Transformer.ToAnyVal.type](Transformer.betweenUnwrappedWrapped[String, Hobby]) } }