Skip to content

Releases: arainko/ducktape

ducktape 0.2.12

29 Dec 23:17
977f6ad

Choose a tag to compare

ducktape 0.2.12

This is purely a bugfix release which addresses #319 (named tuple to tuple transformations being rejected).

What's Changed

Full Changelog: v0.2.11...v0.2.12

ducktape 0.2.11

03 Dec 18:58
b7dc16e

Choose a tag to compare

ducktape 0.2.11

This one's boring, it's mostly a maintenance release with a small bulletproofing of the named tuple check (which now relies on proper Symbol comparisons as opposed to silly silly String comparisons).

What's Changed

Full Changelog: v0.2.10...v0.2.11

ducktape 0.2.10

04 Aug 04:06
e3d1c4a

Choose a tag to compare

ducktape 0.2.10

This release adds support for named tuples while also staying on LTS.

Examples:

case class Person(int: Int, str: String)

assertTransforms((int = 1, str = "str"), Person(1, "str"))

assertTransforms(Person(1, "str"), (int = 1, str = "str"))

assertTransformsConfigured(
    (toplevel = (level1 = (level2 = 1))),
    (toplevel = (level1 = (level2 = 1, field = 2)))
  )(
    Field.const(_.toplevel.level1.field, 2)
  )

What's Changed

Full Changelog: v0.2.9...v0.2.10

ducktape 0.2.9

12 Jun 17:48
5b345ba

Choose a tag to compare

ducktape 0.2.9

This release brings lints to Field.modify{Source,Dest}Names and Case.modify{Source,Dest}Names which provide warnings when a rename config doesn't actually do anything:

    case class Source(int: Int, level1: SourceEnum)
    case class Dest(int: Int, level1: DestEnum)

    enum DestEnum {
      case One(int: Int, str: String)
      case Two(int: Int, str: String, level1: DestLevel1, level2: DestLevel1Enum)
      case Three(int: Int, str: String)
    }

    enum SourceEnum {
      case One(int: Int, str: String)
      case Two(int: Int, str: String, level1: SourceLevel1, level2: SourceLevel1Enum)
      case Three(int: Int, str: String)
    }

    enum SourceLevel1Enum {
      case One
      case Two
    }

    enum DestLevel1Enum {
      case One
      case Two
    }

    case class SourceLevel1(int: Int)
    case class DestLevel1(int: Int)

    assertTransformsConfigured(
      Source(1, SourceEnum.Two(2, "2", SourceLevel1(3), SourceLevel1Enum.Two)),
      Dest(1, DestEnum.Two(2, "2", DestLevel1(3), DestLevel1Enum.Two))
    )(
      Case.modifySourceNames(_.toLowerCase).local(_.level1.at[SourceEnum.Two])
   // ^ WARNING: Config is not actually being used anywhere
      )
    )

...or when one rename overrides another:

  test("local flags with higher priority overwrite flags with lower priority") {
    case class Source(int: Int, str: String)
    case class Dest(INT_ADDITION: Int, STR: String)

    assertTransformsConfigured(
      Source(1, "asd"),
      Dest(1, "asd")
    )(
      Field.modifyDestNames(_.toUpperCase.replace("_WHATEVER_THIS_FLAGS_HAS_LOWER_PRIO", "")).local(a => a),
      // ^ WARNING: Config is being overridden by Field.modifyDestNames(_.toLowerCase.replace("_addition", "")).local(a => a) @ FlagSuite.scala:523:82
      ),
      Field.modifyDestNames(_.toLowerCase.replace("_addition", "")).local(a => a)
    )
  }

What's Changed

Full Changelog: v0.2.8...v0.2.9

ducktape 0.2.8

19 Mar 06:06
cd0aafe

Choose a tag to compare

ducktape 0.2.8

Highlights of this release:

  • Support for field and case name transformations via Field.modifySourceNames, Field.modifyDestNames, Case.modifySourceNames and Case.modifyDestNames and modifiers for those config options that let you fine-tune the transformations further:
    • .regional(_.path.to.field) - this will apply renames to all transformations 'below' the path you've picked (this is the default)
    • .typeSpecific[SomeType] - this will apply the renames to subtypes of a type you've picked
    • .local(_.path.to.field) - this will apply renames to the field corresponding to that field/enum subcase (but not the field and the enum subcase themselves!)

Head on over to the docs to see more.

