{"id":35234556,"url":"https://github.com/arainko/ducktape","last_synced_at":"2026-04-01T20:44:12.846Z","repository":{"id":38383992,"uuid":"413986735","full_name":"arainko/ducktape","owner":"arainko","description":"Automatic and customizable compile time transformations between similar case classes and sealed traits/enums, essentially a thing that glues your code. Scala 3 only. Or is it duct 🤔","archived":false,"fork":false,"pushed_at":"2026-03-29T10:01:09.000Z","size":1838,"stargazers_count":437,"open_issues_count":1,"forks_count":8,"subscribers_count":2,"default_branch":"series/0.2.x","last_synced_at":"2026-03-29T13:20:29.212Z","etag":null,"topics":["dotty","metaprogramming","scala","scala3"],"latest_commit_sha":null,"homepage":"https://arainko.github.io/ducktape/","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arainko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-10-05T21:39:59.000Z","updated_at":"2026-03-29T09:58:58.000Z","dependencies_parsed_at":"2023-02-19T12:16:16.373Z","dependency_job_id":"c9e25c86-dc02-4625-80eb-e21312f719bd","html_url":"https://github.com/arainko/ducktape","commit_stats":null,"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"purl":"pkg:github/arainko/ducktape","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arainko%2Fducktape","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arainko%2Fducktape/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arainko%2Fducktape/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arainko%2Fducktape/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arainko","download_url":"https://codeload.github.com/arainko/ducktape/tar.gz/refs/heads/series/0.2.x","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arainko%2Fducktape/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291782,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dotty","metaprogramming","scala","scala3"],"created_at":"2025-12-30T03:32:44.647Z","updated_at":"2026-04-01T20:44:12.837Z","avatar_url":"https://github.com/arainko.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ![ducktape-logo-32](https://user-images.githubusercontent.com/46346508/236060869-3b118075-f660-44c9-9d0d-d40fba5c8db0.svg) ducktape 0.2.x\n\n*ducktape* is a library for boilerplate-less and configurable transformations between case classes and enums/sealed traits for Scala 3. Directly inspired by [chimney](https://github.com/scalalandio/chimney).\n\nIf this project interests you, please drop a 🌟 - these things are worthless but give me a dopamine rush nonetheless.\n\n## Installation\n```scala\nlibraryDependencies += \"io.github.arainko\" %% \"ducktape\" % \"0.2.12\"\n\n// or if you're using Scala.js or Scala Native\nlibraryDependencies += \"io.github.arainko\" %%% \"ducktape\" % \"0.2.12\"\n```\n\nNOTE: the [version scheme](https://www.scala-lang.org/blog/2021/02/16/preventing-version-conflicts-with-versionscheme.html) is set to `early-semver`\n\nYou're currently browsing the documentation for `ducktape 0.2.x`, if you're looking for the `0.1.x` docs go here: https://github.com/arainko/ducktape/tree/series/0.1.x\n\n## Documentation\n\nHead on over to the [docs site](https://arainko.github.io/ducktape/)!\n\n## Motivating example\n\n`ducktape` is all about painlessly transforming between similiarly structured case classes/enums/sealed traits:\n\n```scala\nimport java.time.Instant\nimport io.github.arainko.ducktape.*\n\n// imagine this is a wire model of some kind - JSON, protobuf, avro, what have you...\nobject wire {\n  final case class Person(\n    firstName: String,\n    lastName: String,\n    paymentMethods: List[wire.PaymentMethod],\n    status: wire.Status,\n    updatedAt: Option[Instant],\n  )\n\n  enum Status:\n    case Registered, PendingRegistration, Removed\n\n  enum PaymentMethod:\n    case Card(name: String, digits: Long, expires: Instant)\n    case PayPal(email: String)\n    case Cash\n}\n\nobject domain {\n  final case class Person( // \u003c-- fields reshuffled \n    lastName: String,\n    firstName: String,\n    status: Option[domain.Status], // \u003c-- 'status' in the domain model is optional\n    paymentMethods: Vector[domain.Payment], // \u003c-- collection type changed from a List to a Vector\n    updatedAt: Option[Instant],\n  )\n\n  enum Status:\n    case Registered, PendingRegistration, Removed\n    case PendingRemoval // \u003c-- additional enum case\n\n  enum Payment:\n    case Card(name: String, digits: Long, expires: Instant)\n    case PayPal(email: String)\n    case Cash\n}\n\nval wirePerson: wire.Person = wire.Person(\n  \"John\",\n  \"Doe\",\n  List(\n    wire.PaymentMethod.Cash,\n    wire.PaymentMethod.PayPal(\"john@doe.com\"),\n    wire.PaymentMethod.Card(\"J. Doe\", 12345, Instant.now)\n  ),\n  wire.Status.PendingRegistration,\n  Some(Instant.ofEpochSecond(0))\n)\n```\n\n```scala\nval domainPerson = wirePerson.to[domain.Person]\n// domainPerson: Person = Person(\n//   lastName = \"Doe\",\n//   firstName = \"John\",\n//   status = Some(value = PendingRegistration),\n//   paymentMethods = Vector(\n//     Cash,\n//     PayPal(email = \"john@doe.com\"),\n//     Card(\n//       name = \"J. Doe\",\n//       digits = 12345L,\n//       expires = 2024-03-10T00:21:33.860394305Z\n//     )\n//   ),\n//   updatedAt = Some(value = 1970-01-01T00:00:00Z)\n// )\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to see the generated code\u003c/summary\u003e\n  \n``` scala \n  (({\n    val paymentMethods$2: Vector[Payment] = MdocApp.this.wirePerson.paymentMethods\n      .map[Payment]((src: PaymentMethod) =\u003e\n        if (src.isInstanceOf[Card])\n          new Card(\n            name = src.asInstanceOf[Card].name,\n            digits = src.asInstanceOf[Card].digits,\n            expires = src.asInstanceOf[Card].expires\n          )\n        else if (src.isInstanceOf[PayPal]) new PayPal(email = src.asInstanceOf[PayPal].email)\n        else if (src.isInstanceOf[Cash.type]) MdocApp.this.domain.Payment.Cash\n        else throw new RuntimeException(\"Unhandled case. This is most likely a bug in ducktape.\")\n      )\n      .to[Vector[Payment]](iterableFactory[Payment])\n    val status$2: Some[Status] = Some.apply[Status](\n      if (MdocApp.this.wirePerson.status.isInstanceOf[Registered.type]) MdocApp.this.domain.Status.Registered\n      else if (MdocApp.this.wirePerson.status.isInstanceOf[PendingRegistration.type])\n        MdocApp.this.domain.Status.PendingRegistration\n      else if (MdocApp.this.wirePerson.status.isInstanceOf[Removed.type]) MdocApp.this.domain.Status.Removed\n      else throw new RuntimeException(\"Unhandled case. This is most likely a bug in ducktape.\")\n    )\n    new Person(\n      lastName = MdocApp.this.wirePerson.lastName,\n      firstName = MdocApp.this.wirePerson.firstName,\n      status = status$2,\n      paymentMethods = paymentMethods$2,\n      updatedAt = MdocApp.this.wirePerson.updatedAt\n    )\n  }: Person): Person)\n```\n\u003c/details\u003e\n\n\nBut 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...\n\n\n```scala\nobject wire {\n  final case class Person(\n    firstName: String,\n    lastName: String,\n    paymentMethods: List[wire.PaymentMethod],\n    status: wire.Status,\n    updatedAt: Option[Instant],\n  )\n\n  enum Status:\n    case Registered, PendingRegistration, Removed\n\n  enum PaymentMethod:\n    case Card(digits: Long, expires: Instant) // \u003c-- poof, 'name' is gone\n    case PayPal(email: String)\n    case Cash\n}\n\nval wirePerson: wire.Person = wire.Person(\n  \"John\",\n  \"Doe\",\n  List(\n    wire.PaymentMethod.Cash,\n    wire.PaymentMethod.PayPal(\"john@doe.com\"),\n    wire.PaymentMethod.Card(12345, Instant.now)\n  ),\n  wire.Status.PendingRegistration,\n  Some(Instant.ofEpochSecond(0))\n)\n```\n...and when you try to transform between these two representations the compiler now yells at you.\n```scala\nval domainPerson = wirePerson.to[domain.Person]\n// error:\n// No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name\n// given Transformer[Int, String] = int =\u003e int.toString\n//                                 ^\n```\n\nNow onto dealing with that, let's first examine the error message:\n\n`No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name`\n\nespecially the part after `@`:\n\n`Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name`\n\nthe 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:\n\n```scala\nval domainPerson = \n  wirePerson\n    .into[domain.Person]\n    .transform(Field.const(_.paymentMethods.element.at[domain.Payment.Card].name, \"CONST NAME\"))\n// domainPerson: Person = Person(\n//   lastName = \"Doe\",\n//   firstName = \"John\",\n//   status = Some(value = PendingRegistration),\n//   paymentMethods = Vector(\n//     Cash,\n//     PayPal(email = \"john@doe.com\"),\n//     Card(\n//       name = \"CONST NAME\",\n//       digits = 12345L,\n//       expires = 2024-03-10T00:21:33.864184449Z\n//     )\n//   ),\n//   updatedAt = Some(value = 1970-01-01T00:00:00Z)\n// )\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to see the generated code\u003c/summary\u003e\n  \n``` scala \n  {\n    val AppliedBuilder_this: AppliedBuilder[Person, Person] = into[Person](MdocApp2.this.wirePerson1)[MdocApp2.this.domain.Person]\n\n    {\n      val value$proxy3: Person = AppliedBuilder_this.inline$value\n\n      {\n        val paymentMethods$4: Vector[Payment] = value$proxy3.paymentMethods\n          .map[Payment]((src: PaymentMethod) =\u003e\n            if (src.isInstanceOf[Card])\n              new Card(name = \"CONST NAME\", digits = src.asInstanceOf[Card].digits, expires = src.asInstanceOf[Card].expires)\n            else if (src.isInstanceOf[PayPal]) new PayPal(email = src.asInstanceOf[PayPal].email)\n            else if (src.isInstanceOf[Cash.type]) MdocApp2.this.domain.Payment.Cash\n            else throw new RuntimeException(\"Unhandled case. This is most likely a bug in ducktape.\")\n          )\n          .to[Vector[Payment]](iterableFactory[Payment])\n        val status$4: Some[Status] = Some.apply[Status](\n          if (value$proxy3.status.isInstanceOf[Registered.type]) MdocApp2.this.domain.Status.Registered\n          else if (value$proxy3.status.isInstanceOf[PendingRegistration.type]) MdocApp2.this.domain.Status.PendingRegistration\n          else if (value$proxy3.status.isInstanceOf[Removed.type]) MdocApp2.this.domain.Status.Removed\n          else throw new RuntimeException(\"Unhandled case. This is most likely a bug in ducktape.\")\n        )\n        new Person(\n          lastName = value$proxy3.lastName,\n          firstName = value$proxy3.firstName,\n          status = status$4,\n          paymentMethods = paymentMethods$4,\n          updatedAt = value$proxy3.updatedAt\n        )\n      }: Person\n    }: Person\n  }\n```\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farainko%2Fducktape","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farainko%2Fducktape","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farainko%2Fducktape/lists"}