Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ lazy val ducktape =
.settings(
scalacOptions ++= List("-Xcheck-macros", "-no-indent", "-old-syntax", "-Xfatal-warnings", "-deprecation"),
libraryDependencies += "org.scalameta" %% "munit" % "1.0.0-M7" % Test,
mimaPreviousArtifacts := Set("io.github.arainko" %% "ducktape" % "0.1.0")
mimaPreviousArtifacts := Set("io.github.arainko" %% "ducktape" % "0.1.0", "io.github.arainko" %% "ducktape" % "0.1.1")
)

lazy val docs =
Expand Down
20 changes: 10 additions & 10 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final case class PersonButMoreFields(firstName: String, lastName: String, age: I

val personWithMoreFields = PersonButMoreFields("John", "Doe", 30, "SOCIAL-NUM-12345")

val transformed = personWithMoreFields.transformInto[Person]
val transformed = personWithMoreFields.to[Person]

```

Expand All @@ -34,7 +34,7 @@ If these requirements are not met, a compiletime error is issued:
```scala mdoc:fail
val person = Person("Jerry", "Smith", 20)

person.transformInto[PersonButMoreFields]
person.to[PersonButMoreFields]

```

Expand All @@ -49,14 +49,14 @@ enum Size:
enum ExtraSize:
case ExtraSmall, Small, Medium, Large, ExtraLarge

val transformed = Size.Small.transformInto[ExtraSize]
val transformed = Size.Small.to[ExtraSize]
// transformed: ExtraSize = Small
```

We can't go to a coproduct that doesn't contain all of our cases (name wise):

```scala
val size = ExtraSize.Small.transformInto[Size]
val size = ExtraSize.Small.to[Size]
// error:
// No child named 'ExtraSmall' in Size
```
Expand Down Expand Up @@ -234,9 +234,9 @@ final case class WrappedString(value: String) extends AnyVal

val wrapped = WrappedString("I am a String")

val unwrapped = wrapped.transformInto[String]
val unwrapped = wrapped.to[String]

val wrappedAgain = unwrapped.transformInto[WrappedString]
val wrappedAgain = unwrapped.to[WrappedString]
```

#### 8. Defining custom `Transformers`
Expand Down Expand Up @@ -301,16 +301,16 @@ case class EvenMoreInside2(str: String, int: Int)

val person = Person(23, Some("str"), Inside("insideStr", 24, EvenMoreInside("evenMoreInsideStr", 25)), Vector.empty)
```
#### Generated code - expansion of `.transformInto`
Calling the `.transformInto` method
#### Generated code - expansion of `.to`
Calling the `.to` method
```scala mdoc:silent
person.transformInto[Person2]
person.to[Person2]
```
expands to:
```scala mdoc:passthrough
import io.github.arainko.ducktape.docs.*

Docs.printCode(person.transformInto[Person2])
Docs.printCode(person.to[Person2])
```

#### Generated code - expansion of `.into`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ private[ducktape] final class ProductTransformerMacros(using val quotes: Quotes)

private def resolveTransformation[Source: Type](sourceValue: Expr[Source], source: Field, destination: Field)(using Quotes) =
source.transformerTo(destination) match {
// even though this is taken care of in LiftTransformation.liftTransformation
// we need to do this here due to a compiler bug where multiple matches on a
// Transformer[A, B >: A] the B type get replaced with `B | dest` but only if
// you refer to a case class defined in an object by NOT its full path (it works if you refer to it as the full path)
// workaround for issue: https://github.com/arainko/ducktape/issues/26 until this gets fixed in dotty.
case '{
type a
$transformer: Transformer.Identity[`a`, `a`]
} =>
accessField(sourceValue, source.name)
case '{ $transformer: Transformer[source, dest] } =>
val field = accessField(sourceValue, source.name).asExprOf[source]
LiftTransformation.liftTransformation(transformer, field).asTerm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import scala.deriving.Mirror
extension [Source](value: Source) {
def into[Dest]: AppliedBuilder[Source, Dest] = AppliedBuilder(value)

inline def transformInto[Dest](using inline transformer: Transformer[Source, Dest]) =
${ LiftTransformation.liftTransformation('transformer, 'value) }
// TODO: Introduce in ducktape 0.2 as a replacement for `.to`, this will break binary compat
// inline def transformInto[Dest](using inline transformer: Transformer[Source, Dest]) =
// ${ LiftTransformation.liftTransformation('transformer, 'value) }

@deprecated(message = "Use '.transformInto' instead, it includes some additional optimizations", since = "0.1.2")
def to[Dest](using Transformer[Source, Dest]): Dest = Transformer[Source, Dest].transform(value)

transparent inline def intoVia[Func](inline function: Func)(using Mirror.ProductOf[Source], FunctionMirror[Func]) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class DerivedTransformerSuite extends DucktapeSuite {

val actualComplex =
List(
expectedPrimitive.transformInto[ComplexPerson],
expectedPrimitive.to[ComplexPerson],
expectedPrimitive.into[ComplexPerson].transform(),
expectedPrimitive.via(ComplexPerson.apply),
expectedPrimitive.intoVia(ComplexPerson.apply).transform(),
Expand All @@ -71,7 +71,7 @@ class DerivedTransformerSuite extends DucktapeSuite {

val actualPrimitive =
List(
expectedComplex.transformInto[PrimitivePerson],
expectedComplex.to[PrimitivePerson],
expectedComplex.into[PrimitivePerson].transform(),
expectedComplex.via(PrimitivePerson.apply),
expectedComplex.intoVia(PrimitivePerson.apply).transform(),
Expand Down Expand Up @@ -105,7 +105,7 @@ class DerivedTransformerSuite extends DucktapeSuite {

val actualComplex =
List(
primitive.transformInto[ComplexPerson],
primitive.to[ComplexPerson],
primitive.into[ComplexPerson].transform(),
primitive.via(ComplexPerson.apply),
primitive.intoVia(ComplexPerson.apply).transform(),
Expand All @@ -126,12 +126,12 @@ class DerivedTransformerSuite extends DucktapeSuite {
val expectedFromEnum2Mapping = expectedFromEnum1Mapping.map(_.swap)

Enum1.values.foreach { value =>
val actual = value.transformInto[Enum2]
val actual = value.to[Enum2]
assertEquals(expectedFromEnum1Mapping(value), actual)
}

Enum2.values.foreach { value =>
val actual = value.transformInto[Enum1]
val actual = value.to[Enum1]
assertEquals(expectedFromEnum2Mapping(value), actual)
}
}
Expand All @@ -144,7 +144,7 @@ class DerivedTransformerSuite extends DucktapeSuite {
val expected = LessFields(1, 2, 3)
val actual =
List(
more.transformInto[LessFields],
more.to[LessFields],
more.into[LessFields].transform(),
more.via(LessFields.apply),
more.intoVia(LessFields.apply).transform(),
Expand All @@ -170,7 +170,7 @@ class DerivedTransformerSuite extends DucktapeSuite {

val actual =
List(
person.transformInto[Person2],
person.to[Person2],
person.into[Person2].transform(),
person.via(Person2.apply),
person.intoVia(Person2.apply).transform(),
Expand All @@ -185,8 +185,8 @@ class DerivedTransformerSuite extends DucktapeSuite {
val wrappedString = Wrapped("asd")
val unwrapped = "asd"

assertEquals(wrappedString.transformInto[String], unwrapped)
assertEquals(unwrapped.transformInto[Wrapped[String]], wrappedString)
assertEquals(wrappedString.to[String], unwrapped)
assertEquals(unwrapped.to[Wrapped[String]], wrappedString)
}

test("products with AnyVal fields with type params roundrip to their primitives") {
Expand All @@ -198,7 +198,7 @@ class DerivedTransformerSuite extends DucktapeSuite {

val actualUnwrapped =
List(
person.transformInto[UnwrappedPerson[Long]],
person.to[UnwrappedPerson[Long]],
person.into[UnwrappedPerson[Long]].transform(),
person.via(UnwrappedPerson.apply[Long]),
person.intoVia(UnwrappedPerson.apply[Long]).transform(),
Expand All @@ -208,7 +208,7 @@ class DerivedTransformerSuite extends DucktapeSuite {

val actualPerson =
List(
unwrapped.transformInto[Person[Long]],
unwrapped.to[Person[Long]],
unwrapped.into[Person[Long]].transform(),
unwrapped.via(Person.apply[Long]),
unwrapped.intoVia(Person.apply[Long]).transform(),
Expand All @@ -220,6 +220,26 @@ class DerivedTransformerSuite extends DucktapeSuite {
actualPerson.foreach(actual => assertEquals(actual, person))
}

test("transformers are derived for products with supertypes of the original product type") {
case class ProductSuper(iterable: Iterable[CharSequence], number: Number, charSeq: CharSequence)
case class ProductSub(iterable: List[String], number: java.lang.Integer, charSeq: String)

val prodSub = ProductSub(List("test"), 1, "test")
val expected = ProductSuper(Iterable("test"), 1, "test")

val actual =
List(
prodSub.to[ProductSuper],
prodSub.into[ProductSuper].transform(),
prodSub.via(ProductSuper.apply),
prodSub.intoVia(ProductSuper.apply).transform(),
Transformer.define[ProductSub, ProductSuper].build().transform(prodSub),
Transformer.defineVia[ProductSub](ProductSuper.apply).build().transform(prodSub)
)

actual.foreach(assertEquals(_, expected))
}

test("derivation fails when going from a product with less fields to a product with more fields") {
assertFailsToCompileWith {
"""
Expand All @@ -228,7 +248,7 @@ class DerivedTransformerSuite extends DucktapeSuite {

val less = LessFields(1, 2, 3)

val derived = less.transformInto[MoreFields]
val derived = less.to[MoreFields]
"""
}("No field named 'field4' found in LessFields")
}
Expand All @@ -242,6 +262,6 @@ class DerivedTransformerSuite extends DucktapeSuite {
}

test("derivation fails when going from a sum with more cases to a sum with less cases") {
assertFailsToCompileWith("MoreCases.Case3.transformInto[LessCases]")("No child named 'Case4' found in LessCases")
assertFailsToCompileWith("MoreCases.Case3.to[LessCases]")("No child named 'Case4' found in LessCases")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.arainko.ducktape.issues

import io.github.arainko.ducktape.*

case class A(anotherCaseClass: A.AnotherCaseClass)

object A {
case class AnotherCaseClass(name: String)

// note how AnotherCaseClass is not referred to as A.AnotherCaseClass
case class B(anotherCaseClass: AnotherCaseClass)
}

// https://github.com/arainko/ducktape/issues/26
class Issue26Spec extends DucktapeSuite {
test("derive a correct transformer no matter how you refer to A.AnotherCaseClass inside of `A.B`") {
val expected = A.B(A.AnotherCaseClass("test"))

val a = A(A.AnotherCaseClass("test"))
val actual =
List(
a.to[A.B],
a.into[A.B].transform(),
a.via(A.B.apply),
a.intoVia(A.B.apply).transform(),
Transformer.define[A, A.B].build().transform(a),
Transformer.defineVia[A](A.B.apply).build().transform(a)
)

actual.foreach(actual => assertEquals(actual, expected))
}

}