Skip to content

Releases: arainko/ducktape

ducktape 0.2.2

27 May 20:28
b889101

Choose a tag to compare

This release brings back lints for configs that override each other, for example:

final case class FieldSource(additionalArg: String, str: String)

val fieldSource = FieldSource("str-sourced", "str2")

val expected = TestClassWithAdditionalString(1, "str2", "str-computed")

testClass
    .into[TestClassWithAdditionalString]
    .transform(
        Field.allMatching(fieldSource),
        Field.allMatching(fieldSource),
      )

will result in a compilation warning that looks like this:

Configs for:
  * TestClassWithAdditionalString.str
  * TestClassWithAdditionalString.additionalArg
are being overriden by Field.allMatching(fieldSource) @ AppliedBuilderSuite.scala:185:41

It also introduces Mode#locally that allows you to use a Mode in a scope of a function so that it doesn't need to be bound to a given.

What's Changed

Full Changelog: v0.2.1...v0.2.2

ducktape 0.2.1

19 May 10:31
e726035

Choose a tag to compare

This release adds a lint that rejects calls to non-Transformer.define* family of methods in given Transformer declarations (#165 )

What's Changed

Full Changelog: v0.2.0...v0.2.1

ducktape 0.2.0

28 Mar 20:57
c2823f8

Choose a tag to compare

ducktape 0.2.0

First of all, head on over to the new docs site.

The library has been rebuilt from the ground-up to not rely on automatic derivation of Transformer and Transformer.Fallible and enable further development of more advanced features (the examples of which are nested configurations and support for regional configs like Field.fallbackToDefault - with more still to come). All of that should result in a much higher quality of code that gets generated underneath (for example, fallible transformations do not generate interim Transformer.Fallible instances anymore).

While this release is not binary-compatible with ducktape 0.1.x, it's aiming to be as source-compatible as possible. All the known breakages are documented in the Coming from 0.1.x section of the docs. There are also a bunch of deprecated forwarders with (hopefully) user-friendly tips on how to make the deprecation warnings go away.

New highlight features

  • ability to configure deeply nested transformations without having to resort to building out new Transformer instances, best showcased in the Configuring transformations section of the docs

  • revamped error reporting - errors are now accumulated and shown at once and are supposed to be actionable e.g. in case of a field that's missing it'll give the user a hint and a path to that field for usage in one of the configuration options, for example:

    No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name
    

    More information is available in the Motivating example section of the docs.

  • new flavor of configuration options - regional configs:

    • Field.fallbackToDefault for falling back to default values in cases where a transformation couldn't be derived,
    • Field.fallbackToNone for falling back to None for Option fields for which a transformation couldn't be derived
      Regional configs can be made to work in only user-selected subregions of the transformations, eg.:
    Field.fallbackToDefault.regional(_.field1.at[Case1])

    will make it apply only 'below' field1 at it's Case1 subtype. More info and examples are available in Configuring transformations

What's Changed

Full Changelog: v0.2.0-RC1...v0.2.0

ducktape 0.2.0-RC1

22 Mar 18:18
ecebb3a

Choose a tag to compare

ducktape 0.2.0-RC1

The first release candidate of the 0.2.x line - to be promoted to a final 0.2.0 release if nothing comes up in the next week.
Starting from this release the default branch will start pointing to the series/0.2.x branch.

The docs are now published on a docs page as opposed to being kept in a readme.

What's Changed

Full Changelog: v0.2.0-M5...v0.2.0-RC1

ducktape 0.2.0-M5

10 Mar 15:24
9560997

Choose a tag to compare

ducktape 0.2.0-M5

This release bring a new encoding of accumulating transformations (well, the 'decoding', really) - the final step destructing the zipped Tuple is now represented as a chain of accessors, i.e. this:

 (value: Tuple2[Type, Tuple2[Type, Type]]) => new Person(name = value._1, age = value._2._1, socialSecurityNo = value._2._2)
  ): Either[List[String], Person]): Either[List[String], Person])

instead of a pattern match like this one:

value: Tuple2[Tuple2[Type, Type], Type]) =>
              value match {
                case Tuple2(Tuple2(name, age), socialSecurityNo) =>
                  new ValidatedPerson(name = name, age = age, socialSecurityNo = socialSecurityNo)
                case x =>
                  throw new MatchError(x)
              }
          )

This release is very likely to become an RC if nothing serious comes up in the next week.

What's Changed

Full Changelog: v0.2.0-M4...v0.2.0-M5

ducktape 0.2.0-M4

24 Feb 13:16
13272b8

Choose a tag to compare

This release is identical to 0.2.0-M3 in terms of features. Publishing should be fixed now.

What's Changed

  • remove tooling, inline Debug, fix publishing by @arainko in #123

Full Changelog: v0.2.0-M3...v0.2.0-M4

