From 008e7cd852bd154f7cef3508618e0367b7ae14f3 Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Thu, 14 Mar 2024 00:46:07 +0100 Subject: [PATCH 01/13] Proof of concept of a way of customizing field/subtype name comparison during macro expansion --- .../compiletime/ChimneyTypesPlatform.scala | 17 +++++++ .../compiletime/ChimneyTypesPlatform.scala | 17 +++++++ .../dsl/TransformedNamesComparison.scala | 33 ++++++++++++++ .../chimney/dsl/TransformerFlagsDsl.scala | 44 +++++++++++++++++++ .../internal/compiletime/ChimneyTypes.scala | 12 +++++ .../transformer/Configurations.scala | 35 +++++++++++++++ .../internal/runtime/TransformerFlags.scala | 4 +- 7 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala 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 b1eb2d45a..72128870f 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 @@ -226,6 +226,23 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi Some(A.param_<[dsls.ImplicitTransformerPreference](0)) else scala.None } + object FieldNameComparison extends FieldNameComparisonModule { + def apply[C <: dsls.TransformedNamesComparison: Type]: Type[runtime.TransformerFlags.FieldNameComparison[C]] = + weakTypeTag[runtime.TransformerFlags.FieldNameComparison[C]] + def unapply[A](A: Type[A]): Option[?<[dsls.TransformedNamesComparison]] = + if (A.isCtor[runtime.TransformerFlags.FieldNameComparison[?]]) + Some(A.param_<[dsls.TransformedNamesComparison](0)) + else scala.None + } + object SubtypeNameComparison extends SubtypeNameComparisonModule { + def apply[C <: dsls.TransformedNamesComparison: Type] + : Type[runtime.TransformerFlags.SubtypeNameComparison[C]] = + weakTypeTag[runtime.TransformerFlags.SubtypeNameComparison[C]] + def unapply[A](A: Type[A]): Option[?<[dsls.TransformedNamesComparison]] = + if (A.isCtor[runtime.TransformerFlags.SubtypeNameComparison[?]]) + Some(A.param_<[dsls.TransformedNamesComparison](0)) + else scala.None + } val MacrosLogging: Type[runtime.TransformerFlags.MacrosLogging] = weakTypeTag[runtime.TransformerFlags.MacrosLogging] } 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 f059a5d4d..6f85e037e 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 @@ -216,6 +216,23 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi Some(Type[r].as_?<[dsls.ImplicitTransformerPreference]) 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]] + def unapply[A](tpe: Type[A]): Option[?<[dsls.TransformedNamesComparison]] = tpe match + case '[runtime.TransformerFlags.FieldNameComparison[c]] => + Some(Type[c].as_?<[dsls.TransformedNamesComparison]) + case _ => scala.None + } + object SubtypeNameComparison extends SubtypeNameComparisonModule { + def apply[C <: dsls.TransformedNamesComparison: Type] + : Type[runtime.TransformerFlags.SubtypeNameComparison[C]] = + quoted.Type.of[runtime.TransformerFlags.SubtypeNameComparison[C]] + def unapply[A](tpe: Type[A]): Option[?<[dsls.TransformedNamesComparison]] = tpe match + case '[runtime.TransformerFlags.SubtypeNameComparison[c]] => + Some(Type[c].as_?<[dsls.TransformedNamesComparison]) + case _ => scala.None + } val MacrosLogging: Type[runtime.TransformerFlags.MacrosLogging] = quoted.Type.of[runtime.TransformerFlags.MacrosLogging] } diff --git a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala new file mode 100644 index 000000000..ba5991052 --- /dev/null +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala @@ -0,0 +1,33 @@ +package io.scalaland.chimney.dsl + +// TODO: documentation + +abstract class TransformedNamesComparison { this: Singleton => + + def namesMatch(fromName: String, toName: String): Boolean +} +object TransformedNamesComparison { + + object BeanAware extends TransformedNamesComparison { + + def namesMatch(fromName: String, toName: String): Boolean = + // TODO: move logic here, so that dsl would not have a dependency on internal.compiletime + io.scalaland.chimney.internal.compiletime.datatypes.ProductTypes.areNamesMatching(fromName, toName) + } + + object StrictEquality extends TransformedNamesComparison { + + def namesMatch(fromName: String, toName: String): Boolean = fromName == toName + } + + object CaseInsensitiveEquality extends TransformedNamesComparison { + + def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) + } + + type FieldDefault = BeanAware.type + val FieldDefault: FieldDefault = BeanAware + + type SubtypeDefault = StrictEquality.type + val SubtypeDefault: SubtypeDefault = StrictEquality +} 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 e7bdcc9e4..ccb676b12 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala @@ -196,6 +196,50 @@ private[dsl] trait TransformerFlagsDsl[UpdateFlag[_ <: TransformerFlags], Flags def disableImplicitConflictResolution: UpdateFlag[Disable[ImplicitConflictResolution[?], Flags]] = disableFlag[ImplicitConflictResolution[?]] + /** Enable custom way of comparing if source fields' names and target fields' names are matching. + * + * @param namesComparison parameter specifying how names should be compared by macro + * + * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * + * @since 1.0.0 + */ + def enableCustomFieldNameComparison[C <: TransformedNamesComparison & Singleton]( + @unused namesComparison: C + ): UpdateFlag[Enable[FieldNameComparison[C], Flags]] = + enableFlag[FieldNameComparison[C]] + + /** Disable any custom way of comparing if source fields' names and target fields' names are matching. + * + * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * + * @since 1.0.0 + */ + def disableCustomFieldNameComparison: UpdateFlag[Disable[FieldNameComparison[?], Flags]] = + disableFlag[FieldNameComparison[?]] + + /** Enable custom way of comparing if source subtypes' names and target fields' names are matching. + * + * @param namesComparison parameter specifying how names should be compared by macro + * + * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * + * @since 1.0.0 + */ + def enableCustomSubtypeNameComparison[C <: TransformedNamesComparison & Singleton]( + @unused namesComparison: C + ): UpdateFlag[Enable[SubtypeNameComparison[C], Flags]] = + enableFlag[SubtypeNameComparison[C]] + + /** Disable any custom way of comparing if source subtypes' names and target fields' names are matching. + * + * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * + * @since 1.0.0 + */ + def disableCustomSubtypeNameComparison: UpdateFlag[Disable[SubtypeNameComparison[?], Flags]] = + disableFlag[SubtypeNameComparison[?]] + /** Enable printing the logs from the derivation process. * * @see [[https://chimney.readthedocs.io/troubleshooting/#debugging-macros]] for more details 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 636451992..1f6def83e 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 @@ -181,6 +181,18 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions => dsls.ImplicitTransformerPreference, runtime.TransformerFlags.ImplicitConflictResolution ] { this: ImplicitConflictResolution.type => } + val FieldNameComparison: FieldNameComparisonModule + trait FieldNameComparisonModule + extends Type.Ctor1UpperBounded[ + dsls.TransformedNamesComparison, + runtime.TransformerFlags.FieldNameComparison + ] { this: FieldNameComparison.type => } + val SubtypeNameComparison: SubtypeNameComparisonModule + trait SubtypeNameComparisonModule + extends Type.Ctor1UpperBounded[ + dsls.TransformedNamesComparison, + runtime.TransformerFlags.SubtypeNameComparison + ] { this: SubtypeNameComparison.type => } val MacrosLogging: Type[runtime.TransformerFlags.MacrosLogging] } } 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 27b0ce181..32cc71cba 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 @@ -19,6 +19,8 @@ private[compiletime] trait Configurations { this: Derivation => optionDefaultsToNone: Boolean = false, partialUnwrapsOption: Boolean = true, implicitConflictResolution: Option[ImplicitTransformerPreference] = None, + fieldNameComparison: dsls.TransformedNamesComparison = dsls.TransformedNamesComparison.FieldDefault, + subtypeNameComparison: dsls.TransformedNamesComparison = dsls.TransformedNamesComparison.SubtypeDefault, displayMacrosLogging: Boolean = false ) { @@ -50,6 +52,12 @@ private[compiletime] trait Configurations { this: Derivation => def setImplicitConflictResolution(preference: Option[ImplicitTransformerPreference]): TransformerFlags = copy(implicitConflictResolution = preference) + def setFieldNameComparison(nameComparison: dsls.TransformedNamesComparison): TransformerFlags = + copy(fieldNameComparison = nameComparison) + + def setSubtypeNameComparison(nameComparison: dsls.TransformedNamesComparison): TransformerFlags = + copy(subtypeNameComparison = nameComparison) + override def toString: String = s"TransformerFlags(${Vector( if (inheritedAccessors) Vector("inheritedAccessors") else Vector.empty, if (methodAccessors) Vector("methodAccessors") else Vector.empty, @@ -274,6 +282,16 @@ private[compiletime] trait Configurations { this: Derivation => reportError("Invalid ImplicitTransformerPreference type!!") // $COVERAGE-ON$ } + case ChimneyType.TransformerFlags.Flags.FieldNameComparison(c) => + import c.Underlying as Comparison + extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison( + extractNameComparisonObject[Comparison] + ) + case ChimneyType.TransformerFlags.Flags.SubtypeNameComparison(c) => + import c.Underlying as Comparison + extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison( + extractNameComparisonObject[Comparison] + ) case _ => extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = true) } @@ -282,6 +300,14 @@ private[compiletime] trait Configurations { this: Derivation => Flag match { case ChimneyType.TransformerFlags.Flags.ImplicitConflictResolution(_) => extractTransformerFlags[Flags2](defaultFlags).setImplicitConflictResolution(None) + case ChimneyType.TransformerFlags.Flags.FieldNameComparison(_) => + extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison( + dsls.TransformedNamesComparison.FieldDefault + ) + case ChimneyType.TransformerFlags.Flags.SubtypeNameComparison(_) => + extractTransformerFlags[Flags2](defaultFlags).setSubtypeNameComparison( + dsls.TransformedNamesComparison.SubtypeDefault + ) case _ => extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = false) } @@ -371,5 +397,14 @@ private[compiletime] trait Configurations { this: Derivation => reportError(s"Invalid internal Path shape: ${Type.prettyPrint[Field]}!!") // $COVERAGE-ON$ } + + private def extractNameComparisonObject[Comparison <: dsls.TransformedNamesComparison: Type]: Comparison = + // TODO: implement this based on https://github.com/MateuszKubuszok/MacroTypeclass ideas + + // $COVERAGE-OFF$ + reportError( + s"Invalid TransformerNamesComparison type - only global objects are allowed: ${Type.prettyPrint[Comparison]}!!" + ) + // $COVERAGE-ON$ } } 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 5aefa9eeb..ff31a3476 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 +import io.scalaland.chimney.dsl.{ImplicitTransformerPreference, TransformedNamesComparison} sealed abstract class TransformerFlags object TransformerFlags { @@ -18,5 +18,7 @@ object TransformerFlags { final class OptionDefaultsToNone extends Flag final class PartialUnwrapsOption extends Flag final class ImplicitConflictResolution[R <: ImplicitTransformerPreference] extends Flag + final class FieldNameComparison[C <: TransformedNamesComparison] extends Flag + final class SubtypeNameComparison[C <: TransformedNamesComparison] extends Flag final class MacrosLogging extends Flag } From 6839b67d0d16c696fd68bb7d79b3ef49d273cbd4 Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Thu, 14 Mar 2024 11:25:32 +0100 Subject: [PATCH 02/13] Drafted code for turning singleton-type into value --- .../transformer/Configurations.scala | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) 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 32cc71cba..525863682 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 @@ -398,13 +398,35 @@ private[compiletime] trait Configurations { this: Derivation => // $COVERAGE-ON$ } - private def extractNameComparisonObject[Comparison <: dsls.TransformedNamesComparison: Type]: Comparison = - // TODO: implement this based on https://github.com/MateuszKubuszok/MacroTypeclass ideas + // TODO: consider moving this utils to Type and requiring <: Singleton type-bound + private val AnsiControlCode = "\u001b\\[([0-9]+)m".r + private def extractNameComparisonObject[Comparison <: dsls.TransformedNamesComparison: Type]: Comparison = { + // based on https://github.com/MateuszKubuszok/MacroTypeclass ideas + object Comparison { + def unapply(className: String): Option[Comparison] = + try + Option(Class.forName(className).getField("MODULE$").get(null).asInstanceOf[Comparison]) + catch { + case _: Throwable => None + } + } - // $COVERAGE-OFF$ - reportError( - s"Invalid TransformerNamesComparison type - only global objects are allowed: ${Type.prettyPrint[Comparison]}!!" - ) - // $COVERAGE-ON$ + // assuming this is "foo.bar.baz"... + val name = AnsiControlCode.replaceAllIn(Type.prettyPrint[Comparison], "") + + Iterator + .iterate(name.replace('.', '$') + '$')(_.replaceFirst("\\$", ".")) + .take(name.count(_ == '.') + 1) // ...then this is: "foo$bar$baz$", "foo.bar$baz$", "foo.bar.baz$"... + .toArray + .reverse // ...and this is: "foo.bar.baz$", "foo.bar$baz$", "foo$bar$baz$" + .collectFirst { case Comparison(value) => value } // attempts: top-level object, object in object, etc + .getOrElse { + // $COVERAGE-OFF$ + reportError( + s"Invalid TransformerNamesComparison type - only global objects are allowed: ${Type.prettyPrint[Comparison]}!!" + ) + // $COVERAGE-ON$ + } + } } } From 1600ab735e71083670c9dfbaf5595d74050ea569 Mon Sep 17 00:00:00 2001 From: saeltz Date: Tue, 19 Mar 2024 23:00:14 +0100 Subject: [PATCH 03/13] Replace all usages of ProductType.areNamesMatching --- .../datatypes/ProductTypesPlatform.scala | 2 +- .../datatypes/ProductTypesPlatform.scala | 2 +- .../compiletime/datatypes/ProductTypes.scala | 18 -------- .../dsl/TransformedNamesComparison.scala | 22 +++++++++- .../transformer/Configurations.scala | 3 -- .../TransformProductToProductRuleModule.scala | 42 ++++++++++++------- .../TransformTypeToValueClassRuleModule.scala | 14 ++++--- ...formValueClassToValueClassRuleModule.scala | 14 ++++--- 8 files changed, 64 insertions(+), 53 deletions(-) diff --git a/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala b/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala index 328eee071..91c489f5d 100644 --- a/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala +++ b/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala @@ -159,7 +159,7 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => if (isVar(setter)) n.stripSuffix("_$eq").stripSuffix("_=") else n name -> setter } - .filter { case (name, _) => !paramTypes.keySet.exists(areNamesMatching(_, name)) } + .filter { case (name, _) => !paramTypes.keySet.contains(name) } .map { case (name, setter) => val termName = setter.asTerm.name.toTermName val tpe = ExistentialType(fromUntyped(paramListsOf(Type[A].tpe, setter).flatten.head.typeSignature)) diff --git a/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala b/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala index 4af7eeff3..92443195a 100644 --- a/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala +++ b/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala @@ -179,7 +179,7 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => .map { setter => setter.name -> setter } - .filter { case (name, _) => !paramTypes.keySet.exists(areNamesMatching(_, name)) } + .filter { case (name, _) => !paramTypes.keySet.contains(name) } .map { case (name, setter) => val tpe = ExistentialType(fromUntyped[Any](paramsWithTypes(A, setter, isConstructor = false).head._2)) ( diff --git a/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala b/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala index e815df448..03e37d2e6 100644 --- a/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala +++ b/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala @@ -98,13 +98,9 @@ trait ProductTypes { this: Definitions => def exprAsInstanceOfMethod[A: Type](args: List[ListMap[String, ??]])(expr: Expr[Any]): Product.Constructor[A] // cached in companion (regexps are expensive to initialize) - def areNamesMatching(fromName: String, toName: String): Boolean = ProductTypes.areNamesMatching(fromName, toName) def isGarbage(name: String): Boolean = ProductTypes.isGarbage(name) def isGetterName(name: String): Boolean = ProductTypes.isGetterName(name) def isSetterName(name: String): Boolean = ProductTypes.isSetterName(name) - def dropGetIs(name: String): String = ProductTypes.dropGetIs(name) - def dropSet(name: String): String = ProductTypes.dropSet(name) - def normalize(name: String): String = ProductTypes.normalize(name) // defaults methods are 1-indexed protected def caseClassApplyDefaultScala2(idx: Int): String = "apply$default$" + idx @@ -167,27 +163,13 @@ object ProductTypes { def isMatching(value: String): Boolean = regexp.pattern.matcher(value).matches() // 2.12 doesn't have .matches } - def areNamesMatching(fromName: String, toName: String): Boolean = - fromName == toName || normalize(fromName) == normalize(toName) - private val getAccessor = raw"(?i)get(.)(.*)".r private val isAccessor = raw"(?i)is(.)(.*)".r - val dropGetIs: String => String = { - case getAccessor(head, tail) => head.toLowerCase + tail - case isAccessor(head, tail) => head.toLowerCase + tail - case other => other - } val isGetterName: String => Boolean = name => getAccessor.isMatching(name) || isAccessor.isMatching(name) private val setAccessor = raw"(?i)set(.)(.*)".r - val dropSet: String => String = { - case setAccessor(head, tail) => head.toLowerCase + tail - case other => other - } val isSetterName: String => Boolean = name => setAccessor.isMatching(name) - val normalize: String => String = dropGetIs.andThen(dropSet) - // methods we can drop from searching scope private val garbage = Set( // constructor diff --git a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala index ba5991052..5ce688bf5 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala @@ -5,24 +5,42 @@ package io.scalaland.chimney.dsl abstract class TransformedNamesComparison { this: Singleton => def namesMatch(fromName: String, toName: String): Boolean + def dropSet(name: String): String } object TransformedNamesComparison { object BeanAware extends TransformedNamesComparison { + private val getAccessor = raw"(?i)get(.)(.*)".r + private val isAccessor = raw"(?i)is(.)(.*)".r + private val setAccessor = raw"(?i)set(.)(.*)".r + + override def dropSet(name: String): String = name match { + case setAccessor(head, tail) => head.toLowerCase + tail + case other => other + } + + private val dropGetIs: String => String = { + case getAccessor(head, tail) => head.toLowerCase + tail + case isAccessor(head, tail) => head.toLowerCase + tail + case other => other + } + private val normalize: String => String = dropGetIs.andThen(dropSet) + def namesMatch(fromName: String, toName: String): Boolean = - // TODO: move logic here, so that dsl would not have a dependency on internal.compiletime - io.scalaland.chimney.internal.compiletime.datatypes.ProductTypes.areNamesMatching(fromName, toName) + fromName == toName || normalize(fromName) == normalize(toName) } object StrictEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName == toName + override def dropSet(name: String): String = name } object CaseInsensitiveEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) + override def dropSet(name: String): String = name } type FieldDefault = BeanAware.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 525863682..099a8ea0b 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 @@ -132,9 +132,6 @@ private[compiletime] trait Configurations { this: Derivation => } } final protected case class DownField(nameFilter: String => Boolean) extends FieldPathUpdate - protected object DownField { - def apply(name: String): DownField = DownField(ProductType.areNamesMatching(_, name)) - } protected case object KeepFieldOverrides extends FieldPathUpdate protected case object CleanFieldOverrides extends FieldPathUpdate 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 b0a247d53..ebfa93297 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 @@ -8,7 +8,7 @@ import io.scalaland.chimney.partial private[compiletime] trait TransformProductToProductRuleModule { this: Derivation => - import Type.Implicits.*, ChimneyType.Implicits.*, ProductType.areNamesMatching + import Type.Implicits.*, ChimneyType.Implicits.* protected object TransformProductToProductRule extends Rule("ProductToProduct") { @@ -96,7 +96,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio val verifyNoOverrideUnused = Traverse[List] .parTraverse( filterOverridesForField(fromName => - !parameters.keys.exists(toName => areNamesMatching(fromName, toName)) + !parameters.keys.exists(toName => flags.fieldNameComparison.namesMatch(fromName, toName)) ).keys.toList ) { fromName => val tpeStr = Type.prettyPrint[To] @@ -133,7 +133,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio import ctorParam.Underlying as CtorParam, ctorParam.value.defaultValue // user might have used _.getName in modifier, to define target we know as _.setName // so simple .get(toName) might not be enough - filterOverridesForField(fromName => areNamesMatching(fromName, toName)).headOption + filterOverridesForField(fromName => flags.fieldNameComparison.namesMatch(fromName, toName)).headOption .map { case (fromName, value) => useOverride[From, To, CtorParam](fromName, toName, value) } @@ -142,7 +142,8 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio if (usePositionBasedMatching) ctorParamToGetter.get(ctorParam) else fromEnabledExtractors.collectFirst { - case (fromName, getter) if areNamesMatching(fromName, toName) => (fromName, toName, getter) + case (fromName, getter) if flags.fieldNameComparison.namesMatch(fromName, toName) => + (fromName, toName, getter) } resolvedExtractor .map { case (fromName, toName, getter) => @@ -163,7 +164,9 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio DerivationResult .missingAccessor[From, To, CtorParam, Existential[TransformationExpr]]( toName, - fromExtractors.exists { case (fromName, _) => areNamesMatching(fromName, toName) } + fromExtractors.exists { case (fromName, _) => + flags.fieldNameComparison.namesMatch(fromName, toName) + } ) case Product.Parameter.TargetType.SetterParameter => if (flags.beanSettersIgnoreUnmatched) @@ -172,8 +175,10 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // TODO: update this for isLocal DerivationResult .missingJavaBeanSetterParam[From, To, CtorParam, Existential[TransformationExpr]]( - ProductType.dropSet(toName), - fromExtractors.exists { case (fromName, _) => areNamesMatching(fromName, toName) } + flags.fieldNameComparison.dropSet(toName), + fromExtractors.exists { case (fromName, _) => + flags.fieldNameComparison.namesMatch(fromName, toName) + } ) } } @@ -322,14 +327,16 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio ) { // We're constructing: // '{ ${ derivedToElement } } // using ${ src.$name } - deriveRecursiveTransformationExpr[ExtractedSrc, CtorParam](extractedSrcExpr, DownField(toName)) - .transformWith { expr => - // If we derived partial.Result[$ctorParam] we are appending - // ${ derivedToElement }.prependErrorPath(PathElement.Accessor("fromName")) - DerivationResult.existential[TransformationExpr, CtorParam](appendPath(expr, sourcePath)) - } { errors => - appendMissingTransformer[From, To, ExtractedSrc, CtorParam](errors, toName) - } + deriveRecursiveTransformationExpr[ExtractedSrc, CtorParam]( + extractedSrcExpr, + new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, toName)) + ).transformWith { expr => + // If we derived partial.Result[$ctorParam] we are appending + // ${ derivedToElement }.prependErrorPath(PathElement.Accessor("fromName")) + DerivationResult.existential[TransformationExpr, CtorParam](appendPath(expr, sourcePath)) + } { errors => + appendMissingTransformer[From, To, ExtractedSrc, CtorParam](errors, toName) + } } } ) @@ -355,7 +362,10 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio ) { // We're constructing: // '{ ${ derivedToElement } } // using ${ src.$name } - deriveRecursiveTransformationExpr[Getter, CtorParam](get(ctx.src), DownField(toName)).transformWith { expr => + deriveRecursiveTransformationExpr[Getter, CtorParam]( + get(ctx.src), + new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, toName)) + ).transformWith { expr => // If we derived partial.Result[$ctorParam] we are appending // ${ derivedToElement }.prependErrorPath(PathElement.Accessor("fromName")) DerivationResult.existential[TransformationExpr, CtorParam](appendPath(expr, fromName)) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala index f0b33b694..a0ee14485 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala @@ -22,12 +22,14 @@ private[compiletime] trait TransformTypeToValueClassRuleModule { private def transformToInnerToAndWrap[From, To, InnerTo: Type]( valueTo: ValueClass[To, InnerTo] )(implicit ctx: TransformationContext[From, To]): DerivationResult[Rule.ExpansionResult[To]] = - deriveRecursiveTransformationExpr[From, InnerTo](ctx.src, DownField(valueTo.fieldName)) - .flatMap { derivedInnerTo => - // We're constructing: - // '{ new $To(${ derivedInnerTo }) } - DerivationResult.expanded(derivedInnerTo.map(valueTo.wrap)) - } + deriveRecursiveTransformationExpr[From, InnerTo]( + ctx.src, + new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, valueTo.fieldName)) + ).flatMap { derivedInnerTo => + // We're constructing: + // '{ new $To(${ derivedInnerTo }) } + DerivationResult.expanded(derivedInnerTo.map(valueTo.wrap)) + } // fall back to case classes expansion; see https://github.com/scalalandio/chimney/issues/297 for more info .orElse(TransformProductToProductRule.expand(ctx)) .orElse( diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala index 516a5413c..811a9d40a 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala @@ -22,10 +22,12 @@ private[compiletime] trait TransformValueClassToValueClassRuleModule { this: Der valueFrom: ValueClass[From, InnerFrom], valueTo: ValueClass[To, InnerTo] )(implicit ctx: TransformationContext[From, To]): DerivationResult[Rule.ExpansionResult[To]] = - deriveRecursiveTransformationExpr[InnerFrom, InnerTo](valueFrom.unwrap(ctx.src), DownField(valueTo.fieldName)) - .flatMap { (derivedInnerTo: TransformationExpr[InnerTo]) => - // We're constructing: - // '{ ${ new $To(${ derivedInnerTo }) } /* using ${ src }.$from internally */ } - DerivationResult.expanded(derivedInnerTo.map(valueTo.wrap)) - } + deriveRecursiveTransformationExpr[InnerFrom, InnerTo]( + valueFrom.unwrap(ctx.src), + new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, valueTo.fieldName)) + ).flatMap { (derivedInnerTo: TransformationExpr[InnerTo]) => + // We're constructing: + // '{ ${ new $To(${ derivedInnerTo }) } /* using ${ src }.$from internally */ } + DerivationResult.expanded(derivedInnerTo.map(valueTo.wrap)) + } } From 0d264345384196b2de4c3dcf0031f855177b17f3 Mon Sep 17 00:00:00 2001 From: saeltz Date: Tue, 19 Mar 2024 23:01:55 +0100 Subject: [PATCH 04/13] Replace all usages of enumNamesMatch --- ...ransformSealedHierarchyToSealedHierarchyRuleModule.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala index e8d610353..1d73b9e67 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala @@ -107,7 +107,9 @@ private[compiletime] trait TransformSealedHierarchyToSealedHierarchyRuleModule { ctx: TransformationContext[From, To] ): DerivationResult[Existential[ExprPromise[*, TransformationExpr[To]]]] = { import fromSubtype.Underlying as FromSubtype, fromSubtype.value.name as fromName - toElements.filter(toSubtype => enumNamesMatch(fromName, toSubtype.value.name)).toList match { + toElements.filter(toSubtype => + ctx.config.flags.subtypeNameComparison.namesMatch(fromName, toSubtype.value.name) + ) match { // 0 matches - no coproduct with the same name case Nil => DerivationResult @@ -223,7 +225,5 @@ private[compiletime] trait TransformSealedHierarchyToSealedHierarchyRuleModule { } .matchOn(ctx.src) ) - - private def enumNamesMatch(fromName: String, toName: String): Boolean = fromName == toName } } From 4b758ca0a0ff582d2f14dd043b7d0c02e78d7319 Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Fri, 22 Mar 2024 17:05:22 +0100 Subject: [PATCH 05/13] Refactor calls to field/subtype names comparators --- .../datatypes/ProductTypesPlatform.scala | 8 ++-- .../datatypes/ProductTypesPlatform.scala | 9 +++-- .../compiletime/datatypes/ProductTypes.scala | 37 ++++++++++++------- .../dsl/TransformedNamesComparison.scala | 22 ++--------- .../derivation/transformer/Contexts.scala | 9 +++++ .../TransformProductToProductRuleModule.scala | 15 +++----- ...HierarchyToSealedHierarchyRuleModule.scala | 4 +- .../TransformTypeToValueClassRuleModule.scala | 2 +- ...formValueClassToValueClassRuleModule.scala | 2 +- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala b/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala index 91c489f5d..16260a978 100644 --- a/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala +++ b/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala @@ -27,11 +27,11 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => // assuming isAccessor was tested earlier def isJavaGetter(getter: MethodSymbol): Boolean = - isGetterName(getter.name.toString) + ProductTypes.BeanAware.isGetterName(getter.name.toString) def isJavaSetter(setter: MethodSymbol): Boolean = setter.isPublic && setter.paramLists.size == 1 && setter.paramLists.head.size == 1 && - isSetterName(setter.asMethod.name.toString) + ProductTypes.BeanAware.isSetterName(setter.asMethod.name.toString) def isVar(setter: Symbol): Boolean = setter.isPublic && setter.isTerm && setter.asTerm.name.toString.endsWith("_$eq") @@ -159,7 +159,7 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => if (isVar(setter)) n.stripSuffix("_$eq").stripSuffix("_=") else n name -> setter } - .filter { case (name, _) => !paramTypes.keySet.contains(name) } + .filter { case (name, _) => !paramTypes.keySet.contains(name) } // _exact_ name match! .map { case (name, setter) => val termName = setter.asTerm.name.toTermName val tpe = ExistentialType(fromUntyped(paramListsOf(Type[A].tpe, setter).flatten.head.typeSignature)) @@ -249,7 +249,7 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => private val getDecodedName = (s: Symbol) => s.name.decodedName.toString - private val isGarbageSymbol = getDecodedName andThen isGarbage + private val isGarbageSymbol = getDecodedName andThen ProductTypes.isGarbageName // Borrowed from jsoniter-scala: https://github.com/plokhotnyuk/jsoniter-scala/blob/b14dbe51d3ae6752e5a9f90f1f3caf5bceb5e4b0/jsoniter-scala-macros/shared/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMaker.scala#L462 private def companionSymbol[A: Type]: Symbol = { diff --git a/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala b/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala index 92443195a..2d002e133 100644 --- a/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala +++ b/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala @@ -31,10 +31,11 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => // assuming isAccessor was tested earlier def isJavaGetter(getter: Symbol): Boolean = - isGetterName(getter.name) + ProductTypes.BeanAware.isGetterName(getter.name) def isJavaSetter(setter: Symbol): Boolean = - setter.isPublic && setter.isDefDef && setter.paramSymss.flatten.size == 1 && isSetterName(setter.name) + setter.isPublic && setter.isDefDef && setter.paramSymss.flatten.size == 1 && ProductTypes.BeanAware + .isSetterName(setter.name) def isVar(setter: Symbol): Boolean = setter.isPublic && setter.isValDef && setter.flags.is(Flags.Mutable) @@ -179,7 +180,7 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => .map { setter => setter.name -> setter } - .filter { case (name, _) => !paramTypes.keySet.contains(name) } + .filter { case (name, _) => !paramTypes.keySet.contains(name) } // _exact_ name match! .map { case (name, setter) => val tpe = ExistentialType(fromUntyped[Any](paramsWithTypes(A, setter, isConstructor = false).head._2)) ( @@ -313,6 +314,6 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform => 22 -> TypeRepr.of[scala.Function22] ) - private val isGarbageSymbol = ((s: Symbol) => s.name) andThen isGarbage + private val isGarbageSymbol = ((s: Symbol) => s.name) andThen ProductTypes.isGarbageName } } diff --git a/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala b/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala index 03e37d2e6..ec55a0176 100644 --- a/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala +++ b/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypes.scala @@ -97,11 +97,6 @@ trait ProductTypes { this: Definitions => def exprAsInstanceOfMethod[A: Type](args: List[ListMap[String, ??]])(expr: Expr[Any]): Product.Constructor[A] - // cached in companion (regexps are expensive to initialize) - def isGarbage(name: String): Boolean = ProductTypes.isGarbage(name) - def isGetterName(name: String): Boolean = ProductTypes.isGetterName(name) - def isSetterName(name: String): Boolean = ProductTypes.isSetterName(name) - // defaults methods are 1-indexed protected def caseClassApplyDefaultScala2(idx: Int): String = "apply$default$" + idx protected def caseClassApplyDefaultScala3(idx: Int): String = "$lessinit$greater$default$" + idx @@ -158,17 +153,31 @@ trait ProductTypes { this: Definitions => } object ProductTypes { - implicit private class RegexpOps(regexp: scala.util.matching.Regex) { + object BeanAware { - def isMatching(value: String): Boolean = regexp.pattern.matcher(value).matches() // 2.12 doesn't have .matches - } + implicit private class RegexpOps(regexp: scala.util.matching.Regex) { - private val getAccessor = raw"(?i)get(.)(.*)".r - private val isAccessor = raw"(?i)is(.)(.*)".r - val isGetterName: String => Boolean = name => getAccessor.isMatching(name) || isAccessor.isMatching(name) + def isMatching(value: String): Boolean = regexp.pattern.matcher(value).matches() // 2.12 doesn't have .matches + } - private val setAccessor = raw"(?i)set(.)(.*)".r - val isSetterName: String => Boolean = name => setAccessor.isMatching(name) + private val getAccessor = raw"(?i)get(.)(.*)".r + private val isAccessor = raw"(?i)is(.)(.*)".r + val isGetterName: String => Boolean = name => getAccessor.isMatching(name) || isAccessor.isMatching(name) + + val dropGetIs: String => String = { + case getAccessor(head, tail) => head.toLowerCase + tail + case isAccessor(head, tail) => head.toLowerCase + tail + case other => other + } + + private val setAccessor = raw"(?i)set(.)(.*)".r + val isSetterName: String => Boolean = name => setAccessor.isMatching(name) + + val dropSet: String => String = { + case setAccessor(head, tail) => head.toLowerCase + tail + case other => other + } + } // methods we can drop from searching scope private val garbage = Set( @@ -202,5 +211,5 @@ object ProductTypes { ) // default arguments has name method$default$index private val defaultElement = raw"$$default$$" - val isGarbage: String => Boolean = name => garbage(name) || name.contains(defaultElement) + val isGarbageName: String => Boolean = name => garbage(name) || name.contains(defaultElement) } diff --git a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala index 5ce688bf5..e293720ab 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala @@ -5,27 +5,15 @@ package io.scalaland.chimney.dsl abstract class TransformedNamesComparison { this: Singleton => def namesMatch(fromName: String, toName: String): Boolean - def dropSet(name: String): String } object TransformedNamesComparison { object BeanAware extends TransformedNamesComparison { - private val getAccessor = raw"(?i)get(.)(.*)".r - private val isAccessor = raw"(?i)is(.)(.*)".r - private val setAccessor = raw"(?i)set(.)(.*)".r - - override def dropSet(name: String): String = name match { - case setAccessor(head, tail) => head.toLowerCase + tail - case other => other - } - - private val dropGetIs: String => String = { - case getAccessor(head, tail) => head.toLowerCase + tail - case isAccessor(head, tail) => head.toLowerCase + tail - case other => other - } - private val normalize: String => String = dropGetIs.andThen(dropSet) + // While it's bad to refer to compiletime package this code should only be used by this compiletime package. + // Additionally, current module has to rely on chimney-macro-commons, not the other way round. + import io.scalaland.chimney.internal.compiletime.datatypes.ProductTypes + private val normalize = ProductTypes.BeanAware.dropGetIs andThen ProductTypes.BeanAware.dropSet def namesMatch(fromName: String, toName: String): Boolean = fromName == toName || normalize(fromName) == normalize(toName) @@ -34,13 +22,11 @@ object TransformedNamesComparison { object StrictEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName == toName - override def dropSet(name: String): String = name } object CaseInsensitiveEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) - override def dropSet(name: String): String = name } type FieldDefault = BeanAware.type diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala index 03d9e147b..e5c2be152 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala @@ -166,5 +166,14 @@ private[compiletime] trait Contexts { this: Derivation => ctx.From implicit final protected def ctx2ToType[From, To](implicit ctx: TransformationContext[From, To]): Type[To] = ctx.To + implicit def areFieldNamesMatching(fromName: String, toName: String)(implicit + ctx: TransformationContext[?, ?] + ): Boolean = + ctx.config.flags.fieldNameComparison.namesMatch(fromName, toName) + implicit def areSubtypeNamesMatching(fromName: String, toName: String)(implicit + ctx: TransformationContext[?, ?] + ): Boolean = + ctx.config.flags.subtypeNameComparison.namesMatch(fromName, toName) + // for unpacking Exprs from Context, pattern matching should be enough } 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 ebfa93297..5a600742b 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 @@ -5,6 +5,7 @@ import io.scalaland.chimney.internal.compiletime.derivation.transformer.Derivati import io.scalaland.chimney.internal.compiletime.fp.Implicits.* import io.scalaland.chimney.internal.compiletime.fp.Traverse import io.scalaland.chimney.partial +import io.scalaland.chimney.internal.compiletime.datatypes.ProductTypes private[compiletime] trait TransformProductToProductRuleModule { this: Derivation => @@ -164,9 +165,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio DerivationResult .missingAccessor[From, To, CtorParam, Existential[TransformationExpr]]( toName, - fromExtractors.exists { case (fromName, _) => - flags.fieldNameComparison.namesMatch(fromName, toName) - } + fromExtractors.exists { case (fromName, _) => areFieldNamesMatching(fromName, toName) } ) case Product.Parameter.TargetType.SetterParameter => if (flags.beanSettersIgnoreUnmatched) @@ -175,10 +174,8 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // TODO: update this for isLocal DerivationResult .missingJavaBeanSetterParam[From, To, CtorParam, Existential[TransformationExpr]]( - flags.fieldNameComparison.dropSet(toName), - fromExtractors.exists { case (fromName, _) => - flags.fieldNameComparison.namesMatch(fromName, toName) - } + ProductTypes.BeanAware.dropSet(toName), + fromExtractors.exists { case (fromName, _) => areFieldNamesMatching(fromName, toName) } ) } } @@ -329,7 +326,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // '{ ${ derivedToElement } } // using ${ src.$name } deriveRecursiveTransformationExpr[ExtractedSrc, CtorParam]( extractedSrcExpr, - new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, toName)) + new DownField(areFieldNamesMatching(_, toName)) ).transformWith { expr => // If we derived partial.Result[$ctorParam] we are appending // ${ derivedToElement }.prependErrorPath(PathElement.Accessor("fromName")) @@ -364,7 +361,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // '{ ${ derivedToElement } } // using ${ src.$name } deriveRecursiveTransformationExpr[Getter, CtorParam]( get(ctx.src), - new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, toName)) + new DownField(areFieldNamesMatching(_, toName)) ).transformWith { expr => // If we derived partial.Result[$ctorParam] we are appending // ${ derivedToElement }.prependErrorPath(PathElement.Accessor("fromName")) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala index 1d73b9e67..1e0c44661 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala @@ -107,9 +107,7 @@ private[compiletime] trait TransformSealedHierarchyToSealedHierarchyRuleModule { ctx: TransformationContext[From, To] ): DerivationResult[Existential[ExprPromise[*, TransformationExpr[To]]]] = { import fromSubtype.Underlying as FromSubtype, fromSubtype.value.name as fromName - toElements.filter(toSubtype => - ctx.config.flags.subtypeNameComparison.namesMatch(fromName, toSubtype.value.name) - ) match { + toElements.filter(toSubtype => areSubtypeNamesMatching(fromName, toSubtype.value.name)) match { // 0 matches - no coproduct with the same name case Nil => DerivationResult diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala index a0ee14485..167a67a00 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformTypeToValueClassRuleModule.scala @@ -24,7 +24,7 @@ private[compiletime] trait TransformTypeToValueClassRuleModule { )(implicit ctx: TransformationContext[From, To]): DerivationResult[Rule.ExpansionResult[To]] = deriveRecursiveTransformationExpr[From, InnerTo]( ctx.src, - new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, valueTo.fieldName)) + new DownField(areFieldNamesMatching(_, valueTo.fieldName)) ).flatMap { derivedInnerTo => // We're constructing: // '{ new $To(${ derivedInnerTo }) } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala index 811a9d40a..fabed6847 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformValueClassToValueClassRuleModule.scala @@ -24,7 +24,7 @@ private[compiletime] trait TransformValueClassToValueClassRuleModule { this: Der )(implicit ctx: TransformationContext[From, To]): DerivationResult[Rule.ExpansionResult[To]] = deriveRecursiveTransformationExpr[InnerFrom, InnerTo]( valueFrom.unwrap(ctx.src), - new DownField(ctx.config.flags.fieldNameComparison.namesMatch(_, valueTo.fieldName)) + new DownField(areFieldNamesMatching(_, valueTo.fieldName)) ).flatMap { (derivedInnerTo: TransformationExpr[InnerTo]) => // We're constructing: // '{ ${ new $To(${ derivedInnerTo }) } /* using ${ src }.$from internally */ } From df1082c449749d230dc61af877177b92c3da6ee4 Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sat, 23 Mar 2024 07:40:14 +0100 Subject: [PATCH 06/13] Test .enableenableCustomFieldNameComparison and .disableCustomFieldNameComparison flags --- .../PartialTransformerProductSpec.scala | 126 ++++++++++++++++++ .../chimney/TotalTransformerProductSpec.scala | 106 +++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala index f74e13e6b..8580e4091 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala @@ -1119,6 +1119,132 @@ class PartialTransformerProductSpec extends ChimneySpec { } } + group("flag .enableCustomFieldNameComparison") { + case class Foo(Baz: Foo.Baz, A: Int) + object Foo { + case class Baz(S: String) + } + + case class Bar(baz: Bar.Baz, a: Int) + object Bar { + case class Baz(s: String) + } + + test("should be disabled by default") { + + compileErrorsFixed("""Foo(Foo.Baz("test"), 1024).transformIntoPartial[Bar]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.PartialTransformerProductSpec.Foo to io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "baz: io.scalaland.chimney.PartialTransformerProductSpec.Bar.Baz - no accessor named baz in source type io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "a: scala.Int - no accessor named a in source type io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Foo(Foo.Baz("test"), 1024).intoPartial[Bar].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.PartialTransformerProductSpec.Foo to io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "baz: io.scalaland.chimney.PartialTransformerProductSpec.Bar.Baz - no accessor named baz in source type io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "a: scala.Int - no accessor named a in source type io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Bar(Bar.Baz("test"), 1024).transformIntoPartial[Foo]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.PartialTransformerProductSpec.Bar to io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "Baz: io.scalaland.chimney.PartialTransformerProductSpec.Foo.Baz - no accessor named Baz in source type io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "A: scala.Int - no accessor named A in source type io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Bar(Bar.Baz("test"), 1024).intoPartial[Foo].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.PartialTransformerProductSpec.Bar to io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "Baz: io.scalaland.chimney.PartialTransformerProductSpec.Foo.Baz - no accessor named Baz in source type io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "A: scala.Int - no accessor named A in source type io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + + test("should allow fields to be matched using user-provided predicate") { + + val result = Foo(Foo.Baz("test"), 1024) + .intoPartial[Bar] + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform + result.asOption ==> Some(Bar(Bar.Baz("test"), 1024)) + result.asEither ==> Right(Bar(Bar.Baz("test"), 1024)) + result.asErrorPathMessageStrings ==> Iterable() + + val result2 = Bar(Bar.Baz("test"), 1024) + .intoPartial[Foo] + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform + result2.asOption ==> Some(Foo(Foo.Baz("test"), 1024)) + result2.asEither ==> Right(Foo(Foo.Baz("test"), 1024)) + result2.asErrorPathMessageStrings ==> Iterable() + + locally { + implicit val config = TransformerConfiguration.default.enableCustomFieldNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + val result3 = Foo(Foo.Baz("test"), 1024).transformIntoPartial[Bar] + result3.asOption ==> Some(Bar(Bar.Baz("test"), 1024)) + result3.asEither ==> Right(Bar(Bar.Baz("test"), 1024)) + result3.asErrorPathMessageStrings ==> Iterable() + val result4 = Foo(Foo.Baz("test"), 1024).intoPartial[Bar].transform + result4.asOption ==> Some(Bar(Bar.Baz("test"), 1024)) + result4.asEither ==> Right(Bar(Bar.Baz("test"), 1024)) + result4.asErrorPathMessageStrings ==> Iterable() + + val result5 = Bar(Bar.Baz("test"), 1024).transformIntoPartial[Foo] + result5.asOption ==> Some(Foo(Foo.Baz("test"), 1024)) + result5.asEither ==> Right(Foo(Foo.Baz("test"), 1024)) + result5.asErrorPathMessageStrings ==> Iterable() + val result6 = Bar(Bar.Baz("test"), 1024).intoPartial[Foo].transform + result6.asOption ==> Some(Foo(Foo.Baz("test"), 1024)) + result6.asEither ==> Right(Foo(Foo.Baz("test"), 1024)) + result6.asErrorPathMessageStrings ==> Iterable() + } + } + } + + group("flag .disableCustomFieldNameComparison") { + @unused case class Foo(Baz: Foo.Baz, A: Int) + object Foo { + case class Baz(S: String) + } + + @unused case class Bar(baz: Bar.Baz, a: Int) + object Bar { + case class Baz(s: String) + } + + test("should disable globally enabled .enableCustomFieldNameComparison") { + @unused implicit val config = TransformerConfiguration.default.enableCustomFieldNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + compileErrorsFixed("""Foo(Foo.Baz("test"), 1024).intoPartial[Bar].disableCustomFieldNameComparison.transform""") + .check( + "Chimney can't derive transformation from io.scalaland.chimney.PartialTransformerProductSpec.Foo to io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "baz: io.scalaland.chimney.PartialTransformerProductSpec.Bar.Baz - no accessor named baz in source type io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "a: scala.Int - no accessor named a in source type io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Bar(Bar.Baz("test"), 1024).intoPartial[Foo].disableCustomFieldNameComparison.transform""") + .check( + "Chimney can't derive transformation from io.scalaland.chimney.PartialTransformerProductSpec.Bar to io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "io.scalaland.chimney.PartialTransformerProductSpec.Foo", + "Baz: io.scalaland.chimney.PartialTransformerProductSpec.Foo.Baz - no accessor named Baz in source type io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "A: scala.Int - no accessor named A in source type io.scalaland.chimney.PartialTransformerProductSpec.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + } + group("transform always fails") { import trip.* diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala index 1f11d867b..10aae1b5c 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala @@ -566,6 +566,112 @@ class TotalTransformerProductSpec extends ChimneySpec { } } + group("flag .enableCustomFieldNameComparison") { + case class Foo(Baz: Foo.Baz, A: Int) + object Foo { + case class Baz(S: String) + } + + case class Bar(baz: Bar.Baz, a: Int) + object Bar { + case class Baz(s: String) + } + + test("should be disabled by default") { + + compileErrorsFixed("""Foo(Foo.Baz("test"), 1024).transformInto[Bar]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.TotalTransformerProductSpec.Foo to io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "baz: io.scalaland.chimney.TotalTransformerProductSpec.Bar.Baz - no accessor named baz in source type io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "a: scala.Int - no accessor named a in source type io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Foo(Foo.Baz("test"), 1024).into[Bar].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.TotalTransformerProductSpec.Foo to io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "baz: io.scalaland.chimney.TotalTransformerProductSpec.Bar.Baz - no accessor named baz in source type io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "a: scala.Int - no accessor named a in source type io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Bar(Bar.Baz("test"), 1024).transformInto[Foo]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.TotalTransformerProductSpec.Bar to io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "Baz: io.scalaland.chimney.TotalTransformerProductSpec.Foo.Baz - no accessor named Baz in source type io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "A: scala.Int - no accessor named A in source type io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Bar(Bar.Baz("test"), 1024).into[Foo].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.TotalTransformerProductSpec.Bar to io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "Baz: io.scalaland.chimney.TotalTransformerProductSpec.Foo.Baz - no accessor named Baz in source type io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "A: scala.Int - no accessor named A in source type io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + + test("should allow fields to be matched using user-provided predicate") { + + Foo(Foo.Baz("test"), 1024) + .into[Bar] + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> Bar(Bar.Baz("test"), 1024) + + Bar(Bar.Baz("test"), 1024) + .into[Foo] + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> Foo(Foo.Baz("test"), 1024) + + locally { + implicit val config = TransformerConfiguration.default.enableCustomFieldNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + Foo(Foo.Baz("test"), 1024).transformInto[Bar] ==> Bar(Bar.Baz("test"), 1024) + Foo(Foo.Baz("test"), 1024).into[Bar].transform ==> Bar(Bar.Baz("test"), 1024) + + Bar(Bar.Baz("test"), 1024).transformInto[Foo] ==> Foo(Foo.Baz("test"), 1024) + Bar(Bar.Baz("test"), 1024).into[Foo].transform ==> Foo(Foo.Baz("test"), 1024) + } + } + } + + group("flag .disableCustomFieldNameComparison") { + @unused case class Foo(Baz: Foo.Baz, A: Int) + object Foo { + case class Baz(S: String) + } + + @unused case class Bar(baz: Bar.Baz, a: Int) + object Bar { + case class Baz(s: String) + } + + test("should disable globally enabled .enableCustomFieldNameComparison") { + @unused implicit val config = TransformerConfiguration.default.enableCustomFieldNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + compileErrorsFixed("""Foo(Foo.Baz("test"), 1024).into[Bar].disableCustomFieldNameComparison.transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.TotalTransformerProductSpec.Foo to io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "baz: io.scalaland.chimney.TotalTransformerProductSpec.Bar.Baz - no accessor named baz in source type io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "a: scala.Int - no accessor named a in source type io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""Bar(Bar.Baz("test"), 1024).into[Foo].disableCustomFieldNameComparison.transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.TotalTransformerProductSpec.Bar to io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "io.scalaland.chimney.TotalTransformerProductSpec.Foo", + "Baz: io.scalaland.chimney.TotalTransformerProductSpec.Foo.Baz - no accessor named Baz in source type io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "A: scala.Int - no accessor named A in source type io.scalaland.chimney.TotalTransformerProductSpec.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + } + group("support using method calls to fill values from target type") { case class Foobar(param: String) { val valField: String = "valField" From d4a7788feaca10eab8278652a0a9ed4df7818e3f Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sat, 23 Mar 2024 08:51:37 +0100 Subject: [PATCH 07/13] Fix subtype matching, test .enableCustomSubtypeNameComparison and .disableCustomSubtypeNameComparison flags --- .../dsl/TransformedNamesComparison.scala | 6 +- .../transformer/Configurations.scala | 24 ++-- .../derivation/transformer/Contexts.scala | 8 +- .../TransformProductToProductRuleModule.scala | 6 +- ...artialTransformerSealedHierarchySpec.scala | 116 ++++++++++++++++++ .../TotalTransformerSealedHierarchySpec.scala | 96 +++++++++++++++ .../chimney/fixtures/renames/Subtypes.scala | 14 +++ 7 files changed, 248 insertions(+), 22 deletions(-) create mode 100644 chimney/src/test/scala/io/scalaland/chimney/fixtures/renames/Subtypes.scala diff --git a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala index e293720ab..16844af95 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala @@ -8,7 +8,7 @@ abstract class TransformedNamesComparison { this: Singleton => } object TransformedNamesComparison { - object BeanAware extends TransformedNamesComparison { + case object BeanAware extends TransformedNamesComparison { // While it's bad to refer to compiletime package this code should only be used by this compiletime package. // Additionally, current module has to rely on chimney-macro-commons, not the other way round. @@ -19,12 +19,12 @@ object TransformedNamesComparison { fromName == toName || normalize(fromName) == normalize(toName) } - object StrictEquality extends TransformedNamesComparison { + case object StrictEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName == toName } - object CaseInsensitiveEquality extends TransformedNamesComparison { + case object CaseInsensitiveEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) } 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 099a8ea0b..3ba06c8d2 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 @@ -19,8 +19,8 @@ private[compiletime] trait Configurations { this: Derivation => optionDefaultsToNone: Boolean = false, partialUnwrapsOption: Boolean = true, implicitConflictResolution: Option[ImplicitTransformerPreference] = None, - fieldNameComparison: dsls.TransformedNamesComparison = dsls.TransformedNamesComparison.FieldDefault, - subtypeNameComparison: dsls.TransformedNamesComparison = dsls.TransformedNamesComparison.SubtypeDefault, + fieldNameComparison: Option[dsls.TransformedNamesComparison] = None, + subtypeNameComparison: Option[dsls.TransformedNamesComparison] = None, displayMacrosLogging: Boolean = false ) { @@ -52,10 +52,10 @@ private[compiletime] trait Configurations { this: Derivation => def setImplicitConflictResolution(preference: Option[ImplicitTransformerPreference]): TransformerFlags = copy(implicitConflictResolution = preference) - def setFieldNameComparison(nameComparison: dsls.TransformedNamesComparison): TransformerFlags = + def setFieldNameComparison(nameComparison: Option[dsls.TransformedNamesComparison]): TransformerFlags = copy(fieldNameComparison = nameComparison) - def setSubtypeNameComparison(nameComparison: dsls.TransformedNamesComparison): TransformerFlags = + def setSubtypeNameComparison(nameComparison: Option[dsls.TransformedNamesComparison]): TransformerFlags = copy(subtypeNameComparison = nameComparison) override def toString: String = s"TransformerFlags(${Vector( @@ -66,6 +66,8 @@ private[compiletime] trait Configurations { this: Derivation => if (beanGetters) Vector("beanGetters") else Vector.empty, if (optionDefaultsToNone) Vector("optionDefaultsToNone") else Vector.empty, implicitConflictResolution.map(r => s"ImplicitTransformerPreference=$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 ).flatten.mkString(", ")})" } @@ -282,12 +284,12 @@ private[compiletime] trait Configurations { this: Derivation => case ChimneyType.TransformerFlags.Flags.FieldNameComparison(c) => import c.Underlying as Comparison extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison( - extractNameComparisonObject[Comparison] + Some(extractNameComparisonObject[Comparison]) ) case ChimneyType.TransformerFlags.Flags.SubtypeNameComparison(c) => import c.Underlying as Comparison - extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison( - extractNameComparisonObject[Comparison] + extractTransformerFlags[Flags2](defaultFlags).setSubtypeNameComparison( + Some(extractNameComparisonObject[Comparison]) ) case _ => extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = true) @@ -298,13 +300,9 @@ private[compiletime] trait Configurations { this: Derivation => case ChimneyType.TransformerFlags.Flags.ImplicitConflictResolution(_) => extractTransformerFlags[Flags2](defaultFlags).setImplicitConflictResolution(None) case ChimneyType.TransformerFlags.Flags.FieldNameComparison(_) => - extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison( - dsls.TransformedNamesComparison.FieldDefault - ) + extractTransformerFlags[Flags2](defaultFlags).setFieldNameComparison(None) case ChimneyType.TransformerFlags.Flags.SubtypeNameComparison(_) => - extractTransformerFlags[Flags2](defaultFlags).setSubtypeNameComparison( - dsls.TransformedNamesComparison.SubtypeDefault - ) + extractTransformerFlags[Flags2](defaultFlags).setSubtypeNameComparison(None) case _ => extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = false) } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala index e5c2be152..5045cb359 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Contexts.scala @@ -1,6 +1,6 @@ package io.scalaland.chimney.internal.compiletime.derivation.transformer -import io.scalaland.chimney.dsl.TransformerDefinitionCommons +import io.scalaland.chimney.dsl.{TransformedNamesComparison, TransformerDefinitionCommons} import io.scalaland.chimney.partial private[compiletime] trait Contexts { this: Derivation => @@ -169,11 +169,13 @@ private[compiletime] trait Contexts { this: Derivation => implicit def areFieldNamesMatching(fromName: String, toName: String)(implicit ctx: TransformationContext[?, ?] ): Boolean = - ctx.config.flags.fieldNameComparison.namesMatch(fromName, toName) + ctx.config.flags.fieldNameComparison.getOrElse(TransformedNamesComparison.FieldDefault).namesMatch(fromName, toName) implicit def areSubtypeNamesMatching(fromName: String, toName: String)(implicit ctx: TransformationContext[?, ?] ): Boolean = - ctx.config.flags.subtypeNameComparison.namesMatch(fromName, toName) + ctx.config.flags.subtypeNameComparison + .getOrElse(TransformedNamesComparison.SubtypeDefault) + .namesMatch(fromName, toName) // for unpacking Exprs from Context, pattern matching should be enough } 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 5a600742b..06f36e374 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 @@ -97,7 +97,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio val verifyNoOverrideUnused = Traverse[List] .parTraverse( filterOverridesForField(fromName => - !parameters.keys.exists(toName => flags.fieldNameComparison.namesMatch(fromName, toName)) + !parameters.keys.exists(toName => areFieldNamesMatching(fromName, toName)) ).keys.toList ) { fromName => val tpeStr = Type.prettyPrint[To] @@ -134,7 +134,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio import ctorParam.Underlying as CtorParam, ctorParam.value.defaultValue // user might have used _.getName in modifier, to define target we know as _.setName // so simple .get(toName) might not be enough - filterOverridesForField(fromName => flags.fieldNameComparison.namesMatch(fromName, toName)).headOption + filterOverridesForField(fromName => areFieldNamesMatching(fromName, toName)).headOption .map { case (fromName, value) => useOverride[From, To, CtorParam](fromName, toName, value) } @@ -143,7 +143,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio if (usePositionBasedMatching) ctorParamToGetter.get(ctorParam) else fromEnabledExtractors.collectFirst { - case (fromName, getter) if flags.fieldNameComparison.namesMatch(fromName, toName) => + case (fromName, getter) if areFieldNamesMatching(fromName, toName) => (fromName, toName, getter) } resolvedExtractor diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala index 4f8a18c32..02eca527e 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala @@ -360,4 +360,120 @@ class PartialTransformerSealedHierarchySpec extends ChimneySpec { ) } } + + group("flag .enableCustomSubtypeNameComparison") { + + import fixtures.renames.Subtypes.* + + test("should be disabled by default") { + + compileErrorsFixed("""(Foo.BAZ: Foo).transformIntoPartial[Bar]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Foo to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Foo.BAZ: Foo).intoPartial[Bar].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Foo to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Bar.Baz: Bar).transformIntoPartial[Foo]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Bar to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Bar.Baz: Bar).intoPartial[Foo].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Bar to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + + test("should allow subtypes to be matched using user-provided predicate") { + val result = (Foo.BAZ: Foo) + .intoPartial[Bar] + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform + result.asOption ==> Some(Bar.Baz) + result.asEither ==> Right(Bar.Baz) + result.asErrorPathMessageStrings ==> Iterable() + + val result2 = (Bar.Baz: Bar) + .intoPartial[Foo] + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform + result2.asOption ==> Some(Foo.BAZ) + result2.asEither ==> Right(Foo.BAZ) + result2.asErrorPathMessageStrings ==> Iterable() + + locally { + implicit val config = TransformerConfiguration.default.enableCustomSubtypeNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + val result3 = (Foo.BAZ: Foo).transformIntoPartial[Bar] + result3.asOption ==> Some(Bar.Baz) + result3.asEither ==> Right(Bar.Baz) + result3.asErrorPathMessageStrings ==> Iterable() + + val result4 = (Foo.BAZ: Foo).intoPartial[Bar].transform + result4.asOption ==> Some(Bar.Baz) + result4.asEither ==> Right(Bar.Baz) + result4.asErrorPathMessageStrings ==> Iterable() + + val result5 = (Bar.Baz: Bar).transformIntoPartial[Foo] + result5.asOption ==> Some(Foo.BAZ) + result5.asEither ==> Right(Foo.BAZ) + result5.asErrorPathMessageStrings ==> Iterable() + + val result6 = (Bar.Baz: Bar).intoPartial[Foo].transform + result6.asOption ==> Some(Foo.BAZ) + result6.asEither ==> Right(Foo.BAZ) + result6.asErrorPathMessageStrings ==> Iterable() + } + } + } + + group("flag .disableCustomSubtypeNameComparison") { + import fixtures.renames.Subtypes.* + + test("should disable globally enabled .enableCustomSubtypeNameComparison") { + @unused implicit val config = TransformerConfiguration.default.enableCustomSubtypeNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + compileErrorsFixed("""(Foo.BAZ: Foo).intoPartial[Bar].disableCustomSubtypeNameComparison.transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Foo to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Bar.Baz: Bar).intoPartial[Foo].disableCustomSubtypeNameComparison.transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Bar to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + } } diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala index 452d6e6e1..3a8698a5e 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala @@ -199,4 +199,100 @@ class TotalTransformerSealedHierarchySpec extends ChimneySpec { ) } } + + group("flag .enableCustomSubtypeNameComparison") { + + import fixtures.renames.Subtypes.* + + test("should be disabled by default") { + + compileErrorsFixed("""(Foo.BAZ: Foo).transformInto[Bar]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Foo to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Foo.BAZ: Foo).into[Bar].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Foo to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Bar.Baz: Bar).transformInto[Foo]""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Bar to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Bar.Baz: Bar).into[Foo].transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Bar to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + + test("should allow subtypes to be matched using user-provided predicate") { + (Foo.BAZ: Foo) + .into[Bar] + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> Bar.Baz + + (Bar.Baz: Bar) + .into[Foo] + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform ==> Foo.BAZ + + locally { + implicit val config = TransformerConfiguration.default.enableCustomSubtypeNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + (Foo.BAZ: Foo).transformInto[Bar] ==> Bar.Baz + (Foo.BAZ: Foo).into[Bar].transform ==> Bar.Baz + + (Bar.Baz: Bar).transformInto[Foo] ==> Foo.BAZ + (Bar.Baz: Bar).into[Foo].transform ==> Foo.BAZ + } + } + } + + group("flag .disableCustomSubtypeNameComparison") { + import fixtures.renames.Subtypes.* + + test("should disable globally enabled .enableCustomSubtypeNameComparison") { + @unused implicit val config = TransformerConfiguration.default.enableCustomSubtypeNameComparison( + TransformedNamesComparison.CaseInsensitiveEquality + ) + + compileErrorsFixed("""(Foo.BAZ: Foo).into[Bar].disableCustomSubtypeNameComparison.transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Foo to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Foo.BAZ to io.scalaland.chimney.fixtures.renames.Subtypes.Bar", + "Consult https://chimney.readthedocs.io for usage examples." + ) + + compileErrorsFixed("""(Bar.Baz: Bar).into[Foo].disableCustomSubtypeNameComparison.transform""").check( + "Chimney can't derive transformation from io.scalaland.chimney.fixtures.renames.Subtypes.Bar to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "derivation from baz: io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo is not supported in Chimney!", + "io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "can't transform coproduct instance io.scalaland.chimney.fixtures.renames.Subtypes.Bar.Baz to io.scalaland.chimney.fixtures.renames.Subtypes.Foo", + "Consult https://chimney.readthedocs.io for usage examples." + ) + } + } } diff --git a/chimney/src/test/scala/io/scalaland/chimney/fixtures/renames/Subtypes.scala b/chimney/src/test/scala/io/scalaland/chimney/fixtures/renames/Subtypes.scala new file mode 100644 index 000000000..5f2a803f2 --- /dev/null +++ b/chimney/src/test/scala/io/scalaland/chimney/fixtures/renames/Subtypes.scala @@ -0,0 +1,14 @@ +package io.scalaland.chimney.fixtures.renames + +object Subtypes { + + sealed trait Foo + object Foo { + case object BAZ extends Foo + } + + sealed trait Bar + object Bar { + case object Baz extends Bar + } +} From 7e68c6b0fed17ce13210d9f468495192583f5ab2 Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sat, 23 Mar 2024 09:34:04 +0100 Subject: [PATCH 08/13] Test build-in name comparators --- .../TransformedNamesComparisonSpec.scala | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 chimney/src/test/scala/io/scalaland/chimney/TransformedNamesComparisonSpec.scala diff --git a/chimney/src/test/scala/io/scalaland/chimney/TransformedNamesComparisonSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TransformedNamesComparisonSpec.scala new file mode 100644 index 000000000..83f27c982 --- /dev/null +++ b/chimney/src/test/scala/io/scalaland/chimney/TransformedNamesComparisonSpec.scala @@ -0,0 +1,82 @@ +package io.scalaland.chimney + +import io.scalaland.chimney.dsl.TransformedNamesComparison + +class TransformedNamesComparisonSpec extends ChimneySpec { + + group("TransformedNamesComparison.BeanAware") { + + test("should match identical names") { + TransformedNamesComparison.BeanAware.namesMatch("someField", "someField") ==> true + } + + test("should allows matching fields with Java Bean getters and setters") { + TransformedNamesComparison.BeanAware.namesMatch("someField", "isSomeField") ==> true + TransformedNamesComparison.BeanAware.namesMatch("isSomeField", "someField") ==> true + TransformedNamesComparison.BeanAware.namesMatch("someField", "getSomeField") ==> true + TransformedNamesComparison.BeanAware.namesMatch("getSomeField", "someField") ==> true + TransformedNamesComparison.BeanAware.namesMatch("someField", "setSomeField") ==> true + TransformedNamesComparison.BeanAware.namesMatch("setSomeField", "someField") ==> true + } + + test("should not match names converted with different conventions") { + TransformedNamesComparison.BeanAware.namesMatch("someField", "some-field") ==> false + TransformedNamesComparison.BeanAware.namesMatch("some-field", "someField") ==> false + TransformedNamesComparison.BeanAware.namesMatch("someField", "some_field") ==> false + TransformedNamesComparison.BeanAware.namesMatch("some_field", "someField") ==> false + TransformedNamesComparison.BeanAware.namesMatch("someField", "SOME_FIELD") ==> false + TransformedNamesComparison.BeanAware.namesMatch("SOME_FIELD", "someField") ==> false + } + } + + group("TransformedNamesComparison.StrictEquality") { + + test("should match identical names") { + TransformedNamesComparison.StrictEquality.namesMatch("someField", "someField") ==> true + } + + test("should not match names converted with different conventions") { + TransformedNamesComparison.StrictEquality.namesMatch("someField", "isSomeField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("isSomeField", "someField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("someField", "getSomeField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("getSomeField", "someField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("someField", "setSomeField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("setSomeField", "someField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("someField", "some-field") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("some-field", "someField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("someField", "some_field") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("some_field", "someField") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("someField", "SOME_FIELD") ==> false + TransformedNamesComparison.StrictEquality.namesMatch("SOME_FIELD", "someField") ==> false + } + } + + group("TransformedNamesComparison.CaseInsensitiveEquality") { + + test("should match identical names") { + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "someField") ==> true + } + + test("should match names which differ only in letter capitalisation") { + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "SomeField") ==> true + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("SomeField", "someField") ==> true + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "SOMEFIELD") ==> true + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("SOMEFIELD", "someField") ==> true + } + + test("should not match names converted with different conventions") { + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "isSomeField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("isSomeField", "someField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "getSomeField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("getSomeField", "someField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "setSomeField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("setSomeField", "someField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "some-field") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("some-field", "someField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "some_field") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("some_field", "someField") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("someField", "SOME_FIELD") ==> false + TransformedNamesComparison.CaseInsensitiveEquality.namesMatch("SOME_FIELD", "someField") ==> false + } + } +} From 4428402b50ec5064c47e20b6897beb0a583cf59d Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sat, 23 Mar 2024 19:47:22 +0100 Subject: [PATCH 09/13] Test error generated on bad TransformedNamesComparison type --- .../derivation/transformer/Configurations.scala | 3 ++- .../chimney/PartialTransformerProductSpec.scala | 14 ++++++++++++++ .../PartialTransformerSealedHierarchySpec.scala | 12 ++++++++++++ .../chimney/TotalTransformerProductSpec.scala | 14 ++++++++++++++ .../TotalTransformerSealedHierarchySpec.scala | 12 ++++++++++++ 5 files changed, 54 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 3ba06c8d2..3b29f95f3 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 @@ -418,7 +418,8 @@ private[compiletime] trait Configurations { this: Derivation => .getOrElse { // $COVERAGE-OFF$ reportError( - s"Invalid TransformerNamesComparison type - only global objects are allowed: ${Type.prettyPrint[Comparison]}!!" + s"Invalid TransformerNamesComparison type - only (case) objects are allowed, and only the ones defined as top-level or in top-level objects, got: ${Type + .prettyPrint[Comparison]}!!!" ) // $COVERAGE-ON$ } diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala index 8580e4091..6f73505f9 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerProductSpec.scala @@ -1165,6 +1165,20 @@ class PartialTransformerProductSpec extends ChimneySpec { ) } + test("should inform user if and why the setting cannot be read") { + @unused object BadNameComparison extends TransformedNamesComparison { + + def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) + } + + compileErrorsFixed( + """Foo(Foo.Baz("test"), 1024).intoPartial[Bar].enableCustomFieldNameComparison(BadNameComparison).transform""" + ) + .check( + "Invalid TransformerNamesComparison type - only (case) objects are allowed, and only the ones defined as top-level or in top-level objects, got: io.scalaland.chimney.PartialTransformerProductSpec.BadNameComparison!!!" + ) + } + test("should allow fields to be matched using user-provided predicate") { val result = Foo(Foo.Baz("test"), 1024) diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala index 02eca527e..899838b71 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerSealedHierarchySpec.scala @@ -404,6 +404,18 @@ class PartialTransformerSealedHierarchySpec extends ChimneySpec { ) } + test("should inform user if and why the setting cannot be read") { + @unused object BadNameComparison extends TransformedNamesComparison { + + def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) + } + + compileErrorsFixed("""(Foo.BAZ: Foo).into[Bar].enableCustomSubtypeNameComparison(BadNameComparison).transform""") + .check( + "Invalid TransformerNamesComparison type - only (case) objects are allowed, and only the ones defined as top-level or in top-level objects, got: io.scalaland.chimney.PartialTransformerSealedHierarchySpec.BadNameComparison!!!" + ) + } + test("should allow subtypes to be matched using user-provided predicate") { val result = (Foo.BAZ: Foo) .intoPartial[Bar] diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala index 10aae1b5c..3e91d542d 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerProductSpec.scala @@ -612,6 +612,20 @@ class TotalTransformerProductSpec extends ChimneySpec { ) } + test("should inform user if and why the setting cannot be read") { + @unused object BadNameComparison extends TransformedNamesComparison { + + def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) + } + + compileErrorsFixed( + """Foo(Foo.Baz("test"), 1024).into[Bar].enableCustomFieldNameComparison(BadNameComparison).transform""" + ) + .check( + "Invalid TransformerNamesComparison type - only (case) objects are allowed, and only the ones defined as top-level or in top-level objects, got: io.scalaland.chimney.TotalTransformerProductSpec.BadNameComparison!!!" + ) + } + test("should allow fields to be matched using user-provided predicate") { Foo(Foo.Baz("test"), 1024) diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala index 3a8698a5e..d3ab64988 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerSealedHierarchySpec.scala @@ -243,6 +243,18 @@ class TotalTransformerSealedHierarchySpec extends ChimneySpec { ) } + test("should inform user if and why the setting cannot be read") { + @unused object BadNameComparison extends TransformedNamesComparison { + + def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) + } + + compileErrorsFixed("""(Foo.BAZ: Foo).into[Bar].enableCustomSubtypeNameComparison(BadNameComparison).transform""") + .check( + "Invalid TransformerNamesComparison type - only (case) objects are allowed, and only the ones defined as top-level or in top-level objects, got: io.scalaland.chimney.TotalTransformerSealedHierarchySpec.BadNameComparison!!!" + ) + } + test("should allow subtypes to be matched using user-provided predicate") { (Foo.BAZ: Foo) .into[Bar] From de8117c0e011e157018acd20d5bc4fc3cbe00f19 Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sat, 23 Mar 2024 20:33:18 +0100 Subject: [PATCH 10/13] MkDocs documentation for field name comparison and subtype name comparison flags --- docs/docs/supported-transformations.md | 184 +++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/docs/docs/supported-transformations.md b/docs/docs/supported-transformations.md index c13d0684d..dc74b52ba 100644 --- a/docs/docs/supported-transformations.md +++ b/docs/docs/supported-transformations.md @@ -1322,6 +1322,7 @@ We are also able to compute values in nested structure: !!! example ```scala + //> using dep io.scalaland::chimney::{{ git.tag or local.tag }} import io.scalaland.chimney.dsl._ case class Foo(a: String, b: Int) @@ -1342,6 +1343,97 @@ We are also able to compute values in nested structure: .transform.asEither // Right(NestedBar(Bar("value", 1248, 2496L))) ``` +### Customizing field name matching + +Be default names are matched in a Java-Bean-aware way - `fieldName` would be considered a match for another `fieldName` +but also for `isFieldName`, `getFieldName` and `setFieldName`. This allows the macro to read both normal `val`s and +Bean getters and write into constructor arguments and Bean setters. (Whether such getters/setters would we admited +for matching is controlled by dedicated flags: [`.enableBeanGetters`](#reading-from-bean-getters) and +[`.enableBeanSetters`](#writing-to-bean-setters)). + +The field name matching predicate can be overrided with a flag: + +!!! example + + ```scala + //> using dep io.scalaland::chimney::{{ git.tag or local.tag }} + import io.scalaland.chimney.dsl._ + + case class Foo(Baz: Foo.Baz, A: Int) + object Foo { + case class Baz(S: String) + } + + case class Bar(baz: Bar.Baz, a: Int) + object Bar { + case class Baz(s: String) + } + + // Foo(Foo.Baz("test"), 1024).transformInto[Bar] or + // Foo(Foo.Baz("test"), 1024).into[Bar].transform results in: + // Chimney can't derive transformation from Foo to Bar + // + // Bar + // baz: Bar.Baz - no accessor named baz in source type Foo + // a: scala.Int - no accessor named a in source type Foo + // + // Consult https://chimney.readthedocs.io for usage examples. + + Foo(Foo.Baz("test"), 1024) + .into[Bar] + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform // Bar(Bar.Baz("test"), 1024) + Foo(Foo.Baz("test"), 1024) + .intoPartial[Bar] + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform.asEither // Right(Bar(Bar.Baz("test"), 1024)) + + locally { + // All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3) + implicit val cfg = TransformerConfiguration.default + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + + Foo(Foo.Baz("test"), 1024).transformInto[Bar] // Bar(Bar.Baz("test"), 1024) + Foo(Foo.Baz("test"), 1024).into[Bar].transform // Bar(Bar.Baz("test"), 1024) + Foo(Foo.Baz("test"), 1024).transformIntoPartial[Bar].asEither // Right(Bar(Bar.Baz("test"), 1024)) + Foo(Foo.Baz("test"), 1024).intoPartial[Bar].transform.asEither // Right(Bar(Bar.Baz("test"), 1024)) + } + ``` + +For details about `TransformedNamesComparison` look at [their dedicated section](#defining-custom-name-matching-predicate). + +If the flag was enabled in the implicit config it can be disabled with `.disableCustomFieldNameComparison`. + +!!! example + + ```scala + //> using dep io.scalaland::chimney::{{ git.tag or local.tag }} + import io.scalaland.chimney.dsl._ + + case class Foo(Baz: Foo.Baz, A: Int) + object Foo { + case class Baz(S: String) + } + + case class Bar(baz: Bar.Baz, a: Int) + object Bar { + case class Baz(s: String) + } + + // All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3) + implicit val cfg = TransformerConfiguration.default + .enableCustomFieldNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + + Foo(Foo.Baz("test"), 1024).into[Bar].disableCustomFieldNameComparison.transform + // Chimney can't derive transformation from Foo to Bar + // + // Bar + // baz: Bar.Baz - no accessor named baz in source type Foo + // a: scala.Int - no accessor named a in source type Foo + // + // Consult https://chimney.readthedocs.io for usage examples. + ``` + ## From/into a `Tuple` Conversion from/to a tuple of any size is almost identical to conversion between other classes. The only difference @@ -1778,6 +1870,94 @@ If the computation needs to allow failure, there is `.withCoproductInstanceParti ColorJ.Black.into[ColorS].withCoproductInstance(blackIsRed).transform // ColorS.Black ``` +### Customizing subtype name matching + +Be default names are matched with a `String` equality - `Subtype` would be considered a match for another `Subtype` +but not for `SUBTYPE` or any other capitalization. + +The subtype name matching predicate can be overrided with a flag: + +!!! example + + ```scala + //> using dep io.scalaland::chimney::{{ git.tag or local.tag }} + import io.scalaland.chimney.dsl._ + + sealed trait Foo + object Foo { + case object BAZ extends Foo + } + + sealed trait Bar + object Bar { + case object Baz extends Bar + } + + // (Foo.BAZ: Foo).transformInto[Bar] or + // (Foo.BAZ: Foo).into[Bar].transform results in: + // Chimney can't derive transformation from Foo to Bar + // + // Bar + // derivation from baz: Foo.BAZ to Bar is not supported in Chimney! + // + // Bar + // can't transform coproduct instance Foo.BAZ to Bar + // + // Consult https://chimney.readthedocs.io for usage examples. + + (Foo.BAZ: Foo) + .into[Bar] + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform // Bar.Baz + (Foo.BAZ: Foo) + .intoPartial[Bar] + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + .transform.asEither // Right(Bar.Baz) + + locally { + // All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3) + implicit val cfg = TransformerConfiguration.default + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + + (Foo.BAZ: Foo).transformInto[Bar] // Bar.Baz + (Foo.BAZ: Foo).into[Bar].transform // Bar.Baz + (Foo.BAZ: Foo).transformIntoPartial[Bar].asEither // Right(Bar.Baz) + (Foo.BAZ: Foo).intoPartial[Bar].transform.asEither // Right(Bar.Baz) + } + ``` + +For details about `TransformedNamesComparison` look at [their dedicated section](#defining-custom-name-matching-predicate). + +!!! example + + ```scala + //> using dep io.scalaland::chimney::{{ git.tag or local.tag }} + import io.scalaland.chimney.dsl._ + + sealed trait Foo + object Foo { + case object BAZ extends Foo + } + + sealed trait Bar + object Bar { + case object Baz extends Bar + } + + // All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3) + implicit val cfg = TransformerConfiguration.default + .enableCustomSubtypeNameComparison(TransformedNamesComparison.CaseInsensitiveEquality) + + (Foo.BAZ: Foo).into[Bar].disableCustomSubtypeNameComparison.transform + // Chimney can't derive transformation from Foo to Bar + // + // Bar + // baz: Bar.Baz - no accessor named baz in source type Foo + // a: scala.Int - no accessor named a in source type Foo + // + // Consult https://chimney.readthedocs.io for usage examples. + ``` + ## From/into an `Option` `Option` type has special support during the derivation of a transformation. @@ -2574,3 +2754,7 @@ If we need to customize it, we can use `.define.buildTransformer`: val foo = Foo(10, Some(Foo(20, None))) val bar = foo.transformInto[Bar] ``` + +## Defining custom name matching predicate + +TODO From 98358d77cb62d18c0f2566bd3b437b095273b9bf Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sun, 24 Mar 2024 11:50:02 +0100 Subject: [PATCH 11/13] MkDocs documentation of TransformedNamesComparison --- docs/docs/supported-transformations.md | 75 +++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/docs/supported-transformations.md b/docs/docs/supported-transformations.md index dc74b52ba..5820f8a98 100644 --- a/docs/docs/supported-transformations.md +++ b/docs/docs/supported-transformations.md @@ -2757,4 +2757,77 @@ If we need to customize it, we can use `.define.buildTransformer`: ## Defining custom name matching predicate -TODO +Arguments taken by both `.enableCustomFieldNameComparison` and `.enableCustomSubtypeNameComparison` are values of type +`TransformedNamesComparison`. Out of the box, Chimney provides: + + * `TransformedNamesComparison.StrictEquality` - 2 names are considered equal only if they are identical `String`s. + This is the default matching strategy for subtype names conparison + * `TransformedNamesComparison.BeanAware` - 2 names are considered equal if they are identical `String`s OR if they are + identical after you convert them from Java Bean naming convention: + * if a name starts with `is`/`get`/`set` prefix (e.g. `isField`, `getField`, `setField`) then + * strip this name from the prefix (obtaining e.g. `Field`) and + * lower case the first letter (obtaining e.g. `field`) +* `TransformedNamesComparison.CaseInsensitiveEquality` - 2 names are considered equal if `equalsIgnoreCase` returns + `true` + +However, these 3 does not exhaust all possible comparisons and you might need to provide one yourself. + +!!! warning + + This is an advanced feature! Due to macros' limitations this feature requires several conditions to work. + +The challenge is that the function you'd like to provie has to be called within macro, so it has to be defined in such +a way that the macro will be able to access it. Normally, there is no way to inject a custom login into existing macro, +but Chimney has a specific solution for this: + + * you need to define your `TransformedNamesComparison` as `object` - objects do not need constructor arguments, so + they can be instantiated easily + * your have to define this `object` as top-level definition or within another object - object defined within a `class`, + a `trait` or locally, does need some logic for instantiation + * you have to define your `object` in a module/subproject that is compiled _before_ the module where you need to use + it, so that the bytecode would already be accesible on classpath. + +!!! example + + ```scala + //> using dep io.scalaland::chimney::{{ git.tag or local.tag }} + package your.organization + + import io.scalaland.chimney.dsl._ + + // Allows matching: UPPERCASE, lowercase, kebab-case, underline_case, + // PascalCase, camelCase and Java Beans conventions together + // + // Object is "case" for better toString output. + case object PermissiveNamesComparison extends TransformedNamesComparison { + + private val normalize(name: String): String = { + val name2 = + if (name.startsWith("is")) name.drop(2) + else (name.startsWith("get")) name.drop(3) + else (name.startsWith("set")) name.drop(3) + else name + name2.replaceAll("[-_]", "") + } + + def namesMatch(fromName: String, toName: String): Boolean = + normalize(fromName).equalsIgnoreCase(normalize(toName)) + } + ``` + + If you define this `object` in module A, and you want to use it in module B, where B depends on A, macros would + be able to use that value. + + ```scala + //> using dep io.scalaland::chimney::{{ git.tag or local.tag }} + + case class Foo(a_name: String, BName: String) + case class Bar(`a-name`: String, getBName: String) + + Foo("value1", "value2") + .into[Bar] + .enableCustomFieldNameComparison(your.organization.PermissiveNamesComparison) + // this would be parsed as well + //.enableCustomSubtypeNameComparison(your.organization.PermissiveNamesComparison) + .transform + ``` \ No newline at end of file From a18ccf812cf71744ebd7ce32648932fb140a8fdb Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sun, 24 Mar 2024 12:18:29 +0100 Subject: [PATCH 12/13] Fixes to both implementation and the documentation --- .../derivation/transformer/Configurations.scala | 4 ++-- docs/docs/supported-transformations.md | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) 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 3b29f95f3..663b05f13 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 @@ -410,8 +410,8 @@ private[compiletime] trait Configurations { this: Derivation => val name = AnsiControlCode.replaceAllIn(Type.prettyPrint[Comparison], "") Iterator - .iterate(name.replace('.', '$') + '$')(_.replaceFirst("\\$", ".")) - .take(name.count(_ == '.') + 1) // ...then this is: "foo$bar$baz$", "foo.bar$baz$", "foo.bar.baz$"... + .iterate(name + '$')(_.reverse.replaceFirst("[.]", "\\$").reverse) + .take(name.count(_ == '.') + 1) // ...then this is: "foo.bar.baz$", "foo.bar$baz$", "foo$bar$baz$"... .toArray .reverse // ...and this is: "foo.bar.baz$", "foo.bar$baz$", "foo$bar$baz$" .collectFirst { case Comparison(value) => value } // attempts: top-level object, object in object, etc diff --git a/docs/docs/supported-transformations.md b/docs/docs/supported-transformations.md index 5820f8a98..38a3a09b2 100644 --- a/docs/docs/supported-transformations.md +++ b/docs/docs/supported-transformations.md @@ -2801,11 +2801,11 @@ but Chimney has a specific solution for this: // Object is "case" for better toString output. case object PermissiveNamesComparison extends TransformedNamesComparison { - private val normalize(name: String): String = { + private def normalize(name: String): String = { val name2 = if (name.startsWith("is")) name.drop(2) - else (name.startsWith("get")) name.drop(3) - else (name.startsWith("set")) name.drop(3) + else if (name.startsWith("get")) name.drop(3) + else if (name.startsWith("set")) name.drop(3) else name name2.replaceAll("[-_]", "") } @@ -2830,4 +2830,6 @@ but Chimney has a specific solution for this: // this would be parsed as well //.enableCustomSubtypeNameComparison(your.organization.PermissiveNamesComparison) .transform - ``` \ No newline at end of file + ``` + +Since this feature relied on ClassLoaders and class path lookup it, testing it with REPL may not work. From c7aff77a6a7980d0bbb7616df056f901e473b200 Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Sun, 24 Mar 2024 12:31:48 +0100 Subject: [PATCH 13/13] Scaladoc documentation for name matching customization --- .../dsl/TransformedNamesComparison.scala | 19 +++++++++++++++++-- .../chimney/dsl/TransformerFlagsDsl.scala | 8 ++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala index 16844af95..3669408db 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformedNamesComparison.scala @@ -1,13 +1,26 @@ package io.scalaland.chimney.dsl -// TODO: documentation - +/** Provides a way of customizing how fields/subtypes shoud get matched betwen source value and target value. + * + * @see [[https://chimney.readthedocs.io/supported-transformations/#defining-custom-name-matching-predicate]] for more details + * + * @since 1.0.0 + */ abstract class TransformedNamesComparison { this: Singleton => + /** Return true if `fromName` should be considered a match for `toName`. + * + * @param fromName name of a field/subtype in the source type + * @param toName name of a field/subtype in the target type + * @return whether fromName should be used as a source for value in toName + */ def namesMatch(fromName: String, toName: String): Boolean } + +/** @since 1.0.0 */ object TransformedNamesComparison { + /** Matches names, dropping is/get/set prefixes and then lowercasing the first letter if it was a Bean name. */ case object BeanAware extends TransformedNamesComparison { // While it's bad to refer to compiletime package this code should only be used by this compiletime package. @@ -19,11 +32,13 @@ object TransformedNamesComparison { fromName == toName || normalize(fromName) == normalize(toName) } + /** Matches only the same Strings. */ case object StrictEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName == toName } + /** Matches Strings ignoring upper/lower case distinction. */ case object CaseInsensitiveEquality extends TransformedNamesComparison { def namesMatch(fromName: String, toName: String): Boolean = fromName.equalsIgnoreCase(toName) 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 ccb676b12..38532f3c9 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala @@ -200,7 +200,7 @@ private[dsl] trait TransformerFlagsDsl[UpdateFlag[_ <: TransformerFlags], Flags * * @param namesComparison parameter specifying how names should be compared by macro * - * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * @see [[https://chimney.readthedocs.io/supported-transformations/#customizing-field-name-matching]] for more details * * @since 1.0.0 */ @@ -211,7 +211,7 @@ private[dsl] trait TransformerFlagsDsl[UpdateFlag[_ <: TransformerFlags], Flags /** Disable any custom way of comparing if source fields' names and target fields' names are matching. * - * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * @see [[https://chimney.readthedocs.io/supported-transformations/#customizing-field-name-matching]] for more details * * @since 1.0.0 */ @@ -222,7 +222,7 @@ private[dsl] trait TransformerFlagsDsl[UpdateFlag[_ <: TransformerFlags], Flags * * @param namesComparison parameter specifying how names should be compared by macro * - * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * @see [[https://chimney.readthedocs.io/supported-transformations/#customizing-subtype-name-matching]] for more details * * @since 1.0.0 */ @@ -233,7 +233,7 @@ private[dsl] trait TransformerFlagsDsl[UpdateFlag[_ <: TransformerFlags], Flags /** Disable any custom way of comparing if source subtypes' names and target fields' names are matching. * - * @see [[https://chimney.readthedocs.io/supported-transformations/#TODO]] for more details + * @see [[https://chimney.readthedocs.io/supported-transformations/#customizing-subtype-name-matching]] for more details * * @since 1.0.0 */