Examples
 test("dest field regional flag covers the selected case class and everything below it") {
    case class Source(int: Int, str: String, level1: SourceLevel1)
    case class SourceLevel1(INT: Int, STR: String, LEVEL2: SourceLevel2)
    case class SourceLevel2(INT: Int, STR: String)

    case class Dest(int: Int, str: String, level1: DestLevel1)
    case class DestLevel1(INT: Int, STR: String, LEVEL2: DestLevel2)
    case class DestLevel2(INT: Int, STR: String)

    val source = Source(1, "1", SourceLevel1(2, "2", SourceLevel2(3, "3")))
    val expected = Dest(1, "1", DestLevel1(2, "2", DestLevel2(3, "3")))

    assertTransformsConfigured(source, expected)(
      Field.modifyDestNames(_.toUpperCase).regional(_.level1)
    )
  }
  test("source case regional flag covers the selected subtype and everything below (picked as a field in case class)") {
    case class Source(int: Int, level1: SourceEnum)
    case class Dest(int: Int, level1: DestEnum)

    enum DestEnum {
      case one(int: Int, str: String)
      case two(int: Int, str: String, level1: DestLevel1, level2: DestLevel1Enum)
      case three(int: Int, str: String)
    }

    enum SourceEnum {
      case ONE(int: Int, str: String)
      case TWO(int: Int, str: String, level1: SourceLevel1, level2: SourceLevel1Enum)
      case THREE(int: Int, str: String)
    }

    enum SourceLevel1Enum {
      case One
      case Two
    }

    enum DestLevel1Enum {
      case one
      case two
    }

    case class SourceLevel1(int: Int)
    case class DestLevel1(int: Int)

    assertTransformsConfigured(
      Source(1, SourceEnum.TWO(2, "2", SourceLevel1(3), SourceLevel1Enum.Two)),
      Dest(1, DestEnum.two(2, "2", DestLevel1(3), DestLevel1Enum.two))
    )(
      Case.modifySourceNames(_.toLowerCase).regional(_.level1)
    )
  }

Examples of local flags:

test("dest field local flag covers the selected case class and nothing else") {
    case class Source(int: Int, str: String, level1: SourceLevel1)
    case class SourceLevel1(INT: Int, STR: String, LEVEL2: SourceLevel2)
    case class SourceLevel2(int: Int, str: String)

    case class Dest(int: Int, str: String, level1: DestLevel1)
    case class DestLevel1(int: Int, str: String, level2: DestLevel2)
    case class DestLevel2(int: Int, str: String)

    val source = Source(1, "1", SourceLevel1(2, "2", SourceLevel2(3, "3")))
    val expected = Dest(1, "1", DestLevel1(2, "2", DestLevel2(3, "3")))

    assertTransformsConfigured(source, expected)(
      Field.modifyDestNames(_.toUpperCase).local(_.level1)
    )
  }

and an example of a type specific flags:

 test("source field type specific flag covers all subtypes of an enum and nothing else (even when the enum is nested)") {
    case class Source(int: Int, level1: SourceEnum)
    case class Dest(int: Int, level1: DestEnum)

    sealed trait SourceEnum

    object SourceEnum {
      sealed trait NestLevel1 extends SourceEnum
      sealed trait NestLevel2 extends SourceEnum

      case class One(int: Int, str: String) extends NestLevel2
      case class Two(int: Int, str: String, level1: SourceLevel1) extends NestLevel1
      case class Three(int: Int, str: String) extends NestLevel2
    }

    sealed trait DestEnum

    object DestEnum {
      sealed trait NestLevel1 extends DestEnum
      sealed trait NestLevel2 extends DestEnum

      case class One(INT: Int, STR: String) extends NestLevel2
      case class Two(INT: Int, STR: String, LEVEL1: DestLevel1) extends NestLevel1
      case class Three(INT: Int, STR: String) extends NestLevel2
    }

    case class SourceLevel1(int: Int)
    case class DestLevel1(int: Int)

    assertTransformsConfigured(
      Source(1, SourceEnum.Two(2, "2", SourceLevel1(3))),
      Dest(1, DestEnum.Two(2, "2", DestLevel1(3)))
    )(
      Field.modifySourceNames(_.toUpperCase).typeSpecific[SourceEnum]
    )
  }
  • Renamer is the thing that fuels the rename DSL:
sealed trait Renamer {

  /**
   * Equivalent to `String#toUpperCase`
   */
  def toUpperCase: Renamer

  /**
   * Equivalent to `String#toLowerCase`
   */
  def toLowerCase: Renamer

  /**
   * Equivalent to the function `(str: String) => if str == from then to else str`
   */
  def rename(from: String, to: String): Renamer

  /**
   * Equivalent to `String#replace(target, replacement)`
   */
  def replace(target: String, replacement: String): Renamer

  /**
   * Equivalent to the function `(str: String) => Pattern.compile(pattern).matcher(str).replaceAll(replacement)`
   */
  def regexReplace(pattern: String, replacement: String): Renamer

  /**
   * Equivalent to `String#stripPrefix(prefix)`
   */
  def stripPrefix(prefix: String): Renamer

  /**
   * Equivalent to `String#stripSuffix(suffix)`
   */
  def stripSuffix(suffix: String): Renamer

  /**
   * Equivalent to `String#capitalize`
   */
  def capitalize: Renamer
}
  • Most of the API has finally had scaladoc with examples added to it

