Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize field/subtype name comparison during macro expansion #478

Merged
merged 13 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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) } // _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))
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -179,7 +180,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) } // _exact_ name match!
.map { case (name, setter) =>
val tpe = ExistentialType(fromUntyped[Any](paramsWithTypes(A, setter, isConstructor = false).head._2))
(
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +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 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
protected def caseClassApplyDefaultScala3(idx: Int): String = "$lessinit$greater$default$" + idx
Expand Down Expand Up @@ -162,31 +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) {

def areNamesMatching(fromName: String, toName: String): Boolean =
fromName == toName || normalize(fromName) == normalize(toName)
def isMatching(value: String): Boolean = regexp.pattern.matcher(value).matches() // 2.12 doesn't have .matches
}

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 getAccessor = raw"(?i)get(.)(.*)".r
private val isAccessor = raw"(?i)is(.)(.*)".r
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 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 normalize: String => String = dropGetIs.andThen(dropSet)
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(
Expand Down Expand Up @@ -220,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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@
Some(A.param_<[dsls.ImplicitTransformerPreference](0))
else scala.None
}
object FieldNameComparison extends FieldNameComparisonModule {
def apply[C <: dsls.TransformedNamesComparison: Type]: Type[runtime.TransformerFlags.FieldNameComparison[C]] =

Check warning on line 230 in chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala#L230

Added line #L230 was not covered by tests
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]]

Check warning on line 240 in chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala#L240

Added line #L240 was not covered by tests
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]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,23 @@
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]]

Check warning on line 221 in chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala#L220-L221

Added lines #L220 - L221 were not covered by tests
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]

Check warning on line 228 in chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala#L228

Added line #L228 was not covered by tests
: Type[runtime.TransformerFlags.SubtypeNameComparison[C]] =
quoted.Type.of[runtime.TransformerFlags.SubtypeNameComparison[C]]

Check warning on line 230 in chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala-3/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala#L230

Added line #L230 was not covered by tests
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]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.scalaland.chimney.dsl

/** 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.
// 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)
}

/** 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)
}

type FieldDefault = BeanAware.type
val FieldDefault: FieldDefault = BeanAware

type SubtypeDefault = StrictEquality.type
val SubtypeDefault: SubtypeDefault = StrictEquality
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,50 @@
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/#customizing-field-name-matching]] 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/#customizing-field-name-matching]] for more details
*
* @since 1.0.0
*/
def disableCustomFieldNameComparison: UpdateFlag[Disable[FieldNameComparison[?], Flags]] =
disableFlag[FieldNameComparison[?]]

Check warning on line 219 in chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala#L218-L219

Added lines #L218 - L219 were not covered by tests

/** 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/#customizing-subtype-name-matching]] 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/#customizing-subtype-name-matching]] for more details
*
* @since 1.0.0
*/
def disableCustomSubtypeNameComparison: UpdateFlag[Disable[SubtypeNameComparison[?], Flags]] =
disableFlag[SubtypeNameComparison[?]]

Check warning on line 241 in chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerFlagsDsl.scala#L240-L241

Added lines #L240 - L241 were not covered by tests

/** Enable printing the logs from the derivation process.
*
* @see [[https://chimney.readthedocs.io/troubleshooting/#debugging-macros]] for more details
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
}
Expand Down
Loading
Loading