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

Transformation failure when a case class has a method starting with set #424

Closed
5 of 7 tasks
ghostdogpr opened this issue Oct 31, 2023 · 7 comments · Fixed by #432 or #485
Closed
5 of 7 tasks

Transformation failure when a case class has a method starting with set #424

ghostdogpr opened this issue Oct 31, 2023 · 7 comments · Fixed by #432 or #485
Labels
bug Erroneous behavior in existing features
Milestone

Comments

@ghostdogpr
Copy link
Contributor

Checklist

Describe the bug
Given 2 case classes A and B that have the same content, if A has a method starting with set, B can't be converted to A.

Reproduction

import io.scalaland.chimney.dsl._

case class A(values: List[Int]) {
  def setValue(value: Int): A = A(List(value))
}
case class B(values: List[Int])

A(Nil).transformInto[B]
B(Nil).transformInto[A]

Scastie: https://scastie.scala-lang.org/ke3lsriNTSG6AOMivpvODQ

Expected behavior
Transformation should work.

Actual behavior
Transformation fails with:

Chimney can't derive transformation from Playground.B to Playground.A

Playground.A
  setValue(value: scala.Int) - no accessor named value in source type Playground.B


Consult https://chimney.readthedocs.io for usage examples.

Which Chimney version do you use
Version you used for confirming bug with a snippet.

Which platform do you use

  • JVM
  • Scala.js
  • Scala Native

If you checked JVM
JDK17

Additional context
Appeared in 0.8.0, working well in 0.7.x.

@ghostdogpr ghostdogpr added the bug Erroneous behavior in existing features label Oct 31, 2023
@MateuszKubuszok
Copy link
Member

MateuszKubuszok commented Oct 31, 2023

Every method starting with set is considered a setter, to write to setters there has to be a flag enabled (.enableBeanSetters). Ignoring setters is currently not supported.

In 0.7.x we also checked if return type is Unit to consider something a setter, but it was removed to allow targeting mutable builders with fluent API and Unit is no longer listed as a requirement in the docs.

@ghostdogpr
Copy link
Contributor Author

It would be nice if those set methods were ignored unless enableBeanSetters is true. I had to rename a few fields (that were not returning Unit) to work around it, nothing serious though 😄

@MateuszKubuszok
Copy link
Member

I'd say I cannot do this: virtually every single time we "let something work out of the box, with opt-out option" we soon received a ticket that this is unsafe, results in unpredicatable code, and the default should be an error with an explicit resolution, otherwise the library is dangerous to use -_-". Examples OTOH:

Additionally, I remember some requests on Gitter, when we still used it, for disabling automatic wrapping/unwrapping of AnyVals.

When Java Bean getters were initially implemented they were opt-out - #81 - but then we changed it to opt-in in c71a429 because of #85.

So all in all, we converge towards disabling features by defaul based on the feedback from the community. If I enable something OOTB, I can start the timer till the mob with torches arrives.

I see 2 things that could be done to address your issue:

  1. workaround for now would be to use implicit TransformerConfiguration to share enableBeanSetters and maybe something like withFieldConst(_.getField, null), this would require having getter though and produce an useless call with an overhead.

    import io.scalaland.chimney.dsl._
    
    implicit val cfg = TransformerConfiguration.default.enableBeanSetters
    
    case class A(values: List[Int]) {
      def getValue: Int = 0 // stub for matching only
      def setValue(value: Int): A = A(List(value))
    }
    case class B(values: List[Int])
    
    B(Nil).into[A].withFieldConst(_.getValue, 0).transform

    If there would be no stub getter, to provide a value, nor other source field to read from with matching name, it would not work

  2. a proper solution could be adding a flag like .enableIgnoreUnmatchedBeanSetters allowing to ignore such unmatched setters, then you could enable it in your whole scope via implicit, it would look something like:

    import io.scalaland.chimney.dsl._
    
    implicit val cfg = TransformerConfiguration.default.enableIgnoreUnmatchedBeanSetters
    
    case class A(values: List[Int]) {
      def setValue(value: Int): A = A(List(value))
    }
    case class B(values: List[Int])
    
    A(Nil).transformInto[B]
    B(Nil).transformInto[A]

@MateuszKubuszok MateuszKubuszok linked a pull request Nov 8, 2023 that will close this issue
3 tasks
@MateuszKubuszok
Copy link
Member

I released 0.8.3 with enableIgnoreUnmatchedBeanSetters flag which should make it easier to deal with your cases.

@ghostdogpr
Copy link
Contributor Author

Thanks!

@MateuszKubuszok
Copy link
Member

I am considering adding in 1.0.0-RC a flag to switch between 0.7.x and 0.8.x behavior, no ETA ATM.

@MateuszKubuszok MateuszKubuszok added this to the 1.0.0-RC milestone Dec 19, 2023
@MateuszKubuszok MateuszKubuszok linked a pull request Mar 29, 2024 that will close this issue
28 tasks
@MateuszKubuszok
Copy link
Member

NoUnitBeanSetters flag added in 1.0.0-M4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Erroneous behavior in existing features
Projects
None yet
2 participants