-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FallibleTransformer derivation for coproducts #69
Changes from 4 commits
271d124
e3b77c3
96dd998
6570899
919e5fc
bceb205
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package io.github.arainko.ducktape.internal.macros | ||
|
||
import io.github.arainko.ducktape.fallible.{ FallibleTransformer, Mode } | ||
import io.github.arainko.ducktape.function.FunctionArguments | ||
import io.github.arainko.ducktape.internal.modules.MaterializedConfiguration.* | ||
import io.github.arainko.ducktape.internal.modules.* | ||
|
||
import scala.deriving.* | ||
import scala.quoted.* | ||
|
||
private[ducktape] object FallibleCoproductTransformations { | ||
|
||
def transform[F[+x]: Type, Source: Type, Dest: Type]( | ||
Source: Expr[Mirror.SumOf[Source]], | ||
Dest: Expr[Mirror.SumOf[Dest]], | ||
F: Expr[Mode[F]], | ||
sourceValue: Expr[Source] | ||
)(using Quotes): Expr[F[Dest]] = { | ||
given Cases.Source = Cases.Source.fromMirror(Source) | ||
given Cases.Dest = Cases.Dest.fromMirror(Dest) | ||
|
||
val ifBranches = coproductBranches[F, Source, Dest](sourceValue, Cases.source.value, F) | ||
ifStatement(ifBranches).asExprOf[F[Dest]] | ||
} | ||
|
||
private def coproductBranches[F[+x]: Type, Source: Type, Dest: Type]( | ||
sourceValue: Expr[Source], | ||
sourceCases: List[Case], | ||
F: Expr[Mode[F]] | ||
)(using Quotes, Cases.Dest) = { | ||
import quotes.reflect.* | ||
|
||
sourceCases.map { source => | ||
source -> Cases.dest | ||
.get(source.name) | ||
.getOrElse(Failure.emit(Failure.NoChildMapping(source.name, summon[Type[Dest]]))) | ||
}.map { (source, dest) => | ||
val cond = IsInstanceOf(sourceValue, source.tpe) | ||
|
||
(source.tpe -> dest.tpe) match { | ||
case '[src] -> '[dest] => | ||
val value = | ||
source.fallibleTransformerTo[F](dest).map { | ||
case '{ $transformer: FallibleTransformer[F, src, dest] } => | ||
'{ | ||
val castedSource = $sourceValue.asInstanceOf[src] | ||
val t = $transformer | ||
t.transform(castedSource) | ||
}.asExprOf[F[Dest]] | ||
} match { | ||
case Right(value) => value | ||
case Left(explanation) => | ||
dest.materializeSingleton.map { singleton => | ||
'{ $F.pure[dest]($singleton.asInstanceOf[dest]) } | ||
} | ||
.getOrElse( | ||
Failure.emit( | ||
Failure.CannotTransformCoproductCaseFallible(summon[Type[F]], source.tpe, dest.tpe, explanation) | ||
) | ||
) | ||
} | ||
IfBranch(cond, value) | ||
} | ||
} | ||
} | ||
|
||
private def ifStatement(using Quotes)(branches: List[IfBranch]): quotes.reflect.Term = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we use duplicates of this, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've extracted the |
||
import quotes.reflect.* | ||
|
||
branches match { | ||
case IfBranch(cond, value) :: xs => | ||
If(cond.asTerm, value.asTerm, ifStatement(xs)) | ||
case Nil => | ||
'{ throw RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation") }.asTerm | ||
} | ||
} | ||
|
||
private def IsInstanceOf(value: Expr[Any], tpe: Type[?])(using Quotes) = | ||
tpe match { | ||
case '[tpe] => '{ $value.isInstanceOf[tpe] } | ||
} | ||
|
||
private case class IfBranch(cond: Expr[Boolean], value: Expr[Any]) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package io.github.arainko.ducktape.internal.macros | ||
|
||
import io.github.arainko.ducktape.fallible.FallibleTransformer | ||
import io.github.arainko.ducktape.{ DucktapeSuite, Transformer } | ||
|
||
import scala.deriving.Mirror | ||
|
||
class AccumulatingCoproductTransformationsSuite extends DucktapeSuite { | ||
type ErrorsOrResult = [X] =>> Either[List[String], X] | ||
|
||
given Transformer.Mode.Accumulating[ErrorsOrResult] = | ||
Transformer.Mode.Accumulating.either[String, List] | ||
|
||
given deriveMandatoryOptionTransformer[A, B](using | ||
transformer: FallibleTransformer[ErrorsOrResult, A, B] | ||
): FallibleTransformer[ErrorsOrResult, Option[A], B] = { | ||
case Some(a) => transformer.transform(a) | ||
case None => Left(List("Missing required field")) | ||
} | ||
|
||
test("Derive sum of products") { | ||
sealed trait From | ||
object From { | ||
case class Product(field: Option[Int]) extends From | ||
case class Product2(field: Option[Int]) extends From | ||
} | ||
|
||
sealed trait To | ||
object To { | ||
case class Product(field: Int) extends To | ||
case class Product2(field: Int) extends To | ||
} | ||
|
||
val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To] | ||
|
||
assert(transformer.transform(From.Product(Some(1))) == Right(To.Product(1))) | ||
assert(transformer.transform(From.Product(None)) == Left(List("Missing required field"))) | ||
assert(transformer.transform(From.Product2(Some(2))) == Right(To.Product2(2))) | ||
} | ||
|
||
test("Derive sum of singletons") { | ||
sealed trait From | ||
object From { | ||
case object Singleton extends From | ||
case object Singleton2 extends From | ||
} | ||
|
||
sealed trait To | ||
object To { | ||
case object Singleton extends To | ||
case object Singleton2 extends To | ||
} | ||
|
||
val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To] | ||
|
||
assert(transformer.transform(From.Singleton) == Right(To.Singleton)) | ||
assert(transformer.transform(From.Singleton2) == Right(To.Singleton2)) | ||
} | ||
|
||
test("Derive sum of singleton and product") { | ||
sealed trait From | ||
object From { | ||
case class Product(field: Option[Int]) extends From | ||
case object Singleton extends From | ||
} | ||
|
||
sealed trait To | ||
object To { | ||
case class Product(field: Int) extends To | ||
case object Singleton extends To | ||
} | ||
|
||
val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To] | ||
|
||
assert(transformer.transform(From.Singleton) == Right(To.Singleton)) | ||
assert(transformer.transform(From.Product(Some(5))) == Right(To.Product(5))) | ||
assert(transformer.transform(From.Product(None)) == Left(List("Missing required field"))) | ||
} | ||
|
||
test("Accumulate problems") { | ||
sealed trait From | ||
object From { | ||
case class Product(field1: Option[Int], field2: Option[String]) extends From | ||
} | ||
|
||
sealed trait To | ||
object To { | ||
case class Product(field1: Int, field2: String) extends To | ||
} | ||
|
||
val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To] | ||
|
||
assert(transformer.transform(From.Product(None, None)) == Left(List("Missing required field", "Missing required field"))) | ||
assert(transformer.transform(From.Product(Some(1), None)) == Left(List("Missing required field"))) | ||
assert(transformer.transform(From.Product(None, Some("s"))) == Left(List("Missing required field"))) | ||
assert(transformer.transform(From.Product(Some(1), Some("s"))) == Right(To.Product(1, "s"))) | ||
} | ||
|
||
test("Derivation fails if the case names don't align") { | ||
sealed trait From | ||
object From { | ||
case class Product() extends From | ||
} | ||
|
||
sealed trait To | ||
object To { | ||
case class ProductWrongName() extends To | ||
} | ||
|
||
assertFailsToCompileWith { | ||
"FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]" | ||
}("No child named 'Product' found in To") | ||
} | ||
|
||
test("Derivation fails if a case can't be transformed") { | ||
sealed trait From | ||
object From { | ||
case class Product(s: String) extends From | ||
} | ||
|
||
sealed trait To | ||
object To { | ||
case class Product(y: String) extends To | ||
} | ||
|
||
assertFailsToCompileWith { | ||
"FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]" | ||
}( | ||
"Neither an instance of Transformer.Fallible[ErrorsOrResult, From.Product, To.Product] was found\nnor are 'Product' 'Product' singletons with the same name" | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's an optimization we can do here where we match on the summoned transformer and if it is a
FallibleTransformer.fallibleFromTotal
we can strip apart the total transformer withLiftTransformation.liftTransformation
which will make it so that the body of the transformer is inlined instead of creating a transformer instance at runtime. Example of how to do it is here:ducktape/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/AccumulatingProductTransformations.scala
Line 34 in 1c50c0e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I've achieved what you had suggested. I had problems getting a test case to match
FallibleTransformer.fallibleFromTotal
. But I've added an optimization to use total transformers, if available. The cost for that optimization is a second implicit search. I'm not sure if that's worth it.