ducktape 0.2.0-M3

24 Feb 12:14
6386b3f

Choose a tag to compare

ducktape 0.2.0-M3

This release brings fallible transformations into the new backend.
There are some issues uncovered when compiling with -Xcheck-macros, namely:

  • Both Accumulating and FailFast transformations report bad owners inside lambdas of non-fallible sub-transformations (eg. ihen transforming between Options, Collections, etc.) - for the time being -Xcheck-macros has been disabled to be pragmatic and actually deliver the feature set
  • compiler crashes when trying to configure stuff under an .element path element with intoVia (but only during direct transformations, transforming with Transformer.define works in those cases as a workaround ()
  • compiler crashes when using local classes when configuring some cases with Case.fallible*

What's Changed

Full Changelog: v0.2.0-M2...v0.2.0-M3

ducktape 0.2.0-M2

18 Dec 15:16
a59f0dc

Choose a tag to compare

ducktape 0.2.0-M2

The 0.2.x series comes with a lot of improvements, some of them being:

  • support for configurations for nested transformations (previously these would need to be their own Transformer instance, no matter how basic the config was),
  • the library now reports all of the transformation errors all at once (with copy-pastable hints on how to fix a given error, eg.
No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name

...and many others.

The 0.2.x series also retains source compatibility with the 0.1.x series to the fullest extent possible (there are SOME changes which will be documented in detail some time in the future) - but generally your code should compile and work as is.

This milestone release only contains support for total transformations, fallible transformations are coming soon.

Read on over to see what is actually possible now or read the docs and try it for yourself:

import java.time.Instant
import io.github.arainko.ducktape.*

// imagine this is a wire model of some kind - JSON, protobuf, avro, what have you...
object wire {
  final case class Person(
    firstName: String,
    lastName: String,
    paymentMethods: List[wire.PaymentMethod],
    status: wire.Status,
    updatedAt: Option[Instant],
  )

  enum Status:
    case Registered, PendingRegistration, Removed

  enum PaymentMethod:
    case Card(name: String, digits: Long, expires: Instant)
    case PayPal(email: String)
    case Cash
}

object domain {
  final case class Person( // <-- fields reshuffled 
    lastName: String,
    firstName: String,
    status: Option[domain.Status], // <-- 'status' in the domain model is optional
    paymentMethods: Vector[domain.Payment], // <-- collection type changed from a List to a Vector
    updatedAt: Option[Instant],
  )

  enum Status:
    case Registered, PendingRegistration, Removed
    case PendingRemoval // <-- additional enum case

  enum Payment:
    case Card(name: String, digits: Long, expires: Instant)
    case PayPal(email: String)
    case Cash
}

val wirePerson: wire.Person = wire.Person(
  "John",
  "Doe",
  List(
    wire.PaymentMethod.Cash,
    wire.PaymentMethod.PayPal("john@doe.com"),
    wire.PaymentMethod.Card("J. Doe", 12345, Instant.now)
  ),
  wire.Status.PendingRegistration,
  Some(Instant.ofEpochSecond(0))
)
val domainPerson = wirePerson.to[domain.Person]
// domainPerson: Person = Person(
//   lastName = "Doe",
//   firstName = "John",
//   status = Some(value = PendingRegistration),
//   paymentMethods = Vector(
//     Cash,
//     PayPal(email = "john@doe.com"),
//     Card(
//       name = "J. Doe",
//       digits = 12345L,
//       expires = 2023-12-17T14:54:13.882678035Z
//     )
//   ),
//   updatedAt = Some(value = 1970-01-01T00:00:00Z)
// )
Click to see the generated code
  (({
    val paymentMethods$2: Vector[Payment] = MdocApp.this.wirePerson.paymentMethods
      .map[Payment]((src: PaymentMethod) =>
        if (src.isInstanceOf[Card])
          new Card(
            name = src.asInstanceOf[Card].name,
            digits = src.asInstanceOf[Card].digits,
            expires = src.asInstanceOf[Card].expires
          )
        else if (src.isInstanceOf[PayPal]) new PayPal(email = src.asInstanceOf[PayPal].email)
        else if (src.isInstanceOf[Cash.type]) MdocApp.this.domain.Payment.Cash
        else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
      )
      .to[Vector[Payment]](iterableFactory[Payment])
    val status$2: Some[Status] = Some.apply[Status](
      if (MdocApp.this.wirePerson.status.isInstanceOf[Registered.type]) MdocApp.this.domain.Status.Registered
      else if (MdocApp.this.wirePerson.status.isInstanceOf[PendingRegistration.type])
        MdocApp.this.domain.Status.PendingRegistration
      else if (MdocApp.this.wirePerson.status.isInstanceOf[Removed.type]) MdocApp.this.domain.Status.Removed
      else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
    )
    new Person(
      lastName = MdocApp.this.wirePerson.lastName,
      firstName = MdocApp.this.wirePerson.firstName,
      status = status$2,
      paymentMethods = paymentMethods$2,
      updatedAt = MdocApp.this.wirePerson.updatedAt
    )
  }: Person): Person)

But now imagine that your wire model differs ever so slightly from your domain model, maybe the wire model's PaymentMethod.Card doesn't have the name field for some inexplicable reason...

object wire {
  final case class Person(
    firstName: String,
    lastName: String,
    paymentMethods: List[wire.PaymentMethod],
    status: wire.Status,
    updatedAt: Option[Instant],
  )

  enum Status:
    case Registered, PendingRegistration, Removed

  enum PaymentMethod:
    case Card(digits: Long, expires: Instant) // <-- poof, 'name' is gone
    case PayPal(email: String)
    case Cash
}

val wirePerson: wire.Person = wire.Person(
  "John",
  "Doe",
  List(
    wire.PaymentMethod.Cash,
    wire.PaymentMethod.PayPal("john@doe.com"),
    wire.PaymentMethod.Card(12345, Instant.now)
  ),
  wire.Status.PendingRegistration,
  Some(Instant.ofEpochSecond(0))
)

...and when you try to transform between these two representations the compiler now yells at you.

val domainPerson = wirePerson.to[domain.Person]
// error:
// No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name
//   case Cash
//       ^

Now onto dealing with that, let's first examine the error message:

No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name

especially the part after @:

Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name

the thing above is basically a path to the field/subtype under which ducktape was not able to create a transformation, these are meant to be copy-pastable for when you're actually trying to fix the error, eg. by setting the name field to a constant value:

val domainPerson = 
  wirePerson
    .into[domain.Person]
    .transform(Field.const(_.paymentMethods.element.at[domain.Payment.Card].name, "CONST NAME"))
// domainPerson: Person = Person(
//   lastName = "Doe",
//   firstName = "John",
//   status = Some(value = PendingRegistration),
//   paymentMethods = Vector(
//     Cash,
//     PayPal(email = "john@doe.com"),
//     Card(
//       name = "CONST NAME",
//       digits = 12345L,
//       expires = 2023-12-17T14:54:13.890982787Z
//     )
//   ),
//   updatedAt = Some(value = 1970-01-01T00:00:00Z)
// )
Click to see the generated code
  {
    val AppliedBuilder_this: AppliedBuilder[Person, Person] = into[Person](MdocApp1.this.wirePerson1)[MdocApp1.this.domain.Person]

    {
      val value$proxy3: Person = AppliedBuilder_this.inline$value

      {
        val paymentMethods$4: Vector[Payment] = value$proxy3.paymentMethods
          .map[Payment]((src: PaymentMethod) =>
            if (src.isInstanceOf[Card])
              new Card(name = "CONST NAME", digits = src.asInstanceOf[Card].digits, expires = src.asInstanceOf[Card].expires)
            else if (src.isInstanceOf[PayPal]) new PayPal(email = src.asInstanceOf[PayPal].email)
            else if (src.isInstanceOf[Cash.type]) MdocApp1.this.domain.Payment.Cash
            else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
          )
          .to[Vector[Payment]](iterableFactory[Payment])
        val status$4: Some[Status] = Some.apply[Status](
          if (value$proxy3.status.isInstanceOf[Registered.type]) MdocApp1.this.domain.Status.Registered
          else if (value$proxy3.status.isInstanceOf[PendingRegistration.type]) MdocApp1.this.domain.Status.PendingRegistration
          else if (value$proxy3.status.isInstanceOf[Removed.type]) MdocApp1.this.domain.Status.Removed
          else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
        )
        new Person(
          lastName = value$proxy3.lastName,
          firstName = value$proxy3.firstName,
          status = status$4,
          paymentMethods = paymentMethods$4,
          updatedAt = value$proxy3.updatedAt
        )
      }: Person
    }: Person
  }

ducktape 0.1.11

16 Aug 21:07
44dfdde

Choose a tag to compare

This is a bugfix release that fixes an issue with wrong flags being set when constructing zipped products. Shout-out to @WojciechMazur and the team working on the community build for finding and fixing this.

What's Changed

New Contributors

Full Changelog: v0.1.10...v0.1.11

ducktape 0.1.10

15 Jul 16:56
9b89f3b

Choose a tag to compare

This release brings Transformer.Fallible automatic derivation for coproducts courtesy of @gregor-i along with new and shiny config options for your fallible coproduct transformations like Case.fallibleConst or Case.fallibleComputed.

This release also bumps the Scala version to 3.3.0 (a LTS release) which means all users of this lib also need to bump their projects to 3.3.0 (there's literally no reason not to as Scala 3 is backwards compatible).

What's Changed

New Contributors

Full Changelog: v0.1.9...v0.1.10