From ff4fa2c52ff843dd73f82440d1013a03bd36094a Mon Sep 17 00:00:00 2001 From: Bogdan Kolbik Date: Fri, 18 Oct 2024 17:31:54 +0200 Subject: [PATCH] Add dummy ReaderDerivation --- .../teleproto/internal/ReaderDerivation.scala | 179 ++++++++++++++++++ .../teleproto/internal/WriterDerivation.scala | 12 +- .../internal/WriterDerivationPlatform.scala | 4 +- 3 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 src/main/scala/io/moia/protos/teleproto/internal/ReaderDerivation.scala diff --git a/src/main/scala/io/moia/protos/teleproto/internal/ReaderDerivation.scala b/src/main/scala/io/moia/protos/teleproto/internal/ReaderDerivation.scala new file mode 100644 index 0000000..e3cd1bd --- /dev/null +++ b/src/main/scala/io/moia/protos/teleproto/internal/ReaderDerivation.scala @@ -0,0 +1,179 @@ +package io.moia.protos.teleproto.internal + +import io.moia.protos.teleproto.{PbFailure, PbResult, PbSuccess, Reader} +import io.scalaland.chimney.internal.compiletime.DerivationEngine +import io.scalaland.chimney.partial +import scalapb.UnknownFieldSet + +trait ReaderDerivation extends DerivationEngine { + + // example of platform-independent type definition + protected val MyTypes: MyTypesModule + protected trait MyTypesModule { this: MyTypes.type => + + // Provides + // - Reader.apply[From, To]: Type[MyTypeClass[From, To]] + // - Reader.unapply(tpe: Type[Any]): Option[(??, ??)] // existential types + val Reader: ReaderModule + trait ReaderModule extends Type.Ctor2[Reader] { this: Reader.type => } + + // use in platform-independent code (it cannot generate Type instances, as opposed to Scala 2/Scala 3 macros) + object Implicits { + implicit def ReaderType[From: Type, To: Type]: Type[Reader[From, To]] = Reader[From, To] + } + } + + // example of platform-independent expr utility + protected val MyExprs: MyExprsModule + protected trait MyExprsModule { this: MyExprs.type => + + import MyTypes.Implicits._ + + def callReader[From: Type, To: Type](tc: Expr[Reader[From, To]], from: Expr[From]): Expr[partial.Result[To]] + + def createReader[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[Reader[From, To]] + + def summonReader[From: Type, To: Type]: Option[Expr[Reader[From, To]]] = + Expr.summonImplicit[Reader[From, To]] + + def matchEnumValues[From: Type, To: Type]( + src: Expr[From], + fromElements: Enum.Elements[From], + toElements: Enum.Elements[To], + mapping: Map[String, String] + ): Expr[To] + + // use in platform-independent code (since it does not have quotes nor quasiquotes) + object Implicits { + + implicit class ReaderOps[From: Type, To: Type](private val tc: Expr[Reader[From, To]]) { + + def read(from: Expr[From]): Expr[partial.Result[To]] = callReader(tc, from) + } + } + } + + import MyExprs.Implicits._ + + // example of a platform-independent Rule + object ReaderImplicitRule extends Rule("ReaderImplicit") { + + override def expand[From, To](implicit + ctx: TransformationContext[From, To] + ): DerivationResult[Rule.ExpansionResult[To]] = { + if (ctx.config.isImplicitSummoningPreventedFor[From, To]) { + // Implicit summoning prevented so + DerivationResult.attemptNextRule + } else { + MyExprs.summonReader[From, To] match { + case Some(reader) => DerivationResult.expandedPartial(reader.read(ctx.src)) + case None => DerivationResult.attemptNextRule + } + } + } + } + + class ProtobufEnumRule(ge: Type[scalapb.GeneratedEnum]) extends Rule("ProtobufEnum") { + + private def tolerantName(name: String): String = + name.toLowerCase.replace("_", "") + + override def expand[From, To](implicit + ctx: TransformationContext[From, To] + ): DerivationResult[Rule.ExpansionResult[To]] = { + + /** The protobuf and model types have to be sealed traits. Iterate through the known subclasses of the model and match the ScalaPB + * side. + * + * If there are more options on the protobuf side, the mapping is forward compatible. If there are more options on the model side, + * the mapping is not possible. + * + * {{{ + * (model: ModelEnum) => p match { + * case ModelEnum.OPTION_1 => ProtoEnum.OPTION_1 + * ... + * case ModelEnum.OPTION_N => ProtoEnum.OPTION_N + * } + * }}} + */ + def compileEnumerationMapping( + fromElements: Enum.Elements[From], + toElements: Enum.Elements[To] + ): DerivationResult[Rule.ExpansionResult[To]] = { + val protoPrefix = simpleName[To] + + val fromElementsByTolerantName = + fromElements.map(element => tolerantName(element.value.name) -> element).toMap + val toElementsByTolerantName = toElements.map(element => tolerantName(element.value.name) -> element).toMap ++ + toElements.map(element => tolerantName(element.value.name).stripPrefix(tolerantName(protoPrefix)) -> element).toMap + + // Does not retrieve local names to compare (yet) + val unmatchedModelOptions = fromElementsByTolerantName.collect { + case (elementName, element) if !toElementsByTolerantName.contains(elementName) => element + } + + if (unmatchedModelOptions.nonEmpty) { + return DerivationResult.attemptNextRuleBecause( + s"Found unmatched subtypes: ${unmatchedModelOptions.map(tpe => Type.prettyPrint(tpe.Underlying)).mkString(", ")}" + ) + } + + val mapping = (for { + (modelName, modelElement) <- fromElementsByTolerantName.toList + protoElement <- toElementsByTolerantName.get(modelName) + } yield modelElement.value.name -> protoElement.value.name).toMap + + val result = MyExprs.matchEnumValues(ctx.src, fromElements, toElements, mapping) + DerivationResult.expandedTotal(result) + } + + (Type[From], Type[To]) match { + case (SealedHierarchy(Enum(fromElements)), SealedHierarchy(Enum(toElements))) if Type[To] <:< ge => + compileEnumerationMapping(fromElements, toElements) + case _ => DerivationResult.attemptNextRule + } + } + + private def simpleName[A: Type]: String = { + val colored = Type.prettyPrint[A] + val mono = "\u001b\\[([0-9]+)m".r.replaceAllIn(colored, "") + val start = mono.lastIndexOf(".") + 1 + val end = mono.indexOf("[", start) - 1 + mono.substring(start.max(0), if (end < 0) mono.length else end) + } + } + + // TODO: use? + protected def fromPbResult[T](result: PbResult[T]): partial.Result[T] = { + result match { + case PbSuccess(value) => partial.Result.Value(value) + case PbFailure(errors) => { + def toError(pbError: (String, String)) = partial.Error( + partial.ErrorMessage.StringMessage(pbError._2), + partial.Path.Empty.prepend(partial.PathElement.Accessor(pbError._1)) + ) + + errors.toList match { + case head :: tail => partial.Result.Errors(toError(head), tail.map(toError): _*) + case Nil => partial.Result.Errors(partial.Error(partial.ErrorMessage.StringMessage("Unknown error"))) + } + } + } + } + + def readerDerivation[From: Type, To: Type](implicit + ufst: Type[UnknownFieldSet], + ge: Type[scalapb.GeneratedEnum] + ): Expr[Reader[From, To]] = + MyExprs.createReader[From, To] { (from: Expr[From]) => + val cfg = TransformerConfiguration( + flags = TransformerFlags() + ) // customize, read config with DSL etc + val context = TransformationContext.ForTotal.create[From, To](from, cfg) + + deriveFinalTransformationResultExpr(context).toEither.fold( + derivationErrors => reportError(derivationErrors.toString), // customize + identity + ) + } +} diff --git a/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivation.scala b/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivation.scala index 1c4b97c..bd8b708 100644 --- a/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivation.scala +++ b/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivation.scala @@ -28,11 +28,11 @@ trait WriterDerivation extends DerivationEngine { import MyTypes.Implicits._ - def callMyTypeClass[From: Type, To: Type](tc: Expr[Writer[From, To]], from: Expr[From]): Expr[To] + def callWriter[From: Type, To: Type](tc: Expr[Writer[From, To]], from: Expr[From]): Expr[To] - def createTypeClass[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[Writer[From, To]] + def createWriter[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[Writer[From, To]] - def summonMyTypeClass[From: Type, To: Type]: Option[Expr[Writer[From, To]]] = + def summonWriter[From: Type, To: Type]: Option[Expr[Writer[From, To]]] = Expr.summonImplicit[Writer[From, To]] def matchEnumValues[From: Type, To: Type]( @@ -47,7 +47,7 @@ trait WriterDerivation extends DerivationEngine { implicit class WriterOps[From: Type, To: Type](private val tc: Expr[Writer[From, To]]) { - def write(from: Expr[From]): Expr[To] = callMyTypeClass(tc, from) + def write(from: Expr[From]): Expr[To] = callWriter(tc, from) } } } @@ -64,7 +64,7 @@ trait WriterDerivation extends DerivationEngine { // Implicit summoning prevented so DerivationResult.attemptNextRule } else { - MyExprs.summonMyTypeClass[From, To] match { + MyExprs.summonWriter[From, To] match { case Some(writer) => DerivationResult.expandedTotal(writer.write(ctx.src)) case None => DerivationResult.attemptNextRule } @@ -148,7 +148,7 @@ trait WriterDerivation extends DerivationEngine { ufst: Type[UnknownFieldSet], ge: Type[scalapb.GeneratedEnum] ): Expr[Writer[From, To]] = - MyExprs.createTypeClass[From, To] { (from: Expr[From]) => + MyExprs.createWriter[From, To] { (from: Expr[From]) => val cfg = TransformerConfiguration( flags = TransformerFlags() .setDefaultValueOfType[UnknownFieldSet](true) diff --git a/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivationPlatform.scala b/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivationPlatform.scala index 3fe62b0..4a02cdb 100644 --- a/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivationPlatform.scala +++ b/src/main/scala/io/moia/protos/teleproto/internal/WriterDerivationPlatform.scala @@ -23,10 +23,10 @@ trait WriterDerivationPlatform extends DerivationEnginePlatform with WriterDeriv protected object MyExprs extends MyExprsModule { - def callMyTypeClass[From: Type, To: Type](tc: Expr[Writer[From, To]], from: Expr[From]): Expr[To] = + def callWriter[From: Type, To: Type](tc: Expr[Writer[From, To]], from: Expr[From]): Expr[To] = c.Expr[To](q"""$tc.write($from)""") - def createTypeClass[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[Writer[From, To]] = { + def createWriter[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[Writer[From, To]] = { val name = freshTermName("from") // remember to use full qualified names in Scala 2 macros!!! c.Expr[Writer[From, To]](