What's Changed

Full Changelog: v0.2.7...v0.2.8

ducktape 0.2.7

30 Dec 10:24
20c5e60

Choose a tag to compare

ducktape 0.2.7

This release brings two new config options: Field.computedDeep and Field.fallibleComputedDeep that allows you to compute a field by using the closest possible source value:

 case class SourceToplevel1(level1: Option[SourceLevel1])
    case class SourceLevel1(level2: Option[SourceLevel2])
    case class SourceLevel2(level3: SourceLevel3)
    case class SourceLevel3(int: Int)

    case class DestToplevel1(level1: Option[DestLevel1])
    case class DestLevel1(level2: Option[DestLevel2])
    case class DestLevel2(level3: Option[DestLevel3])
    case class DestLevel3(int: Long)

    val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(SourceLevel3(1))))))
    val expected = DestToplevel1(Some(DestLevel1(Some(DestLevel2(Some(DestLevel3(11)))))))

    assertTransformsConfigured(source, expected)(
      Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10)
    )

while also 'cutting through' Options, collections and other things that disallowed using 'Field.computed' in the past.

What's Changed

Full Changelog: v0.2.6...v0.2.7

ducktape 0.2.6

22 Oct 06:25
eb6844d

Choose a tag to compare

This is a bugfix release that fixes a compiletime issue where the wrong owners were used for a lambda in fallible accumulating transformations which sometimes manifested itself in an error after macro expansion.

It also fixes a runtime issue that resulted in an exception being thrown when directly configuring coproduct cases with Field configs.

What's Changed

Full Changelog: v0.2.5...v0.2.6

ducktape 0.2.5

31 Aug 06:21
ec336e5

Choose a tag to compare

This release removes a limitation of transformation configs that disallowed using them like lenses for updates, eg. the following:

case class Person(age: Int, name: String)

Person(1, "Joe").into[Person].transform(Field.const(_.age, 23))

would previously fail to compile because the transformation underneath was an identity transformation (i.e. not a configurable product-to-product transformation) - this has now been lifted and code like the above now works as you'd expect.

What's Changed

Full Changelog: v0.2.4...v0.2.5

ducktape 0.2.4

29 Jul 20:04
ba40550

Choose a tag to compare

ducktape 0.2.4

This a bugfix release.

Special-cased Option transformations shouldn't get dropped when doing fallible transformations with a Mode[Option] in scope.

Additionally, match-type-based tuple transformations are now bulletproof to most of the quirks of match types so stuff like tuple concatenation shouldn't throw it off.

What's Changed

  • Tuple docs by @arainko in #184
  • Update sbt-typelevel-ci-release, ... to 0.7.2 by @scala-steward in #186
  • Make Context.current a transparent inline to make it work under Scala 3.4.+ by @WojciechMazur in #189
  • [Issues #187 and #190] Make F-unwrapping not interfere with special-cased Option transformations and make transformations more bulletproof in relation to match types by @arainko in #191

Full Changelog: v0.2.3...v0.2.4

ducktape 0.2.3

15 Jul 20:54
54d7942

Choose a tag to compare

ducktape 0.2.3

This release brings the ability to transform tuples in all kind of ways (tuple-to-tuple, product-to-tuple, tuple-to-product), so stuff like this is now possible:

import io.github.arainko.ducktape.*

case class Source(field1: Int, field2: List[Int], field3: Int, field4: Int)

Source(1, List(2, 2, 2), 3, 4).to[(Int, Vector[Int], Option[Int])]
// res18: Tuple3[Int, Vector[Int], Option[Int]] = (
//   1,
//   Vector(2, 2, 2),
//   Some(value = 3)
// )

(1, List(2, 2, 2), 3, 4).to[Source]
// res19: Source = Source(
//  field1 = 1,
//  field2 = List(2, 2, 2),
//  field3 = 3,
//  field4 = 4
// )

The newly added ability for F-unwrapping (i.e. lifting the wrapper type into the 'outside' of a fallible transformation) in conjuction with tuple transformation enables some fantastic use cases for match types, like the ability to 'traverse' a tuple:

import io.github.arainko.ducktape.*

val source =
  (
    Right(1),
    Right("str"),
    Right(List(3, 3, 3)),
    Right(4)
  )

Mode.Accumulating.either[String, List].locally {
  source.fallibleTo[Tuple.InverseMap[source.type, Mode.current.Self]]
}
// res0: Either[List[String], *:[Int, *:[String, *:[List[Int], *:[Int, EmptyTuple]]]]] = Right(
//   value = (1, "str", List(3, 3, 3), 4)
// )

...which is really just the tip of the iceberg - stay tuned for a more in-depth look on how this mechanism can be leveraged in other use cases.

Head on over to the docs on configuring tuples to see how the configuration DSL works with tuples.

What's Changed

Full Changelog: v0.2.2...v0.2.3