From 333f10562698042317e734d25392d12182113c6c Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sat, 28 Sep 2024 23:00:53 -0700 Subject: [PATCH 01/12] feat: add PathList to support vararg macros --- .../compiletime/ChimneyTypesPlatform.scala | 13 +++++++ .../compiletime/dsl/utils/DslMacroUtils.scala | 39 +++++++++++++++++++ .../internal/compiletime/ChimneyTypes.scala | 17 ++++++++ .../transformer/Configurations.scala | 7 ++++ .../chimney/internal/runtime/PathList.scala | 7 ++++ 5 files changed, 83 insertions(+) create mode 100644 chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala index 4cc5d154c..8409a9e00 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala @@ -405,6 +405,19 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi } } + object PathList extends PathListModule { + val Empty: Type[runtime.PathList.Empty] = quoted.Type.of[runtime.PathList.Empty] + object List extends ListModule { + def apply[Head <: runtime.Path: Type, Tail <: runtime.PathList: Type]: Type[runtime.PathList.List[Head, Tail]] = + quoted.Type.of[runtime.PathList.List[Head, Tail]] + def unapply[A](tpe: Type[A]): Option[(?<[runtime.Path], ?<[runtime.PathList])] = tpe match { + case '[runtime.PathList.List[head, tail]] => + Some((Type[head].as_?<[runtime.Path], Type[tail].as_?<[runtime.PathList])) + case _ => scala.None + } + } + } + object DefaultValue extends DefaultValueModule { def apply[Value: Type]: Type[integrations.DefaultValue[Value]] = quoted.Type.of[integrations.DefaultValue[Value]] diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala index 48b421a0c..68a28dce0 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala @@ -1,6 +1,7 @@ package io.scalaland.chimney.internal.compiletime.dsl.utils import io.scalaland.chimney.internal.runtime +import io.scalaland.chimney.internal.runtime.PathList import scala.annotation.nowarn import scala.quoted.* @@ -284,6 +285,44 @@ private[chimney] class DslMacroUtils()(using quotes: Quotes) { s"Expected function, instead got: ${t.show(using Printer.TreeAnsiCode)}: ${t.asInstanceOf[Term].tpe.show(using Printer.TypeReprAnsiCode)}" } + private trait ExistentialPathList { + type Underlying <: runtime.PathList + implicit val Underlying: Type[Underlying] + } + private object ExistentialPathList { + def parse(t: Expr[Seq[?]]): Either[String, ExistentialPathList] = + (t match { + case Varargs(selectors: Seq[Expr[Any]]) => + selectors + .map(selector => ExistentialPath.parse(selector.asTerm)) + .foldLeft[Either[String, List[ExistentialPath]]](Right(Nil)) { + case (err @ Left(_), _) => err + case (Right(acc), Left(error)) => Left(error) + case (Right(acc), Right(path)) => Right(acc :+ path) + } + }).map { params => + val value1: Type[PathList] = combine(params) + + new ExistentialPathList { + type Underlying = runtime.PathList + implicit val Underlying: Type[Underlying] = value1 + } + } + + private def combine(paths: Seq[ExistentialPath]): Type[runtime.PathList] = { + object Combine { + def apply[A <: runtime.Path: Type, Args <: runtime.PathList: Type]: Type[runtime.PathList.List[A, Args]] = + Type.of[runtime.PathList.List[A, Args]] + } + + paths + .foldLeft[Type[? <: runtime.PathList]](Type.of[runtime.PathList.Empty]) { (acc, path) => + Combine(path.Underlying, acc) + } + .asInstanceOf[Type[runtime.PathList]] + } + } + def applyFieldNameType[Out](f: [A <: runtime.Path] => Type[A] ?=> Out)(selector: Expr[?]): Out = ExistentialPath .parse(selector.asTerm) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala index f799f2c59..b34c7b1b2 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala @@ -289,6 +289,23 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => ] { this: EveryMapValue.type => } } + val PathList: PathListModule + trait PathListModule { + this: PathList.type => + val Empty: Type[runtime.PathList.Empty] + + val List: ListModule + + trait ListModule + extends Type.Ctor2UpperBounded[ + runtime.Path, + runtime.PathList, + runtime.PathList.List + ] { + this: List.type => + } + } + val DefaultValue: DefaultValueModule trait DefaultValueModule extends Type.Ctor1[integrations.DefaultValue] { this: DefaultValue.type => } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala index f90e36a32..67102f12e 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala @@ -574,6 +574,13 @@ private[compiletime] trait Configurations { this: Derivation => // $COVERAGE-ON$ } + private def extractPathList[PathType <: runtime.PathList: Type]: List[Path] = Type[PathType] match { + case empty if empty =:= ChimneyType.PathList.Empty => List.empty + case ChimneyType.PathList.List(head, tail) => + import head.Underlying as Head, tail.Underlying as Tail + extractPath[Head] :: extractPathList[Tail] + } + private def extractPath[PathType <: runtime.Path: Type]: Path = Type[PathType] match { case root if root =:= ChimneyType.Path.Root => Path.Root diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala new file mode 100644 index 000000000..04bccc3ba --- /dev/null +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala @@ -0,0 +1,7 @@ +package io.scalaland.chimney.internal.runtime + +sealed abstract class PathList +object PathList { + final class Empty extends PathList + final class List[Head <: Path, Tail <: PathList] extends PathList +} From 848908e54168530c7b70dc04729918b46fd5f4b5 Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sun, 29 Sep 2024 08:40:54 -0700 Subject: [PATCH 02/12] feat: add requireSourceFieldsExcept to DSL --- .../chimney/dsl/TransformerInto.scala | 5 +++++ .../compiletime/ChimneyTypesPlatform.scala | 19 +++++++++++++++++++ .../dsl/TransformerIntoMacros.scala | 19 +++++++++++++++++++ .../compiletime/dsl/utils/DslMacroUtils.scala | 8 ++++++++ .../internal/compiletime/ChimneyTypes.scala | 8 ++++++++ .../derivation/transformer/ResultOps.scala | 11 ++++++++++- .../runtime/TransformerOverrides.scala | 2 ++ 7 files changed, 71 insertions(+), 1 deletion(-) diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala index b0e1bbd50..57a11390f 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala @@ -211,6 +211,11 @@ final class TransformerInto[From, To, Overrides <: TransformerOverrides, Flags < )(using IsFunction.Of[Ctor, To]): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = ${ TransformerIntoMacros.withConstructorImpl('this, 'f) } + transparent inline def requireSourceFieldsUsedExcept( + inline selectorFrom: From => Any* + ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = + ${ TransformerIntoMacros.requireSourceFieldsUsedExceptImpl('this, 'selectorFrom) } + /** Apply configured transformation in-place. * * It runs macro that tries to derive instance of `Transformer[From, To]` and immediately apply it to captured diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala index 8409a9e00..103cdb649 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala @@ -219,6 +219,25 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi case _ => scala.None } } + + object RequireSourceFieldsExcept extends RequireSourceFieldsExceptModule { + def apply[ + FromPathList <: runtime.PathList: Type, + Tail <: runtime.TransformerOverrides: Type + ]: Type[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] = + quoted.Type.of[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] + def unapply[A](tpe: Type[A]): Option[(?<[runtime.PathList], ?<[runtime.TransformerOverrides])] = + tpe match { + case '[runtime.TransformerOverrides.RequireSourceFieldsExcept[fromPath, cfg]] => + Some( + ( + Type[fromPath].as_?<[runtime.PathList], + Type[cfg].as_?<[runtime.TransformerOverrides] + ) + ) + case _ => scala.None + } + } } object TransformerFlags extends TransformerFlagsModule { diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala index 6f46e8eb7..d5400a8c7 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala @@ -8,6 +8,7 @@ import io.scalaland.chimney.internal.compiletime.dsl.utils.DslMacroUtils import io.scalaland.chimney.internal.runtime.{ ArgumentLists, Path, + PathList, TransformerFlags, TransformerOverrides, WithRuntimeDataStore @@ -143,4 +144,22 @@ object TransformerIntoMacros { .asInstanceOf[TransformerInto[From, To, Constructor[args, Path.Root, Overrides], Flags]] } }(f) + + def requireSourceFieldsUsedExceptImpl[ + From: Type, + To: Type, + Overrides <: TransformerOverrides: Type, + Flags <: TransformerFlags: Type, + Ctor: Type + ]( + ti: Expr[TransformerInto[From, To, Overrides, Flags]], + selectorFrom: Expr[Seq[From => Any]] + )(using Quotes): Expr[TransformerInto[From, To, ? <: TransformerOverrides, Flags]] = + DslMacroUtils().applyRequireSourceFieldsExceptType { + [args <: PathList] => + (_: Type[args]) ?=> + '{ + $ti.asInstanceOf[TransformerInto[From, To, RequireSourceFieldsExcept[args, Overrides], Flags]] + } + }(selectorFrom) } diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala index 68a28dce0..b44aaf316 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala @@ -346,4 +346,12 @@ private[chimney] class DslMacroUtils()(using quotes: Quotes) { case Right(ctorType) => f(using ctorType.Underlying) case Left(error) => report.errorAndAbort(error, Position.ofMacroExpansion) } + + def applyRequireSourceFieldsExceptType[Out]( + f: [A <: runtime.PathList] => Type[A] ?=> Out + )(fieldSelectors: Expr[Seq[?]]): Out = + ExistentialPathList.parse(fieldSelectors) match { + case Right(pathList) => f(using pathList.Underlying) + case Left(error) => report.errorAndAbort(error, Position.ofMacroExpansion) + } } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala index b34c7b1b2..bd55cc06b 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala @@ -151,6 +151,14 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => runtime.TransformerOverrides, runtime.TransformerOverrides.RenamedTo ] { this: RenamedTo.type => } + + val RequireSourceFieldsExcept: RequireSourceFieldsExceptModule + trait RequireSourceFieldsExceptModule + extends Type.Ctor2UpperBounded[ + runtime.PathList, + runtime.TransformerOverrides, + runtime.TransformerOverrides.RequireSourceFieldsExcept + ] { this: RequireSourceFieldsExcept.type => } } val TransformerFlags: TransformerFlagsModule diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala index 8fb4ef269..0ca074f68 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala @@ -11,7 +11,8 @@ import io.scalaland.chimney.internal.compiletime.{ MissingJavaBeanSetterParam, MissingSubtypeTransformer, NotSupportedTransformerDerivation, - TupleArityMismatch + TupleArityMismatch, + UnusedButRequiredToUseSourceFields } import io.scalaland.chimney.{partial, PartialTransformer, Transformer} @@ -167,6 +168,14 @@ private[compiletime] trait ResultOps { this: Derivation => )(fromType = Type.prettyPrint[From], toType = Type.prettyPrint[To]) ) + def requiredFieldNotUsed[From, To, A](unusedRequiredFields: Set[String])(implicit + ctx: TransformationContext[From, To] + ): DerivationResult[A] = DerivationResult.transformerError( + UnusedButRequiredToUseSourceFields( + unused = unusedRequiredFields + )(fromType = Type.prettyPrint[From], toType = Type.prettyPrint[To]) + ) + def summonImplicit[A: Type]: DerivationResult[Expr[A]] = DerivationResult(Expr.summonImplicitUnsafe[A]) } } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala index 2ca37e2ad..7e3105b13 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala @@ -21,4 +21,6 @@ object TransformerOverrides { final class RenamedFrom[FromPath <: Path, ToPath <: Path, Tail <: Overrides] extends Overrides // Computes a value from matched subtype, targeting another subtype final class RenamedTo[FromPath <: Path, ToPath <: Path, Tail <: Overrides] extends Overrides + // Throws a compile error if not all fields of the source are not used + final class RequireSourceFieldsExcept[FromPaths <: PathList, Tail <: Overrides] extends Overrides } From 9c2d8f2950d28b27952b2062e792775f1b1f8f32 Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sun, 29 Sep 2024 10:44:45 -0700 Subject: [PATCH 03/12] feat: add validations to Configurations to accommodate settings that are validation in nature, the first one being RequireAllSourceFieldsUsed(except) --- .../transformer/Configurations.scala | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala index 67102f12e..7cd362a8b 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala @@ -226,6 +226,13 @@ private[compiletime] trait Configurations { this: Derivation => } } + sealed trait Verification extends scala.Product with Serializable + object Verification { + final case class RequireAllSourceFieldsUsedExcept(sourceFields: Set[String]) extends Verification { + override def toString: String = s"RequireAllSourceFieldsUsedExcept(sourceFields=${sourceFields.mkString(", ")})" + } + } + sealed protected trait TransformerOverride extends scala.Product with Serializable protected object TransformerOverride { sealed trait ForField extends TransformerOverride @@ -278,7 +285,9 @@ private[compiletime] trait Configurations { this: Derivation => /** Stores all customizations provided by user */ private val runtimeOverrides: Vector[(Path, TransformerOverride)] = Vector.empty, /** Let us prevent `implicit val foo = foo` but allow `implicit val foo = new Foo { def sth = foo }` */ - private val preventImplicitSummoningForTypes: Option[(??, ??)] = None + private val preventImplicitSummoningForTypes: Option[(??, ??)] = None, + /** Stores all verification settings provided by user */ + verifications: Vector[Verification] = Vector.empty ) { private lazy val runtimeOverridesForCurrent = runtimeOverrides.filter { @@ -304,6 +313,8 @@ private[compiletime] trait Configurations { this: Derivation => def areLocalFlagsEmpty: Boolean = !localFlagsOverridden + def addVerification(verification: Verification): TransformerConfiguration = + copy(verifications = verifications :+ verification) def addTransformerOverride(path: Path, runtimeOverride: TransformerOverride): TransformerConfiguration = copy(runtimeOverrides = runtimeOverrides :+ (path -> runtimeOverride)) def areOverridesEmpty: Boolean = @@ -568,6 +579,15 @@ private[compiletime] trait Configurations { this: Derivation => extractPath[FromPath], TransformerOverride.RenamedTo(extractPath[ToPath]) ) + case ChimneyType.TransformerOverrides.RequireSourceFieldsExcept(fromPathList, cfg) => + import fromPathList.Underlying as FromPathList, cfg.Underlying as Tail2 + val fields = extractPathList[FromPathList].map { + case Path.AtField(fromName, _) => fromName + case path => reportError(s"$path is not a field selector!") + }.toSet + + extractTransformerConfig[Tail2](runtimeDataIdx, runtimeDataStore) + .addVerification(Verification.RequireAllSourceFieldsUsedExcept(fields)) // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation case _ => reportError(s"Invalid internal TransformerOverrides type shape: ${Type.prettyPrint[Tail]}!!") From 123b923ab83434c36deb5b5222d7afa3597e7319 Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sun, 29 Sep 2024 10:46:00 -0700 Subject: [PATCH 04/12] feat: add usedSourceFields to DerivationResult state to be able to track source field usage across the processing workflow without polluting the derivation code --- .../internal/compiletime/DerivationResult.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala index 6e620534f..11a74caa5 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala @@ -107,6 +107,16 @@ sealed private[compiletime] trait DerivationResult[+A] { } } + // tracking + + final def registerSourceFieldUseOnSuccess(field: String): DerivationResult[A] = this match { + case _: Success[?] => + updateState(_.appendToUsedSourceFields(field)).logSuccess(_ => s"source field $field usage registered") + case _: Failure => this + } + + final def withUsedSourceFields: DerivationResult[(Set[String], A)] = map(a => (state.usedSourceFields, a)) + // logging final def log(msg: => String): DerivationResult[A] = updateState(_.log(msg)) @@ -136,16 +146,21 @@ private[compiletime] object DerivationResult { final case class State( journal: Log.Journal = Log.Journal(logs = Vector.empty), + usedSourceFields: Set[String] = Set.empty, macroLogging: Option[State.MacroLogging] = None ) { private[DerivationResult] def log(msg: => String): State = copy(journal = journal.append(msg)) + private[DerivationResult] def appendToUsedSourceFields(field: String): State = + copy(usedSourceFields = usedSourceFields + field) + private[DerivationResult] def nestScope(scopeName: String): State = copy(journal = Log.Journal(Vector(Log.Scope(scopeName = scopeName, journal = journal)))) private[DerivationResult] def appendedTo(previousState: State): State = State( journal = Log.Journal(logs = previousState.journal.logs ++ this.journal.logs), + usedSourceFields = previousState.usedSourceFields ++ usedSourceFields, macroLogging = previousState.macroLogging.orElse(macroLogging) ) } From 7be2c7ac191b374d8be651b77afb2a7ced5b135b Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sun, 29 Sep 2024 10:46:52 -0700 Subject: [PATCH 05/12] impl: RequireAllSourceFieldsUsedExcept validation in product-to-product module --- .../TransformerDerivationError.scala | 7 ++++++ .../TransformProductToProductRuleModule.scala | 24 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala index 67001cabe..c0ad335ea 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala @@ -76,6 +76,11 @@ final case class NotSupportedTransformerDerivation( )(val fromType: String, val toType: String) extends TransformerDerivationError +final case class UnusedButRequiredToUseSourceFields( + unused: Set[String] +)(val fromType: String, val toType: String) + extends TransformerDerivationError + object TransformerDerivationError { def printErrors(errors: Seq[TransformerDerivationError]): String = errors @@ -108,6 +113,8 @@ object TransformerDerivationError { | Please eliminate total/partial ambiguity from implicit scope or use ${MAGENTA}enableImplicitConflictResolution$RESET/${MAGENTA}withFieldComputed$RESET/${MAGENTA}withFieldComputedPartial$RESET to decide which one should be used.""".stripMargin case NotSupportedTransformerDerivation(exprPrettyPrint) => s" derivation from $exprPrettyPrint: $fromType to $toType is not supported in Chimney!" + case UnusedButRequiredToUseSourceFields(unusedFields) => + s" field(s) $MAGENTA${unusedFields.mkString(", ")}$RESET of $MAGENTA${fromType}$RESET are required to be used in the transformation but are not used!" } def prettyFieldList(fields: Seq[String])(use: String => String): String = diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala index 4ebf800a3..f9465b296 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala @@ -196,6 +196,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio case Right(possibleSourceField) => possibleSourceField.map { case (fromName, toName, getter) => useExtractor[From, To, CtorParam](ctorParam.value.targetType, fromName, toName, getter) + .registerSourceFieldUseOnSuccess(fromName) } case Left(foundFromNames) => Some( @@ -260,6 +261,25 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio .map(toName -> _) } .map(_.filterNot(_._2 == unmatchedSetter).filterNot(_._2 == nonUnitSetter)) + .withUsedSourceFields + .logSuccess { case (usedSourceFields, _) => + (for (verification <- verifications) yield verification match { + case Verification.RequireAllSourceFieldsUsedExcept(exceptFields) => + s"validate all source fields used except ${exceptFields.mkString(",")} (all=${fromEnabledExtractors.keySet.mkString(",")}, used=${usedSourceFields.mkString(",")})" + }).mkString("\n") + } + .flatMap { case (usedSourceFields, res) => + verifications + .flatMap { case Verification.RequireAllSourceFieldsUsedExcept(exceptFields) => + Option(fromEnabledExtractors.keySet -- exceptFields -- usedSourceFields) // unused but required fields + .filter(_.nonEmpty) + .map( + DerivationResult.requiredFieldNotUsed[From, To, List[(String, Existential[TransformationExpr])]](_) + ) + } + .headOption + .getOrElse(DerivationResult.pure(res)) + } .logSuccess { args => val totals = args.count(_._2.value.isTotal) val partials = args.count(_._2.value.isPartial) @@ -366,9 +386,9 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio s"""|Assumed that field $sourceName is a part of ${Type.prettyPrint[Source]}, but wasn't found |available methods: ${getters.keys.map(n => s"`$n`").mkString(", ")}""".stripMargin ) - case (_, getter) :: Nil => + case (name, getter) :: Nil => import getter.Underlying as Getter, getter.value.get - DerivationResult.pure(get(extractedSrcExpr).as_??) + DerivationResult.pure(get(extractedSrcExpr).as_??).registerSourceFieldUseOnSuccess(name) case matchingGetters => DerivationResult.ambiguousFieldOverrides[From, To, ExistentialExpr]( sourceName, From 4720b2d697a30f56330d2ad712bb3858eea5fd07 Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sun, 29 Sep 2024 10:47:02 -0700 Subject: [PATCH 06/12] test: RequireAllSourceFieldsUsedExcept validation in product-to-product module --- .../chimney/TotalTransformerProductSpec.scala | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala index a5b807c24..728517960 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala @@ -519,6 +519,93 @@ class TotalTransformerProductSpec extends ChimneySpec { } } + group("""setting .requireSourceFieldsUsedExcept(_.field1, _.field2)""") { + import shapes1.{Point, Rectangle, Triangle} + + test("should fail if not all required source fields are used") { + compileErrors( + """ + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[Rectangle] + .requireSourceFieldsUsedExcept() + .transform + """ + ).check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.shapes1.Triangle to io.scalaland.chimney.fixtures.shapes1.Rectangle", + "field(s) p3 of io.scalaland.chimney.fixtures.shapes1.Triangle are required to be used in the transformation but are not used!" + ) + } + + test("should pass if all required source fields are either used or included in exceptions") { + Triangle(p1 = Point(0, 0), p2 = shapes1.Point(2, 2), p3 = shapes1.Point(2, 0)) + .into[Rectangle] + .requireSourceFieldsUsedExcept(_.p3) + .transform ==> Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) + } + + test("should pass if all required source fields are used (withFieldRenamed)") { + case class AnotherRectangle(p1: Point, PPPP: Point) + + Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) + .into[AnotherRectangle] + .withFieldRenamed(_.p2, _.PPPP) + .requireSourceFieldsUsedExcept() + .transform ==> AnotherRectangle(p1 = Point(0, 0), PPPP = Point(2, 2)) + } + + test("should pass if all required source fields are used (enableCustomFieldNameComparison)") { + case class AnotherRectangle(p1: Point, P2: Point) + + Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) + .into[AnotherRectangle] + .requireSourceFieldsUsedExcept() + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> AnotherRectangle(p1 = Point(0, 0), P2 = Point(2, 2)) + } + + test("should pass if all required source fields are used (withFieldRenamed + enableCustomFieldNameComparison)") { + case class AnotherTriangle(p1: Point, P2: Point, PPPP: Point) + + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[AnotherTriangle] + .withFieldRenamed(_.p3, _.PPPP) + .requireSourceFieldsUsedExcept() + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> AnotherTriangle(p1 = Point(0, 0), P2 = Point(2, 2), PPPP = Point(2, 0)) + } + + test( + "should pass if all required source fields are either used or included in exceptions (withFieldRenamed + enableCustomFieldNameComparison)" + ) { + case class AnotherTriangle(p11111: Point, P2: Point, PPPP: Point) + + // without p1 in exceptions + compileErrors( + """ + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[AnotherTriangle] + .withFieldRenamed(_.p3, _.PPPP) + .withFieldConst(_.p11111, Point(0, 0)) + .requireSourceFieldsUsedExcept() + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> AnotherTriangle(p11111 = Point(0, 0), P2 = Point(2, 2), PPPP = Point(2, 0)) + """ + ).check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.shapes1.Triangle to io.scalaland.chimney.TotalTransformerProductSpec.AnotherTriangle", + "field(s) p1 of io.scalaland.chimney.fixtures.shapes1.Triangle are required to be used in the transformation but are not used!" + ) + + // with p1 in exceptions + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[AnotherTriangle] + .withFieldRenamed(_.p3, _.PPPP) + .withFieldConst(_.p11111, Point(0, 0)) + .requireSourceFieldsUsedExcept(_.p1) + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> AnotherTriangle(p11111 = Point(0, 0), P2 = Point(2, 2), PPPP = Point(2, 0)) + } + } + group("flag .enableDefaultValues") { test("should be disabled by default") { From 612e39676c2098fb1d8c3b84656bd52456ede4dd Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sun, 29 Sep 2024 15:31:39 -0700 Subject: [PATCH 07/12] impl: RequireAllSourceFieldsUsedExcept for scala2 --- .../chimney/dsl/TransformerInto.scala | 12 ++ .../compiletime/ChimneyTypesPlatform.scala | 22 ++++ .../dsl/TransformerIntoMacros.scala | 15 ++- .../compiletime/dsl/utils/DslMacroUtils.scala | 103 ++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala b/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala index 1e8be563f..d9e5352f0 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala @@ -205,6 +205,18 @@ final class TransformerInto[From, To, Overrides <: TransformerOverrides, Flags < ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = macro TransformerIntoMacros.withConstructorImpl[From, To, Overrides, Flags] + /** Require that all fields of the source object except fields mentioned in `selectorFrom` are used in the + * transformation. and fail compilation otherwise. + * + * @param selectorFrom + * exception fields that are not required to be used in the transformation + * @return + */ + def requireSourceFieldsUsedExcept[T, U]( + selectorFrom: From => Any* + ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = + macro TransformerIntoMacros.requireSourceFieldsUsedExceptImpl[From, To, Overrides, Flags] + /** Apply configured transformation in-place. * * It runs macro that tries to derive instance of `Transformer[From, To]` and immediately apply it to captured diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala index 241cfa616..64e4cd268 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala @@ -249,6 +249,17 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi ) } } + object RequireSourceFieldsExcept extends RequireSourceFieldsExceptModule { + def apply[ + FromPathList <: runtime.PathList: Type, + Tail <: runtime.TransformerOverrides: Type + ]: Type[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] = + weakTypeTag[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] + def unapply[A](A: Type[A]): Option[(?<[runtime.PathList], ?<[runtime.TransformerOverrides])] = + A.asCtor[runtime.TransformerOverrides.RequireSourceFieldsExcept[?, ?]].map { A0 => + (A0.param_<[runtime.PathList](0), A0.param_<[runtime.TransformerOverrides](1)) + } + } } object TransformerFlags extends TransformerFlagsModule { @@ -406,6 +417,17 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi } } + object PathList extends PathListModule { + val Empty: Type[runtime.PathList.Empty] = weakTypeTag[runtime.PathList.Empty] + object List extends ListModule { + def apply[Head <: runtime.Path: Type, Tail <: runtime.PathList: Type]: Type[runtime.PathList.List[Head, Tail]] = + weakTypeTag[runtime.PathList.List[Head, Tail]] + def unapply[A](A: Type[A]): Option[(?<[runtime.Path], ?<[runtime.PathList])] = + A.asCtor[runtime.PathList.List[?, ?]] + .map(A0 => A0.param_<[runtime.Path](0) -> A0.param_<[runtime.PathList](1)) + } + } + object DefaultValue extends DefaultValueModule { def apply[Value: Type]: Type[integrations.DefaultValue[Value]] = weakTypeTag[integrations.DefaultValue[Value]] diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala index f4b399ee1..7fd656f32 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala @@ -1,7 +1,7 @@ package io.scalaland.chimney.internal.compiletime.dsl import io.scalaland.chimney.dsl.TransformerInto -import io.scalaland.chimney.internal.runtime.{ArgumentLists, Path, TransformerFlags, TransformerOverrides} +import io.scalaland.chimney.internal.runtime.{ArgumentLists, Path, PathList, TransformerFlags, TransformerOverrides} import io.scalaland.chimney.internal.runtime.TransformerOverrides.* import scala.annotation.unused @@ -96,4 +96,17 @@ class TransformerIntoMacros(val c: whitebox.Context) extends utils.DslMacroUtils .addOverride(f) .asInstanceOfExpr[TransformerInto[From, To, Constructor[Args, Path.Root, Overrides], Flags]] }.applyFromBody(f) + + def requireSourceFieldsUsedExceptImpl[ + From: WeakTypeTag, + To: WeakTypeTag, + Overrides <: TransformerOverrides: WeakTypeTag, + Flags <: TransformerFlags: WeakTypeTag + ](selectorFrom: Tree*): Tree = c.prefix.tree + .asInstanceOfExpr( + new ApplyFieldNamesType { + def apply[FromPathList <: PathList: WeakTypeTag]: c.WeakTypeTag[?] = + weakTypeTag[TransformerInto[From, To, RequireSourceFieldsExcept[FromPathList, Overrides], Flags]] + }.applyFromSelector(selectorFrom) + ) } diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala index 46bfd8f1d..5dbe938b8 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala @@ -266,6 +266,99 @@ private[chimney] trait DslMacroUtils { s"Expected function, instead got: $MAGENTA$t$RESET: $MAGENTA${t.tpe}$RESET" } + private trait ExistentialPathList { + type Underlying <: runtime.PathList + val Underlying: c.WeakTypeTag[Underlying] + } + private object ExistentialPathList { + def parse(selectors: Seq[Tree]): Either[String, ExistentialPathList] = + selectors + .map(selector => ExistentialPath.parse(selector)) + .foldLeft[Either[String, List[ExistentialPath]]](Right(Nil)) { + case (err @ Left(_), _) => err + case (_, Left(error)) => Left(error) + case (Right(acc), Right(path)) => Right(acc :+ path) + } + .map { params => + new ExistentialPathList { + type Underlying = runtime.PathList + implicit val Underlying: WeakTypeTag[Underlying] = combine(params) + } + } + + private def combine(paths: Seq[ExistentialPath]): WeakTypeTag[runtime.PathList] = { + object Combine { + def apply[A <: runtime.Path: WeakTypeTag, Args <: runtime.PathList: WeakTypeTag] + : WeakTypeTag[runtime.PathList.List[A, Args]] = + weakTypeTag[runtime.PathList.List[A, Args]] + } + + paths + .foldLeft[WeakTypeTag[? <: runtime.PathList]](weakTypeTag[runtime.PathList.Empty]) { (acc, path) => + Combine(path.Underlying, acc) + } + .asInstanceOf[WeakTypeTag[runtime.PathList]] + } + +// selectors +// .map(ExistentialPath.parse) +// +// extractParams(t).map { params => +// new ExistentialCtor { +// type Underlying = runtime.ArgumentLists +// implicit val Underlying: WeakTypeTag[runtime.ArgumentLists] = paramsToType(params) +// } +// } +// } +// +// private def paramsToType(paramsLists: List[List[ValDef]]): WeakTypeTag[runtime.ArgumentLists] = +// paramsLists +// .map { paramList => +// paramList.foldRight[WeakTypeTag[? <: runtime.ArgumentList]](weakTypeTag[runtime.ArgumentList.Empty])( +// constructArgumentListType +// ) +// } +// .foldRight[WeakTypeTag[? <: runtime.ArgumentLists]](weakTypeTag[runtime.ArgumentLists.Empty])( +// constructArgumentListsType +// ) +// .asInstanceOf[WeakTypeTag[runtime.ArgumentLists]] +// +// private def constructArgumentListsType( +// head: WeakTypeTag[? <: runtime.ArgumentList], +// tail: WeakTypeTag[? <: runtime.ArgumentLists] +// ): WeakTypeTag[? <: runtime.ArgumentLists] = { +// object ApplyParams { +// def apply[Head <: runtime.ArgumentList: WeakTypeTag, Tail <: runtime.ArgumentLists: WeakTypeTag] +// : WeakTypeTag[runtime.ArgumentLists.List[Head, Tail]] = +// weakTypeTag[runtime.ArgumentLists.List[Head, Tail]] +// } +// +// ApplyParams(head, tail) +// } +// +// private def constructArgumentListType( +// t: ValDef, +// args: WeakTypeTag[? <: runtime.ArgumentList] +// ): WeakTypeTag[? <: runtime.ArgumentList] = { +// object ApplyParam { +// def apply[ParamName <: String: WeakTypeTag, ParamType: WeakTypeTag, Args <: runtime.ArgumentList: WeakTypeTag] +// : WeakTypeTag[runtime.ArgumentList.Argument[ParamName, ParamType, Args]] = +// weakTypeTag[runtime.ArgumentList.Argument[ParamName, ParamType, Args]] +// } +// +// ApplyParam( +// c.WeakTypeTag(c.internal.constantType(Constant(t.name.decodedName.toString))), +// c.WeakTypeTag(t.tpt.tpe), +// args +// ) +// } +// +// import Console.* +// +// private def invalidConstructor(t: Tree): String = +// s"Expected function, instead got: $MAGENTA$t$RESET: $MAGENTA${t.tpe}$RESET" + } + // If we try to do: // // implicit val toPath = fieldName.Underlying @@ -304,6 +397,16 @@ private[chimney] trait DslMacroUtils { } } + protected trait ApplyFieldNamesType { + def apply[A <: runtime.PathList: WeakTypeTag]: WeakTypeTag[?] + + final def applyFromSelector(t: Seq[Tree]): WeakTypeTag[?] = + apply(extractSelectorAsType(t).Underlying) + + private def extractSelectorAsType(t: Seq[Tree]): ExistentialPathList = + ExistentialPathList.parse(t).fold(error => c.abort(c.enclosingPosition, error), path => path) + } + /** Workaround for Java Enums, see [[io.scalaland.chimney.internal.runtime.RefinedJavaEnum]]. */ protected trait ApplyFixedCoproductType { From 13ebbaff228be94c9affde9752d3f40c0e7b2f91 Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Sun, 29 Sep 2024 16:27:29 -0700 Subject: [PATCH 08/12] doc: add api-docs and website documentation --- .../chimney/dsl/TransformerInto.scala | 7 ++++ docs/docs/supported-transformations.md | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala index 57a11390f..347961b5b 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala @@ -211,6 +211,13 @@ final class TransformerInto[From, To, Overrides <: TransformerOverrides, Flags < )(using IsFunction.Of[Ctor, To]): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = ${ TransformerIntoMacros.withConstructorImpl('this, 'f) } + /** Require that all fields of the source object except fields mentioned in `selectorFrom` are used in the + * transformation. and fail compilation otherwise. + * + * @param selectorFrom + * exception fields that are not required to be used in the transformation + * @return + */ transparent inline def requireSourceFieldsUsedExcept( inline selectorFrom: From => Any* ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = diff --git a/docs/docs/supported-transformations.md b/docs/docs/supported-transformations.md index 74318232a..85466e526 100644 --- a/docs/docs/supported-transformations.md +++ b/docs/docs/supported-transformations.md @@ -759,6 +759,42 @@ If the flag was enabled in the implicit config it can be disabled with `.disable // Consult https://chimney.readthedocs.io for usage examples. ``` +### Require source fields to be used + +If you want to enforce that every field of the source type is used in the transformation, you can enable the +`.requireSourceFieldsUsedExcept` setting. This setting also allows you to specify a certain subset of fields to be +exempt from this requirement. + +!!! example + + ```scala + //> using dep io.scalaland::chimney::{{ chimney_version() }} + import io.scalaland.chimney.dsl._ + + case class Source(a: String, b: Int, c: String) + case class Target(a: String) + + Source("value", 512, "anotherValue") + .into[Target] + .requireSourceFieldsUsedExcept() + .transform + // Chimney can't derive transformation from Source to Target + // + // Target + // field(s) b, c of Source are required to be used in the transformation but are not used! + // + // Consult https://chimney.readthedocs.io for usage examples. + + pprint.pprintln( + Source("value", 512, "anotherValue") + .into[Target] + .requireSourceFieldsUsedExcept(_.b, _.c) + .transform + ) + // expected output: + // Target(a = "value") + ``` + ### Writing to Bean setters If we want to write to `def setFieldName(fieldName: A): Unit` as if it was `fieldName: A` argument of a constructor - From 6cddfa865885e050b5cf27489b6acb370bd4bc6a Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Mon, 30 Sep 2024 19:47:23 -0700 Subject: [PATCH 09/12] Revert "feat: add PathList to support vararg macros" This reverts commit 333f10562698042317e734d25392d12182113c6c. --- .../compiletime/ChimneyTypesPlatform.scala | 13 ------- .../compiletime/dsl/utils/DslMacroUtils.scala | 39 ------------------- .../internal/compiletime/ChimneyTypes.scala | 17 -------- .../transformer/Configurations.scala | 7 ---- .../chimney/internal/runtime/PathList.scala | 7 ---- 5 files changed, 83 deletions(-) delete mode 100644 chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala index 103cdb649..67cb36584 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala @@ -424,19 +424,6 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi } } - object PathList extends PathListModule { - val Empty: Type[runtime.PathList.Empty] = quoted.Type.of[runtime.PathList.Empty] - object List extends ListModule { - def apply[Head <: runtime.Path: Type, Tail <: runtime.PathList: Type]: Type[runtime.PathList.List[Head, Tail]] = - quoted.Type.of[runtime.PathList.List[Head, Tail]] - def unapply[A](tpe: Type[A]): Option[(?<[runtime.Path], ?<[runtime.PathList])] = tpe match { - case '[runtime.PathList.List[head, tail]] => - Some((Type[head].as_?<[runtime.Path], Type[tail].as_?<[runtime.PathList])) - case _ => scala.None - } - } - } - object DefaultValue extends DefaultValueModule { def apply[Value: Type]: Type[integrations.DefaultValue[Value]] = quoted.Type.of[integrations.DefaultValue[Value]] diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala index b44aaf316..411bca5b7 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala @@ -1,7 +1,6 @@ package io.scalaland.chimney.internal.compiletime.dsl.utils import io.scalaland.chimney.internal.runtime -import io.scalaland.chimney.internal.runtime.PathList import scala.annotation.nowarn import scala.quoted.* @@ -285,44 +284,6 @@ private[chimney] class DslMacroUtils()(using quotes: Quotes) { s"Expected function, instead got: ${t.show(using Printer.TreeAnsiCode)}: ${t.asInstanceOf[Term].tpe.show(using Printer.TypeReprAnsiCode)}" } - private trait ExistentialPathList { - type Underlying <: runtime.PathList - implicit val Underlying: Type[Underlying] - } - private object ExistentialPathList { - def parse(t: Expr[Seq[?]]): Either[String, ExistentialPathList] = - (t match { - case Varargs(selectors: Seq[Expr[Any]]) => - selectors - .map(selector => ExistentialPath.parse(selector.asTerm)) - .foldLeft[Either[String, List[ExistentialPath]]](Right(Nil)) { - case (err @ Left(_), _) => err - case (Right(acc), Left(error)) => Left(error) - case (Right(acc), Right(path)) => Right(acc :+ path) - } - }).map { params => - val value1: Type[PathList] = combine(params) - - new ExistentialPathList { - type Underlying = runtime.PathList - implicit val Underlying: Type[Underlying] = value1 - } - } - - private def combine(paths: Seq[ExistentialPath]): Type[runtime.PathList] = { - object Combine { - def apply[A <: runtime.Path: Type, Args <: runtime.PathList: Type]: Type[runtime.PathList.List[A, Args]] = - Type.of[runtime.PathList.List[A, Args]] - } - - paths - .foldLeft[Type[? <: runtime.PathList]](Type.of[runtime.PathList.Empty]) { (acc, path) => - Combine(path.Underlying, acc) - } - .asInstanceOf[Type[runtime.PathList]] - } - } - def applyFieldNameType[Out](f: [A <: runtime.Path] => Type[A] ?=> Out)(selector: Expr[?]): Out = ExistentialPath .parse(selector.asTerm) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala index bd55cc06b..a420690d7 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala @@ -297,23 +297,6 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => ] { this: EveryMapValue.type => } } - val PathList: PathListModule - trait PathListModule { - this: PathList.type => - val Empty: Type[runtime.PathList.Empty] - - val List: ListModule - - trait ListModule - extends Type.Ctor2UpperBounded[ - runtime.Path, - runtime.PathList, - runtime.PathList.List - ] { - this: List.type => - } - } - val DefaultValue: DefaultValueModule trait DefaultValueModule extends Type.Ctor1[integrations.DefaultValue] { this: DefaultValue.type => } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala index 7cd362a8b..76f163c71 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala @@ -594,13 +594,6 @@ private[compiletime] trait Configurations { this: Derivation => // $COVERAGE-ON$ } - private def extractPathList[PathType <: runtime.PathList: Type]: List[Path] = Type[PathType] match { - case empty if empty =:= ChimneyType.PathList.Empty => List.empty - case ChimneyType.PathList.List(head, tail) => - import head.Underlying as Head, tail.Underlying as Tail - extractPath[Head] :: extractPathList[Tail] - } - private def extractPath[PathType <: runtime.Path: Type]: Path = Type[PathType] match { case root if root =:= ChimneyType.Path.Root => Path.Root diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala deleted file mode 100644 index 04bccc3ba..000000000 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/PathList.scala +++ /dev/null @@ -1,7 +0,0 @@ -package io.scalaland.chimney.internal.runtime - -sealed abstract class PathList -object PathList { - final class Empty extends PathList - final class List[Head <: Path, Tail <: PathList] extends PathList -} From b611575c396712985ff025d1e12c37906f65f1cd Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Mon, 30 Sep 2024 19:56:19 -0700 Subject: [PATCH 10/12] feat: change DSL to `.enableUnusedFieldPolicy(FailOnUnused)` flag and `.withIgnoreUnusedField(_.field)` and a few review comments addressed --- .../chimney/dsl/TransformerInto.scala | 23 ++-- .../compiletime/ChimneyTypesPlatform.scala | 39 +++---- .../dsl/TransformerIntoMacros.scala | 20 ++-- .../compiletime/dsl/utils/DslMacroUtils.scala | 103 ------------------ .../chimney/dsl/TransformerInto.scala | 15 ++- .../compiletime/ChimneyTypesPlatform.scala | 32 +++--- .../dsl/TransformerIntoMacros.scala | 18 ++- .../compiletime/dsl/utils/DslMacroUtils.scala | 7 -- .../chimney/dsl/ActionOnUnused.scala | 20 ++++ .../chimney/dsl/TransformerFlagsDsl.scala | 21 ++++ .../internal/compiletime/ChimneyTypes.scala | 19 +++- .../TransformerDerivationError.scala | 4 +- .../transformer/Configurations.scala | 59 +++++++--- .../derivation/transformer/ResultOps.scala | 2 +- .../TransformProductToProductRuleModule.scala | 24 ++-- .../internal/runtime/TransformerFlags.scala | 3 +- .../runtime/TransformerOverrides.scala | 5 +- .../chimney/TotalTransformerProductSpec.scala | 60 ++++++---- docs/docs/supported-transformations.md | 25 ++++- 19 files changed, 252 insertions(+), 247 deletions(-) create mode 100644 chimney/src/main/scala/io/scalaland/chimney/dsl/ActionOnUnused.scala diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala b/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala index d9e5352f0..6a2534c7f 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala @@ -205,17 +205,18 @@ final class TransformerInto[From, To, Overrides <: TransformerOverrides, Flags < ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = macro TransformerIntoMacros.withConstructorImpl[From, To, Overrides, Flags] - /** Require that all fields of the source object except fields mentioned in `selectorFrom` are used in the - * transformation. and fail compilation otherwise. - * - * @param selectorFrom - * exception fields that are not required to be used in the transformation - * @return - */ - def requireSourceFieldsUsedExcept[T, U]( - selectorFrom: From => Any* - ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = - macro TransformerIntoMacros.requireSourceFieldsUsedExceptImpl[From, To, Overrides, Flags] + /** Ignore if a source field is not used in the transformation. This can be useful when `.enableUnusedFieldPolicy` is + * enabled. + * + * @param selectorFrom + * the field is that not required to be used in the transformation + * @return + * [[io.scalaland.chimney.dsl.TransformerInto]] + * + * @since 1.5.0 + */ + def withIgnoreUnusedField(selectorFrom: From => ?): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = + macro TransformerIntoMacros.withIgnoreUnusedField[From, To, Overrides, Flags] /** Apply configured transformation in-place. * diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala index 64e4cd268..95d8a2f86 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala @@ -96,6 +96,9 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi val PreferPartialTransformer: Type[io.scalaland.chimney.dsl.PreferPartialTransformer.type] = weakTypeTag[io.scalaland.chimney.dsl.PreferPartialTransformer.type] + val FailOnUnused: Type[io.scalaland.chimney.dsl.FailOnUnused.type] = + weakTypeTag[io.scalaland.chimney.dsl.FailOnUnused.type] + val RuntimeDataStore: Type[dsls.TransformerDefinitionCommons.RuntimeDataStore] = weakTypeTag[dsls.TransformerDefinitionCommons.RuntimeDataStore] @@ -249,15 +252,15 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi ) } } - object RequireSourceFieldsExcept extends RequireSourceFieldsExceptModule { + object IgnoreUnusedField extends IgnoreUnusedFieldModule { def apply[ - FromPathList <: runtime.PathList: Type, - Tail <: runtime.TransformerOverrides: Type - ]: Type[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] = - weakTypeTag[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] - def unapply[A](A: Type[A]): Option[(?<[runtime.PathList], ?<[runtime.TransformerOverrides])] = - A.asCtor[runtime.TransformerOverrides.RequireSourceFieldsExcept[?, ?]].map { A0 => - (A0.param_<[runtime.PathList](0), A0.param_<[runtime.TransformerOverrides](1)) + FromPath <: runtime.Path: Type, + Tail <: runtime.TransformerOverrides: Type + ]: Type[runtime.TransformerOverrides.IgnoreUnusedField[FromPath, Tail]] = + weakTypeTag[runtime.TransformerOverrides.IgnoreUnusedField[FromPath, Tail]] + def unapply[A](A: Type[A]): Option[(?<[runtime.Path], ?<[runtime.TransformerOverrides])] = + A.asCtor[runtime.TransformerOverrides.IgnoreUnusedField[?, ?]].map { A0 => + (A0.param_<[runtime.Path](0), A0.param_<[runtime.TransformerOverrides](1)) } } } @@ -320,6 +323,15 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi A0.param_<[dsls.ImplicitTransformerPreference](0) } } + object UnusedFieldPolicy extends UnusedFieldPolicyModule { + def apply[R <: dsls.ActionOnUnused: Type] + : Type[runtime.TransformerFlags.UnusedFieldPolicy[R]] = + weakTypeTag[runtime.TransformerFlags.UnusedFieldPolicy[R]] + def unapply[A](A: Type[A]): Option[?<[dsls.ActionOnUnused]] = + A.asCtor[runtime.TransformerFlags.UnusedFieldPolicy[?]].map { A0 => + A0.param_<[dsls.ActionOnUnused](0) + } + } object FieldNameComparison extends FieldNameComparisonModule { def apply[C <: dsls.TransformedNamesComparison: Type]: Type[runtime.TransformerFlags.FieldNameComparison[C]] = weakTypeTag[runtime.TransformerFlags.FieldNameComparison[C]] @@ -417,17 +429,6 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi } } - object PathList extends PathListModule { - val Empty: Type[runtime.PathList.Empty] = weakTypeTag[runtime.PathList.Empty] - object List extends ListModule { - def apply[Head <: runtime.Path: Type, Tail <: runtime.PathList: Type]: Type[runtime.PathList.List[Head, Tail]] = - weakTypeTag[runtime.PathList.List[Head, Tail]] - def unapply[A](A: Type[A]): Option[(?<[runtime.Path], ?<[runtime.PathList])] = - A.asCtor[runtime.PathList.List[?, ?]] - .map(A0 => A0.param_<[runtime.Path](0) -> A0.param_<[runtime.PathList](1)) - } - } - object DefaultValue extends DefaultValueModule { def apply[Value: Type]: Type[integrations.DefaultValue[Value]] = weakTypeTag[integrations.DefaultValue[Value]] diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala index 7fd656f32..1a6465076 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala @@ -1,7 +1,7 @@ package io.scalaland.chimney.internal.compiletime.dsl import io.scalaland.chimney.dsl.TransformerInto -import io.scalaland.chimney.internal.runtime.{ArgumentLists, Path, PathList, TransformerFlags, TransformerOverrides} +import io.scalaland.chimney.internal.runtime.{ArgumentLists, Path, TransformerFlags, TransformerOverrides} import io.scalaland.chimney.internal.runtime.TransformerOverrides.* import scala.annotation.unused @@ -97,16 +97,16 @@ class TransformerIntoMacros(val c: whitebox.Context) extends utils.DslMacroUtils .asInstanceOfExpr[TransformerInto[From, To, Constructor[Args, Path.Root, Overrides], Flags]] }.applyFromBody(f) - def requireSourceFieldsUsedExceptImpl[ - From: WeakTypeTag, - To: WeakTypeTag, - Overrides <: TransformerOverrides: WeakTypeTag, - Flags <: TransformerFlags: WeakTypeTag - ](selectorFrom: Tree*): Tree = c.prefix.tree + def withIgnoreUnusedField[ + From: WeakTypeTag, + To: WeakTypeTag, + Overrides <: TransformerOverrides: WeakTypeTag, + Flags <: TransformerFlags: WeakTypeTag + ](selectorFrom: Tree): Tree = c.prefix.tree .asInstanceOfExpr( - new ApplyFieldNamesType { - def apply[FromPathList <: PathList: WeakTypeTag]: c.WeakTypeTag[?] = - weakTypeTag[TransformerInto[From, To, RequireSourceFieldsExcept[FromPathList, Overrides], Flags]] + new ApplyFieldNameType { + def apply[FromPath <: Path: WeakTypeTag]: c.WeakTypeTag[?] = + weakTypeTag[TransformerInto[From, To, IgnoreUnusedField[FromPath, Overrides], Flags]] }.applyFromSelector(selectorFrom) ) } diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala index 5dbe938b8..46bfd8f1d 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala @@ -266,99 +266,6 @@ private[chimney] trait DslMacroUtils { s"Expected function, instead got: $MAGENTA$t$RESET: $MAGENTA${t.tpe}$RESET" } - private trait ExistentialPathList { - type Underlying <: runtime.PathList - val Underlying: c.WeakTypeTag[Underlying] - } - private object ExistentialPathList { - def parse(selectors: Seq[Tree]): Either[String, ExistentialPathList] = - selectors - .map(selector => ExistentialPath.parse(selector)) - .foldLeft[Either[String, List[ExistentialPath]]](Right(Nil)) { - case (err @ Left(_), _) => err - case (_, Left(error)) => Left(error) - case (Right(acc), Right(path)) => Right(acc :+ path) - } - .map { params => - new ExistentialPathList { - type Underlying = runtime.PathList - implicit val Underlying: WeakTypeTag[Underlying] = combine(params) - } - } - - private def combine(paths: Seq[ExistentialPath]): WeakTypeTag[runtime.PathList] = { - object Combine { - def apply[A <: runtime.Path: WeakTypeTag, Args <: runtime.PathList: WeakTypeTag] - : WeakTypeTag[runtime.PathList.List[A, Args]] = - weakTypeTag[runtime.PathList.List[A, Args]] - } - - paths - .foldLeft[WeakTypeTag[? <: runtime.PathList]](weakTypeTag[runtime.PathList.Empty]) { (acc, path) => - Combine(path.Underlying, acc) - } - .asInstanceOf[WeakTypeTag[runtime.PathList]] - } - -// selectors -// .map(ExistentialPath.parse) -// -// extractParams(t).map { params => -// new ExistentialCtor { -// type Underlying = runtime.ArgumentLists -// implicit val Underlying: WeakTypeTag[runtime.ArgumentLists] = paramsToType(params) -// } -// } -// } -// -// private def paramsToType(paramsLists: List[List[ValDef]]): WeakTypeTag[runtime.ArgumentLists] = -// paramsLists -// .map { paramList => -// paramList.foldRight[WeakTypeTag[? <: runtime.ArgumentList]](weakTypeTag[runtime.ArgumentList.Empty])( -// constructArgumentListType -// ) -// } -// .foldRight[WeakTypeTag[? <: runtime.ArgumentLists]](weakTypeTag[runtime.ArgumentLists.Empty])( -// constructArgumentListsType -// ) -// .asInstanceOf[WeakTypeTag[runtime.ArgumentLists]] -// -// private def constructArgumentListsType( -// head: WeakTypeTag[? <: runtime.ArgumentList], -// tail: WeakTypeTag[? <: runtime.ArgumentLists] -// ): WeakTypeTag[? <: runtime.ArgumentLists] = { -// object ApplyParams { -// def apply[Head <: runtime.ArgumentList: WeakTypeTag, Tail <: runtime.ArgumentLists: WeakTypeTag] -// : WeakTypeTag[runtime.ArgumentLists.List[Head, Tail]] = -// weakTypeTag[runtime.ArgumentLists.List[Head, Tail]] -// } -// -// ApplyParams(head, tail) -// } -// -// private def constructArgumentListType( -// t: ValDef, -// args: WeakTypeTag[? <: runtime.ArgumentList] -// ): WeakTypeTag[? <: runtime.ArgumentList] = { -// object ApplyParam { -// def apply[ParamName <: String: WeakTypeTag, ParamType: WeakTypeTag, Args <: runtime.ArgumentList: WeakTypeTag] -// : WeakTypeTag[runtime.ArgumentList.Argument[ParamName, ParamType, Args]] = -// weakTypeTag[runtime.ArgumentList.Argument[ParamName, ParamType, Args]] -// } -// -// ApplyParam( -// c.WeakTypeTag(c.internal.constantType(Constant(t.name.decodedName.toString))), -// c.WeakTypeTag(t.tpt.tpe), -// args -// ) -// } -// -// import Console.* -// -// private def invalidConstructor(t: Tree): String = -// s"Expected function, instead got: $MAGENTA$t$RESET: $MAGENTA${t.tpe}$RESET" - } - // If we try to do: // // implicit val toPath = fieldName.Underlying @@ -397,16 +304,6 @@ private[chimney] trait DslMacroUtils { } } - protected trait ApplyFieldNamesType { - def apply[A <: runtime.PathList: WeakTypeTag]: WeakTypeTag[?] - - final def applyFromSelector(t: Seq[Tree]): WeakTypeTag[?] = - apply(extractSelectorAsType(t).Underlying) - - private def extractSelectorAsType(t: Seq[Tree]): ExistentialPathList = - ExistentialPathList.parse(t).fold(error => c.abort(c.enclosingPosition, error), path => path) - } - /** Workaround for Java Enums, see [[io.scalaland.chimney.internal.runtime.RefinedJavaEnum]]. */ protected trait ApplyFixedCoproductType { diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala index 347961b5b..bedf17196 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala @@ -211,17 +211,20 @@ final class TransformerInto[From, To, Overrides <: TransformerOverrides, Flags < )(using IsFunction.Of[Ctor, To]): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = ${ TransformerIntoMacros.withConstructorImpl('this, 'f) } - /** Require that all fields of the source object except fields mentioned in `selectorFrom` are used in the - * transformation. and fail compilation otherwise. + /** Ignore if a source field is not used in the transformation. This can be useful when `.enableUnusedFieldPolicy` is + * enabled. * * @param selectorFrom - * exception fields that are not required to be used in the transformation + * the field is that not required to be used in the transformation * @return + * [[io.scalaland.chimney.dsl.TransformerInto]] + * + * @since 1.5.0 */ - transparent inline def requireSourceFieldsUsedExcept( - inline selectorFrom: From => Any* + transparent inline def withIgnoreUnusedField( + inline selectorFrom: From => ? ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = - ${ TransformerIntoMacros.requireSourceFieldsUsedExceptImpl('this, 'selectorFrom) } + ${ TransformerIntoMacros.withIgnoreUnusedFieldImpl('this, 'selectorFrom) } /** Apply configured transformation in-place. * diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala index 67cb36584..9f3de00a8 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala @@ -43,6 +43,9 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi val PreferPartialTransformer: Type[io.scalaland.chimney.dsl.PreferPartialTransformer.type] = quoted.Type.of[io.scalaland.chimney.dsl.PreferPartialTransformer.type] + val FailOnUnused: Type[io.scalaland.chimney.dsl.FailOnUnused.type] = + quoted.Type.of[io.scalaland.chimney.dsl.FailOnUnused.type] + val RuntimeDataStore: Type[dsls.TransformerDefinitionCommons.RuntimeDataStore] = quoted.Type.of[dsls.TransformerDefinitionCommons.RuntimeDataStore] @@ -219,22 +222,16 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi case _ => scala.None } } - - object RequireSourceFieldsExcept extends RequireSourceFieldsExceptModule { + object IgnoreUnusedField extends IgnoreUnusedFieldModule { def apply[ - FromPathList <: runtime.PathList: Type, + FromPath <: runtime.Path: Type, Tail <: runtime.TransformerOverrides: Type - ]: Type[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] = - quoted.Type.of[runtime.TransformerOverrides.RequireSourceFieldsExcept[FromPathList, Tail]] - def unapply[A](tpe: Type[A]): Option[(?<[runtime.PathList], ?<[runtime.TransformerOverrides])] = + ]: Type[runtime.TransformerOverrides.IgnoreUnusedField[FromPath, Tail]] = + quoted.Type.of[runtime.TransformerOverrides.IgnoreUnusedField[FromPath, Tail]] + def unapply[A](tpe: Type[A]): Option[(?<[runtime.Path], ?<[runtime.TransformerOverrides])] = tpe match { - case '[runtime.TransformerOverrides.RequireSourceFieldsExcept[fromPath, cfg]] => - Some( - ( - Type[fromPath].as_?<[runtime.PathList], - Type[cfg].as_?<[runtime.TransformerOverrides] - ) - ) + case '[runtime.TransformerOverrides.IgnoreUnusedField[fromPath, cfg]] => + Some((Type[fromPath].as_?<[runtime.Path], Type[cfg].as_?<[runtime.TransformerOverrides])) case _ => scala.None } } @@ -305,6 +302,15 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi case _ => scala.None } } + object UnusedFieldPolicy extends UnusedFieldPolicyModule { + def apply[R <: dsls.ActionOnUnused: Type]: Type[runtime.TransformerFlags.UnusedFieldPolicy[R]] = + quoted.Type.of[runtime.TransformerFlags.UnusedFieldPolicy[R]] + def unapply[A](tpe: Type[A]): Option[?<[dsls.ActionOnUnused]] = tpe match { + case '[runtime.TransformerFlags.UnusedFieldPolicy[r]] => + Some(Type[r].as_?<[dsls.ActionOnUnused]) + case _ => scala.None + } + } object FieldNameComparison extends FieldNameComparisonModule { def apply[C <: dsls.TransformedNamesComparison: Type]: Type[runtime.TransformerFlags.FieldNameComparison[C]] = quoted.Type.of[runtime.TransformerFlags.FieldNameComparison[C]] diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala index d5400a8c7..816fe7aac 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerIntoMacros.scala @@ -8,7 +8,6 @@ import io.scalaland.chimney.internal.compiletime.dsl.utils.DslMacroUtils import io.scalaland.chimney.internal.runtime.{ ArgumentLists, Path, - PathList, TransformerFlags, TransformerOverrides, WithRuntimeDataStore @@ -145,21 +144,20 @@ object TransformerIntoMacros { } }(f) - def requireSourceFieldsUsedExceptImpl[ + def withIgnoreUnusedFieldImpl[ From: Type, To: Type, Overrides <: TransformerOverrides: Type, - Flags <: TransformerFlags: Type, - Ctor: Type + Flags <: TransformerFlags: Type ]( ti: Expr[TransformerInto[From, To, Overrides, Flags]], - selectorFrom: Expr[Seq[From => Any]] + fromSelector: Expr[From => ?] )(using Quotes): Expr[TransformerInto[From, To, ? <: TransformerOverrides, Flags]] = - DslMacroUtils().applyRequireSourceFieldsExceptType { - [args <: PathList] => - (_: Type[args]) ?=> + DslMacroUtils().applyFieldNameType { + [fromPath <: Path] => + (_: Type[fromPath]) ?=> '{ - $ti.asInstanceOf[TransformerInto[From, To, RequireSourceFieldsExcept[args, Overrides], Flags]] + $ti.asInstanceOf[TransformerInto[From, To, IgnoreUnusedField[fromPath, Overrides], Flags]] } - }(selectorFrom) + }(fromSelector) } diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala index 411bca5b7..a2be78bd3 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/utils/DslMacroUtils.scala @@ -308,11 +308,4 @@ private[chimney] class DslMacroUtils()(using quotes: Quotes) { case Left(error) => report.errorAndAbort(error, Position.ofMacroExpansion) } - def applyRequireSourceFieldsExceptType[Out]( - f: [A <: runtime.PathList] => Type[A] ?=> Out - )(fieldSelectors: Expr[Seq[?]]): Out = - ExistentialPathList.parse(fieldSelectors) match { - case Right(pathList) => f(using pathList.Underlying) - case Left(error) => report.errorAndAbort(error, Position.ofMacroExpansion) - } } diff --git a/chimney/src/main/scala/io/scalaland/chimney/dsl/ActionOnUnused.scala b/chimney/src/main/scala/io/scalaland/chimney/dsl/ActionOnUnused.scala new file mode 100644 index 000000000..a28f64080 --- /dev/null +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/ActionOnUnused.scala @@ -0,0 +1,20 @@ +package io.scalaland.chimney.dsl + +/** Action to take when some fields of the source are not used in the target. + * + * @see + * [[https://chimney.readthedocs.io/supported-transformations/#unused-source-fields-policies]] for more details + * + * @since 1.5.0 + */ +sealed abstract class ActionOnUnused + +/** Fail the derivation if not all fields of the source are not used. Exceptions can be made using + * `ignoreUnusedField` overrides. + * + * @see + * [[https://chimney.readthedocs.io/supported-transformations/#unused-source-fields-policies]] for more details + * + * @since 1.5.0 + */ +case object FailOnUnused extends ActionOnUnused diff --git a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala index e4716b0e5..95c75e988 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala @@ -377,6 +377,27 @@ private[dsl] trait TransformerFlagsDsl[UpdateFlag[_ <: TransformerFlags], Flags def disableMacrosLogging: UpdateFlag[Disable[MacrosLogging, Flags]] = disableFlag[MacrosLogging] + /** Enable an action to be executed upon unused fields in the source type. + * + * @param action + * parameter specifying what to do when some fields of the source are not used in the target + * @see + * [[https://chimney.readthedocs.io/supported-transformations/#unused-source-fields-policies]] for more details for more details + * + * @since 1.5.0 + */ + def enableUnusedFieldPolicy[R <: ActionOnUnused](@unused action: R): UpdateFlag[Enable[UnusedFieldPolicy[R], Flags]] = + enableFlag[UnusedFieldPolicy[R]] + + /** Disable any action registered to be executed upon unused fields in the source type. + * + * @see + * [[https://chimney.readthedocs.io/TODO:???]] for more details for more details + * @since 1.5.0 + */ + def disableUnusedFieldPolicy: UpdateFlag[Disable[UnusedFieldPolicy[?], Flags]] = + disableFlag[UnusedFieldPolicy[?]] + private def enableFlag[F <: TransformerFlags.Flag]: UpdateFlag[Enable[F, Flags]] = this.asInstanceOf[UpdateFlag[Enable[F, Flags]]] diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala index a420690d7..3facf23ea 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/ChimneyTypes.scala @@ -35,6 +35,8 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => val PreferTotalTransformer: Type[io.scalaland.chimney.dsl.PreferTotalTransformer.type] val PreferPartialTransformer: Type[io.scalaland.chimney.dsl.PreferPartialTransformer.type] + val FailOnUnused: Type[io.scalaland.chimney.dsl.FailOnUnused.type] + val RuntimeDataStore: Type[dsls.TransformerDefinitionCommons.RuntimeDataStore] val ArgumentList: ArgumentListModule @@ -152,13 +154,13 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => runtime.TransformerOverrides.RenamedTo ] { this: RenamedTo.type => } - val RequireSourceFieldsExcept: RequireSourceFieldsExceptModule - trait RequireSourceFieldsExceptModule + val IgnoreUnusedField: IgnoreUnusedFieldModule + trait IgnoreUnusedFieldModule extends Type.Ctor2UpperBounded[ - runtime.PathList, + runtime.Path, runtime.TransformerOverrides, - runtime.TransformerOverrides.RequireSourceFieldsExcept - ] { this: RequireSourceFieldsExcept.type => } + runtime.TransformerOverrides.IgnoreUnusedField + ] { this: IgnoreUnusedField.type => } } val TransformerFlags: TransformerFlagsModule @@ -204,6 +206,13 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => dsls.ImplicitTransformerPreference, runtime.TransformerFlags.ImplicitConflictResolution ] { this: ImplicitConflictResolution.type => } + val UnusedFieldPolicy: UnusedFieldPolicyModule + trait UnusedFieldPolicyModule + extends Type.Ctor1UpperBounded[ + dsls.ActionOnUnused, + runtime.TransformerFlags.UnusedFieldPolicy + ] { + this: UnusedFieldPolicy.type => } val FieldNameComparison: FieldNameComparisonModule trait FieldNameComparisonModule extends Type.Ctor1UpperBounded[ diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala index c0ad335ea..c8a66c3c4 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/TransformerDerivationError.scala @@ -77,7 +77,7 @@ final case class NotSupportedTransformerDerivation( extends TransformerDerivationError final case class UnusedButRequiredToUseSourceFields( - unused: Set[String] + unusedFields: Set[String] )(val fromType: String, val toType: String) extends TransformerDerivationError @@ -114,7 +114,7 @@ object TransformerDerivationError { case NotSupportedTransformerDerivation(exprPrettyPrint) => s" derivation from $exprPrettyPrint: $fromType to $toType is not supported in Chimney!" case UnusedButRequiredToUseSourceFields(unusedFields) => - s" field(s) $MAGENTA${unusedFields.mkString(", ")}$RESET of $MAGENTA${fromType}$RESET are required to be used in the transformation but are not used!" + s" field(s) $MAGENTA${unusedFields.mkString(", ")}$RESET of $MAGENTA${fromType}$RESET were required to be used in the transformation but are not used!" } def prettyFieldList(fields: Seq[String])(use: String => String): String = diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala index 76f163c71..9d295d5da 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala @@ -26,6 +26,7 @@ private[compiletime] trait Configurations { this: Derivation => implicitConflictResolution: Option[ImplicitTransformerPreference] = None, fieldNameComparison: Option[dsls.TransformedNamesComparison] = None, subtypeNameComparison: Option[dsls.TransformedNamesComparison] = None, + unusedFieldPolicy: Option[dsls.ActionOnUnused] = None, displayMacrosLogging: Boolean = false ) { @@ -85,6 +86,9 @@ private[compiletime] trait Configurations { this: Derivation => def setSubtypeNameComparison(nameComparison: Option[dsls.TransformedNamesComparison]): TransformerFlags = copy(subtypeNameComparison = nameComparison) + def setUnusedFieldPolicy(action: Option[dsls.ActionOnUnused]): TransformerFlags = + copy(unusedFieldPolicy = action) + override def toString: String = s"TransformerFlags(${Vector( if (inheritedAccessors) Vector("inheritedAccessors") else Vector.empty, if (methodAccessors) Vector("methodAccessors") else Vector.empty, @@ -98,6 +102,7 @@ private[compiletime] trait Configurations { this: Derivation => if (optionDefaultsToNone) Vector("optionDefaultsToNone") else Vector.empty, if (nonAnyValWrappers) Vector("nonAnyValWrappers") else Vector.empty, implicitConflictResolution.map(r => s"ImplicitTransformerPreference=$r").toList.toVector, + unusedFieldPolicy.map(r => s"UnusedFieldPolicy=$r").toList.toVector, fieldNameComparison.map(r => s"fieldNameComparison=$r").toList.toVector, subtypeNameComparison.map(r => s"subtypeNameComparison=$r").toList.toVector, if (displayMacrosLogging) Vector("displayMacrosLogging") else Vector.empty @@ -124,6 +129,11 @@ private[compiletime] trait Configurations { this: Derivation => case "PreferPartialTransformer" => Some(dsls.PreferPartialTransformer) case "none" => None }) + case (cfg, transformerFlag"UnusedFieldPolicy=$value") => + cfg.copy(unusedFieldPolicy = value match { + case "FailOnUnused" => Some(dsls.FailOnUnused) + case "none" => None + }) case (cfg, transformerFlag"MacrosLogging=$value") => cfg.copy(displayMacrosLogging = value.toBoolean) case (cfg, _) => cfg } @@ -226,15 +236,10 @@ private[compiletime] trait Configurations { this: Derivation => } } - sealed trait Verification extends scala.Product with Serializable - object Verification { - final case class RequireAllSourceFieldsUsedExcept(sourceFields: Set[String]) extends Verification { - override def toString: String = s"RequireAllSourceFieldsUsedExcept(sourceFields=${sourceFields.mkString(", ")})" - } - } sealed protected trait TransformerOverride extends scala.Product with Serializable protected object TransformerOverride { + sealed trait ForFieldPolicy extends TransformerOverride sealed trait ForField extends TransformerOverride sealed trait ForSubtype extends TransformerOverride sealed trait ForConstructor extends TransformerOverride @@ -271,6 +276,10 @@ private[compiletime] trait Configurations { this: Derivation => final case class RenamedFrom(sourcePath: Path) extends ForField final case class RenamedTo(targetPath: Path) extends ForSubtype + final case class IgnoreUnusedField(fieldName: String) extends ForFieldPolicy { + override def toString: String = s"IgnoreUnusedField(${fieldName})" + } + private def printArgs(args: Args): String = { import ExistentialType.prettyPrint as printTpe if (args.isEmpty) "" @@ -286,8 +295,6 @@ private[compiletime] trait Configurations { this: Derivation => private val runtimeOverrides: Vector[(Path, TransformerOverride)] = Vector.empty, /** Let us prevent `implicit val foo = foo` but allow `implicit val foo = new Foo { def sth = foo }` */ private val preventImplicitSummoningForTypes: Option[(??, ??)] = None, - /** Stores all verification settings provided by user */ - verifications: Vector[Verification] = Vector.empty ) { private lazy val runtimeOverridesForCurrent = runtimeOverrides.filter { @@ -313,8 +320,6 @@ private[compiletime] trait Configurations { this: Derivation => def areLocalFlagsEmpty: Boolean = !localFlagsOverridden - def addVerification(verification: Verification): TransformerConfiguration = - copy(verifications = verifications :+ verification) def addTransformerOverride(path: Path, runtimeOverride: TransformerOverride): TransformerConfiguration = copy(runtimeOverrides = runtimeOverrides :+ (path -> runtimeOverride)) def areOverridesEmpty: Boolean = @@ -395,6 +400,7 @@ private[compiletime] trait Configurations { this: Derivation => case _: TransformerOverride.ForField | _: TransformerOverride.ForSubtype => true // Constructor is always matched at "_" Path, and dropped only when going inward case _: TransformerOverride.ForConstructor => false + case _: TransformerOverride.ForFieldPolicy => false } newPath <- path.drop(toPath).to(Vector) if !(newPath == Path.Root && alwaysDropOnRoot) @@ -402,6 +408,12 @@ private[compiletime] trait Configurations { this: Derivation => preventImplicitSummoningForTypes = None ) + def getIgnoreUnusedFields: Set[String] = ListSet.from { + runtimeOverrides.collect { + case (_, TransformerOverride.IgnoreUnusedField(fieldName)) => fieldName + } + } + override def toString: String = { val runtimeOverridesString = runtimeOverrides.map { case (path, runtimeOverride) => s"$path -> $runtimeOverride" }.mkString(", ") @@ -480,6 +492,16 @@ private[compiletime] trait Configurations { this: Derivation => extractTransformerFlags[Flags2](defaultFlags).setSubtypeNameComparison( Some(extractNameComparisonObject[Comparison]) ) + case ChimneyType.TransformerFlags.Flags.UnusedFieldPolicy(r) => + if (r.Underlying =:= ChimneyType.FailOnUnused) + extractTransformerFlags[Flags2](defaultFlags).setUnusedFieldPolicy( + Some(dsls.FailOnUnused) + ) + else { + // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation + reportError("Invalid ActionOnUnused type!!") + // $COVERAGE-ON$ + } case _ => extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = true) } @@ -495,6 +517,8 @@ private[compiletime] trait Configurations { this: Derivation => extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison(None) case ChimneyType.TransformerFlags.Flags.SubtypeNameComparison(_) => extractTransformerFlags[Flags2](defaultFlags).setSubtypeNameComparison(None) + case ChimneyType.TransformerFlags.Flags.UnusedFieldPolicy(_) => + extractTransformerFlags[Flags2](defaultFlags).setUnusedFieldPolicy(None) case _ => extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = false) } @@ -579,15 +603,14 @@ private[compiletime] trait Configurations { this: Derivation => extractPath[FromPath], TransformerOverride.RenamedTo(extractPath[ToPath]) ) - case ChimneyType.TransformerOverrides.RequireSourceFieldsExcept(fromPathList, cfg) => - import fromPathList.Underlying as FromPathList, cfg.Underlying as Tail2 - val fields = extractPathList[FromPathList].map { - case Path.AtField(fromName, _) => fromName + case ChimneyType.TransformerOverrides.IgnoreUnusedField(fromPath, cfg) => + import fromPath.Underlying as FromPath, cfg.Underlying as Tail2 + extractPath[FromPath] match { + case path @ Path.AtField(fromName, _) => + extractTransformerConfig[Tail2](runtimeDataIdx, runtimeDataStore) + .addTransformerOverride(path, TransformerOverride.IgnoreUnusedField(fromName)) case path => reportError(s"$path is not a field selector!") - }.toSet - - extractTransformerConfig[Tail2](runtimeDataIdx, runtimeDataStore) - .addVerification(Verification.RequireAllSourceFieldsUsedExcept(fields)) + } // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation case _ => reportError(s"Invalid internal TransformerOverrides type shape: ${Type.prettyPrint[Tail]}!!") diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala index 0ca074f68..067ae3149 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/ResultOps.scala @@ -172,7 +172,7 @@ private[compiletime] trait ResultOps { this: Derivation => ctx: TransformationContext[From, To] ): DerivationResult[A] = DerivationResult.transformerError( UnusedButRequiredToUseSourceFields( - unused = unusedRequiredFields + unusedFields = unusedRequiredFields )(fromType = Type.prettyPrint[From], toType = Type.prettyPrint[To]) ) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala index f9465b296..d92dd08a6 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala @@ -1,5 +1,6 @@ package io.scalaland.chimney.internal.compiletime.derivation.transformer.rules +import io.scalaland.chimney.dsl.FailOnUnused import io.scalaland.chimney.internal.compiletime.{DerivationErrors, DerivationResult} import io.scalaland.chimney.internal.compiletime.derivation.transformer.Derivation import io.scalaland.chimney.internal.compiletime.fp.Implicits.* @@ -263,21 +264,20 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio .map(_.filterNot(_._2 == unmatchedSetter).filterNot(_._2 == nonUnitSetter)) .withUsedSourceFields .logSuccess { case (usedSourceFields, _) => - (for (verification <- verifications) yield verification match { - case Verification.RequireAllSourceFieldsUsedExcept(exceptFields) => - s"validate all source fields used except ${exceptFields.mkString(",")} (all=${fromEnabledExtractors.keySet.mkString(",")}, used=${usedSourceFields.mkString(",")})" - }).mkString("\n") + if (ctx.config.flags.unusedFieldPolicy.contains(FailOnUnused)) { + "unused-field-policy(fail-on-used) is enabled\n" + + s"all source fields: ${fromEnabledExtractors.keySet.mkString(",")}\n" + + s"used fields: ${usedSourceFields.mkString(",")}) \n" + + s"ignore unused field(s) ${ctx.config.getIgnoreUnusedFields.mkString(",")}" + } else "" } .flatMap { case (usedSourceFields, res) => - verifications - .flatMap { case Verification.RequireAllSourceFieldsUsedExcept(exceptFields) => - Option(fromEnabledExtractors.keySet -- exceptFields -- usedSourceFields) // unused but required fields - .filter(_.nonEmpty) - .map( - DerivationResult.requiredFieldNotUsed[From, To, List[(String, Existential[TransformationExpr])]](_) - ) + Option + .when(ctx.config.flags.unusedFieldPolicy.contains(FailOnUnused)) { + fromEnabledExtractors.keySet -- ctx.config.getIgnoreUnusedFields -- usedSourceFields // unused but required fields } - .headOption + .filter(_.nonEmpty) + .map(DerivationResult.requiredFieldNotUsed[From, To, List[(String, Existential[TransformationExpr])]](_)) .getOrElse(DerivationResult.pure(res)) } .logSuccess { args => diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerFlags.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerFlags.scala index c74d57ce4..3ee86c633 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerFlags.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerFlags.scala @@ -1,6 +1,6 @@ package io.scalaland.chimney.internal.runtime -import io.scalaland.chimney.dsl.{ImplicitTransformerPreference, TransformedNamesComparison} +import io.scalaland.chimney.dsl.{ImplicitTransformerPreference, ActionOnUnused, TransformedNamesComparison} sealed abstract class TransformerFlags object TransformerFlags { @@ -21,6 +21,7 @@ object TransformerFlags { final class PartialUnwrapsOption extends Flag final class NonAnyValWrappers extends Flag final class ImplicitConflictResolution[R <: ImplicitTransformerPreference] extends Flag + final class UnusedFieldPolicy[R <: ActionOnUnused] extends Flag final class FieldNameComparison[C <: TransformedNamesComparison] extends Flag final class SubtypeNameComparison[C <: TransformedNamesComparison] extends Flag final class MacrosLogging extends Flag diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala index 7e3105b13..49937da21 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/runtime/TransformerOverrides.scala @@ -21,6 +21,7 @@ object TransformerOverrides { final class RenamedFrom[FromPath <: Path, ToPath <: Path, Tail <: Overrides] extends Overrides // Computes a value from matched subtype, targeting another subtype final class RenamedTo[FromPath <: Path, ToPath <: Path, Tail <: Overrides] extends Overrides - // Throws a compile error if not all fields of the source are not used - final class RequireSourceFieldsExcept[FromPaths <: PathList, Tail <: Overrides] extends Overrides + // Flags a source field to be ignored if not used in the target + // @see TransformerFlags.UnusedFieldPolicy + final class IgnoreUnusedField[FromPath <: Path, Tail <: Overrides] extends Overrides } diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala index 728517960..aefca2db6 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala @@ -519,93 +519,111 @@ class TotalTransformerProductSpec extends ChimneySpec { } } - group("""setting .requireSourceFieldsUsedExcept(_.field1, _.field2)""") { + group("flag .enableUnusedFieldPolicy") { import shapes1.{Point, Rectangle, Triangle} - test("should fail if not all required source fields are used") { + test("should fail transform if unused source fields exist") { compileErrors( """ Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) .into[Rectangle] - .requireSourceFieldsUsedExcept() + .enableUnusedFieldPolicy(FailOnUnused) .transform """ ).check( "Chimney can't derive transformation from io.scalaland.chimney.fixtures.shapes1.Triangle to io.scalaland.chimney.fixtures.shapes1.Rectangle", - "field(s) p3 of io.scalaland.chimney.fixtures.shapes1.Triangle are required to be used in the transformation but are not used!" + "field(s) p3 of io.scalaland.chimney.fixtures.shapes1.Triangle were required to be used in the transformation but are not used!" ) } - test("should pass if all required source fields are either used or included in exceptions") { - Triangle(p1 = Point(0, 0), p2 = shapes1.Point(2, 2), p3 = shapes1.Point(2, 0)) + test("should not fail transform if unused source fields exist but are ignored through .withIgnoreUnusedField") { + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) .into[Rectangle] - .requireSourceFieldsUsedExcept(_.p3) + .withIgnoreUnusedField(_.p3) + .enableUnusedFieldPolicy(FailOnUnused) .transform ==> Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) } - test("should pass if all required source fields are used (withFieldRenamed)") { + test("should not fail transform if all source fields are used (withFieldRenamed)") { case class AnotherRectangle(p1: Point, PPPP: Point) Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) .into[AnotherRectangle] .withFieldRenamed(_.p2, _.PPPP) - .requireSourceFieldsUsedExcept() + .enableUnusedFieldPolicy(FailOnUnused) .transform ==> AnotherRectangle(p1 = Point(0, 0), PPPP = Point(2, 2)) } - test("should pass if all required source fields are used (enableCustomFieldNameComparison)") { + test("should not fail transform if all source fields are used (enableCustomFieldNameComparison)") { case class AnotherRectangle(p1: Point, P2: Point) Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) .into[AnotherRectangle] - .requireSourceFieldsUsedExcept() + .enableUnusedFieldPolicy(FailOnUnused) .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) .transform ==> AnotherRectangle(p1 = Point(0, 0), P2 = Point(2, 2)) } - test("should pass if all required source fields are used (withFieldRenamed + enableCustomFieldNameComparison)") { + test("should not fail transform if all source fields are used (withFieldRenamed + enableCustomFieldNameComparison)") { case class AnotherTriangle(p1: Point, P2: Point, PPPP: Point) Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) .into[AnotherTriangle] .withFieldRenamed(_.p3, _.PPPP) - .requireSourceFieldsUsedExcept() + .enableUnusedFieldPolicy(FailOnUnused) .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) .transform ==> AnotherTriangle(p1 = Point(0, 0), P2 = Point(2, 2), PPPP = Point(2, 0)) } - test( - "should pass if all required source fields are either used or included in exceptions (withFieldRenamed + enableCustomFieldNameComparison)" - ) { + test("should fail transform if unused source fields exist (withFieldRenamed + enableCustomFieldNameComparison)") { + @unused case class AnotherTriangle(p11111: Point, P2: Point, PPPP: Point) - // without p1 in exceptions compileErrors( """ Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) .into[AnotherTriangle] .withFieldRenamed(_.p3, _.PPPP) .withFieldConst(_.p11111, Point(0, 0)) - .requireSourceFieldsUsedExcept() + .enableUnusedFieldPolicy(FailOnUnused) .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) .transform ==> AnotherTriangle(p11111 = Point(0, 0), P2 = Point(2, 2), PPPP = Point(2, 0)) """ ).check( "Chimney can't derive transformation from io.scalaland.chimney.fixtures.shapes1.Triangle to io.scalaland.chimney.TotalTransformerProductSpec.AnotherTriangle", - "field(s) p1 of io.scalaland.chimney.fixtures.shapes1.Triangle are required to be used in the transformation but are not used!" + "field(s) p1 of io.scalaland.chimney.fixtures.shapes1.Triangle were required to be used in the transformation but are not used!" ) + } + + test( + "should not fail transform if unused source fields exist but are ignored through .withIgnoreUnusedField (withFieldRenamed + enableCustomFieldNameComparison)" + ) { + case class AnotherTriangle(p11111: Point, P2: Point, PPPP: Point) - // with p1 in exceptions Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) .into[AnotherTriangle] .withFieldRenamed(_.p3, _.PPPP) .withFieldConst(_.p11111, Point(0, 0)) - .requireSourceFieldsUsedExcept(_.p1) + .withIgnoreUnusedField(_.p1) + .enableUnusedFieldPolicy(FailOnUnused) .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) .transform ==> AnotherTriangle(p11111 = Point(0, 0), P2 = Point(2, 2), PPPP = Point(2, 0)) } } + group("flag .disableUnusedFieldPolicy") { + import shapes1.{Point, Rectangle, Triangle} + + test("should disable globally enabled .enableUnusedFieldPolicy") { + @unused implicit val config = TransformerConfiguration.default.enableUnusedFieldPolicy(FailOnUnused) + + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[Rectangle] + .disableUnusedFieldPolicy + .transform ==> Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) + } + } + group("flag .enableDefaultValues") { test("should be disabled by default") { diff --git a/docs/docs/supported-transformations.md b/docs/docs/supported-transformations.md index 85466e526..870adc068 100644 --- a/docs/docs/supported-transformations.md +++ b/docs/docs/supported-transformations.md @@ -759,11 +759,10 @@ If the flag was enabled in the implicit config it can be disabled with `.disable // Consult https://chimney.readthedocs.io for usage examples. ``` -### Require source fields to be used +### Unused source fields policies If you want to enforce that every field of the source type is used in the transformation, you can enable the -`.requireSourceFieldsUsedExcept` setting. This setting also allows you to specify a certain subset of fields to be -exempt from this requirement. +`.enableUnusedFieldPolicy(FailOnUnused)` setting. !!! example @@ -776,19 +775,33 @@ exempt from this requirement. Source("value", 512, "anotherValue") .into[Target] - .requireSourceFieldsUsedExcept() + .enableUnusedFieldPolicy(FailOnUnused) .transform // Chimney can't derive transformation from Source to Target // // Target - // field(s) b, c of Source are required to be used in the transformation but are not used! + // field(s) b, c of Source were required to be used in the transformation but are not used! // // Consult https://chimney.readthedocs.io for usage examples. + ``` + +The setting `.withIgnoreUnusedField` allows you to specify a certain subset of fields to be ignored if left unused. + +!!! example + + ```scala + //> using dep io.scalaland::chimney::{{ chimney_version() }} + import io.scalaland.chimney.dsl._ + + case class Source(a: String, b: Int, c: String) + case class Target(a: String) pprint.pprintln( Source("value", 512, "anotherValue") .into[Target] - .requireSourceFieldsUsedExcept(_.b, _.c) + .enableUnusedFieldPolicy(FailOnUnused) + .withIgnoreUnusedField(_.b) + .withIgnoreUnusedField(_.c) .transform ) // expected output: From 633bcf7a0bce734e9259d67df537f451aa174013 Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Tue, 1 Oct 2024 09:25:03 -0700 Subject: [PATCH 11/12] feat: add API to TransformerDefinition --- .../chimney/dsl/TransformerDefinition.scala | 13 +++++++++++++ .../dsl/TransformerDefinitionMacros.scala | 13 +++++++++++++ .../chimney/dsl/TransformerDefinition.scala | 14 ++++++++++++++ .../dsl/TransformerDefinitionMacros.scala | 17 +++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerDefinition.scala b/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerDefinition.scala index f8b82a2da..b5690c4a9 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerDefinition.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerDefinition.scala @@ -218,6 +218,19 @@ final class TransformerDefinition[From, To, Overrides <: TransformerOverrides, F )(implicit ev: IsFunction.Of[Ctor, To]): TransformerDefinition[From, To, ? <: TransformerOverrides, Flags] = macro TransformerDefinitionMacros.withConstructorImpl[From, To, Overrides, Flags] + /** Ignore if a source field is not used in the transformation. This can be useful when `.enableUnusedFieldPolicy` is + * enabled. + * + * @param selectorFrom + * the field is that not required to be used in the transformation + * @return + * [[io.scalaland.chimney.dsl.TransformerDefinition]] + * + * @since 1.5.0 + */ + def withIgnoreUnusedField(selectorFrom: From => ?): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = + macro TransformerDefinitionMacros.withIgnoreUnusedField[From, To, Overrides, Flags] + /** Build Transformer using current configuration. * * It runs macro that tries to derive instance of `Transformer[From, To]`. When transformation can't be derived, it diff --git a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala index c68ea179d..f8fc8728f 100644 --- a/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala +++ b/chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala @@ -96,4 +96,17 @@ class TransformerDefinitionMacros(val c: whitebox.Context) extends utils.DslMacr .addOverride(f) .asInstanceOfExpr[TransformerDefinition[From, To, Constructor[Args, Path.Root, Overrides], Flags]] }.applyFromBody(f) + + def withIgnoreUnusedField[ + From: WeakTypeTag, + To: WeakTypeTag, + Overrides <: TransformerOverrides : WeakTypeTag, + Flags <: TransformerFlags : WeakTypeTag + ](selectorFrom: Tree): Tree = c.prefix.tree + .asInstanceOfExpr( + new ApplyFieldNameType { + def apply[FromPath <: Path : WeakTypeTag]: c.WeakTypeTag[?] = + weakTypeTag[TransformerDefinition[From, To, IgnoreUnusedField[FromPath, Overrides], Flags]] + }.applyFromSelector(selectorFrom) + ) } diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerDefinition.scala b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerDefinition.scala index 19c0e9cb9..c11a9b756 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerDefinition.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerDefinition.scala @@ -230,6 +230,20 @@ final class TransformerDefinition[From, To, Overrides <: TransformerOverrides, F )(using IsFunction.Of[Ctor, To]): TransformerDefinition[From, To, ? <: TransformerOverrides, Flags] = ${ TransformerDefinitionMacros.withConstructorImpl('this, 'f) } + /** Ignore if a source field is not used in the transformation. This can be useful when `.enableUnusedFieldPolicy` is + * enabled. + * + * @param selectorFrom + * the field is that not required to be used in the transformation + * @return + * [[io.scalaland.chimney.dsl.TransformerInto]] + * @since 1.5.0 + */ + transparent inline def withIgnoreUnusedField( + inline selectorFrom: From => ? + ): TransformerDefinition[From, To, ? <: TransformerOverrides, Flags] = + ${ TransformerDefinitionMacros.withIgnoreUnusedFieldImpl('this, 'selectorFrom) } + /** Build Transformer using current configuration. * * It runs macro that tries to derive instance of `Transformer[From, To]`. When transformation can't be derived, it diff --git a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala index 88b80098c..693cd51d8 100644 --- a/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala +++ b/chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/dsl/TransformerDefinitionMacros.scala @@ -145,4 +145,21 @@ object TransformerDefinitionMacros { .asInstanceOf[TransformerDefinition[From, To, Constructor[args, Path.Root, Overrides], Flags]] } }(f) + + def withIgnoreUnusedFieldImpl[ + From: Type, + To: Type, + Overrides <: TransformerOverrides : Type, + Flags <: TransformerFlags : Type + ]( + ti: Expr[TransformerDefinition[From, To, Overrides, Flags]], + fromSelector: Expr[From => ?] + )(using Quotes): Expr[TransformerDefinition[From, To, ? <: TransformerOverrides, Flags]] = + DslMacroUtils().applyFieldNameType { + [fromPath <: Path] => + (_: Type[fromPath]) ?=> + '{ + $ti.asInstanceOf[TransformerDefinition[From, To, IgnoreUnusedField[fromPath, Overrides], Flags]] + } + }(fromSelector) } From 8803e72c7d095670aa735cb6f0edfad5766c1ae9 Mon Sep 17 00:00:00 2001 From: Naeim Taheri Date: Tue, 1 Oct 2024 13:19:44 -0700 Subject: [PATCH 12/12] refactor: move source-field tracker to product-to-product rule --- .../compiletime/DerivationResult.scala | 15 --- .../TransformProductToProductRuleModule.scala | 108 +++++++++++------- 2 files changed, 64 insertions(+), 59 deletions(-) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala index 11a74caa5..6e620534f 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationResult.scala @@ -107,16 +107,6 @@ sealed private[compiletime] trait DerivationResult[+A] { } } - // tracking - - final def registerSourceFieldUseOnSuccess(field: String): DerivationResult[A] = this match { - case _: Success[?] => - updateState(_.appendToUsedSourceFields(field)).logSuccess(_ => s"source field $field usage registered") - case _: Failure => this - } - - final def withUsedSourceFields: DerivationResult[(Set[String], A)] = map(a => (state.usedSourceFields, a)) - // logging final def log(msg: => String): DerivationResult[A] = updateState(_.log(msg)) @@ -146,21 +136,16 @@ private[compiletime] object DerivationResult { final case class State( journal: Log.Journal = Log.Journal(logs = Vector.empty), - usedSourceFields: Set[String] = Set.empty, macroLogging: Option[State.MacroLogging] = None ) { private[DerivationResult] def log(msg: => String): State = copy(journal = journal.append(msg)) - private[DerivationResult] def appendToUsedSourceFields(field: String): State = - copy(usedSourceFields = usedSourceFields + field) - private[DerivationResult] def nestScope(scopeName: String): State = copy(journal = Log.Journal(Vector(Log.Scope(scopeName = scopeName, journal = journal)))) private[DerivationResult] def appendedTo(previousState: State): State = State( journal = Log.Journal(logs = previousState.journal.logs ++ this.journal.logs), - usedSourceFields = previousState.usedSourceFields ++ usedSourceFields, macroLogging = previousState.macroLogging.orElse(macroLogging) ) } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala index d92dd08a6..c6d9dae0a 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala @@ -1,6 +1,6 @@ package io.scalaland.chimney.internal.compiletime.derivation.transformer.rules -import io.scalaland.chimney.dsl.FailOnUnused +import io.scalaland.chimney.dsl.{ActionOnUnused, FailOnUnused} import io.scalaland.chimney.internal.compiletime.{DerivationErrors, DerivationResult} import io.scalaland.chimney.internal.compiletime.derivation.transformer.Derivation import io.scalaland.chimney.internal.compiletime.fp.Implicits.* @@ -121,7 +121,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio .parTraverse[ DerivationResult, (String, Existential[Product.Parameter]), - (String, Existential[TransformationExpr]) + ResolvedArgument ]( if (flags.nonUnitBeanSetters) parameters.toList else @@ -172,15 +172,22 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio .orElse(filterCurrentOverridesForField(areFieldNamesMatching(_, toName)).headOption) .map { case AmbiguousOverrides(overrideName, foundOverrides) => - DerivationResult.ambiguousFieldOverrides[From, To, Existential[TransformationExpr]]( + DerivationResult.ambiguousFieldOverrides[From, To, ResolvedArgument]( overrideName, foundOverrides, flags.getFieldNameComparison.toString ) case (_, value) => - useOverride[From, To, CtorParam](toName, value).flatMap( - DerivationResult.existential[TransformationExpr, CtorParam](_) - ) + useOverride[From, To, CtorParam](toName, value) + .flatMap(DerivationResult.existential[TransformationExpr, CtorParam](_)) + .map { expr => + value match { + case TransformerOverride.RenamedFrom(Path.AtField(sourceName, _)) => + ResolvedArgument(expr, toName, Some(sourceName)) + case _ => + ResolvedArgument(expr, toName) + } + } } .orElse { val ambiguityOrPossibleSourceField = @@ -197,11 +204,11 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio case Right(possibleSourceField) => possibleSourceField.map { case (fromName, toName, getter) => useExtractor[From, To, CtorParam](ctorParam.value.targetType, fromName, toName, getter) - .registerSourceFieldUseOnSuccess(fromName) + .map(ResolvedArgument(_, toName, Some(fromName))) } case Left(foundFromNames) => Some( - DerivationResult.ambiguousFieldSources[From, To, Existential[TransformationExpr]]( + DerivationResult.ambiguousFieldSources[From, To, ResolvedArgument]( foundFromNames, toName ) @@ -211,9 +218,9 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio .orElse { useFallbackValues[From, To, CtorParam]( defaultValue.orElse(summonDefaultValue[CtorParam].map(_.provide())) - ) + ).map(_.map(ResolvedArgument(_, toName))) } - .getOrElse[DerivationResult[Existential[TransformationExpr]]] { + .getOrElse[DerivationResult[ResolvedArgument]] { if (usePositionBasedMatching) DerivationResult.tupleArityMismatch(fromArity = fromEnabledExtractors.size, toArity = parameters.size) else { @@ -230,7 +237,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio ctorParam.value.targetType match { case Product.Parameter.TargetType.ConstructorParameter => DerivationResult - .missingConstructorArgument[From, To, CtorParam, Existential[TransformationExpr]]( + .missingConstructorArgument[From, To, CtorParam, ResolvedArgument]( toName, availableMethodAccessors, availableInheritedAccessors, @@ -244,7 +251,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio DerivationResult.pure(nonUnitSetter) case Product.Parameter.TargetType.SetterParameter(_) => DerivationResult - .missingJavaBeanSetterParam[From, To, CtorParam, Existential[TransformationExpr]]( + .missingJavaBeanSetterParam[From, To, CtorParam, ResolvedArgument]( toName, availableMethodAccessors, availableInheritedAccessors, @@ -257,37 +264,18 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio case `unmatchedSetter` => s"Setter `$toName` not resolved but ignoring setters is allowed" case `nonUnitSetter` => s"Setter `$toName` not resolved it has non-Unit return type and they are ignored" - case expr => s"Resolved `$toName` field value to ${expr.value.prettyPrint}" - } - .map(toName -> _) - } - .map(_.filterNot(_._2 == unmatchedSetter).filterNot(_._2 == nonUnitSetter)) - .withUsedSourceFields - .logSuccess { case (usedSourceFields, _) => - if (ctx.config.flags.unusedFieldPolicy.contains(FailOnUnused)) { - "unused-field-policy(fail-on-used) is enabled\n" + - s"all source fields: ${fromEnabledExtractors.keySet.mkString(",")}\n" + - s"used fields: ${usedSourceFields.mkString(",")}) \n" + - s"ignore unused field(s) ${ctx.config.getIgnoreUnusedFields.mkString(",")}" - } else "" - } - .flatMap { case (usedSourceFields, res) => - Option - .when(ctx.config.flags.unusedFieldPolicy.contains(FailOnUnused)) { - fromEnabledExtractors.keySet -- ctx.config.getIgnoreUnusedFields -- usedSourceFields // unused but required fields + case resolvedArgument => s"Resolved `$toName` field value to ${resolvedArgument.expr.value.prettyPrint}" } - .filter(_.nonEmpty) - .map(DerivationResult.requiredFieldNotUsed[From, To, List[(String, Existential[TransformationExpr])]](_)) - .getOrElse(DerivationResult.pure(res)) } + .map(_.filterNot(Seq(unmatchedSetter, nonUnitSetter).contains)) + .flatMap(verifyUnusedFieldPolicies(fromEnabledExtractors.keySet, _)) .logSuccess { args => - val totals = args.count(_._2.value.isTotal) - val partials = args.count(_._2.value.isPartial) + val totals = args.count(_.expr.value.isTotal) + val partials = args.count(_.expr.value.isPartial) s"Resolved ${args.size} arguments, $totals as total and $partials as partial Expr" } - .map[TransformationExpr[ToOrPartialTo]] { - (resolvedArguments: List[(String, Existential[TransformationExpr])]) => - wireArgumentsToConstructor[From, To, ToOrPartialTo](resolvedArguments, constructor) + .map[TransformationExpr[ToOrPartialTo]] { resolvedArguments => + wireArgumentsToConstructor[From, To, ToOrPartialTo](resolvedArguments, constructor) } .flatMap(DerivationResult.expanded) } @@ -388,7 +376,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio ) case (name, getter) :: Nil => import getter.Underlying as Getter, getter.value.get - DerivationResult.pure(get(extractedSrcExpr).as_??).registerSourceFieldUseOnSuccess(name) + DerivationResult.pure(get(extractedSrcExpr).as_??) case matchingGetters => DerivationResult.ambiguousFieldOverrides[From, To, ExistentialExpr]( sourceName, @@ -524,15 +512,15 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio } private def wireArgumentsToConstructor[From, To, ToOrPartialTo: Type]( - resolvedArguments: List[(String, Existential[TransformationExpr])], + resolvedArguments: List[ResolvedArgument], constructor: Product.Arguments => Expr[ToOrPartialTo] )(implicit ctx: TransformationContext[From, To]): TransformationExpr[ToOrPartialTo] = { val totalConstructorArguments: Map[String, ExistentialExpr] = resolvedArguments.collect { - case (name, exprE) if exprE.value.isTotal => name -> exprE.mapK[Expr](_ => _.ensureTotal) + case ResolvedArgument(exprE, name, _) if exprE.value.isTotal => name -> exprE.mapK[Expr](_ => _.ensureTotal) }.toMap resolvedArguments.collect { - case (name, exprE) if exprE.value.isPartial => + case ResolvedArgument(exprE, name, _) if exprE.value.isPartial => name -> exprE.mapK[PartialExpr] { implicit ExprE: Type[exprE.Underlying] => _.ensurePartial } } match { case Nil => @@ -763,10 +751,42 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio newError.parTuple(oldErrors).map[Nothing](_ => ???) } + private def verifyUnusedFieldPolicies[From, To]( + allSourceFields: Set[String], + resolvedArguments: List[ResolvedArgument] + )(implicit ctx: TransformationContext[From, To]): DerivationResult[List[ResolvedArgument]] = { + val used = resolvedArguments.flatMap(_.fromSourceField).toSet + val ignored = ctx.config.getIgnoreUnusedFields + val fatalUnused = allSourceFields -- ignored -- used + + val failOnUnused = + if (ctx.config.flags.unusedFieldPolicy.contains(FailOnUnused)) { + val result = + if (fatalUnused.nonEmpty) DerivationResult.requiredFieldNotUsed[From, To, Unit](fatalUnused) + else DerivationResult.unit + result.log { + "UnusedFieldPolicy(FailOnUsed) is enabled\n" + + s"all source fields: ${allSourceFields.mkString(",")}\n" + + s"used fields: ${used.mkString(",")}) \n" + + s"ignore unused field(s) ${ignored.mkString(",")}" + } + } else DerivationResult.unit + + failOnUnused.map(_ => resolvedArguments) + } + // Stub to use when the setter's return type is not Unit and nonUnitBeanSetters flag is off. - private val nonUnitSetter = Existential[TransformationExpr, Null](TransformationExpr.fromTotal(Expr.Null)) + private val nonUnitSetter = + ResolvedArgument(Existential[TransformationExpr, Null](TransformationExpr.fromTotal(Expr.Null)), "") // Stub to use when the setter's was not matched and beanSettersIgnoreUnmatched flag is on. - private val unmatchedSetter = Existential[TransformationExpr, Null](TransformationExpr.fromTotal(Expr.Null)) + private val unmatchedSetter = + ResolvedArgument(Existential[TransformationExpr, Null](TransformationExpr.fromTotal(Expr.Null)), "") } + + private case class ResolvedArgument( + expr: Existential[TransformationExpr], + toTargetField: String, + fromSourceField: Option[String] = None + ) }