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

issue 71: reading from java beans #81

Merged
merged 13 commits into from
Sep 12, 2018
Merged
12 changes: 4 additions & 8 deletions chimney/src/main/scala/io/scalaland/chimney/dsl.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
package io.scalaland.chimney

import io.scalaland.chimney.internal.{
ChimneyBlackboxMacros,
ChimneyWhiteboxMacros,
DisableDefaults,
Empty,
Cfg,
DisableOptionDefaultsToNone
}
import io.scalaland.chimney.internal._

import scala.language.experimental.macros

Expand All @@ -29,6 +22,9 @@ object dsl {
def disableDefaultValues: TransformerInto[From, To, DisableDefaults[C]] =
new TransformerInto[From, To, DisableDefaults[C]](source, overrides, instances)

def disableBeanGetterLookup: TransformerInto[From, To, DisableBeanGetterLookup[C]] =
new TransformerInto[From, To, DisableBeanGetterLookup[C]](source, overrides, instances)

def disableOptionDefaultsToNone: TransformerInto[From, To, DisableOptionDefaultsToNone[C]] =
new TransformerInto[From, To, DisableOptionDefaultsToNone[C]](source, overrides, instances)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ sealed abstract class Cfg

final class Empty extends Cfg
final class DisableDefaults[C <: Cfg] extends Cfg
final class DisableBeanGetterLookup[C <: Cfg] extends Cfg
final class FieldConst[Name <: String, C <: Cfg] extends Cfg
final class FieldComputed[Name <: String, C <: Cfg] extends Cfg
final class FieldRelabelled[FromName <: String, ToName <: String, C <: Cfg] extends Cfg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ trait DerivationConfig {
val c: blackbox.Context

case class Config(disableDefaultValues: Boolean = false,
disableBeanGetterLookup: Boolean = false,
overridenFields: Set[String] = Set.empty,
renamedFields: Map[String, String] = Map.empty,
coproductInstances: Set[(c.Symbol, c.Type)] = Set.empty, // pair: inst type, target type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ trait DslBlackboxMacros {

val emptyT = typeOf[Empty]
val disableDefaultsT = typeOf[DisableDefaults[_]].typeConstructor
val disableBeansT = typeOf[DisableBeanGetterLookup[_]].typeConstructor
val disableOptionDefaultsToNone = typeOf[DisableOptionDefaultsToNone[_]].typeConstructor
val fieldConstT = typeOf[FieldConst[_, _]].typeConstructor
val fieldComputedT = typeOf[FieldComputed[_, _]].typeConstructor
Expand All @@ -36,6 +37,8 @@ trait DslBlackboxMacros {
config
} else if (cfgTpe.typeConstructor == disableDefaultsT) {
captureConfiguration(cfgTpe.typeArgs.head, config.copy(disableDefaultValues = true))
} else if (cfgTpe.typeConstructor == disableBeansT) {
captureConfiguration(cfgTpe.typeArgs.head, config.copy(disableBeanGetterLookup = true))
} else if (cfgTpe.typeConstructor == disableOptionDefaultsToNone) {
captureConfiguration(cfgTpe.typeArgs.head, config.copy(optionDefaultsToNone = false))
} else if (Set(fieldConstT, fieldComputedT).contains(cfgTpe.typeConstructor)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ trait MacroUtils extends CompanionUtils {
}

def getterMethods: Iterable[MethodSymbol] = {
def isParameterless(m: MethodSymbol) = m.paramLists.isEmpty || m.paramLists == List(List())
t.decls.collect {
case m: MethodSymbol if m.isGetter || ((m.paramLists.isEmpty || m.paramLists == List(List())) && m.isPublic) =>
case m: MethodSymbol if m.isPublic && (m.isGetter || isParameterless(m)) =>
m.asMethod
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,18 @@ trait TransformerMacros {

val fieldName = targetField.name.decodedName.toString

val fieldNameLookup = (m: MethodSymbol) => {
val sourceName = m.name.decodedName.toString
val targetNameCapitalized = fieldName.capitalize
if (config.disableBeanGetterLookup) {
sourceName == fieldName
} else {
sourceName == fieldName ||
sourceName == s"get$targetNameCapitalized" ||
(sourceName == s"is$targetNameCapitalized" && m.returnType == typeTag[Boolean].tpe)
}
}

if (config.overridenFields.contains(fieldName)) {
Some {
ResolvedFieldTree {
Expand All @@ -398,11 +410,11 @@ trait TransformerMacros {
}
} else {
fromParams
.find(_.name == targetField.name)
.find(fieldNameLookup)
.map { ms =>
if (ms.typeSignatureIn(tFrom) <:< targetField.typeSignatureIn(tTo)) {
ResolvedFieldTree {
q"$srcPrefixTree.${targetField.name}"
q"$srcPrefixTree.${ms.name}"
}
} else {
MatchingField(ms)
Expand Down
61 changes: 50 additions & 11 deletions chimney/src/test/scala/io/scalaland/chimney/DslSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -624,18 +624,50 @@ class DslSpec extends WordSpec with MustMatchers {
target.name mustBe source.name
}

"support java beans" in {
val source = new JavaBeanSource("test-id", "test-name")
val target = source
.into[CasesTarget]
.withFieldRenamed(_.getId, _.id)
.withFieldRenamed(_.getName, _.name)
.transform

target.id mustBe source.getId
target.name mustBe source.getName
}
"support java beans" should {

"work with basic renaming when bean getter lookup is disabled" in {
val source = new JavaBeanSource("test-id", "test-name")
val target = source
.into[CasesTarget]
.withFieldRenamed(_.getId, _.id)
.withFieldRenamed(_.getName, _.name)
.disableBeanGetterLookup
.transform

target.id mustBe source.getId
target.name mustBe source.getName
}

"support automatic reading from java bean getters" in {
val source = new JavaBeanSourceWithFlag(id = "test-id", name = "test-name", flag = true)
val target = source
.into[CasesTargetWithFlag]
.transform
target.id mustBe source.getId
target.name mustBe source.getName
target.flag mustBe source.isFlag
}

"not compile when bean getter lookup is disabled" in {
assertTypeError(
"""
new JavaBeanSourceWithFlag(id = "test-id", name = "test-name", flag = true).into[CasesTargetWithFlag].disableBeanGetterLookup.transform
"""
)
}

"not compile when matching an is- getter with type other than Boolean" in {
assertTypeError("""
|case class MistypedTarget(flag: Int)
|class MistypedSource(private var flag: Int) {
| def isFlag: Int = flag
|}
|new MistypedSource(1).into[MistypedTarget].transform
""".stripMargin)
}

}
}

}
Expand Down Expand Up @@ -682,6 +714,7 @@ object Poly {

object NonCaseDomain {
case class CasesTarget(val id: String, val name: String)
case class CasesTargetWithFlag(val id: String, val name: String, val flag: Boolean)

class ClassSource(val id: String, val name: String)

Expand All @@ -695,4 +728,10 @@ object NonCaseDomain {
def getId: String = id
def getName: String = name
}

class JavaBeanSourceWithFlag(private var id: String, private var name: String, private var flag: Boolean) {
def getId: String = id
def getName: String = name
def isFlag: Boolean = flag
}
}
31 changes: 31 additions & 0 deletions readme/Readme.scalatex
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,34 @@
@p
The @code{phone} remained the same as in the @code{exampleUser}, while the optional
e-mail string got transformed to an @code{Email} instance.

@sect{Java Beans}
@p
Chimney supports automatic field renaming for classes that follow Java beans naming convention.
Let's assume the following classes:

@hl.scala
class MyBean(private var id: Long,
private var name: String,
private var flag: Boolean) {
def getId: Long = id
def getName: String = name
def isFlag: Boolean = flag
}

case class MyCaseClass(id: String, name: String, flag: Boolean)

The conversion will work out-of-the-box, without any other requirements:

@hl.scala
val cc = new MyBean(1, "beanie", true).transformInto[MyCaseClass]

Please note that Chimney matches accessor methods solely based on name and return type, and has no way of ensuring
that a method named similarly to a getter is idempotent and does not actually perform side effects in its body.
If you'd like to disable this feature, you can use the @code{.disableBeanGetterLookup} operation:

@hl.scala
val cc = new MyBean(1, "beanie", true)
.into[MyCaseClass]
.disableBeanGetterLookup
.transform