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/dsl/TransformerInto.scala b/chimney/src/main/scala-2/io/scalaland/chimney/dsl/TransformerInto.scala index 1e8be563f..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,6 +205,19 @@ final class TransformerInto[From, To, Overrides <: TransformerOverrides, Flags < ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = macro TransformerIntoMacros.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.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. * * 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..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,6 +252,17 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi ) } } + object IgnoreUnusedField extends IgnoreUnusedFieldModule { + def apply[ + 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)) + } + } } object TransformerFlags extends TransformerFlagsModule { @@ -309,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]] 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-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..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 @@ -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 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[TransformerInto[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/dsl/TransformerInto.scala b/chimney/src/main/scala-3/io/scalaland/chimney/dsl/TransformerInto.scala index b0e1bbd50..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,6 +211,21 @@ final class TransformerInto[From, To, Overrides <: TransformerOverrides, Flags < )(using IsFunction.Of[Ctor, To]): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = ${ TransformerIntoMacros.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 => ? + ): TransformerInto[From, To, ? <: TransformerOverrides, Flags] = + ${ TransformerIntoMacros.withIgnoreUnusedFieldImpl('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 4cc5d154c..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,6 +222,19 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi case _ => scala.None } } + object IgnoreUnusedField extends IgnoreUnusedFieldModule { + def apply[ + FromPath <: runtime.Path: Type, + Tail <: runtime.TransformerOverrides: Type + ]: 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.IgnoreUnusedField[fromPath, cfg]] => + Some((Type[fromPath].as_?<[runtime.Path], Type[cfg].as_?<[runtime.TransformerOverrides])) + case _ => scala.None + } + } } object TransformerFlags extends TransformerFlagsModule { @@ -286,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/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) } 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..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 @@ -143,4 +143,21 @@ object TransformerIntoMacros { .asInstanceOf[TransformerInto[From, To, Constructor[args, Path.Root, Overrides], Flags]] } }(f) + + def withIgnoreUnusedFieldImpl[ + From: Type, + To: Type, + Overrides <: TransformerOverrides: Type, + Flags <: TransformerFlags: Type + ]( + ti: Expr[TransformerInto[From, To, Overrides, Flags]], + fromSelector: Expr[From => ?] + )(using Quotes): Expr[TransformerInto[From, To, ? <: TransformerOverrides, Flags]] = + DslMacroUtils().applyFieldNameType { + [fromPath <: Path] => + (_: Type[fromPath]) ?=> + '{ + $ti.asInstanceOf[TransformerInto[From, To, IgnoreUnusedField[fromPath, Overrides], Flags]] + } + }(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 48b421a0c..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 @@ -307,4 +307,5 @@ private[chimney] class DslMacroUtils()(using quotes: Quotes) { case Right(ctorType) => f(using ctorType.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 f799f2c59..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 @@ -151,6 +153,14 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => runtime.TransformerOverrides, runtime.TransformerOverrides.RenamedTo ] { this: RenamedTo.type => } + + val IgnoreUnusedField: IgnoreUnusedFieldModule + trait IgnoreUnusedFieldModule + extends Type.Ctor2UpperBounded[ + runtime.Path, + runtime.TransformerOverrides, + runtime.TransformerOverrides.IgnoreUnusedField + ] { this: IgnoreUnusedField.type => } } val TransformerFlags: TransformerFlagsModule @@ -196,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 67001cabe..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 @@ -76,6 +76,11 @@ final case class NotSupportedTransformerDerivation( )(val fromType: String, val toType: String) extends TransformerDerivationError +final case class UnusedButRequiredToUseSourceFields( + unusedFields: 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 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 f90e36a32..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,8 +236,10 @@ private[compiletime] trait Configurations { this: Derivation => } } + 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 @@ -264,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) "" @@ -278,7 +294,7 @@ 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, ) { private lazy val runtimeOverridesForCurrent = runtimeOverrides.filter { @@ -384,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) @@ -391,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(", ") @@ -469,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) } @@ -484,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) } @@ -568,6 +603,14 @@ private[compiletime] trait Configurations { this: Derivation => extractPath[FromPath], TransformerOverride.RenamedTo(extractPath[ToPath]) ) + 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!") + } // $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 8fb4ef269..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 @@ -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( + unusedFields = 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/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala index 4ebf800a3..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,5 +1,6 @@ package io.scalaland.chimney.internal.compiletime.derivation.transformer.rules +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.* @@ -120,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 @@ -171,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 = @@ -196,10 +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) + .map(ResolvedArgument(_, toName, Some(fromName))) } case Left(foundFromNames) => Some( - DerivationResult.ambiguousFieldSources[From, To, Existential[TransformationExpr]]( + DerivationResult.ambiguousFieldSources[From, To, ResolvedArgument]( foundFromNames, toName ) @@ -209,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 { @@ -228,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, @@ -242,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, @@ -255,19 +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}" + case resolvedArgument => s"Resolved `$toName` field value to ${resolvedArgument.expr.value.prettyPrint}" } - .map(toName -> _) } - .map(_.filterNot(_._2 == unmatchedSetter).filterNot(_._2 == nonUnitSetter)) + .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) } @@ -366,7 +374,7 @@ 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_??) case matchingGetters => @@ -504,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 => @@ -743,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 + ) } 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 2ca37e2ad..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,4 +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 + // 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 a5b807c24..aefca2db6 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala @@ -519,6 +519,111 @@ class TotalTransformerProductSpec extends ChimneySpec { } } + group("flag .enableUnusedFieldPolicy") { + import shapes1.{Point, Rectangle, Triangle} + + 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] + .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 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") { + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[Rectangle] + .withIgnoreUnusedField(_.p3) + .enableUnusedFieldPolicy(FailOnUnused) + .transform ==> Rectangle(p1 = Point(0, 0), p2 = Point(2, 2)) + } + + 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) + .enableUnusedFieldPolicy(FailOnUnused) + .transform ==> AnotherRectangle(p1 = Point(0, 0), PPPP = Point(2, 2)) + } + + 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] + .enableUnusedFieldPolicy(FailOnUnused) + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> AnotherRectangle(p1 = Point(0, 0), P2 = Point(2, 2)) + } + + 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) + .enableUnusedFieldPolicy(FailOnUnused) + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> AnotherTriangle(p1 = Point(0, 0), P2 = Point(2, 2), PPPP = Point(2, 0)) + } + + test("should fail transform if unused source fields exist (withFieldRenamed + enableCustomFieldNameComparison)") { + @unused + case class AnotherTriangle(p11111: Point, P2: Point, PPPP: Point) + + compileErrors( + """ + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[AnotherTriangle] + .withFieldRenamed(_.p3, _.PPPP) + .withFieldConst(_.p11111, Point(0, 0)) + .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 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) + + Triangle(p1 = Point(0, 0), p2 = Point(2, 2), p3 = Point(2, 0)) + .into[AnotherTriangle] + .withFieldRenamed(_.p3, _.PPPP) + .withFieldConst(_.p11111, Point(0, 0)) + .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 74318232a..870adc068 100644 --- a/docs/docs/supported-transformations.md +++ b/docs/docs/supported-transformations.md @@ -759,6 +759,55 @@ If the flag was enabled in the implicit config it can be disabled with `.disable // Consult https://chimney.readthedocs.io for usage examples. ``` +### 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 +`.enableUnusedFieldPolicy(FailOnUnused)` setting. + +!!! 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] + .enableUnusedFieldPolicy(FailOnUnused) + .transform + // Chimney can't derive transformation from Source to Target + // + // Target + // 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] + .enableUnusedFieldPolicy(FailOnUnused) + .withIgnoreUnusedField(_.b) + .withIgnoreUnusedField(_.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 -