scalalandio / chimney

Scala library for boilerplate-free, type-safe data transformations

Home Page:https://chimney.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Silent invalid behavior of `withFieldConst` when field name starts with "set"

Tvaroh opened this issue · comments

Reproducing example:

object ChimneyBug extends App {

  import io.scalaland.chimney.dsl.*
  import java.util.UUID

  case class From(x: String)
  case class To(uuid: UUID, setUuid: UUID, x: String)

  val result: To =
    From("meh").into[To]
      .withFieldConst(_.uuid, UUID.randomUUID())
      .withFieldConst(_.setUuid, UUID.randomUUID())
      .transform

  println(result)

}

Result:

To(96720c38-267d-4559-855a-02cee6cc8443,96720c38-267d-4559-855a-02cee6cc8443,meh)

If renamed to e.g. settUuid:

object ChimneyBug extends App {

  import io.scalaland.chimney.dsl.*
  import java.util.UUID

  case class From(x: String)
  case class To(uuid: UUID, settUuid: UUID, x: String)

  val result: To =
    From("meh").into[To]
      .withFieldConst(_.uuid, UUID.randomUUID())
      .withFieldConst(_.settUuid, UUID.randomUUID())
      .transform

  println(result)

}

Result:

To(59711053-b512-4601-9f70-e41c4b05a694,130971f2-7720-4707-b81b-f7c0cbdd6c25,meh)

Notice if I rename the other uuid field to something else then it works correctly. Chimney 0.8.5.

I saw the ignored #449 bug too. Just worth mentioning that in our domain setUuid wasn't considered a setter, but rather a uuid of a set of entities. Anyway, it shouldn't allow to compile it silently, if using "setter"-like names is prohibited.

I saw the ignored #449 bug too. Just worth mentioning that in our domain setUuid wasn't considered a setter, but rather a uuid of a set of entities. Anyway, it shouldn't allow to compile it silently, if using "setter"-like names is prohibited.

Setter names are NOT prohibited - we are explicitly testing that if all bean-like names are provided manually then there is no need to use a flag to enable automatic matching of bean names. The lack of errors here is a feature, that for the last 5+ years (we're testing for this since Nov 2018).

Sure it, might brings some corner cases like this were:

  • some people would prefer it to FAIL if their names are not matched
  • other people would prefer to NOT have to provide an additional configuration if they already provided the matching

where the difference shows when there is a case where only human can say what is expected behavior from the context. There might be several possible approaches like:

  • prioritizing exact matches before "relaxed" matches
  • checking if there are not fields/values something could be confused with before doing a relaxed matching
  • checking more than just name (e.g. checking number of parameter lists at the same time names are compared)
  • etc

so, any solution would require some discussion rather just stating fix it the way I like.

Besides, the #499 is NOT ignored - it was if it was closed with wontfix label. Instead, I wrote there that I do not have a capacity for such fixes, but I would happily discuss how they can be fixed and review+accept PRs. I have very little time for any OSS work these days (and I spend way too much time the last year to get this library to where is it now) so current if anyone is bothered by such bug, then, I as said, this is OSS, we can discuss what would be an acceptable change of the behavior, someone can make a PR, I could merge it and release it.

Hey @MateuszKubuszok - maybe I didn't spell it clear enough, but the problem is that setUuid field is set to the to other uuid field value silently, instead of the value provided. We found that only after a bug happened. I.e. if you have a foo field and a setFoo field and you set both to different values using withFieldConst, they both get set to the value of foo, ignoring the value of setFoo. Do you imagine some different behavior here rather than throwing a compilation error or setting the fields to the correct provided values?

It seems to be an interaction between legitimate features @Tvaroh - support for java beans is interfering with your naming convention.

Do you imagine some different behavior here rather than throwing a compilation error or setting the fields to the correct provided values?

Yes, I do.

The issue stems from the fact that:

  • there is some List(setUuid, uuid)
  • we are doing (more or less) method.collectFirst { case inName if matchNames(inName, outName) => ... }
  • this match is not checking arity
  • this match is not checking that some values might have only setter but not getter, or vice versa
  • there can more than 1 value which can be matched, and these values are not identical (e.g. val name and def getName paired with it - can happen with case classes with @BeanProperty - or var name and def setName - same situation - in those cases having 2 methods is expected, but there might be similar names which are completely unrelated)

Therefore before throwing any sort of simple compile time error - which would introduce a regression to a lot of users who DO NOT EXPECT NOR WANTS IT - it would make more sense to:

  • refine cases where things should work OOTB
  • refine cases where things would be a clear error
  • rethink more edge cases
  • improve a specification by adding specification in a way that would try to not break things for legit cases
  • then change the algorithm

and I can imagine more than one way to fix it. Like the ones I already mentioned.

Simply throwing an error if there are 2 similar cases can break cases like e.g.

// Scala 2.13

// what you see:
case class Foo(@BeanProperty a: Int)

class Bar {
  @BeanProperty
  var a: Int = 0
}

// what compiler sees:
class Foo(@val a: Int) {
  def getA(): Int = a
  // ...
}
object Foo { /* ... */ }

class Bar {
  var a: Int = 0
  def getA(): Int = a
  def setA(a: Int): Unit = this.a = a
}
// oh no! Foo has a and getA, and Bar has a, getA and setA!

which is pretty damn legit case. Here generated with @BeanProperty (in 2.13, Scala 3 has a different behavior for @BeanProperty) but sometimes people write such code by hand, to provide an interoperability between Scala and Java libraries.

I am pretty damn sure, that if I used some "simple and naive" approach like throwing compiler error always, in 2 weeks I'll get another bug, this time from a frustrated legacy-integration developers who used Chimney effortlessly with Java Beans before and now they cannot. The solution usually looks simple if you have a statistical pool of 1, but after putting here 500+ tests of various parts of the library I can assure you that sometimes a good solution is pretty fucking far from obvious, and "just failing compilation" doesn't always make a cut.

It seems to be an interaction between legitimate features @Tvaroh - support for java beans is interfering with your naming convention.

It is, I checked how much effort would take to fix #449 for instance (by fix I mean "make the test case for it pass without failing other test cases"), and I can confidently say - more than 1 evening.

Fixed in 1.0.0-M4