├── project ├── build.properties └── plugins.sbt ├── .scalafmt.conf ├── banner.jpg ├── .github ├── release-drafter.yml ├── labeler.yml └── workflows │ ├── scala-steward.yml │ └── ci.yml ├── examples └── src │ └── main │ └── scala │ └── magnolia1 │ └── examples │ ├── JavaAnnotatedCase.scala │ ├── passthrough.scala │ ├── exported.scala │ ├── printRepeated.scala │ ├── hash.scala │ ├── nocombine.scala │ ├── csv.scala │ ├── typename.scala │ ├── semidefault.scala │ ├── eq.scala │ ├── print.scala │ ├── semiauto.scala │ ├── SubtypeInfo.scala │ ├── patch.scala │ ├── default.scala │ ├── decode.scala │ ├── decodeSafe.scala │ └── show.scala ├── test └── src │ └── test │ ├── java │ └── magnolia1 │ │ └── tests │ │ ├── JavaExampleAnnotation.java │ │ └── WeekDay.java │ ├── scala │ └── magnolia1 │ │ └── tests │ │ ├── TypeAliasesTests.scala │ │ ├── ScopesTests.scala │ │ ├── ModifiersTests.scala │ │ ├── ValueClassesTests.scala │ │ ├── DefaultValuesTests.scala │ │ ├── OtherTests.scala │ │ ├── VarianceTests.scala │ │ ├── RecursiveTypesTests.scala │ │ ├── AnnotationsTests.scala │ │ ├── SumsTests.scala │ │ └── ProductsTests.scala │ └── scalajvm │ └── magnolia1 │ └── tests │ └── SerializationTests.scala ├── .gitignore ├── .git-blame-ignore-revs ├── .scala-steward.conf ├── core └── src │ └── main │ └── scala │ └── magnolia1 │ ├── monadic.scala │ ├── magnolia.scala │ ├── macro.scala │ ├── interface.scala │ └── impl.scala ├── readme.md └── license.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.9.10 2 | runner.dialect = scala3 3 | maxColumn = 140 -------------------------------------------------------------------------------- /banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/magnolia/HEAD/banner.jpg -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What's Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/JavaAnnotatedCase.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | @Deprecated 4 | case class JavaAnnotatedCase(v: Int) 5 | -------------------------------------------------------------------------------- /test/src/test/java/magnolia1/tests/JavaExampleAnnotation.java: -------------------------------------------------------------------------------- 1 | package magnolia1.tests; 2 | 3 | public @interface JavaExampleAnnotation { 4 | String description(); 5 | } 6 | -------------------------------------------------------------------------------- /test/src/test/java/magnolia1/tests/WeekDay.java: -------------------------------------------------------------------------------- 1 | package magnolia1.tests; 2 | 3 | public enum WeekDay { 4 | Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | *.swp 4 | *.swo 5 | .bloop/ 6 | .fury 7 | .idea 8 | *~ 9 | **/#* 10 | **/.#* 11 | tmp/ 12 | project 13 | target 14 | .bsp 15 | .metals/ 16 | .*.bak 17 | .vscode/ 18 | .cursor -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.5.9 2 | de114205d5ae0ba856c1bf5fbda09a88d2120f21 3 | 4 | # Scala Steward: Reformat with scalafmt 3.6.0 5 | f16708f8ede48a7118612bcfee960cc4cab56a50 6 | 7 | # Reformat 8 | 5fe167e4e0a3bd8fa3ce7e4810aa2b67a1ad7970 9 | 10 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | labels: 3 | - label: "automerge" 4 | authors: ["softwaremill-ci"] 5 | files: 6 | - "build.sbt" 7 | - "project/plugins.sbt" 8 | - label: "dependency" 9 | authors: ["softwaremill-ci"] 10 | files: 11 | - "build.sbt" 12 | - "project/plugins.sbt" 13 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [ 2 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.12."}, 3 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.13."}, 4 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "3."} 5 | ] 6 | updates.pin = [ 7 | {groupId = "org.scala-lang", artifactId = "scala3-library", version = "3.3."}, 8 | {groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = "3.3."} 9 | ] 10 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/passthrough.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | case class Passthrough[T]( 6 | ctx: Option[Either[CaseClass[_, T], SealedTrait[_, T]]] 7 | ) 8 | object Passthrough extends Derivation[Passthrough]: 9 | def join[T](ctx: CaseClass[Passthrough, T]) = Passthrough(Some(Left(ctx))) 10 | override def split[T](ctx: SealedTrait[Passthrough, T]) = Passthrough( 11 | Some( 12 | Right(ctx) 13 | ) 14 | ) 15 | 16 | given [T]: Passthrough[T] = Passthrough(None) 17 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.11.0") 2 | 3 | val sbtSoftwareMillVersion = "2.0.25" 4 | addSbtPlugin( 5 | "com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % sbtSoftwareMillVersion 6 | ) 7 | addSbtPlugin( 8 | "com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % sbtSoftwareMillVersion 9 | ) 10 | 11 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") 12 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") 13 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 14 | -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | name: Scala Steward 2 | 3 | # This workflow will launch at 00:00 every day 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write # Required to checkout and push changes 11 | pull-requests: write # Required to create PRs for dependency updates 12 | 13 | jobs: 14 | scala-steward: 15 | uses: softwaremill/github-actions-workflows/.github/workflows/scala-steward.yml@main 16 | secrets: 17 | github-token: ${{ secrets.SOFTWAREMILL_CI_PR_TOKEN }} 18 | with: 19 | java-version: '21' 20 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/exported.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | class ExportedTypeclass[T]() 6 | 7 | object ExportedTypeclass extends Derivation[ExportedTypeclass]: 8 | case class Exported[T]() extends ExportedTypeclass[T] 9 | def join[T](ctx: CaseClass[Typeclass, T]): Exported[T] = Exported() 10 | override def split[T](ctx: SealedTrait[Typeclass, T]): Exported[T] = 11 | Exported() 12 | 13 | given Typeclass[Int] = new ExportedTypeclass() 14 | given Typeclass[String] = new ExportedTypeclass() 15 | given seqInstance[T: Typeclass]: Typeclass[Seq[T]] = new ExportedTypeclass() 16 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/printRepeated.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1.* 4 | 5 | trait PrintRepeated[T]: 6 | def print(t: T): String 7 | 8 | object PrintRepeated extends AutoDerivation[PrintRepeated]: 9 | def join[T](ctx: CaseClass[Typeclass, T]): PrintRepeated[T] = _ => ctx.params.filter(_.repeated).map(_.label).toList.toString 10 | 11 | override def split[T](ctx: SealedTrait[PrintRepeated, T]): PrintRepeated[T] = 12 | ctx.choose(_) { sub => sub.typeclass.print(sub.value) } 13 | 14 | given PrintRepeated[String] = _ => "" 15 | given seq[T](using printT: PrintRepeated[T]): PrintRepeated[Seq[T]] = _ => "" 16 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/hash.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1.* 4 | 5 | trait WeakHash[T]: 6 | def hash(value: T): Int 7 | 8 | object WeakHash extends Derivation[WeakHash]: 9 | def join[T](ctx: CaseClass[WeakHash, T]): WeakHash[T] = value => 10 | ctx.params 11 | .map { param => param.typeclass.hash(param.deref(value)) } 12 | .foldLeft(0)(_ ^ _) 13 | 14 | override def split[T](ctx: SealedTrait[WeakHash, T]): WeakHash[T] = 15 | new WeakHash[T]: 16 | def hash(value: T): Int = ctx.choose(value) { sub => 17 | sub.typeclass.hash(sub.value) 18 | } 19 | 20 | given WeakHash[String] = _.map(_.toInt).sum 21 | given WeakHash[Int] = identity(_) 22 | given WeakHash[Double] = _.toInt 23 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/nocombine.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1.* 4 | 5 | trait NoCombine[A]: 6 | def nameOf(value: A): String 7 | 8 | object NoCombine extends AutoDerivation[NoCombine]: 9 | type Typeclass[T] = NoCombine[T] 10 | 11 | def join[T](ctx: CaseClass[magnolia1.examples.NoCombine, T]): NoCombine[T] = 12 | instance { value => 13 | ctx.typeInfo.short 14 | } 15 | 16 | override def split[T](ctx: SealedTrait[NoCombine, T]): NoCombine[T] = 17 | instance { value => 18 | ctx.choose(value)(sub => sub.typeclass.nameOf(sub.cast(value))) 19 | } 20 | 21 | def instance[T](name: T => String): NoCombine[T] = new Typeclass[T] { 22 | def nameOf(value: T): String = name(value) 23 | } 24 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/csv.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1.* 4 | 5 | extension [A: Csv](value: A) def csv: List[String] = summon[Csv[A]](value) 6 | 7 | trait Csv[A]: 8 | def apply(a: A): List[String] 9 | 10 | object Csv extends Derivation[Csv]: 11 | def join[A](ctx: CaseClass[Csv, A]): Csv[A] = a => 12 | ctx.params.foldLeft(List[String]()) { (acc, p) => 13 | acc ++ p.typeclass(p.deref(a)) 14 | } 15 | 16 | def split[A](ctx: SealedTrait[Csv, A]): Csv[A] = a => ctx.choose(a) { sub => sub.typeclass(sub.value) } 17 | 18 | given Csv[String] = List(_) 19 | given Csv[Int] = i => List(i.toString) 20 | given Csv[Char] = c => List(c.toString) 21 | given [T: Csv]: Csv[Seq[T]] = _.to(List).flatMap(summon[Csv[T]](_)) 22 | 23 | case class Foo(x: Int, y: String) derives Csv 24 | case class Bar(c: Char, fs: Foo*) derives Csv 25 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/typename.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | trait TypeNameInfo[T] { 6 | def name: TypeInfo 7 | 8 | def subtypeNames: Seq[TypeInfo] 9 | } 10 | 11 | object TypeNameInfo extends Derivation[TypeNameInfo]: 12 | def join[T](ctx: CaseClass[TypeNameInfo, T]): TypeNameInfo[T] = 13 | new TypeNameInfo[T]: 14 | def name: TypeInfo = ctx.typeInfo 15 | def subtypeNames: Seq[TypeInfo] = Nil 16 | 17 | override def split[T](ctx: SealedTrait[TypeNameInfo, T]): TypeNameInfo[T] = 18 | new TypeNameInfo[T]: 19 | def name: TypeInfo = ctx.typeInfo 20 | def subtypeNames: Seq[TypeInfo] = ctx.subtypes.map(_.typeInfo) 21 | 22 | given fallback[T]: TypeNameInfo[T] = 23 | new TypeNameInfo[T]: 24 | def name: TypeInfo = TypeInfo("", "Unknown Type", Seq.empty) 25 | def subtypeNames: Seq[TypeInfo] = Nil 26 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/semidefault.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | trait SemiDefault[A]: 6 | def default: A 7 | 8 | object SemiDefault extends AutoDerivation[SemiDefault]: 9 | inline def apply[A](using A: SemiDefault[A]): SemiDefault[A] = A 10 | 11 | type Typeclass[T] = SemiDefault[T] 12 | 13 | def join[T](ctx: CaseClass[SemiDefault, T]): SemiDefault[T] = 14 | new SemiDefault[T] { 15 | def default = ctx.construct(p => p.default.getOrElse(p.typeclass.default)) 16 | } 17 | 18 | override def split[T](ctx: SealedTrait[SemiDefault, T]): SemiDefault[T] = 19 | new SemiDefault[T] { 20 | def default = ctx.subtypes.head.typeclass.default 21 | } 22 | 23 | given string: SemiDefault[String] = new SemiDefault[String] { 24 | def default = "" 25 | } 26 | 27 | given int: SemiDefault[Int] = new SemiDefault[Int] { def default = 0 } 28 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/eq.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1.* 4 | 5 | trait Eq[T]: 6 | def equal(value: T, value2: T): Boolean 7 | 8 | object Eq extends AutoDerivation[Eq]: 9 | def join[T](ctx: CaseClass[Eq, T]): Eq[T] = (v1, v2) => ctx.params.forall { p => p.typeclass.equal(p.deref(v1), p.deref(v2)) } 10 | 11 | override def split[T](ctx: SealedTrait[Eq, T]): Eq[T] = (v1, v2) => 12 | ctx.choose(v1) { sub => 13 | sub.typeclass.equal(sub.value, sub.cast(v2)) 14 | } 15 | 16 | given Eq[String] = _ == _ 17 | given Eq[Int] = _ == _ 18 | 19 | given [T: Eq]: Eq[Option[T]] = 20 | case (Some(v1), Some(v2)) => summon[Eq[T]].equal(v1, v2) 21 | case (None, None) => true 22 | case _ => false 23 | 24 | given [T: Eq, C[x] <: Iterable[x]]: Eq[C[T]] = (v1, v2) => 25 | v1.size == v2.size && (v1.iterator zip v2.iterator).forall( 26 | (summon[Eq[T]].equal).tupled 27 | ) 28 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/print.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | // Prints a type, only requires read access to fields 6 | trait Print[T] { 7 | def print(t: T): String 8 | } 9 | 10 | trait GenericPrint extends AutoDerivation[Print]: 11 | def join[T](ctx: CaseClass[Typeclass, T]): Print[T] = value => 12 | if ctx.isValueClass then 13 | val param = ctx.params.head 14 | param.typeclass.print(param.deref(value)) 15 | else 16 | ctx.params 17 | .map { param => 18 | param.typeclass.print(param.deref(value)) 19 | } 20 | .mkString(s"${ctx.typeInfo.short}(", ",", ")") 21 | 22 | override def split[T](ctx: SealedTrait[Print, T]): Print[T] = 23 | ctx.choose(_) { sub => sub.typeclass.print(sub.value) } 24 | 25 | object Print extends GenericPrint: 26 | given Print[String] = identity(_) 27 | given Print[Int] = _.toString 28 | given seq[T](using printT: Print[T]): Print[Seq[T]] = 29 | _.map(printT.print).mkString("[", ",", "]") 30 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/TypeAliasesTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | class TypeAliasesTests extends munit.FunSuite: 7 | 8 | import TypeAliasesTests.* 9 | 10 | // TODO not working: Cannot get a tree of no symbol 11 | // test("show a type aliased case class") { 12 | // type T = Person 13 | // val res = Show.derived[T].show(Person("Donald Duck", 313)) 14 | // assertEquals(res, "Person(name=Donald Duck,age=313)") 15 | // } 16 | 17 | // TODO - not working: assertion failed: Cannot get tree of no symbol 18 | // test("resolve aliases for type names") { 19 | // type LO[X] = Leaf[Option[X]] 20 | 21 | // val res = Show.derived[LO[String]].show(Leaf(None)) 22 | // assertEquals(res,"Leaf[Option[String]](value=None())") 23 | // } 24 | 25 | object TypeAliasesTests: 26 | 27 | sealed trait Entity 28 | case class Company(name: String) extends Entity 29 | case class Person(name: String, age: Int) extends Entity 30 | case class Address(line1: String, occupant: Person) 31 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/semiauto.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import scala.language.experimental.macros 4 | import magnolia1._ 5 | 6 | trait SemiPrint[A]: 7 | def print(a: A): String 8 | 9 | object SemiPrint extends Derivation[SemiPrint]: 10 | def join[T](ctx: CaseClass[Typeclass, T]): SemiPrint[T] = value => 11 | if ctx.isValueClass then 12 | val param = ctx.params.head 13 | param.typeclass.print(param.deref(value)) 14 | else 15 | ctx.params 16 | .map { param => 17 | param.typeclass.print(param.deref(value)) 18 | } 19 | .mkString(s"${ctx.typeInfo.short}(", ",", ")") 20 | 21 | override def split[T](ctx: SealedTrait[SemiPrint, T]): SemiPrint[T] = 22 | ctx.choose(_) { sub => sub.typeclass.print(sub.value) } 23 | 24 | given SemiPrint[String] with 25 | def print(s: String) = s 26 | 27 | given SemiPrint[Int] with 28 | def print(i: Int) = i.toString 29 | 30 | given seq[T](using spt: SemiPrint[T]): SemiPrint[Seq[T]] with 31 | def print(t: Seq[T]) = t.map(spt.print).mkString(", ") 32 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/SubtypeInfo.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | trait SubtypeInfo[T] { 6 | def subtypeIsObject: Seq[Boolean] 7 | def traitAnnotations: Seq[Any] 8 | def subtypeAnnotations: Seq[Seq[Any]] 9 | def isEnum: Boolean 10 | } 11 | 12 | object SubtypeInfo extends Derivation[SubtypeInfo]: 13 | def join[T](ctx: CaseClass[SubtypeInfo, T]): SubtypeInfo[T] = 14 | new SubtypeInfo[T]: 15 | def subtypeIsObject: Seq[Boolean] = Nil 16 | def traitAnnotations: List[Any] = Nil 17 | def subtypeAnnotations: List[List[Any]] = Nil 18 | def isEnum: Boolean = false 19 | 20 | override def split[T](ctx: SealedTrait[SubtypeInfo, T]): SubtypeInfo[T] = 21 | new SubtypeInfo[T]: 22 | def subtypeIsObject: Seq[Boolean] = ctx.subtypes.map(_.isObject) 23 | def traitAnnotations: Seq[Any] = ctx.annotations 24 | def subtypeAnnotations: Seq[Seq[Any]] = 25 | ctx.subtypes.map(_.annotations.toList).toList 26 | def isEnum: Boolean = ctx.isEnum 27 | 28 | given fallback[T]: SubtypeInfo[T] = 29 | new SubtypeInfo[T]: 30 | def subtypeIsObject: Seq[Boolean] = Nil 31 | def traitAnnotations: Seq[Any] = Nil 32 | def subtypeAnnotations: Seq[Seq[Any]] = Nil 33 | def isEnum: Boolean = false 34 | -------------------------------------------------------------------------------- /test/src/test/scalajvm/magnolia1/tests/SerializationTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | import java.io.* 7 | class SerializationTests extends munit.FunSuite: 8 | import SerializationTests.* 9 | 10 | private def serializeToByteArray(value: Serializable): Array[Byte] = 11 | val buffer = new ByteArrayOutputStream() 12 | val oos = new ObjectOutputStream(buffer) 13 | oos.writeObject(value) 14 | buffer.toByteArray 15 | 16 | private def deserializeFromByteArray(encodedValue: Array[Byte]): AnyRef = 17 | val ois = new ObjectInputStream(new ByteArrayInputStream(encodedValue)) 18 | ois.readObject() 19 | 20 | def ensureSerializable[T <: Serializable](value: T): T = 21 | deserializeFromByteArray(serializeToByteArray(value)).asInstanceOf[T] 22 | 23 | test("generate serializable type-classes") { 24 | ensureSerializable(new Outer().showAddress) 25 | ensureSerializable(new Outer().showColor) 26 | } 27 | 28 | object SerializationTests: 29 | sealed trait Entity 30 | case class Company(name: String) extends Entity 31 | case class Person(name: String, age: Int) extends Entity 32 | case class Address(line1: String, occupant: Person) 33 | 34 | sealed trait Color 35 | case object Red extends Color 36 | case object Green extends Color 37 | case object Blue extends Color 38 | case object Orange extends Color 39 | case object Pink extends Color 40 | class Outer: 41 | val showAddress: Show[String, Address] = summon[Show[String, Address]] 42 | val showColor: Show[String, Color] = summon[Show[String, Color]] 43 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/ScopesTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | class ScopesTests extends munit.FunSuite: 7 | import ScopesTests.* 8 | 9 | test("local implicit beats Magnolia") { 10 | given showPerson: Show[String, Person] = _ => "nobody" 11 | val res = summon[Show[String, Address]].show( 12 | Address("Home", Person("John Smith", 44)) 13 | ) 14 | assertEquals(res, "Address(line1=Home,occupant=nobody)") 15 | } 16 | 17 | test("even low-priority implicit beats Magnolia for nested case") { 18 | val res = 19 | summon[Show[String, Lunchbox]].show(Lunchbox(Fruit("apple"), "lemonade")) 20 | assertEquals(res, "Lunchbox(fruit=apple,drink=lemonade)") 21 | } 22 | 23 | test("low-priority implicit beats Magnolia when not nested") { 24 | val res = summon[Show[String, Fruit]].show(Fruit("apple")) 25 | assertEquals(res, "apple") 26 | } 27 | 28 | test("low-priority implicit beats Magnolia when chained") { 29 | val res = summon[Show[String, FruitBasket]].show( 30 | FruitBasket(Fruit("apple"), Fruit("banana")) 31 | ) 32 | assertEquals(res, "FruitBasket(fruits=[apple,banana])") 33 | } 34 | 35 | test("typeclass implicit scope has lower priority than ADT implicit scope") { 36 | val res = summon[Show[String, Fruit]].show(Fruit("apple")) 37 | assertEquals(res, "apple") 38 | } 39 | 40 | object ScopesTests: 41 | 42 | sealed trait Entity 43 | case class Company(name: String) extends Entity 44 | case class Person(name: String, age: Int) extends Entity 45 | 46 | case class Address(line1: String, occupant: Person) 47 | 48 | case class Fruit(name: String) 49 | object Fruit: 50 | given showFruit: Show[String, Fruit] = (f: Fruit) => f.name 51 | 52 | case class FruitBasket(fruits: Fruit*) 53 | 54 | case class Lunchbox(fruit: Fruit, drink: String) 55 | -------------------------------------------------------------------------------- /core/src/main/scala/magnolia1/monadic.scala: -------------------------------------------------------------------------------- 1 | package magnolia1 2 | 3 | import scala.concurrent.{Future, ExecutionContext} 4 | import scala.util.{Try, Success} 5 | 6 | trait Monadic[F[_]]: 7 | type Apply[X] = F[X] 8 | def point[A](value: A): F[A] 9 | def map[A, B](from: F[A])(fn: A => B): F[B] 10 | def flatMap[A, B](from: F[A])(fn: A => F[B]): F[B] 11 | 12 | object Monadic: 13 | given Monadic[Option] with 14 | def point[A](value: A): Option[A] = Some(value) 15 | def map[A, B](from: Option[A])(fn: A => B): Option[B] = from.map(fn) 16 | def flatMap[A, B](from: Option[A])(fn: A => Option[B]): Option[B] = 17 | from.flatMap(fn) 18 | 19 | given Monadic[List] with 20 | def point[A](value: A): List[A] = List(value) 21 | def map[A, B](from: List[A])(fn: A => B): List[B] = from.map(fn) 22 | def flatMap[A, B](from: List[A])(fn: A => List[B]): List[B] = 23 | from.flatMap(fn) 24 | 25 | given (using ec: ExecutionContext): Monadic[Future] with 26 | def point[A](value: A): Future[A] = Future(value) 27 | def map[A, B](from: Future[A])(fn: A => B): Future[B] = from.map(fn) 28 | def flatMap[A, B](from: Future[A])(fn: A => Future[B]): Future[B] = 29 | from.flatMap(fn) 30 | 31 | given [Err]: Monadic[[X] =>> Either[Err, X]] with 32 | def point[A](value: A): Either[Err, A] = Right(value) 33 | def map[A, B](from: Either[Err, A])(fn: A => B): Either[Err, B] = 34 | from.map(fn) 35 | def flatMap[A, B](from: Either[Err, A])( 36 | fn: A => Either[Err, B] 37 | ): Either[Err, B] = from.flatMap(fn) 38 | 39 | given Monadic[Try] with 40 | def point[A](value: A): Try[A] = Success(value) 41 | def map[A, B](from: Try[A])(fn: A => B): Try[B] = from.map(fn) 42 | def flatMap[A, B](from: Try[A])(fn: A => Try[B]): Try[B] = from.flatMap(fn) 43 | 44 | extension [F[_], A, B](fv: F[A])(using monadic: Monadic[F]) 45 | def map(f: A => B): F[B] = monadic.map(fv)(f) 46 | def flatMap(f: A => F[B]): F[B] = monadic.flatMap(fv)(f) 47 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/ModifiersTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | class ModifiersTests extends munit.FunSuite: 7 | import ModifiersTests.* 8 | 9 | test("construct a Show instance for product with partially private fields") { 10 | val res = Show.derived[Abc].show(Abc(12, 54, "pm")) 11 | assertEquals(res, "Abc(a=12,b=54L,c=pm)") 12 | } 13 | 14 | test("serialize case class with protected constructor") { 15 | val res = ProtectedCons.show.show(ProtectedCons("dada", "phil")) 16 | assertEquals(res, "ProtectedCons(name=dada phil)") 17 | } 18 | 19 | test( 20 | "read-only typeclass can serialize case class with protected constructor" 21 | ) { 22 | val res = summon[Print[ProtectedCons]].print(ProtectedCons("dada", "phil")) 23 | assertEquals(res, "ProtectedCons(dada phil)") 24 | } 25 | 26 | test( 27 | "read-only typeclass can serialize case class with inaccessible private constructor" 28 | ) { 29 | val res = summon[Print[PrivateCons]].print(PrivateCons("dada", "phil")) 30 | assertEquals(res, "PrivateCons(dada phil)") 31 | } 32 | 33 | test("serialize case class with accessible private constructor") { 34 | val res = PrivateCons.show.show(PrivateCons("dada", "phil")) 35 | assertEquals(res, "PrivateCons(name=dada phil)") 36 | } 37 | 38 | object ModifiersTests: 39 | 40 | final case class Abc(private val a: Int, private val b: Long, c: String) 41 | 42 | case class ProtectedCons protected (name: String) 43 | 44 | object ProtectedCons: 45 | def apply(firstName: String, familyName: String): ProtectedCons = 46 | new ProtectedCons(firstName + " " + familyName) 47 | given show: Show[String, ProtectedCons] = Show.derived 48 | 49 | case class PrivateCons private (name: String) 50 | 51 | object PrivateCons: 52 | def apply(firstName: String, familyName: String): PrivateCons = 53 | new PrivateCons(firstName + " " + familyName) 54 | given show: Show[String, PrivateCons] = Show.derived 55 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/patch.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | /** Type class for copying an instance of some type `T`, thereby replacing certain fields with other values. 6 | */ 7 | sealed abstract class Patcher[T]: 8 | 9 | /** Returns a copy of `value` whereby all non-null elements of `fieldValues` replace the respective fields of `value`. For all null 10 | * elements of `fieldValues` the original value of the respective field of `value` is maintained. 11 | * 12 | * If the size of `fieldValues` doesn't exactly correspond to the number of fields of `value` an [[IllegalArgumentException]] is thrown. 13 | */ 14 | def patch(value: T, fieldValues: Seq[Any]): T 15 | 16 | object Patcher extends LowerPriorityPatcher with AutoDerivation[Patcher]: 17 | def join[T](ctx: CaseClass[Patcher, T]): Patcher[T] = 18 | new Patcher[T]: 19 | def patch(value: T, fieldValues: Seq[Any]): T = 20 | if fieldValues.lengthCompare(ctx.params.size) != 0 then 21 | throw new IllegalArgumentException( 22 | s"Cannot patch value `$value`, expected ${ctx.params.size} fields but got ${fieldValues.size}" 23 | ) 24 | 25 | val effectiveFields = ctx.params.zip(fieldValues).map { (param, x) => 26 | if (x.asInstanceOf[AnyRef] ne null) x else param.deref(value) 27 | } 28 | 29 | ctx.rawConstruct(effectiveFields) 30 | 31 | def split[T](ctx: SealedTrait[Patcher, T]): Patcher[T] = new Patcher[T]: 32 | def patch(value: T, fieldValues: Seq[Any]): T = 33 | ctx.choose(value)(sub => sub.typeclass.patch(sub.value, fieldValues)) 34 | 35 | sealed abstract class LowerPriorityPatcher: 36 | private[this] val _forSingleValue = 37 | new Patcher[Any]: 38 | def patch(value: Any, fieldValues: Seq[Any]): Any = { 39 | if (fieldValues.lengthCompare(1) != 0) 40 | throw new IllegalArgumentException( 41 | s"Cannot patch single value `$value` with patch sequence of size ${fieldValues.size}" 42 | ) 43 | val head = fieldValues.head 44 | if (head.getClass != value.getClass) 45 | throw new IllegalArgumentException( 46 | s"Illegal patch value type. Expected `${value.getClass}` but got `${head.getClass}`" 47 | ) 48 | head 49 | } 50 | 51 | def forSingleValue[T]: Patcher[T] = _forSingleValue.asInstanceOf[Patcher[T]] 52 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/default.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | /** typeclass for providing a default value for a particular type */ 6 | trait HasDefault[T]: 7 | def defaultValue: Either[String, T] 8 | def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = None 9 | 10 | /** companion object and derivation object for [[HasDefault]] */ 11 | object HasDefault extends AutoDerivation[HasDefault]: 12 | 13 | /** constructs a default for each parameter, using the constructor default (if provided), otherwise using a typeclass-provided default 14 | */ 15 | def join[T](ctx: CaseClass[HasDefault, T]): HasDefault[T] = 16 | new HasDefault[T] { 17 | def defaultValue = ctx.constructMonadic { param => 18 | param.default match { 19 | case Some(arg) => Right(arg) 20 | case None => param.typeclass.defaultValue 21 | } 22 | } 23 | 24 | override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = 25 | IArray 26 | .genericWrapArray { 27 | ctx.params 28 | .filter(_.label == paramLabel) 29 | } 30 | .toArray 31 | .headOption 32 | .flatMap(_.evaluateDefault.map(res => res())) 33 | } 34 | 35 | /** chooses which subtype to delegate to */ 36 | override def split[T](ctx: SealedTrait[HasDefault, T]): HasDefault[T] = 37 | new HasDefault[T]: 38 | def defaultValue = ctx.subtypes.headOption match 39 | case Some(sub) => sub.typeclass.defaultValue 40 | case None => Left("no subtypes") 41 | 42 | override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = 43 | ctx.subtypes.headOption match { 44 | case Some(sub) => sub.typeclass.getDynamicDefaultValueForParam(paramLabel) 45 | case _ => None 46 | } 47 | 48 | /** default value for a string; the empty string */ 49 | given string: HasDefault[String] with 50 | def defaultValue = Right("") 51 | 52 | /** default value for ints; 0 */ 53 | given int: HasDefault[Int] with { def defaultValue = Right(0) } 54 | 55 | /** oh, no, there is no default Boolean... whatever will we do? */ 56 | given boolean: HasDefault[Boolean] with 57 | def defaultValue = Left("truth is a lie") 58 | 59 | given double: HasDefault[Double] with 60 | def defaultValue = Right(0) 61 | 62 | /** default value for sequences; the empty sequence */ 63 | given seq[A]: HasDefault[Seq[A]] with 64 | def defaultValue = Right(Seq.empty) 65 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/decode.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | /** very basic decoder for converting strings to other types */ 6 | trait Decoder[T]: 7 | def decode(str: String): T 8 | 9 | /** derivation object (and companion object) for [[Decoder]] instances */ 10 | object Decoder extends AutoDerivation[Decoder]: 11 | 12 | given Decoder[String] = (s: String) => s 13 | given Decoder[Int] = _.toInt 14 | 15 | /** defines how new [[Decoder]]s for case classes should be constructed */ 16 | def join[T](ctx: CaseClass[Decoder, T]): Decoder[T] = value => 17 | val (_, values) = parse(value) 18 | ctx.construct { param => 19 | values 20 | .get(param.label) 21 | .map(param.typeclass.decode) 22 | .orElse(param.default) 23 | .getOrElse(sys.error(s"missing ${param.label}")) 24 | } 25 | 26 | /** defines how to choose which subtype of the sealed trait to use for decoding 27 | */ 28 | override def split[T](ctx: SealedTrait[Decoder, T]): Decoder[T] = param => 29 | val (name, _) = parse(param) 30 | val subtype = ctx.subtypes.find(_.typeInfo.full == name).get 31 | 32 | subtype.typeclass.decode(param) 33 | 34 | /** very simple extractor for grabbing an entire parameter value, assuming matching parentheses 35 | */ 36 | private def parse(value: String): (String, Map[String, String]) = 37 | val end = value.indexOf('(') 38 | val name = value.substring(0, end) 39 | 40 | def parts( 41 | value: String, 42 | idx: Int = 0, 43 | depth: Int = 0, 44 | collected: List[String] = List("") 45 | ): List[String] = 46 | def plus(char: Char): List[String] = 47 | collected.head + char :: collected.tail 48 | 49 | if (idx == value.length) collected 50 | else 51 | value(idx) match 52 | case '(' => 53 | parts(value, idx + 1, depth + 1, plus('(')) 54 | case ')' => 55 | if depth == 1 then plus(')') 56 | else parts(value, idx + 1, depth - 1, plus(')')) 57 | case ',' => 58 | if depth == 0 then parts(value, idx + 1, depth, "" :: collected) 59 | else parts(value, idx + 1, depth, plus(',')) 60 | case char => 61 | parts(value, idx + 1, depth, plus(char)) 62 | 63 | def keyValue(str: String): (String, String) = 64 | val List(label, value) = str.split("=", 2).to(List) 65 | (label, value) 66 | 67 | ( 68 | name, 69 | parts(value.substring(end + 1, value.length - 1)) 70 | .filter(_.nonEmpty) 71 | .map(keyValue) 72 | .toMap 73 | ) 74 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/ValueClassesTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | /** TODO: Support for value classes is missing for scala3 branch. Eventually refactor and uncomment the tests below once the feature is 7 | * implemented. 8 | */ 9 | class ValueClassesTests extends munit.FunSuite: 10 | import ValueClassesTests.* 11 | 12 | // test("serialize a value class") { 13 | // val res = Show.derived[Length].show(new Length(100)) 14 | // assertEquals(res, "100") 15 | // } 16 | 17 | // test("construct a Show instance for value case class") { 18 | // val res = Show.derived[ServiceName1].show(ServiceName1("service")) 19 | // assertEquals(res, "service") 20 | // } 21 | 22 | // test("read-only typeclass can serialize value case class with inaccessible private constructor") { 23 | // val res = implicitly[Print[PrivateValueClass]].print(PrivateValueClass(42)) 24 | // assertEquals(res, "42") 25 | // } 26 | 27 | // test("not assume full auto derivation of external value classes") { 28 | // val error = compileErrors(""" 29 | // case class LoggingConfig(n: ServiceName1) 30 | // object LoggingConfig { 31 | // implicit val semi: SemiDefault[LoggingConfig] = SemiDefault.gen 32 | // } 33 | // """) 34 | // assert(error contains """ 35 | // |magnolia: could not find SemiDefault.Typeclass for type magnolia1.tests.ServiceName1 36 | // | in parameter 'n' of product type LoggingConfig 37 | // |""".stripMargin) 38 | // } 39 | 40 | // test("serialize value case class with accessible private constructor") { 41 | // class PrivateValueClass private (val value: Int) extends AnyVal 42 | // object PrivateValueClass { 43 | // def apply(l: Int) = new PrivateValueClass(l) 44 | // implicit val show: Show[String, PrivateValueClass] = Show.derived[PrivateValueClass] 45 | // } 46 | // val res = PrivateValueClass.show.show(PrivateValueClass(42)) 47 | // assertEquals(res, "42") 48 | // } 49 | 50 | // test("allow derivation result to have arbitrary type") { 51 | // val res = (ExportedTypeclass.derived[Length], ExportedTypeclass.derived[Color]) 52 | // assertEquals(res, (ExportedTypeclass.Exported[Length](), ExportedTypeclass.Exported[Color]())) 53 | // } 54 | 55 | object ValueClassesTests: 56 | 57 | class Length(val value: Int) extends AnyVal 58 | 59 | final case class ServiceName1(value: String) extends AnyVal 60 | 61 | class PrivateValueClass private (val value: Int) extends AnyVal 62 | object PrivateValueClass { 63 | def apply(l: Int) = new PrivateValueClass(l) 64 | // given Show[String, PrivateValueClass] = Show.derived 65 | } 66 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/decodeSafe.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | /** decoder for converting strings to other types providing good error messages 6 | */ 7 | trait DecoderSafe[T] { def decode(str: String): Either[String, T] } 8 | 9 | /** derivation object (and companion object) for [[DecoderSafe]] instances */ 10 | object DecoderSafe extends Derivation[DecoderSafe]: 11 | 12 | /** decodes strings */ 13 | given DecoderSafe[String] = Right(_) 14 | 15 | /** decodes ints */ 16 | given DecoderSafe[Int] = k => 17 | try Right(k.toInt) 18 | catch case _: NumberFormatException => Left(s"illegal number: $k") 19 | 20 | /** defines how new [[DecoderSafe]]s for case classes should be constructed */ 21 | def join[T](ctx: CaseClass[DecoderSafe, T]): DecoderSafe[T] = value => 22 | val (_, values) = parse(value) 23 | 24 | ctx 25 | .constructEither { param => param.typeclass.decode(values(param.label)) } 26 | .left 27 | .map(_.reduce(_ + "\n" + _)) 28 | 29 | /** defines how to choose which subtype of the sealed trait to use for decoding 30 | */ 31 | override def split[T](ctx: SealedTrait[DecoderSafe, T]): DecoderSafe[T] = 32 | param => 33 | val (name, _) = parse(param) 34 | val subtype = ctx.subtypes.find(_.typeInfo.full == name).get 35 | 36 | subtype.typeclass.decode(param) 37 | 38 | /** very simple extractor for grabbing an entire parameter value, assuming matching parentheses 39 | */ 40 | private def parse(value: String): (String, Map[String, String]) = 41 | val end = value.indexOf('(') 42 | val name = value.substring(0, end) 43 | 44 | def parts( 45 | value: String, 46 | idx: Int = 0, 47 | depth: Int = 0, 48 | collected: List[String] = List("") 49 | ): List[String] = 50 | def plus(char: Char): List[String] = 51 | collected.head + char :: collected.tail 52 | 53 | if (idx == value.length) collected 54 | else 55 | value(idx) match 56 | case '(' => 57 | parts(value, idx + 1, depth + 1, plus('(')) 58 | case ')' => 59 | if (depth == 1) plus(')') 60 | else parts(value, idx + 1, depth - 1, plus(')')) 61 | case ',' => 62 | if (depth == 0) parts(value, idx + 1, depth, "" :: collected) 63 | else parts(value, idx + 1, depth, plus(',')) 64 | case char => 65 | parts(value, idx + 1, depth, plus(char)) 66 | 67 | def keyValue(str: String): (String, String) = 68 | val List(label, value) = str.split("=", 2).to(List) 69 | (label, value) 70 | 71 | ( 72 | name, 73 | parts(value.substring(end + 1, value.length - 1)).map(keyValue).to(Map) 74 | ) 75 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/DefaultValuesTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | class DefaultValuesTests extends munit.FunSuite: 7 | import DefaultValuesTests.* 8 | 9 | test("construct a Show instance for a product with multiple default values") { 10 | val res = Show.derived[ParamsWithDefault].show(ParamsWithDefault()) 11 | assertEquals(res, "ParamsWithDefault(a=3,b=4)") 12 | } 13 | 14 | test("decode using default") { 15 | val res = summon[Decoder[WithDefault]].decode( 16 | """WithDefault()""" 17 | ) 18 | assertEquals(res, WithDefault(x = 2)) 19 | } 20 | 21 | // TODO - will not work if object is in external scope 22 | test("decode not using default") { 23 | val res = summon[Decoder[WithDefault]].decode( 24 | """WithDefault(x=1)""" 25 | ) 26 | assertEquals(res, WithDefault(x = 1)) 27 | } 28 | 29 | test("construct a failed NoDefault") { 30 | val res = HasDefault.derived[NoDefault].defaultValue 31 | assertEquals(res, Left("truth is a lie")) 32 | } 33 | 34 | // TODO - will not work if object is in external scope 35 | test("access default constructor values") { 36 | val res = summon[HasDefault[Item]].defaultValue 37 | assertEquals(res, Right(Item("", 1, 0))) 38 | } 39 | 40 | test("access dynamic default constructor values") { 41 | val res1 = summon[HasDefault[ParamsWithDynamicDefault]].getDynamicDefaultValueForParam("a") 42 | val res2 = summon[HasDefault[ParamsWithDynamicDefault]].getDynamicDefaultValueForParam("a") 43 | 44 | assertEquals(res1.isDefined, true) 45 | assertEquals(res2.isDefined, true) 46 | 47 | for { 48 | default1 <- res1 49 | default2 <- res2 50 | } yield assertNotEquals(default1, default2) 51 | } 52 | 53 | test("issue 571") { 54 | given list[A]: HasDefault[List[A]] with 55 | def defaultValue = Right(Nil) 56 | override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = None 57 | 58 | val res1 = summon[HasDefault[::[String]]].getDynamicDefaultValueForParam("value") 59 | val res2 = summon[HasDefault[::[String]]].getDynamicDefaultValueForParam("next") 60 | 61 | assertEquals(res1.isDefined, false) 62 | assertEquals(res2.isDefined, false) 63 | } 64 | 65 | test("construct a HasDefault instance for a generic product with default values") { 66 | val res = HasDefault.derived[ParamsWithDefaultGeneric[String, Int]].defaultValue 67 | assertEquals(res, Right(ParamsWithDefaultGeneric("A", 0))) 68 | } 69 | 70 | // Fails because unsafeCast in impl works on Any, which casts Option[Int] to Option[String] 71 | // test("construct a HasDefault instance for a generic product with default generic values") { 72 | // val res = HasDefault.derived[ParamsWithDefaultDeepGeneric[String, Int]].defaultValue 73 | // assertEquals(res, Right(ParamsWithDefaultDeepGeneric(Some("A"), None))) 74 | // } 75 | 76 | object DefaultValuesTests: 77 | 78 | case class ParamsWithDefault(a: Int = 3, b: Int = 4) 79 | 80 | case class ParamsWithDynamicDefault(a: Double = scala.math.random()) 81 | 82 | case class ParamsWithDefaultGeneric[A, B](a: A = "A", b: B = "B") 83 | 84 | case class ParamsWithDefaultDeepGeneric[A, B](a: Option[A] = Some("A"), b: Option[B] = Some("B")) 85 | case class Item(name: String, quantity: Int = 1, price: Int) 86 | 87 | case class WithDefault(x: Int = 2) 88 | 89 | case class NoDefault(value: Boolean) 90 | -------------------------------------------------------------------------------- /examples/src/main/scala/magnolia1/examples/show.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.examples 2 | 3 | import magnolia1._ 4 | 5 | /** shows one type as another, often as a string 6 | * 7 | * Note that this is a more general form of `Show` than is usual, as it permits the return type to be something other than a string. 8 | */ 9 | trait Show[Out, T] extends Serializable { def show(value: T): Out } 10 | 11 | trait GenericShow[Out] extends AutoDerivation[[X] =>> Show[Out, X]] { 12 | 13 | def joinElems(typeName: String, strings: Seq[String]): Out 14 | def prefix(s: String, out: Out): Out 15 | 16 | /** creates a new [[Show]] instance by labelling and joining (with `mkString`) the result of showing each parameter, and prefixing it with 17 | * the class name 18 | */ 19 | def join[T](ctx: CaseClass[Typeclass, T]): Show[Out, T] = { value => 20 | if ctx.isValueClass then 21 | val param = ctx.params.head 22 | param.typeclass.show(param.deref(value)) 23 | else 24 | val paramStrings = ctx.params.map { param => 25 | val attribStr = 26 | if (param.annotations.isEmpty && param.inheritedAnnotations.isEmpty) 27 | "" 28 | else { 29 | (param.annotations.map(_.toString) ++ param.inheritedAnnotations.map(a => s"[i]$a")).distinct 30 | .mkString("{", ",", "}") 31 | } 32 | 33 | val tpeAttribStr = 34 | if (param.typeAnnotations.isEmpty) "" 35 | else { 36 | param.typeAnnotations.mkString("{", ",", "}") 37 | } 38 | 39 | s"${param.label}$attribStr$tpeAttribStr=${param.typeclass.show(param.deref(value))}" 40 | } 41 | 42 | val anns = (ctx.annotations ++ ctx.inheritedAnnotations).distinct 43 | val annotationStr = if (anns.isEmpty) "" else anns.mkString("{", ",", "}") 44 | 45 | val tpeAnns = ctx.typeAnnotations 46 | val typeAnnotationStr = 47 | if (tpeAnns.isEmpty) "" else tpeAnns.mkString("{", ",", "}") 48 | 49 | def typeArgsString(typeInfo: TypeInfo): String = 50 | if typeInfo.typeParams.isEmpty then "" 51 | else 52 | typeInfo.typeParams 53 | .map(arg => s"${arg.short}${typeArgsString(arg)}") 54 | .mkString("[", ",", "]") 55 | 56 | joinElems( 57 | ctx.typeInfo.short + typeArgsString( 58 | ctx.typeInfo 59 | ) + annotationStr + typeAnnotationStr, 60 | paramStrings 61 | ) 62 | } 63 | 64 | /** choose which typeclass to use based on the subtype of the sealed trait and prefix with the annotations as discovered on the subtype. 65 | */ 66 | override def split[T](ctx: SealedTrait[Typeclass, T]): Show[Out, T] = 67 | (value: T) => 68 | ctx.choose(value) { sub => 69 | val anns = (sub.annotations ++ sub.inheritedAnnotations).distinct 70 | 71 | val annotationStr = 72 | if (anns.isEmpty) "" else anns.mkString("{", ",", "}") 73 | 74 | prefix(annotationStr, sub.typeclass.show(sub.value)) 75 | } 76 | } 77 | 78 | /** companion object to [[Show]] */ 79 | object Show extends GenericShow[String]: 80 | 81 | def prefix(s: String, out: String): String = s + out 82 | def joinElems(typeName: String, params: Seq[String]): String = 83 | params.mkString(s"$typeName(", ",", ")") 84 | 85 | given Show[String, String] = identity(_) 86 | given Show[String, Int] = _.toString 87 | given Show[String, Long] = _.toString + "L" 88 | given Show[String, Boolean] = _.toString 89 | given [A](using A: Show[String, A]): Show[String, Seq[A]] = 90 | _.iterator.map(A.show).mkString("[", ",", "]") 91 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: ['**'] 5 | push: 6 | branches: ['**'] 7 | tags: [scala3-v*] 8 | permissions: 9 | contents: write # release-drafter, auto-merge requirement 10 | pull-requests: write # labeler, auto-merge requirement 11 | jobs: 12 | build: 13 | uses: softwaremill/github-actions-workflows/.github/workflows/build-scala.yml@main 14 | # run on external PRs, but not on internal PRs since those will be run by push to branch 15 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 16 | with: 17 | java-opts: '-Xmx4G -Xss16M' 18 | 19 | mima: 20 | runs-on: ubuntu-22.04 21 | env: 22 | JAVA_OPTS: '-Xmx4G -Xss16M' 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 # checkout tags so that dynver works properly (we need the version for MiMa) 28 | - name: Set up JDK 11 29 | uses: actions/setup-java@v1 30 | with: 31 | java-version: 11 32 | - name: Cache sbt 33 | uses: coursier/cache-action@v6 34 | with: 35 | extraKey: sbt-cache-${{ runner.os }} 36 | - name: Check MiMa # disable for major releases 37 | run: sbt -v core3/mimaReportBinaryIssues 38 | 39 | label: 40 | # only for PRs by softwaremill-ci 41 | if: github.event.pull_request.user.login == 'softwaremill-ci' 42 | uses: softwaremill/github-actions-workflows/.github/workflows/label.yml@main 43 | secrets: inherit 44 | 45 | auto-merge: 46 | # only for PRs by softwaremill-ci 47 | if: github.event.pull_request.user.login == 'softwaremill-ci' 48 | needs: [ build, mima, label ] 49 | uses: softwaremill/github-actions-workflows/.github/workflows/auto-merge.yml@main 50 | secrets: inherit 51 | 52 | publish: 53 | name: Publish release 54 | needs: [build] 55 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/scala3-v')) 56 | runs-on: ubuntu-22.04 57 | env: 58 | STTP_NATIVE: 1 59 | JAVA_OPTS: '-Xmx4G -Xss16M' 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v2 63 | - name: Set up JDK 64 | uses: actions/setup-java@v4 65 | with: 66 | distribution: 'temurin' 67 | cache: 'sbt' 68 | java-version: ${{ matrix.java }} 69 | - uses: sbt/setup-sbt@3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd # v1, specifically v1.1.14 70 | - name: Compile 71 | run: sbt compile 72 | - name: Publish artifacts 73 | run: sbt ci-release 74 | env: 75 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 76 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 77 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 78 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 79 | - name: Extract version from commit message 80 | run: | 81 | version=${GITHUB_REF/refs\/tags\/scala3-v/} 82 | echo "VERSION=$version" >> $GITHUB_ENV 83 | env: 84 | COMMIT_MSG: ${{ github.event.head_commit.message }} 85 | - name: Publish release notes 86 | uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6, specifically v6.1.0 87 | with: 88 | config-name: release-drafter.yml 89 | publish: true 90 | name: "scala3-v${{ env.VERSION }}" 91 | tag: "scala3-v${{ env.VERSION }}" 92 | version: "v${{ env.VERSION }}" 93 | env: 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/OtherTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | import scala.util.control.NonFatal 6 | 7 | class OtherTests extends munit.FunSuite: 8 | 9 | import OtherTests.* 10 | 11 | test("show error stack") { 12 | val error = compileErrors(""" 13 | case class Alpha(integer: Double) 14 | case class Beta(alpha: Alpha) 15 | Show.derived[Beta] 16 | """) 17 | assert( 18 | clue(error) contains "No given instance of type magnolia1.examples.Show[String, Alpha] was found." 19 | ) 20 | } 21 | 22 | test("not attempt to instantiate Unit when producing error stack") { 23 | val error = compileErrors(""" 24 | case class Gamma(unit: Unit) 25 | Show.derived[Gamma] 26 | """) 27 | assert( 28 | clue(error) contains "No given instance of type magnolia1.examples.Show[String, Unit] was found." 29 | ) 30 | } 31 | 32 | test("not attempt to derive instances for refined types") { 33 | val error = compileErrors("Show.derived[Character]") 34 | assert( 35 | clue( 36 | error 37 | ) contains "No given instance of type magnolia1.examples.Show[String, Long & magnolia1.tests.OtherTests.Character.Tag] was found." 38 | ) 39 | } 40 | 41 | test("derive instances for types with refined types if implicit provided") { 42 | val error = compileErrors("Show.derived[AnotherCharacter]") 43 | assert(error.isEmpty) 44 | } 45 | 46 | // TODO - not working: "Maximal number of successive inlines (32) exceeded" 47 | // test("not attempt to derive instances for Java enums") { 48 | // val error = compileErrors("Show.derived[WeekDay]") 49 | // assert(error contains "No given instance of type deriving.Mirror.Of[magnolia1.tests.WeekDay] was found for parameter x$1 of method derived in trait Derivation.") 50 | // } 51 | 52 | test("patch a Person via a Patcher[Entity]") { 53 | given Patcher[String] = Patcher.forSingleValue[String] 54 | given Patcher[Int] = Patcher.forSingleValue[Int] 55 | val person = Person("Bob", 42) 56 | val res = summon[Patcher[Entity]].patch(person, Seq(null, 21)) 57 | 58 | assertEquals(res, Person("Bob", 21)) 59 | } 60 | 61 | test("throw on an illegal patch attempt with field count mismatch") { 62 | // these two implicits can be removed once https://github.com/propensive/magnolia/issues/58 is closed 63 | given Patcher[String] = Patcher.forSingleValue[String] 64 | given Patcher[Int] = Patcher.forSingleValue[Int] 65 | 66 | val res = 67 | try { 68 | val person = Person("Bob", 42) 69 | summon[Patcher[Entity]].patch(person, Seq(null, 21, "killer")) 70 | } catch { 71 | case NonFatal(e) => e.getMessage 72 | } 73 | assertEquals(res, "Cannot patch value `Person(Bob,42)`, expected 2 fields but got 3") 74 | } 75 | 76 | // TODO - test hanging 77 | // test("throw on an illegal patch attempt with field type mismatch") { 78 | // // these two implicits can be removed once https://github.com/propensive/magnolia/issues/58 is closed 79 | // given Patcher[String] = Patcher.forSingleValue[String] 80 | // given Patcher[Int] = Patcher.forSingleValue[Int] 81 | // 82 | // val res = try { 83 | // val person = Person("Bob", 42) 84 | // summon[Patcher[Entity]].patch(person, Seq(null, "killer")) 85 | // "it worked" 86 | // } catch { 87 | // case NonFatal(e) => e.getMessage 88 | // } 89 | // assert(res.contains("java.lang.String cannot be cast to")) 90 | // assert(res.contains("java.lang.Integer")) 91 | // } 92 | 93 | object OtherTests: 94 | case class Character(id: Character.Id) 95 | object Character: 96 | trait Tag extends Any 97 | type Id = Long with Tag 98 | 99 | case class AnotherCharacter(id: AnotherCharacter.Id) 100 | object AnotherCharacter: 101 | trait Tag extends Any 102 | type Id = Long with Tag 103 | given Show[String, Id] = _.toString 104 | 105 | sealed trait Entity 106 | case class Company(name: String) extends Entity 107 | case class Person(name: String, age: Int) extends Entity 108 | case class Address(line1: String, occupant: Person) 109 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/VarianceTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | class VarianceTests extends munit.FunSuite: 7 | 8 | import VarianceTests.* 9 | 10 | // Corrupt being covariant in L <: Seq[Company] enables the derivation for Corrupt[String, _] 11 | test("show a Politician with covariant lobby") { 12 | val res = Show 13 | .derived[Politician[String]] 14 | .show(Corrupt("wall", Seq(Company("Alice Inc")))) 15 | assertEquals( 16 | res, 17 | "Corrupt[String,Seq[Company]](slogan=wall,lobby=[Company(name=Alice Inc)])" 18 | ) 19 | } 20 | 21 | test("show a Box with invariant label") { 22 | val res = summon[Show[String, Box[Int]]].show(LabelledBox(17, "justLabel")) 23 | assertEquals(res, "LabelledBox[Int,String](value=17,label=justLabel)") 24 | } 25 | 26 | // TODO: yields [Any | Custom | Int | Nothing | String] 27 | // test("determine subtypes of Exactly[Int]") { 28 | // given TypeNameInfo[Int] = TypeNameInfo.fallback[Int] 29 | // val res = TypeNameInfo.derived[Exactly[Int]].subtypeNames.map(_.short).mkString(" | ") 30 | // assertEquals(res, "Custom | Int") 31 | // } 32 | 33 | // TODO: yields [Any | Custom | Int | Nothing | String] 34 | // test("determine subtypes of Covariant[String]") { 35 | // given hideFallbackWarning: TypeNameInfo[String] = TypeNameInfo.fallback[String] 36 | 37 | // val res = TypeNameInfo.derived[Covariant[String]].subtypeNames.map(_.short).mkString(" | ") 38 | // assertEquals(res, "Custom | Nothing | String") 39 | // } 40 | 41 | // TODO: yields [Any | Custom | Int | Nothing | String] 42 | // test("determine subtypes of Contravariant[Double]") { 43 | // given hideFallbackWarning: TypeNameInfo[Double] = TypeNameInfo.fallback[Double] 44 | 45 | // val res = TypeNameInfo.derived[Contravariant[Double]].subtypeNames.map(_.short).mkString(" | ") 46 | // assertEquals(res, "Any | Custom") 47 | // } 48 | 49 | // TODO - not working as expected 50 | // test("dependencies between derived type classes") { 51 | // given [T: [X] =>> Show[String, X]] : Show[String, Path[T]] = Show.derived 52 | // implicit def showDefaultOption[A]( 53 | // implicit showA: Show[String, A], 54 | // defaultA: HasDefault[A] 55 | // ): Show[String, Option[A]] = (optA: Option[A]) => showA.show(optA.getOrElse(defaultA.defaultValue.right.get)) 56 | 57 | // val res = Show.derived[Path[String]].show(OffRoad(Some(Crossroad(Destination("A"), Destination("B"))))) 58 | // val destinationA = Destination("A") 59 | // val destinationB = Destination("B") 60 | // val crossroad = Crossroad(destinationA, destinationB) 61 | // val offroad = OffRoad(Some(crossroad)) 62 | 63 | // val destinationAShow = summon[Show[String, Destination[String]]].show(destinationA) 64 | // val crossroadShow = summon[Show[String, Crossroad[String]]].show(crossroad) 65 | // val crossroadShow2 = summon[Show[String, Path[String]]].show(crossroad) 66 | // val offroadShow = summon[Show[String, Path[String]]].show(offroad) 67 | 68 | // assertEquals(res, "OffRoad[String](path=Crossroad[String](left=Destination[String](value=A),right=Destination[String](value=B)))") 69 | // } 70 | 71 | object VarianceTests: 72 | 73 | sealed trait Covariant[+A] 74 | 75 | sealed trait Contravariant[-A] 76 | 77 | sealed trait Exactly[A] extends Covariant[A], Contravariant[A] 78 | 79 | object Exactly: 80 | case object Any extends Exactly[Any] 81 | case class Custom[A](value: A) extends Exactly[A] 82 | case object Int extends Exactly[Int] 83 | case object Nothing extends Exactly[Nothing] 84 | case object String extends Exactly[String] 85 | 86 | sealed trait Politician[+S] 87 | case class Accountable[+S](slogan: S) extends Politician[S] 88 | case class Corrupt[+S, +L <: Seq[Company]](slogan: S, lobby: L) extends Politician[S] 89 | 90 | sealed trait Entity 91 | case class Company(name: String) extends Entity 92 | case class Person(name: String, age: Int) extends Entity 93 | case class Address(line1: String, occupant: Person) 94 | 95 | sealed trait Box[+A] 96 | case class SimpleBox[+A](value: A) extends Box[A] 97 | case class LabelledBox[+A, L <: String](value: A, var label: L) extends Box[A] 98 | 99 | sealed trait Path[+A] derives Print 100 | case class Destination[+A](value: A) extends Path[A] 101 | case class Crossroad[+A](left: Path[A], right: Path[A]) extends Path[A] 102 | case class OffRoad[+A](path: Option[Path[A]]) extends Path[A] 103 | -------------------------------------------------------------------------------- /core/src/main/scala/magnolia1/magnolia.scala: -------------------------------------------------------------------------------- 1 | package magnolia1 2 | 3 | import scala.deriving.Mirror 4 | 5 | trait CommonDerivation[TypeClass[_]]: 6 | type Typeclass[T] = TypeClass[T] 7 | 8 | /** Must be implemented by the user of Magnolia to construct a typeclass for case class `T` using the provided type info. E.g. if we are 9 | * deriving `Show[T]` typeclasses, and `T` is a case class `Foo(...)`, we need to constuct `Show[Foo]`. 10 | * 11 | * This method is called 'join' because typically it will _join_ together the typeclasses for all the parameters of the case class, into 12 | * a single typeclass for the case class itself. The field [[CaseClass.params]] can provide useful information for doing this. 13 | * 14 | * @param caseClass 15 | * information about the case class `T`, its parameters, and _their_ typeclasses 16 | */ 17 | def join[T](caseClass: CaseClass[Typeclass, T]): Typeclass[T] 18 | 19 | inline def derivedMirrorProduct[A]( 20 | product: Mirror.ProductOf[A] 21 | ): Typeclass[A] = join(CaseClassDerivation.fromMirror(product)) 22 | 23 | inline def getParams__[T, Labels <: Tuple, Params <: Tuple]( 24 | annotations: Map[String, List[Any]], 25 | inheritedAnnotations: Map[String, List[Any]], 26 | typeAnnotations: Map[String, List[Any]], 27 | repeated: Map[String, Boolean], 28 | defaults: Map[String, Option[() => Any]], 29 | idx: Int = 0 30 | ): List[CaseClass.Param[Typeclass, T]] = CaseClassDerivation.paramsFromMaps( 31 | annotations, 32 | inheritedAnnotations, 33 | typeAnnotations, 34 | repeated, 35 | defaults 36 | ) 37 | 38 | // for backward compatibility with v1.1.1 39 | inline def getParams_[T, Labels <: Tuple, Params <: Tuple]( 40 | annotations: Map[String, List[Any]], 41 | inheritedAnnotations: Map[String, List[Any]], 42 | typeAnnotations: Map[String, List[Any]], 43 | repeated: Map[String, Boolean], 44 | idx: Int = 0 45 | ): List[CaseClass.Param[Typeclass, T]] = 46 | getParams__(annotations, Map.empty, typeAnnotations, repeated, Map(), idx) 47 | 48 | // for backward compatibility with v1.0.0 49 | inline def getParams[T, Labels <: Tuple, Params <: Tuple]( 50 | annotations: Map[String, List[Any]], 51 | typeAnnotations: Map[String, List[Any]], 52 | repeated: Map[String, Boolean], 53 | idx: Int = 0 54 | ): List[CaseClass.Param[Typeclass, T]] = 55 | getParams__(annotations, Map.empty, typeAnnotations, repeated, Map(), idx) 56 | 57 | end CommonDerivation 58 | 59 | trait ProductDerivation[TypeClass[_]] extends CommonDerivation[TypeClass]: 60 | inline def derivedMirror[A](using mirror: Mirror.Of[A]): Typeclass[A] = 61 | inline mirror match 62 | case product: Mirror.ProductOf[A] => derivedMirrorProduct[A](product) 63 | 64 | inline given derived[A](using Mirror.Of[A]): Typeclass[A] = derivedMirror[A] 65 | end ProductDerivation 66 | 67 | trait Derivation[TypeClass[_]] extends CommonDerivation[TypeClass] with SealedTraitDerivation: 68 | 69 | /** This must be implemented by the user of Magnolia to construct a Typeclass for 'T', where 'T' is a Sealed Trait or Scala 3 Enum, using 70 | * the provided type info. E.g. if we are deriving 'Show[T]' typeclasses, and T is an enum 'Suit' (eg with values Diamonds, Clubs, etc), 71 | * we need to constuct 'Show[Suit]'. 72 | * 73 | * This method is called 'split' because it will ''split'' the different possible types of the SealedTrait, and handle each one to 74 | * finally produce a typeclass capable of handling any possible subtype of the trait. 75 | * 76 | * A useful function for implementing this method is [[SealedTrait#choose]], which can take a value instance and provide information on 77 | * the specific subtype of the sealedTrait which that value is. 78 | */ 79 | def split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] 80 | 81 | transparent inline def subtypes[T, SubtypeTuple <: Tuple]( 82 | m: Mirror.SumOf[T], 83 | idx: Int = 0 // no longer used, kept for bincompat 84 | ): List[SealedTrait.Subtype[Typeclass, T, _]] = 85 | subtypesFromMirror[T, SubtypeTuple](m, idx) 86 | 87 | inline def derivedMirrorSum[A](sum: Mirror.SumOf[A]): Typeclass[A] = 88 | split(sealedTraitFromMirror(sum)) 89 | 90 | inline def derivedMirror[A](using mirror: Mirror.Of[A]): Typeclass[A] = 91 | inline mirror match 92 | case sum: Mirror.SumOf[A] => derivedMirrorSum[A](sum) 93 | case product: Mirror.ProductOf[A] => derivedMirrorProduct[A](product) 94 | 95 | inline def derived[A](using Mirror.Of[A]): Typeclass[A] = derivedMirror[A] 96 | 97 | protected override inline def deriveSubtype[s]( 98 | m: Mirror.Of[s] 99 | ): Typeclass[s] = derivedMirror[s](using m) 100 | end Derivation 101 | 102 | trait AutoDerivation[TypeClass[_]] extends Derivation[TypeClass]: 103 | inline given autoDerived[A](using Mirror.Of[A]): TypeClass[A] = derived 104 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/RecursiveTypesTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | 6 | class RecursiveTypesTests extends munit.FunSuite: 7 | import RecursiveTypesTests.* 8 | 9 | test("serialize a Leaf") { 10 | val res = summon[Show[String, Leaf[String]]].show(Leaf("testing")) 11 | assertEquals(res, "Leaf[String](value=testing)") 12 | } 13 | 14 | // TODO not working - not serializing the concrete type down the hierarchy: showing "T" instead of String 15 | // test("serialize a Branch") { 16 | // val res = summon[Show[String, Branch[String]]].show(Branch(Leaf("LHS"), Leaf("RHS"))) 17 | // assertEquals(res, "Branch[String](left=Leaf[String](value=LHS),right=Leaf[String](value=RHS))") 18 | // } 19 | 20 | // TODO not working - not serializing the concrete type down the hierarchy: "T" instead of String 21 | // test("serialize a Branch") { 22 | // val res = summon[Show[String, Tree[String]]].show(Branch(Leaf("LHS"), Leaf("RHS"))) 23 | // assertEquals(res, "Branch[String](left=Leaf[String](value=LHS),right=Leaf[String](value=RHS))") 24 | // } 25 | 26 | test("test branch equality true") { 27 | val res = Eq 28 | .derived[Tree[String]] 29 | .equal(Branch(Leaf("one"), Leaf("two")), Branch(Leaf("one"), Leaf("two"))) 30 | assert(res) 31 | } 32 | 33 | test("serialize self recursive type in base case") { 34 | val res = summon[Show[String, GPerson]].show(GPerson(Nil)) 35 | assertEquals(res, "GPerson(children=[])") 36 | } 37 | 38 | test("serialize self recursive type in nonbase case") { 39 | val alice = RPerson(0, "Alice", Nil) 40 | val bob = RPerson(0, "Bob", Nil) 41 | val granny = GPerson(List(RPerson(1, "Mama", List(alice, bob)))) 42 | 43 | val res = summon[Show[String, GPerson]].show(granny) 44 | assertEquals( 45 | res, 46 | "GPerson(children=[RPerson(age=1,name=Mama,children=[RPerson(age=0,name=Alice,children=[]),RPerson(age=0,name=Bob,children=[])])])" 47 | ) 48 | } 49 | 50 | test("construct a semi print for recursive hierarchy") { 51 | given instance: SemiPrint[Recursive] = SemiPrint.derived 52 | val res = instance.print(Recursive(Seq(Recursive(Seq.empty)))) 53 | assertEquals(res, "Recursive(Recursive())") 54 | } 55 | 56 | test("construct a semmi print for a recursive, generic type") { 57 | given instance: SemiPrint[Tree[Int]] = SemiPrint.derived 58 | val res = instance.print(Branch(Branch(Leaf(0), Leaf(1)), Leaf(2))) 59 | assertEquals(res, "Branch(Branch(Leaf(0),Leaf(1)),Leaf(2))") 60 | } 61 | 62 | test("equality of Wrapper") { 63 | val res = Eq 64 | .derived[Wrapper] 65 | .equal( 66 | Wrapper(Some(KArray(KArray(Nil) :: Nil))), 67 | Wrapper(Some(KArray(KArray(Nil) :: KArray(Nil) :: Nil))) 68 | ) 69 | assert(!res) 70 | } 71 | 72 | test("construction of Show instance for Tree") { 73 | val error = compileErrors("summon[Show[String, Tree[String]]]") 74 | assert(error.isEmpty) 75 | } 76 | 77 | test("construction of Show instance for Leaf") { 78 | val error = compileErrors("summon[Show[String, Leaf[String]]]") 79 | assert(error.isEmpty) 80 | } 81 | 82 | test("show a recursive case class") { 83 | val res = Show.derived[Recursive].show(Recursive(Seq(Recursive(Nil)))) 84 | assertEquals(res, "Recursive(children=[Recursive(children=[])])") 85 | } 86 | 87 | test("manually derive a recursive case class instance") { 88 | val res = Recursive.showRecursive.show(Recursive(Seq(Recursive(Nil)))) 89 | assertEquals(res, "Recursive(children=[Recursive(children=[])])") 90 | } 91 | 92 | test( 93 | "no support for arbitrary derivation result type for recursive classes yet" 94 | ) { 95 | val error = compileErrors("ExportedTypeclass.derived[Recursive]") 96 | val expectedError = 97 | """Seq[magnolia1.tests.RecursiveTypesTests.Recursive]] was found.""" 98 | assert(clue(error) contains expectedError) 99 | } 100 | 101 | test("serialize a CeList") { 102 | val printResult = summon[Print[CeList]].print(CeColon(3, CeColon(2, CeNil(2)))) 103 | val showResult = summon[Show[String, CeList]].show(CeColon(3, CeColon(2, CeNil(2)))) 104 | 105 | assert(clue(printResult) == "CeColon(3,CeColon(2,CeNil(2)))") 106 | assert(clue(showResult) == "CeColon(head=3,tail=CeColon(head=2,tail=CeNil(head=2)))") 107 | } 108 | 109 | object RecursiveTypesTests: 110 | 111 | sealed trait Tree[+T] derives Eq 112 | object Tree: 113 | given [T: [X] =>> Show[String, X]]: Show[String, Tree[T]] = Show.derived 114 | case class Leaf[+L](value: L) extends Tree[L] 115 | case class Branch[+B](left: Tree[B], right: Tree[B]) extends Tree[B] 116 | 117 | case class RPerson(age: Int, name: String, children: Seq[RPerson]) 118 | object RPerson: 119 | given Show[String, RPerson] = Show.derived 120 | case class GPerson(children: Seq[RPerson]) 121 | 122 | case class Recursive(children: Seq[Recursive]) 123 | object Recursive: 124 | given showRecursive: Show[String, Recursive] = Show.derived[Recursive] 125 | 126 | case class KArray(value: List[KArray]) derives Eq 127 | case class Wrapper(v: Option[KArray]) 128 | 129 | sealed trait CeList 130 | case class CeColon(head: Int, tail: CeList) extends CeList 131 | case class CeNil(head: Int) extends CeList 132 | 133 | object CeList: 134 | given Show[String, CeList] = Show.derived 135 | given Print[CeList] = Print.derived[CeList] 136 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/AnnotationsTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | import scala.annotation.StaticAnnotation 6 | 7 | class AnnotationsTests extends munit.FunSuite: 8 | import AnnotationsTests.* 9 | 10 | test("capture attributes against params") { 11 | val res = summon[Show[String, Attributed]].show(Attributed("xyz", 100)) 12 | assertEquals( 13 | res, 14 | "Attributed{MyAnnotation(0)}{MyTypeAnnotation(2)}(p1{MyAnnotation(1)}{MyTypeAnnotation(0)}=xyz,p2{MyAnnotation(2)}{MyTypeAnnotation(1)}=100)" 15 | ) 16 | } 17 | 18 | test("show the scala.deprecated annotation on a field") { 19 | val res = summon[Show[String, Deprecated]].show(Deprecated(10)) 20 | assert(clue(res).contains("MyAnnotation(0)")) 21 | assert(clue(res).contains("scala.deprecated")) 22 | } 23 | 24 | test("inherit annotations from parent trait") { 25 | val res = Show.derived[Pet].show(Dog("Alex", 10, likesMeat = true)) 26 | assertEquals( 27 | res, 28 | "{MyTypeAnnotation(2),MyTypeAnnotation(1)}Dog{MyTypeAnnotation(2),MyTypeAnnotation(1)}(name{[i]MyAnnotation(1)}=Alex,age{[i]MyAnnotation(2)}=10,likesMeat{MyAnnotation(3)}=true)" 29 | ) 30 | } 31 | 32 | test("inherit annotations from all parent traits in hierarchy") { 33 | val res = Show 34 | .derived[Rodent] 35 | .show(Hamster("Alex", 10, likesNuts = true, likesVeggies = true)) 36 | assertEquals( 37 | res, 38 | "{MyTypeAnnotation(1)}Hamster{MyTypeAnnotation(1)}(name{[i]MyAnnotation(1)}=Alex,age{MyAnnotation(6),[i]MyAnnotation(2)}=10,likesNuts{[i]MyAnnotation(3)}=true,likesVeggies{MyAnnotation(4)}=true)" 39 | ) 40 | } 41 | 42 | test("inherit annotations from base class constructor parameters") { 43 | val res = Show.derived[Foo].show(Foo("foo")) 44 | assertEquals(res, "Foo(foo{MyAnnotation(2),[i]MyAnnotation(1)}=foo)") 45 | } 46 | 47 | test( 48 | "inherit annotations from all base class constructor parameters in hierarchy" 49 | ) { 50 | val res = Show.derived[Bar].show(Bar("foo", "bar")) 51 | assertEquals( 52 | res, 53 | "Bar(foo{MyAnnotation(2),[i]MyAnnotation(1)}=foo,bar{MyAnnotation(2),[i]MyAnnotation(1)}=bar)" 54 | ) 55 | } 56 | 57 | test("capture attributes against subtypes") { 58 | val res = Show.derived[AttributeParent].show(Attributed("xyz", 100)) 59 | assertEquals( 60 | res, 61 | "{MyAnnotation(0)}Attributed{MyAnnotation(0)}{MyTypeAnnotation(2)}(p1{MyAnnotation(1)}{MyTypeAnnotation(0)}=xyz,p2{MyAnnotation(2)}{MyTypeAnnotation(1)}=100)" 62 | ) 63 | } 64 | 65 | test("sealed trait enumeration should provide trait annotations") { 66 | val traitAnnotations = 67 | SubtypeInfo.derived[Sport].traitAnnotations.map(_.toString) 68 | assertEquals(traitAnnotations.mkString, "MyAnnotation(0)") 69 | } 70 | 71 | test("sealed trait enumeration should provide subtype annotations") { 72 | val subtypeAnnotations = SubtypeInfo.derived[Sport].subtypeAnnotations 73 | assertEquals(subtypeAnnotations(0).mkString, "MyAnnotation(1)") 74 | assertEquals(subtypeAnnotations(1).mkString, "MyAnnotation(2)") 75 | } 76 | 77 | test("serialize case class with Java annotations by skipping them") { 78 | val res = Show.derived[MyDto].show(MyDto("foo", 42)) 79 | assertEquals(res, "MyDto{MyAnnotation(0)}(foo=foo,bar=42)") 80 | } 81 | 82 | test("serialize case class with Java annotations which comes from external module by skipping them") { 83 | val res = Show.derived[JavaAnnotatedCase].show(JavaAnnotatedCase(1)) 84 | assertEquals(res, "JavaAnnotatedCase(v=1)") 85 | } 86 | 87 | object AnnotationsTests: 88 | 89 | case class MyAnnotation(order: Int) extends StaticAnnotation 90 | 91 | case class MyTypeAnnotation(order: Int) extends StaticAnnotation 92 | 93 | sealed trait AttributeParent 94 | @MyAnnotation(0) 95 | case class Attributed( 96 | @MyAnnotation(1) p1: String @MyTypeAnnotation(0), 97 | @MyAnnotation(2) p2: Int @MyTypeAnnotation(1) 98 | ) extends AttributeParent @MyTypeAnnotation(2) 99 | 100 | case class Deprecated(@MyAnnotation(0) @deprecated f: Int) 101 | 102 | class Base( 103 | @MyAnnotation(1) 104 | val foo: String 105 | ) 106 | 107 | case class Foo( 108 | @MyAnnotation(2) 109 | override val foo: String 110 | ) extends Base(foo) 111 | 112 | class Base2( 113 | override val foo: String, 114 | @MyAnnotation(1) 115 | val bar: String 116 | ) extends Base(foo) 117 | 118 | case class Bar( 119 | @MyAnnotation(2) 120 | override val foo: String, 121 | @MyAnnotation(2) 122 | override val bar: String 123 | ) extends Base2(foo, bar) 124 | 125 | @MyAnnotation(0) 126 | sealed trait Sport 127 | 128 | @MyAnnotation(1) 129 | case object Boxing extends Sport 130 | 131 | @MyAnnotation(2) 132 | case class Soccer(players: Int) extends Sport 133 | 134 | @MyAnnotation(0) 135 | @SuppressWarnings(Array("deprecation")) 136 | @JavaExampleAnnotation(description = "Some model") 137 | case class MyDto(foo: String, bar: Int) 138 | 139 | @MyTypeAnnotation(1) 140 | sealed trait Pet { 141 | @MyAnnotation(1) 142 | def name: String 143 | @MyAnnotation(2) 144 | def age: Int 145 | } 146 | 147 | @MyTypeAnnotation(2) 148 | case class Dog(name: String, age: Int, @MyAnnotation(3) likesMeat: Boolean) extends Pet 149 | 150 | sealed trait Rodent extends Pet { 151 | @MyAnnotation(3) 152 | def likesNuts: Boolean 153 | } 154 | 155 | case class Hamster( 156 | name: String, 157 | @MyAnnotation(6) 158 | age: Int, 159 | likesNuts: Boolean, 160 | @MyAnnotation(4) likesVeggies: Boolean 161 | ) extends Rodent 162 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Magnolia](https://github.com/softwaremill/magnolia/raw/scala3/banner.jpg) 2 | 3 | [GitHub Workflow](https://github.com/softwaremill/magnolia/actions) 4 | [](https://softwaremill.community/c/magnolia) 5 | [](https://index.scala-lang.org/softwaremill/magnolia/magnolia) 6 | 7 | # Magnolia 8 | 9 | __Magnolia__ is a generic macro for automatic materialization of typeclasses for datatypes composed from product types (e.g. case classes) and coproduct types (e.g. enums). It supports recursively-defined datatypes out-of-the-box, and incurs no significant time-penalty during compilation. 10 | 11 | ## Features 12 | 13 | - derives typeclasses for case classes, case objects and sealed traits 14 | - offers a lightweight syntax for writing derivations without needing to understand complex parts of Scala 15 | - builds upon Scala 3's built-in generic derivation 16 | - works with recursive and mutually-recursive definitions 17 | - supports parameterized ADTs (GADTs), including those in recursive types 18 | - supports typeclasses whose generic type parameter is used in either covariant and contravariant positions 19 | 20 | ## Getting Started 21 | 22 | Given an ADT such as, 23 | ```scala 24 | enum Tree[+T] derives Print: 25 | case Branch(left: Tree[T], right: Tree[T]) 26 | case Leaf(value: T) 27 | ``` 28 | and provided a given instance of `Print[Int]` is in scope, and a Magnolia derivation for the `Print` typeclass 29 | has been provided, we can automatically derive given typeclass instances of `Print[Tree[Int]]` on-demand, like 30 | so, 31 | ```scala 32 | Tree.Branch(Tree.Branch(Tree.Leaf(1), Tree.Leaf(2)), Tree.Leaf(3)).print 33 | ``` 34 | Typeclass authors may provide Magnolia derivations in the typeclass's companion object, but it is easy to create 35 | your own. 36 | 37 | Creating a generic derivation with Magnolia requires implementing two methods on `magnolia1.Derivation`: 38 | 39 | * `join()` : create typeclasses for case classes ('product types') 40 | * `split()` : create typeclasses for sealed-traits/enums ('sum types') 41 | 42 | ### Example derivations 43 | 44 | There are many examples in the [`examples`](examples/src/main/scala/magnolia1/examples) sub-project. 45 | 46 | The definition of a `Print` typeclass with generic derivation might look like this 47 | (note we're using the [Lambda syntax for Single Abstract Method types](https://www.scala-lang.org/news/2.12.0/#lambda-syntax-for-sam-types) 48 | to instantiate the `Print` instances in `join` & `split` - that's possible because 49 | `Print` has only a single abstract method, `print`): 50 | ```scala 51 | import magnolia1.* 52 | 53 | trait Print[T] { 54 | extension (x: T) def print: String 55 | } 56 | 57 | object Print extends AutoDerivation[Print]: 58 | def join[T](ctx: CaseClass[Print, T]): Print[T] = value => 59 | ctx.params.map { param => 60 | param.typeclass.print(param.deref(value)) 61 | }.mkString(s"${ctx.typeInfo.short}(", ",", ")") 62 | 63 | override def split[T](ctx: SealedTrait[Print, T]): Print[T] = value => 64 | ctx.choose(value) { sub => sub.typeclass.print(sub.cast(value)) } 65 | 66 | given Print[Int] = _.toString 67 | ``` 68 | 69 | The `AutoDerivation` trait provides a given `autoDerived` method which will attempt to construct a corresponding typeclass 70 | instance for the type passed to it. Importing `Print.autoDerived` as defined in the example above will make generic 71 | derivation for `Print` typeclasses available in the scope of the import. 72 | 73 | While any object may be used to define a derivation, if you control the typeclass you are deriving for, the 74 | companion object of the typeclass is the obvious choice since it generic derivations for that typeclass will 75 | be automatically available for consideration during contextual search. 76 | 77 | If you don't want to make the automatic derivation available in the given scope, consider using the `Derivation` trait which provides semi-auto derivation with `derived` method, but also brings some additional limitations. 78 | ## Limitations 79 | 80 | For accessing default values for case class parameters we recommend compilation with `-Yretain-trees` on. 81 | 82 | For a recursive structures it is required to assign the derived value to an implicit variable e.g. 83 | ```Scala 84 | given instance: SemiPrint[Recursive] = SemiPrint.derived 85 | ``` 86 | ## Availability 87 | 88 | For Scala 3: 89 | 90 | ```scala 91 | val magnolia = "com.softwaremill.magnolia1_3" %% "magnolia" % "1.3.18" 92 | ``` 93 | 94 | For Scala 2, see the [scala2 branch](https://github.com/softwaremill/magnolia/tree/scala2). 95 | 96 | ## Package and artifact naming, versioning 97 | 98 | The main magnolia package is `magnolia1`, so that magnolia 1.x can be used alongside magnolia 0.17 (which are binary-incompatible). 99 | Future major releases of magnolia can change the package name for the same reason. 100 | 101 | The group id for magnolia follows the naming scheme: `com.softwaremill.magnolia[major version]_[scala major version]`. 102 | The scala major version suffix is necessary to allow evolving and publishing versions for Scala 2 & Scala 3 independently. 103 | The magnolia major version is included for consistency with the package name, and so that future major releases may be 104 | used alongside this release. 105 | 106 | ## Contributing 107 | 108 | Contributors to Magnolia are welcome and encouraged. New contributors may like to look for issues marked 109 | label: good first issue. 111 | 112 | ## Credits 113 | 114 | Magnolia was originally designed and developed by [Jon Pretty](https://github.com/propensive), and is currently 115 | maintained by [SoftwareMill](https://softwaremill.com). 116 | 117 | ## License 118 | 119 | Magnolia is made available under the [Apache 2.0 License](/license.md). 120 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/SumsTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | import scala.annotation.StaticAnnotation 6 | 7 | object SumsTests: 8 | 9 | case class MyAnnotation(order: Int) extends StaticAnnotation 10 | case class MyTypeAnnotation(order: Int) extends StaticAnnotation 11 | 12 | sealed trait Entity 13 | case class Company(name: String) extends Entity 14 | case class Person(name: String, age: Int) extends Entity 15 | case class Address(line1: String, occupant: Person) 16 | 17 | sealed trait Color 18 | case object Red extends Color 19 | case object Green extends Color 20 | case object Blue extends Color 21 | case object Orange extends Color 22 | case object Pink extends Color 23 | 24 | sealed trait Y 25 | case object A extends Y 26 | case class B(s: String) extends Y 27 | 28 | enum Size: 29 | case S, M, L 30 | 31 | sealed trait Sport 32 | case object Boxing extends Sport 33 | case class Soccer(players: Int) extends Sport 34 | 35 | sealed trait Complex 36 | object Complex: 37 | case object Object extends G 38 | sealed trait A extends Complex 39 | sealed trait B extends A 40 | case object ObjectC extends Complex 41 | case object ObjectD extends A 42 | case object ObjectE extends B 43 | case object ObjectF extends A with Complex 44 | sealed trait G extends B 45 | case class ClassH(i: Int) extends A with G 46 | object Scoped: 47 | case object Object extends A 48 | end Complex 49 | 50 | object ExtendingTraits: 51 | trait One 52 | trait Two 53 | 54 | enum ExtendingTraits: 55 | case A extends ExtendingTraits with ExtendingTraits.One 56 | case B extends ExtendingTraits with ExtendingTraits.Two 57 | case C extends ExtendingTraits with ExtendingTraits.Two 58 | 59 | sealed trait Parent 60 | trait BadChild extends Parent // escape hatch! 61 | sealed trait GoodChild extends Parent 62 | final case class Huey(height: Int) extends GoodChild 63 | class Dewey(val height: Int) extends GoodChild 64 | final case class Louie(height: Int) extends BadChild 65 | 66 | sealed abstract class Halfy 67 | final case class Lefty() extends Halfy 68 | object Lefty: 69 | given NoCombine[Lefty] = NoCombine.instance(_ => "Lefty") 70 | final case class Righty() extends Halfy 71 | object Righty: 72 | given NoCombine[Righty] = NoCombine.instance(_ => "Righty") 73 | 74 | // format: off 75 | enum VeryLong: 76 | case _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, 77 | _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, 78 | _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, 79 | _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, 80 | _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, 81 | _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, 82 | _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, 83 | _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, 84 | _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, 85 | _91, _92, _93, _94, _95, _96, _97, _98, _99, _100, 86 | _101, _102, _103, _104, _105, _106, _107, _108, _109, _110, 87 | _111, _112, _113, _114, _115, _116, _117, _118, _119, _120, 88 | _121, _122, _123, _124, _125, _126, _127, _128, _129, _130, 89 | _131, _132, _133, _134, _135, _136, _137, _138, _139, _140, 90 | _141, _142, _143, _144, _145, _146, _147, _148, _149, _150, 91 | _151, _152, _153, _154, _155, _156, _157, _158, _159, _160, 92 | _161, _162, _163, _164, _165, _166, _167, _168, _169, _170, 93 | _171, _172, _173, _174, _175, _176, _177, _178, _179, _180, 94 | _181, _182, _183, _184, _185, _186, _187, _188, _189, _190, 95 | _191, _192, _193, _194, _195, _196, _197, _198, _199, _200, 96 | _201, _202, _203, _204, _205, _206, _207, _208, _209, _210, 97 | _211, _212, _213, _214, _215, _216, _217, _218, _219, _220, 98 | _221, _222, _223, _224, _225, _226, _227, _228, _229, _230, 99 | _231, _232, _233, _234, _235, _236, _237, _238, _239, _240, 100 | _241, _242, _243, _244, _245, _246, _247, _248, _249, _250, 101 | _251, _252, _253, _254 102 | // format: on 103 | end SumsTests 104 | 105 | class SumsTests extends munit.FunSuite: 106 | 107 | import SumsTests.* 108 | 109 | test("serialize case object as a sealed trait") { 110 | val res = summon[Show[String, Color]].show(Blue) 111 | assertEquals(res, "Blue()") 112 | } 113 | 114 | test("construct a Show coproduct instance") { 115 | val res = Show.derived[Entity].show(Person("John Smith", 34)) 116 | assertEquals(res, "Person(name=John Smith,age=34)") 117 | } 118 | 119 | test("construct a default value") { 120 | val res = HasDefault.derived[Entity].defaultValue 121 | assertEquals(res, Right(Company(""))) 122 | } 123 | 124 | test("decode a Person as an Entity") { 125 | val res = summon[Decoder[Entity]].decode( 126 | """magnolia1.tests.SumsTests.Person(name=John Smith,age=32)""" 127 | ) 128 | assertEquals(res, Person("John Smith", 32)) 129 | } 130 | 131 | test("construct a semi print for sealed hierarchy") { 132 | val res = SemiPrint.derived[Y].print(A) 133 | assertEquals(res, "A()") 134 | } 135 | 136 | test("not find a given for semi print") { 137 | val res = compileErrors("""summon[SemiPrint[Y]].print(A)""") 138 | assert(res.nonEmpty) 139 | } 140 | 141 | test("isEnum field in SubtypeInfo should be true for enum") { 142 | val derivedSubtypeInfo = SubtypeInfo.derived[Size] 143 | assertEquals(derivedSubtypeInfo.isEnum, true) 144 | } 145 | 146 | test("isEnum field in SubtypeInfo should be false for sealed trait") { 147 | val derivedSubtypeInfo = SubtypeInfo.derived[Sport] 148 | assertEquals(derivedSubtypeInfo.isEnum, false) 149 | } 150 | 151 | test("construct a Show instance for an enum") { 152 | val res = Show.derived[Size].show(Size.S) 153 | assertEquals(res, "S()") 154 | } 155 | 156 | test("construct a Show instance for very long enum") { 157 | val res = Show.derived[VeryLong].show(VeryLong._254) 158 | assertEquals(res, "_254()") 159 | } 160 | 161 | test("choose a enum") { 162 | val res = Passthrough.derived[Size].ctx.get.toOption.get 163 | List( 164 | Size.S, 165 | Size.M, 166 | Size.L 167 | ).foreach { o => 168 | val chosen = res.choose(o)(identity) 169 | assertEquals(chosen.value, o) 170 | assertEquals( 171 | chosen.typeInfo.short, 172 | o.toString 173 | ) 174 | } 175 | } 176 | 177 | test("should derive Show for a enum extending a trait") { 178 | val res = Show.derived[ExtendingTraits.A.type].show(ExtendingTraits.A) 179 | assertEquals(res, "A()") 180 | } 181 | 182 | test("sealed trait enumeration should detect isObject") { 183 | val subtypeIsObjects = SubtypeInfo.derived[Color].subtypeIsObject 184 | assertEquals(subtypeIsObjects, Seq(true, true, true, true, true)) 185 | } 186 | 187 | test("sealed trait subtypes should be ordered") { 188 | val res = TypeNameInfo.derived[Color].subtypeNames.map(_.short) 189 | assertEquals(res, Seq("Blue", "Green", "Orange", "Pink", "Red")) 190 | } 191 | 192 | test("sealed trait subtypes should detect isObject") { 193 | val subtypeIsObjects = SubtypeInfo.derived[Sport].subtypeIsObject 194 | assertEquals(subtypeIsObjects, Seq(true, false)) 195 | } 196 | 197 | test("sealed trait typeName should be complete and unchanged") { 198 | val res = TypeNameInfo.derived[Color].name 199 | assertEquals(res.full, "magnolia1.tests.SumsTests.Color") 200 | } 201 | 202 | test( 203 | "report an error when an abstract member of a sealed hierarchy is not sealed" 204 | ) { 205 | val error = compileErrors("Show.derived[Parent]") 206 | assert( 207 | clue( 208 | error 209 | ) contains "No given instance of type scala.deriving.Mirror.Of[magnolia1.tests.SumsTests.Parent] was found for parameter x$1 of method derived in trait Derivation." 210 | ) 211 | assert( 212 | clue( 213 | error 214 | ) contains "trait Parent is not a generic sum because its child trait BadChild is not a generic product because it is not a case class" 215 | ) 216 | } 217 | 218 | test( 219 | "report an error when a concrete member of a sealed hierarchy is neither final nor a case class" 220 | ) { 221 | val error = compileErrors("Show.derived[GoodChild]") 222 | assert( 223 | clue( 224 | error 225 | ) contains "trait GoodChild is not a generic sum because its child class Dewey is not a generic product because it is not a case class" 226 | ) 227 | } 228 | 229 | test("not assume full auto derivation of external coproducts") { 230 | case class LoggingConfig(o: Option[String]) 231 | object LoggingConfig: 232 | given SemiDefault[LoggingConfig] = SemiDefault.derived 233 | 234 | val res = summon[SemiDefault[LoggingConfig]].default 235 | assertEquals(res, LoggingConfig(None)) 236 | } 237 | 238 | test("half auto derivation of sealed families") { 239 | val res = SemiDefault.derived[Halfy].default 240 | assertEquals(res, Lefty()) 241 | } 242 | 243 | test("derive all subtypes in complex hierarchy") { 244 | val res = Passthrough.derived[Complex].ctx.get.toOption.get 245 | 246 | val pkg = "magnolia1.tests.SumsTests.Complex" 247 | val expected = List( 248 | s"$pkg.ClassH", 249 | s"$pkg.Object", 250 | s"$pkg.ObjectC", 251 | s"$pkg.ObjectD", 252 | s"$pkg.ObjectE", 253 | s"$pkg.ObjectF", 254 | s"$pkg.Scoped.Object" 255 | ) 256 | 257 | assertEquals(res.subtypes.map(_.typeInfo.full).toList, expected) 258 | } 259 | 260 | test("support split without join") { 261 | val res = summon[NoCombine[Halfy]].nameOf(Righty()) 262 | assertEquals(res, "Righty") 263 | } 264 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | 3 | _Version 2.0, January 2004_ 4 | _[http://www.apache.org/licenses/](http://www.apache.org/licenses/)_ 5 | 6 | ## Terms and Conditions for use, reproduction, and distribution 7 | 8 | ### 1. Definitions 9 | 10 | “License” shall mean the terms and conditions for use, reproduction, and 11 | distribution as defined by Sections 1 through 9 of this document. 12 | 13 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 14 | owner that is granting the License. 15 | 16 | “Legal Entity” shall mean the union of the acting entity and all other entities 17 | that control, are controlled by, or are under common control with that entity. 18 | For the purposes of this definition, “control” means **(i)** the power, direct or 19 | indirect, to cause the direction or management of such entity, whether by 20 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 21 | outstanding shares, or **(iii)** beneficial ownership of such entity. 22 | 23 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | “Source” form shall mean the preferred form for making modifications, including 27 | but not limited to software source code, documentation source, and configuration 28 | files. 29 | 30 | “Object” form shall mean any form resulting from mechanical transformation or 31 | translation of a Source form, including but not limited to compiled object code, 32 | generated documentation, and conversions to other media types. 33 | 34 | “Work” shall mean the work of authorship, whether in Source or Object form, made 35 | available under the License, as indicated by a copyright notice that is included 36 | in or attached to the work (an example is provided in the Appendix below). 37 | 38 | “Derivative Works” shall mean any work, whether in Source or Object form, that 39 | is based on (or derived from) the Work and for which the editorial revisions, 40 | annotations, elaborations, or other modifications represent, as a whole, an 41 | original work of authorship. For the purposes of this License, Derivative Works 42 | shall not include works that remain separable from, or merely link (or bind by 43 | name) to the interfaces of, the Work and Derivative Works thereof. 44 | 45 | “Contribution” shall mean any work of authorship, including the original version 46 | of the Work and any modifications or additions to that Work or Derivative Works 47 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 48 | by the copyright owner or by an individual or Legal Entity authorized to submit 49 | on behalf of the copyright owner. For the purposes of this definition, 50 | “submitted” means any form of electronic, verbal, or written communication sent 51 | to the Licensor or its representatives, including but not limited to 52 | communication on electronic mailing lists, source code control systems, and 53 | issue tracking systems that are managed by, or on behalf of, the Licensor for 54 | the purpose of discussing and improving the Work, but excluding communication 55 | that is conspicuously marked or otherwise designated in writing by the copyright 56 | owner as “Not a Contribution.” 57 | 58 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 59 | of whom a Contribution has been received by Licensor and subsequently 60 | incorporated within the Work. 61 | 62 | ### 2. Grant of Copyright License 63 | 64 | Subject to the terms and conditions of this License, each Contributor hereby 65 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 66 | irrevocable copyright license to reproduce, prepare Derivative Works of, 67 | publicly display, publicly perform, sublicense, and distribute the Work and such 68 | Derivative Works in Source or Object form. 69 | 70 | ### 3. Grant of Patent License 71 | 72 | Subject to the terms and conditions of this License, each Contributor hereby 73 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 74 | irrevocable (except as stated in this section) patent license to make, have 75 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 76 | such license applies only to those patent claims licensable by such Contributor 77 | that are necessarily infringed by their Contribution(s) alone or by combination 78 | of their Contribution(s) with the Work to which such Contribution(s) was 79 | submitted. If You institute patent litigation against any entity (including a 80 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 81 | Contribution incorporated within the Work constitutes direct or contributory 82 | patent infringement, then any patent licenses granted to You under this License 83 | for that Work shall terminate as of the date such litigation is filed. 84 | 85 | ### 4. Redistribution 86 | 87 | You may reproduce and distribute copies of the Work or Derivative Works thereof 88 | in any medium, with or without modifications, and in Source or Object form, 89 | provided that You meet the following conditions: 90 | 91 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 92 | this License; and 93 | * **(b)** You must cause any modified files to carry prominent notices stating that You 94 | changed the files; and 95 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 96 | all copyright, patent, trademark, and attribution notices from the Source form 97 | of the Work, excluding those notices that do not pertain to any part of the 98 | Derivative Works; and 99 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 100 | Derivative Works that You distribute must include a readable copy of the 101 | attribution notices contained within such NOTICE file, excluding those notices 102 | that do not pertain to any part of the Derivative Works, in at least one of the 103 | following places: within a NOTICE text file distributed as part of the 104 | Derivative Works; within the Source form or documentation, if provided along 105 | with the Derivative Works; or, within a display generated by the Derivative 106 | Works, if and wherever such third-party notices normally appear. The contents of 107 | the NOTICE file are for informational purposes only and do not modify the 108 | License. You may add Your own attribution notices within Derivative Works that 109 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 110 | provided that such additional attribution notices cannot be construed as 111 | modifying the License. 112 | 113 | You may add Your own copyright statement to Your modifications and may provide 114 | additional or different license terms and conditions for use, reproduction, or 115 | distribution of Your modifications, or for any such Derivative Works as a whole, 116 | provided Your use, reproduction, and distribution of the Work otherwise complies 117 | with the conditions stated in this License. 118 | 119 | ### 5. Submission of Contributions 120 | 121 | Unless You explicitly state otherwise, any Contribution intentionally submitted 122 | for inclusion in the Work by You to the Licensor shall be under the terms and 123 | conditions of this License, without any additional terms or conditions. 124 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 125 | any separate license agreement you may have executed with Licensor regarding 126 | such Contributions. 127 | 128 | ### 6. Trademarks 129 | 130 | This License does not grant permission to use the trade names, trademarks, 131 | service marks, or product names of the Licensor, except as required for 132 | reasonable and customary use in describing the origin of the Work and 133 | reproducing the content of the NOTICE file. 134 | 135 | ### 7. Disclaimer of Warranty 136 | 137 | Unless required by applicable law or agreed to in writing, Licensor provides the 138 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 139 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 140 | including, without limitation, any warranties or conditions of TITLE, 141 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 142 | solely responsible for determining the appropriateness of using or 143 | redistributing the Work and assume any risks associated with Your exercise of 144 | permissions under this License. 145 | 146 | ### 8. Limitation of Liability 147 | 148 | In no event and under no legal theory, whether in tort (including negligence), 149 | contract, or otherwise, unless required by applicable law (such as deliberate 150 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 151 | liable to You for damages, including any direct, indirect, special, incidental, 152 | or consequential damages of any character arising as a result of this License or 153 | out of the use or inability to use the Work (including but not limited to 154 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 155 | any and all other commercial damages or losses), even if such Contributor has 156 | been advised of the possibility of such damages. 157 | 158 | ### 9. Accepting Warranty or Additional Liability 159 | 160 | While redistributing the Work or Derivative Works thereof, You may choose to 161 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 162 | other liability obligations and/or rights consistent with this License. However, 163 | in accepting such obligations, You may act only on Your own behalf and on Your 164 | sole responsibility, not on behalf of any other Contributor, and only if You 165 | agree to indemnify, defend, and hold each Contributor harmless for any liability 166 | incurred by, or claims asserted against, such Contributor by reason of your 167 | accepting any such warranty or additional liability. 168 | 169 | _END OF TERMS AND CONDITIONS_ 170 | 171 | ## APPENDIX: How to apply the Apache License to your work 172 | 173 | To apply the Apache License to your work, attach the following boilerplate 174 | notice, with the fields enclosed by brackets `[]` replaced with your own 175 | identifying information. (Don't include the brackets!) The text should be 176 | enclosed in the appropriate comment syntax for the file format. We also 177 | recommend that a file or class name and description of purpose be included on 178 | the same “printed page” as the copyright notice for easier identification within 179 | third-party archives. 180 | 181 | Copyright [yyyy] [name of copyright owner] 182 | 183 | Licensed under the Apache License, Version 2.0 (the "License"); 184 | you may not use this file except in compliance with the License. 185 | You may obtain a copy of the License at 186 | 187 | http://www.apache.org/licenses/LICENSE-2.0 188 | 189 | Unless required by applicable law or agreed to in writing, software 190 | distributed under the License is distributed on an "AS IS" BASIS, 191 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 192 | See the License for the specific language governing permissions and 193 | limitations under the License. 194 | 195 | -------------------------------------------------------------------------------- /core/src/main/scala/magnolia1/macro.scala: -------------------------------------------------------------------------------- 1 | package magnolia1 2 | 3 | import scala.quoted.* 4 | 5 | object Macro: 6 | inline def isObject[T]: Boolean = ${ isObject[T] } 7 | inline def isEnum[T]: Boolean = ${ isEnum[T] } 8 | inline def anns[T]: List[Any] = ${ anns[T] } 9 | inline def inheritedAnns[T]: List[Any] = ${ inheritedAnns[T] } 10 | inline def typeAnns[T]: List[Any] = ${ typeAnns[T] } 11 | inline def paramAnns[T]: List[(String, List[Any])] = ${ paramAnns[T] } 12 | inline def inheritedParamAnns[T]: List[(String, List[Any])] = ${ 13 | inheritedParamAnns[T] 14 | } 15 | inline def isValueClass[T]: Boolean = ${ isValueClass[T] } 16 | inline def defaultValue[T]: List[(String, Option[() => Any])] = ${ 17 | defaultValue[T] 18 | } 19 | inline def paramTypeAnns[T]: List[(String, List[Any])] = ${ paramTypeAnns[T] } 20 | inline def repeated[T]: List[(String, Boolean)] = ${ repeated[T] } 21 | inline def typeInfo[T]: TypeInfo = ${ typeInfo[T] } 22 | 23 | def isObject[T: Type](using Quotes): Expr[Boolean] = 24 | import quotes.reflect.* 25 | 26 | Expr(TypeRepr.of[T].typeSymbol.flags.is(Flags.Module)) 27 | 28 | def isEnum[T: Type](using Quotes): Expr[Boolean] = 29 | import quotes.reflect.* 30 | 31 | Expr(TypeRepr.of[T].typeSymbol.flags.is(Flags.Enum)) 32 | 33 | def anns[T: Type](using Quotes): Expr[List[Any]] = 34 | new CollectAnnotations[T].anns 35 | 36 | def inheritedAnns[T: Type](using Quotes): Expr[List[Any]] = 37 | new CollectAnnotations[T].inheritedAnns 38 | 39 | def typeAnns[T: Type](using Quotes): Expr[List[Any]] = 40 | new CollectAnnotations[T].typeAnns 41 | 42 | def paramAnns[T: Type](using Quotes): Expr[List[(String, List[Any])]] = 43 | new CollectAnnotations[T].paramAnns 44 | 45 | def inheritedParamAnns[T: Type](using 46 | Quotes 47 | ): Expr[List[(String, List[Any])]] = 48 | new CollectAnnotations[T].inheritedParamAnns 49 | 50 | def isValueClass[T: Type](using Quotes): Expr[Boolean] = 51 | import quotes.reflect.* 52 | 53 | Expr( 54 | TypeRepr.of[T].baseClasses.contains(Symbol.classSymbol("scala.AnyVal")) 55 | ) 56 | 57 | def defaultValue[T: Type](using 58 | Quotes 59 | ): Expr[List[(String, Option[() => Any])]] = 60 | import quotes.reflect._ 61 | def exprOfOption( 62 | oet: (Expr[String], Option[Expr[Any]]) 63 | ): Expr[(String, Option[() => Any])] = oet match { 64 | case (label, None) => Expr(label.valueOrAbort -> None) 65 | case (label, Some(et)) => '{ $label -> Some(() => $et) } 66 | } 67 | val tpe = TypeRepr.of[T].typeSymbol 68 | val terms = tpe.primaryConstructor.paramSymss.flatten 69 | .filter(_.isValDef) 70 | .zipWithIndex 71 | .map { case (field, i) => 72 | exprOfOption { 73 | val defaultMethodName = s"$$lessinit$$greater$$default$$${i + 1}" 74 | Expr(field.name) -> tpe.companionClass 75 | .declaredMethod(defaultMethodName) 76 | .headOption 77 | .map { defaultMethod => 78 | val callDefault = { 79 | val base = Ident(tpe.companionModule.termRef).select(defaultMethod) 80 | val tParams = defaultMethod.paramSymss.headOption.filter(_.forall(_.isType)) 81 | tParams match 82 | case Some(tParams) => TypeApply(base, tParams.map(TypeTree.ref)) 83 | case _ => base 84 | } 85 | 86 | defaultMethod.tree match { 87 | case tree: DefDef => tree.rhs.getOrElse(callDefault) 88 | case _ => callDefault 89 | } 90 | } 91 | .map(_.asExprOf[Any]) 92 | } 93 | } 94 | Expr.ofList(terms) 95 | 96 | def paramTypeAnns[T: Type](using Quotes): Expr[List[(String, List[Any])]] = 97 | import quotes.reflect._ 98 | 99 | def getAnnotations(t: TypeRepr): List[Term] = t match 100 | case AnnotatedType(inner, ann) => ann :: getAnnotations(inner) 101 | case _ => Nil 102 | 103 | Expr.ofList { 104 | val typeRepr = TypeRepr.of[T] 105 | typeRepr.typeSymbol.caseFields 106 | .map { field => 107 | val tpeRepr = typeRepr.memberType(field) 108 | 109 | Expr(field.name) -> getAnnotations(tpeRepr) 110 | .filter { a => 111 | a.tpe.typeSymbol.maybeOwner.isNoSymbol || 112 | a.tpe.typeSymbol.owner.fullName != "scala.annotation.internal" 113 | } 114 | .map(_.asExpr.asInstanceOf[Expr[Any]]) 115 | } 116 | .filter(_._2.nonEmpty) 117 | .map { (name, annots) => Expr.ofTuple(name, Expr.ofList(annots)) } 118 | } 119 | 120 | def repeated[T: Type](using Quotes): Expr[List[(String, Boolean)]] = 121 | import quotes.reflect.* 122 | 123 | val tpe = TypeRepr.of[T] 124 | val areRepeated = 125 | if tpe.typeSymbol.isNoSymbol then Nil 126 | else { 127 | val symbol = tpe.typeSymbol 128 | val ctor = symbol.primaryConstructor 129 | for param <- ctor.paramSymss.flatten 130 | yield 131 | val isRepeated = tpe.memberType(param) match { 132 | case AnnotatedType(_, annot) => annot.tpe.typeSymbol == defn.RepeatedAnnot 133 | case _ => false 134 | } 135 | param.name -> isRepeated 136 | } 137 | 138 | Expr(areRepeated) 139 | 140 | def typeInfo[T: Type](using Quotes): Expr[TypeInfo] = 141 | import quotes.reflect._ 142 | 143 | def normalizedName(s: Symbol): String = 144 | if s.flags.is(Flags.Module) then s.name.stripSuffix("$") else s.name 145 | def name(tpe: TypeRepr): Expr[String] = tpe.dealias match 146 | case matchedTpe @ TermRef(typeRepr, name) if matchedTpe.typeSymbol.flags.is(Flags.Module) => 147 | Expr(name.stripSuffix("$")) 148 | case TermRef(typeRepr, name) => Expr(name) 149 | case matchedTpe => Expr(normalizedName(matchedTpe.typeSymbol)) 150 | 151 | def ownerNameChain(sym: Symbol): List[String] = 152 | if sym.isNoSymbol then List.empty 153 | else if sym == defn.EmptyPackageClass then List.empty 154 | else if sym == defn.RootPackage then List.empty 155 | else if sym == defn.RootClass then List.empty 156 | else ownerNameChain(sym.owner) :+ normalizedName(sym) 157 | 158 | def owner(tpe: TypeRepr): Expr[String] = Expr( 159 | ownerNameChain(tpe.dealias.typeSymbol.maybeOwner).mkString(".") 160 | ) 161 | 162 | def typeInfo(tpe: TypeRepr): Expr[TypeInfo] = tpe match 163 | case AppliedType(tpe, args) => 164 | '{ 165 | TypeInfo( 166 | ${ owner(tpe) }, 167 | ${ name(tpe) }, 168 | ${ Expr.ofList(args.map(typeInfo)) } 169 | ) 170 | } 171 | case _ => 172 | '{ TypeInfo(${ owner(tpe) }, ${ name(tpe) }, Nil) } 173 | 174 | typeInfo(TypeRepr.of[T]) 175 | 176 | private class CollectAnnotations[T: Type](using val quotes: Quotes) { 177 | import quotes.reflect.* 178 | 179 | private val tpe: TypeRepr = TypeRepr.of[T] 180 | 181 | def anns: Expr[List[scala.annotation.Annotation]] = 182 | Expr.ofList { 183 | tpe.typeSymbol.annotations 184 | .filter(filterAnnotation) 185 | .map(_.asExpr.asInstanceOf[Expr[scala.annotation.Annotation]]) 186 | } 187 | 188 | def inheritedAnns: Expr[List[scala.annotation.Annotation]] = 189 | Expr.ofList { 190 | tpe.baseClasses 191 | .filterNot(isObjectOrScala) 192 | .collect { 193 | case s if s != tpe.typeSymbol => s.annotations 194 | } // skip self 195 | .flatten 196 | .filter(filterAnnotation) 197 | .map(_.asExpr.asInstanceOf[Expr[scala.annotation.Annotation]]) 198 | } 199 | 200 | def typeAnns: Expr[List[scala.annotation.Annotation]] = { 201 | 202 | def getAnnotations(t: TypeRepr): List[Term] = t match 203 | case AnnotatedType(inner, ann) => ann :: getAnnotations(inner) 204 | case _ => Nil 205 | 206 | val symbol: Option[Symbol] = 207 | if tpe.typeSymbol.isNoSymbol then None else Some(tpe.typeSymbol) 208 | Expr.ofList { 209 | symbol.toList.map(_.tree).flatMap { 210 | case ClassDef(_, _, parents, _, _) => 211 | parents 212 | .collect { case t: TypeTree => t.tpe } 213 | .flatMap(getAnnotations) 214 | .filter(filterAnnotation) 215 | .map(_.asExpr.asInstanceOf[Expr[scala.annotation.Annotation]]) 216 | case _ => 217 | // Best effort in case whe -Yretain-trees is not used 218 | // Does not support class parent annotations (in the extends clouse) 219 | tpe.baseClasses 220 | .map(tpe.baseType(_)) 221 | .flatMap(getAnnotations(_)) 222 | .filter(filterAnnotation) 223 | .map(_.asExprOf[scala.annotation.Annotation]) 224 | } 225 | } 226 | } 227 | 228 | def paramAnns: Expr[List[(String, List[scala.annotation.Annotation])]] = 229 | Expr.ofList { 230 | groupByParamName { 231 | (fromConstructor(tpe.typeSymbol) ++ fromDeclarations(tpe.typeSymbol)) 232 | .filter { case (_, anns) => anns.nonEmpty } 233 | } 234 | } 235 | 236 | def inheritedParamAnns: Expr[List[(String, List[scala.annotation.Annotation])]] = 237 | Expr.ofList { 238 | groupByParamName { 239 | tpe.baseClasses 240 | .filterNot(isObjectOrScala) 241 | .collect { 242 | case s if s != tpe.typeSymbol => 243 | (fromConstructor(s)).filter { case (_, anns) => 244 | anns.nonEmpty 245 | } 246 | } 247 | .flatten ++ fromDeclarations(tpe.typeSymbol, inherited = true) 248 | } 249 | } 250 | 251 | private def fromConstructor(from: Symbol): List[(String, List[Expr[scala.annotation.Annotation]])] = 252 | from.primaryConstructor.paramSymss.flatten.map { field => 253 | field.name -> field.annotations 254 | .filter(filterAnnotation) 255 | .map(_.asExpr.asInstanceOf[Expr[scala.annotation.Annotation]]) 256 | } 257 | 258 | private def fromDeclarations( 259 | from: Symbol, 260 | inherited: Boolean = false 261 | ): List[(String, List[Expr[scala.annotation.Annotation]])] = 262 | from.fieldMembers.collect { case field: Symbol => 263 | val annotations = if (!inherited) field.annotations else field.allOverriddenSymbols.flatMap(_.annotations).toList 264 | field.name -> annotations 265 | .filter(filterAnnotation) 266 | .map(_.asExpr.asInstanceOf[Expr[scala.annotation.Annotation]]) 267 | } 268 | 269 | private def groupByParamName(anns: List[(String, List[Expr[scala.annotation.Annotation]])]) = 270 | anns 271 | .groupBy { case (name, _) => name } 272 | .toList 273 | .map { case (name, l) => name -> l.flatMap(_._2) } 274 | .map { (name, anns) => Expr.ofTuple(Expr(name), Expr.ofList(anns)) } 275 | 276 | private def isObjectOrScala(bc: Symbol) = 277 | bc.name.contains("java.lang.Object") || bc.fullName.startsWith("scala.") 278 | 279 | private def filterAnnotation(a: Term): Boolean = 280 | scala.util.Try(a.tpe <:< TypeRepr.of[scala.annotation.Annotation]).toOption.contains(true) && 281 | (a.tpe.typeSymbol.maybeOwner.isNoSymbol || 282 | (a.tpe.typeSymbol.owner.fullName != "scala.annotation.internal" && 283 | a.tpe.typeSymbol.owner.fullName != "jdk.internal")) 284 | } 285 | -------------------------------------------------------------------------------- /test/src/test/scala/magnolia1/tests/ProductsTests.scala: -------------------------------------------------------------------------------- 1 | package magnolia1.tests 2 | 3 | import magnolia1.* 4 | import magnolia1.examples.* 5 | import java.time.LocalDate 6 | 7 | class ProductsTests extends munit.FunSuite: 8 | import ProductsTests.* 9 | 10 | test("serialize a case object") { 11 | val res = summon[Show[String, JustCaseObject.type]].show(JustCaseObject) 12 | assertEquals(res, "JustCaseObject()") 13 | } 14 | 15 | test("serialize a case class") { 16 | val res = summon[Show[String, JustCaseClass]].show( 17 | (JustCaseClass(42, "Hello World", true)) 18 | ) 19 | assertEquals(res, "JustCaseClass(int=42,string=Hello World,boolean=true)") 20 | } 21 | 22 | test("construct a Show product instance") { 23 | val res = Show.derived[Person].show(Person("John Smith", 34)) 24 | assertEquals(res, """Person(name=John Smith,age=34)""") 25 | } 26 | 27 | test("serialize a tuple") { 28 | val res = summon[Show[String, (Int, String)]].show((42, "Hello World")) 29 | assertEquals(res, "Tuple2[Int,String](_1=42,_2=Hello World)") 30 | } 31 | 32 | test("serialize case object within custom ADT") { 33 | val res = summon[Show[String, Red.type]].show(Red) 34 | assertEquals(res, "Red()") 35 | } 36 | 37 | test("construct a Show product instance with alternative apply functions") { 38 | val res = Show.derived[TestEntry].show(TestEntry("a", "b")) 39 | assertEquals(res, """TestEntry(param=Param(a=a,b=b))""") 40 | } 41 | 42 | test("decode a company") { 43 | val res = Decoder.derived[Company].decode("""Company(name=Acme Inc)""") 44 | assertEquals(res, Company("Acme Inc")) 45 | } 46 | 47 | test("test equality false") { 48 | val res = Eq.derived[Entity].equal(Person("John Smith", 34), Person("", 0)) 49 | assert(!res) 50 | } 51 | 52 | test("test equality true") { 53 | val res = Eq 54 | .derived[Entity] 55 | .equal(Person("John Smith", 34), Person("John Smith", 34)) 56 | assert(res) 57 | } 58 | 59 | test("decode a product nested in objects") { 60 | import Obj1.Obj2.* 61 | val res = summon[Decoder[NestedInObjects]].decode( 62 | """magnolia1.tests.Obj1.Obj2.NestedInObjects(i=42)""" 63 | ) 64 | assertEquals(res, NestedInObjects(42)) 65 | } 66 | 67 | test("decode a nested product") { 68 | val res = summon[Decoder[Address]].decode( 69 | """Address(line1=53 High Street,occupant=Person(name=Richard Jones,age=44))""" 70 | ) 71 | assertEquals(res, Address("53 High Street", Person("Richard Jones", 44))) 72 | } 73 | 74 | test("typenames and labels are not encoded") { 75 | val res = summon[Show[String, `%%`]].show(`%%`(1, "two")) 76 | assertEquals(res, "%%(/=1,#=two)") 77 | } 78 | 79 | test("very long") { 80 | val vl = 81 | // format: off 82 | VeryLong( 83 | "p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", "p10", 84 | "p11", "p12", "p13", "p14", "p15", "p16", "p17", "p18", "p19", "p20", 85 | "p21", "p22", "p23", "p24", "p25", "p26", "p27", "p28", "p29", "p30", 86 | "p31", "p32", "p33", "p34", "p35", "p36", "p37", "p38", "p39", "p40", 87 | "p41", "p42", "p43", "p44", "p45", "p46", "p47", "p48", "p49", "p50", 88 | "p51", "p52", "p53", "p54", "p55", "p56", "p57", "p58", "p59", "p60", 89 | "p61", "p62", "p63", "p64", "p65", "p66", "p67", "p68", "p69", "p70", 90 | "p71", "p72", "p73", "p74", "p75", "p76", "p77", "p78", "p79", "p80", 91 | "p81", "p82", "p83", "p84", "p85", "p86", "p87", "p88", "p89", "p90", 92 | "p91", "p92", "p93", "p94", "p95", "p96", "p97", "p98", "p99", "p100", 93 | "p101", "p102", "p103", "p104", "p105", "p106", "p107", "p108", "p109", "p110", 94 | "p111", "p112", "p113", "p114", "p115", "p116", "p117", "p118", "p119", "p120", 95 | "p121", "p122", "p123", "p124", "p125", "p126", "p127", "p128", "p129", "p130", 96 | "p131", "p132", "p133", "p134", "p135", "p136", "p137", "p138", "p139", "p140", 97 | "p141", "p142", "p143", "p144", "p145", "p146", "p147", "p148", "p149", "p150", 98 | "p151", "p152", "p153", "p154", "p155", "p156", "p157", "p158", "p159", "p160", 99 | "p161", "p162", "p163", "p164", "p165", "p166", "p167", "p168", "p169", "p170", 100 | "p171", "p172", "p173", "p174", "p175", "p176", "p177", "p178", "p179", "p180", 101 | "p181", "p182", "p183", "p184", "p185", "p186", "p187", "p188", "p189", "p190", 102 | "p191", "p192", "p193", "p194", "p195", "p196", "p197", "p198", "p199", "p200", 103 | "p201", "p202", "p203", "p204", "p205", "p206", "p207", "p208", "p209", "p210", 104 | "p211", "p212", "p213", "p214", "p215", "p216", "p217", "p218", "p219", "p220", 105 | "p221", "p222", "p223", "p224", "p225", "p226", "p227", "p228", "p229", "p230", 106 | "p231", "p232", "p233", "p234", "p235", "p236", "p237", "p238", "p239", "p240", 107 | "p241", "p242", "p243", "p244", "p245", "p246", "p247", "p248", "p249", "p250", 108 | "p251", "p252", "p253", "p254" 109 | ) 110 | // format: on 111 | 112 | val res = Eq.derived[VeryLong].equal(vl, vl) 113 | assert(res) 114 | } 115 | 116 | test("show an Account") { 117 | val res = Show 118 | .derived[Account] 119 | .show(Account("john_doe", "john.doe@yahoo.com", "john.doe@gmail.com")) 120 | assertEquals( 121 | res, 122 | "Account(id=john_doe,emails=[john.doe@yahoo.com,john.doe@gmail.com])" 123 | ) 124 | } 125 | 126 | test("construct a default Account") { 127 | val res = HasDefault.derived[Account].defaultValue 128 | assertEquals(res, Right(Account(""))) 129 | } 130 | 131 | test("should print repeated") { 132 | val res = 133 | PrintRepeated.derived[Account].print(Account("id", "email1", "email2")) 134 | assertEquals(res, "List(emails)") 135 | } 136 | 137 | test("show underivable type with fallback") { 138 | val res = summon[TypeNameInfo[NotDerivable]].name 139 | assertEquals(res, TypeInfo("", "Unknown Type", Seq.empty)) 140 | } 141 | 142 | test("show a Portfolio of Companies") { 143 | val res = Show 144 | .derived[Portfolio] 145 | .show(Portfolio(Company("Alice Inc"), Company("Bob & Co"))) 146 | assertEquals( 147 | res, 148 | "Portfolio(companies=[Company(name=Alice Inc),Company(name=Bob & Co)])" 149 | ) 150 | } 151 | 152 | test("allow no-coproduct derivation definitions") { 153 | val error = compileErrors("WeakHash.derived[Person]") 154 | assert(error.isEmpty) 155 | } 156 | 157 | test("assume full auto derivation of external products") { 158 | case class Input(value: String) 159 | case class LoggingConfig(input: Input) 160 | object LoggingConfig: 161 | given SemiDefault[LoggingConfig] = SemiDefault.derived 162 | 163 | val res = summon[SemiDefault[LoggingConfig]].default 164 | assertEquals(res, LoggingConfig(Input(""))) 165 | 166 | } 167 | 168 | // TODO - not working as expected: showing "T" type instead of Int 169 | // test("show a list of ints") { 170 | // given [T: [X] =>> Show[String, X]]: Show[String, List[T]] = Show.derived 171 | // val res = Show.derived[List[Int]].show(List(1, 2, 3)) 172 | 173 | // assertEquals( 174 | // res, 175 | // "::[Int](head=1,next$access$1=::[Int](head=2,next$access$1=::[Int](head=3,next$access$1=Nil())))" 176 | // ) 177 | // } 178 | 179 | test("case class typeName should be complete and unchanged") { 180 | given stringTypeName: TypeNameInfo[String] with { 181 | def name = ??? 182 | 183 | def subtypeNames = ??? 184 | } 185 | val res = TypeNameInfo.derived[Fruit].name 186 | assertEquals(res.full, "magnolia1.tests.ProductsTests.Fruit") 187 | } 188 | 189 | test("case class parameter typeName should be dealiased") { 190 | given stringTypeName: TypeNameInfo[String] with { 191 | def name = ??? 192 | 193 | def subtypeNames = ??? 194 | } 195 | val res1 = TypeNameInfo.derived[Parameterized[Domain1.Type]].name 196 | val res2 = TypeNameInfo.derived[Parameterized[Domain2.Type]].name 197 | assertEquals(res1.typeParams.head.short, "Int") 198 | assertEquals(res2.typeParams.head.short, "String") 199 | } 200 | 201 | test("show chained error stack when leaf instance is missing") { 202 | val error = compileErrors("Show.derived[Schedule]") 203 | assert( 204 | clue(error) contains "No given instance of type magnolia1.examples.Show[String, Seq[magnolia1.tests.ProductsTests.Event]] was found." 205 | ) 206 | } 207 | 208 | test("show chained error stack") { 209 | val error = compileErrors("Show.derived[(Int, Seq[(Double, String)])]") 210 | assert( 211 | clue(error) contains "No given instance of type magnolia1.examples.Show[String, Seq[(Double, String)]] was found." 212 | ) 213 | } 214 | 215 | object ProductsTests: 216 | 217 | class NotDerivable 218 | 219 | case object JustCaseObject 220 | 221 | case class JustCaseClass(int: Int, string: String, boolean: Boolean) 222 | 223 | case class TestEntry(param: Param) 224 | object TestEntry: 225 | def apply(): TestEntry = TestEntry(Param("", "")) 226 | def apply(a: String)(using b: Int): TestEntry = TestEntry( 227 | Param(a, b.toString) 228 | ) 229 | def apply(a: String, b: String): TestEntry = TestEntry(Param(a, b)) 230 | 231 | sealed trait Entity 232 | case class Company(name: String) extends Entity 233 | case class Person(name: String, age: Int) extends Entity 234 | case class Address(line1: String, occupant: Person) 235 | 236 | case class Portfolio(companies: Company*) 237 | 238 | sealed trait Color 239 | case object Red extends Color 240 | case object Green extends Color 241 | case object Blue extends Color 242 | case object Orange extends Color 243 | case object Pink extends Color 244 | 245 | object Obj1: 246 | object Obj2: 247 | case class NestedInObjects(i: Int) 248 | 249 | case class `%%`(`/`: Int, `#`: String) 250 | 251 | // format: off 252 | case class VeryLong( 253 | p1: String, p2: String, p3: String, p4: String, p5: String, p6: String, p7: String, p8: String, p9: String, p10: String, 254 | p11: String, p12: String, p13: String, p14: String, p15: String, p16: String, p17: String, p18: String, p19: String, p20: String, 255 | p21: String, p22: String, p23: String, p24: String, p25: String, p26: String, p27: String, p28: String, p29: String, p30: String, 256 | p31: String, p32: String, p33: String, p34: String, p35: String, p36: String, p37: String, p38: String, p39: String, p40: String, 257 | p41: String, p42: String, p43: String, p44: String, p45: String, p46: String, p47: String, p48: String, p49: String, p50: String, 258 | p51: String, p52: String, p53: String, p54: String, p55: String, p56: String, p57: String, p58: String, p59: String, p60: String, 259 | p61: String, p62: String, p63: String, p64: String, p65: String, p66: String, p67: String, p68: String, p69: String, p70: String, 260 | p71: String, p72: String, p73: String, p74: String, p75: String, p76: String, p77: String, p78: String, p79: String, p80: String, 261 | p81: String, p82: String, p83: String, p84: String, p85: String, p86: String, p87: String, p88: String, p89: String, p90: String, 262 | p91: String, p92: String, p93: String, p94: String, p95: String, p96: String, p97: String, p98: String, p99: String, p100: String, 263 | p101: String, p102: String, p103: String, p104: String, p105: String, p106: String, p107: String, p108: String, p109: String, p110: String, 264 | p111: String, p112: String, p113: String, p114: String, p115: String, p116: String, p117: String, p118: String, p119: String, p120: String, 265 | p121: String, p122: String, p123: String, p124: String, p125: String, p126: String, p127: String, p128: String, p129: String, p130: String, 266 | p131: String, p132: String, p133: String, p134: String, p135: String, p136: String, p137: String, p138: String, p139: String, p140: String, 267 | p141: String, p142: String, p143: String, p144: String, p145: String, p146: String, p147: String, p148: String, p149: String, p150: String, 268 | p151: String, p152: String, p153: String, p154: String, p155: String, p156: String, p157: String, p158: String, p159: String, p160: String, 269 | p161: String, p162: String, p163: String, p164: String, p165: String, p166: String, p167: String, p168: String, p169: String, p170: String, 270 | p171: String, p172: String, p173: String, p174: String, p175: String, p176: String, p177: String, p178: String, p179: String, p180: String, 271 | p181: String, p182: String, p183: String, p184: String, p185: String, p186: String, p187: String, p188: String, p189: String, p190: String, 272 | p191: String, p192: String, p193: String, p194: String, p195: String, p196: String, p197: String, p198: String, p199: String, p200: String, 273 | p201: String, p202: String, p203: String, p204: String, p205: String, p206: String, p207: String, p208: String, p209: String, p210: String, 274 | p211: String, p212: String, p213: String, p214: String, p215: String, p216: String, p217: String, p218: String, p219: String, p220: String, 275 | p221: String, p222: String, p223: String, p224: String, p225: String, p226: String, p227: String, p228: String, p229: String, p230: String, 276 | p231: String, p232: String, p233: String, p234: String, p235: String, p236: String, p237: String, p238: String, p239: String, p240: String, 277 | p241: String, p242: String, p243: String, p244: String, p245: String, p246: String, p247: String, p248: String, p249: String, p250: String, 278 | p251: String, p252: String, p253: String, p254: String 279 | ) 280 | // format: on 281 | 282 | case class Account(id: String, emails: String*) 283 | 284 | @SerialVersionUID(42) case class Schedule(events: Seq[Event]) 285 | 286 | case class Event(date: LocalDate) 287 | 288 | case class Param(a: String, b: String) 289 | 290 | case class Fruit(name: String) 291 | 292 | case class Parameterized[T](t: T) 293 | 294 | object Domain1: 295 | type Type = Int 296 | 297 | object Domain2: 298 | type Type = String 299 | 300 | object Fruit: 301 | given showFruit: Show[String, Fruit] = (f: Fruit) => f.name 302 | -------------------------------------------------------------------------------- /core/src/main/scala/magnolia1/interface.scala: -------------------------------------------------------------------------------- 1 | package magnolia1 2 | 3 | import magnolia1.CaseClass.getDefaultEvaluatorFromDefaultVal 4 | 5 | import scala.annotation.tailrec 6 | import scala.collection.immutable.ArraySeq 7 | import scala.reflect.* 8 | 9 | case class TypeInfo( 10 | owner: String, 11 | short: String, 12 | typeParams: Iterable[TypeInfo] 13 | ): 14 | def full: String = s"$owner.$short" 15 | 16 | object CaseClass: 17 | trait Param[Typeclass[_], Type]( 18 | val label: String, 19 | val index: Int, 20 | val repeated: Boolean, 21 | val annotations: IArray[Any], 22 | val typeAnnotations: IArray[Any] 23 | ) extends Serializable: 24 | 25 | type PType 26 | 27 | /** Gives the constructed typeclass for the parameter's type. Eg for a `case class Foo(bar: String, baz: Int)`, where this [[Param]] 28 | * denotes 'baz', the `typeclass` field returns an instance of `Typeclass[Int]`. 29 | */ 30 | def typeclass: Typeclass[PType] 31 | 32 | /** Get the value of this param out of the supplied instance of the case class. 33 | * 34 | * @param value 35 | * an instance of the case class 36 | * @return 37 | * the value of this parameter in the case class 38 | */ 39 | def deref(param: Type): PType 40 | 41 | /** Recommended compilation with `-Yretain-trees` on. 42 | * @return 43 | * default argument value, if any 44 | */ 45 | def default: Option[PType] 46 | 47 | /** provides a function to evaluate the default value for this parameter, as defined in the case class constructor */ 48 | def evaluateDefault: Option[() => PType] = None 49 | def inheritedAnnotations: IArray[Any] = IArray.empty[Any] 50 | override def toString: String = s"Param($label)" 51 | 52 | object Param: 53 | def apply[F[_], T, P]( 54 | name: String, 55 | idx: Int, 56 | repeated: Boolean, 57 | cbn: CallByNeed[F[P]], 58 | defaultVal: CallByNeed[Option[P]], 59 | annotations: IArray[Any], 60 | inheritedAnns: IArray[Any], 61 | typeAnnotations: IArray[Any] 62 | ): Param[F, T] = 63 | new CaseClass.Param[F, T]( 64 | name, 65 | idx, 66 | repeated, 67 | annotations, 68 | typeAnnotations 69 | ): 70 | type PType = P 71 | def default: Option[PType] = defaultVal.value 72 | override def evaluateDefault: Option[() => PType] = CaseClass.getDefaultEvaluatorFromDefaultVal(defaultVal) 73 | def typeclass = cbn.value 74 | override def inheritedAnnotations = inheritedAnns 75 | def deref(value: T): P = 76 | value.asInstanceOf[Product].productElement(idx).asInstanceOf[P] 77 | 78 | // for backward compatibility with v1.0.0 79 | def apply[F[_], T, P]( 80 | name: String, 81 | idx: Int, 82 | repeated: Boolean, 83 | cbn: CallByNeed[F[P]], 84 | defaultVal: CallByNeed[Option[P]], 85 | annotations: IArray[Any], 86 | typeAnnotations: IArray[Any] 87 | ): Param[F, T] = 88 | new CaseClass.Param[F, T]( 89 | name, 90 | idx, 91 | repeated, 92 | annotations, 93 | typeAnnotations 94 | ): 95 | type PType = P 96 | def default: Option[PType] = defaultVal.value 97 | override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) 98 | def typeclass = cbn.value 99 | def deref(value: T): P = 100 | value.asInstanceOf[Product].productElement(idx).asInstanceOf[P] 101 | end Param 102 | 103 | private def getDefaultEvaluatorFromDefaultVal[P](defaultVal: CallByNeed[Option[P]]): Option[() => P] = 104 | defaultVal.valueEvaluator.flatMap { evaluator => 105 | evaluator().fold[Option[() => P]](None) { _ => 106 | Some(() => evaluator().get) 107 | } 108 | } 109 | end CaseClass 110 | 111 | /** In the terminology of Algebraic Data Types (ADTs), case classes are known as 'product types'. 112 | * 113 | * @param parameters 114 | * an array giving information about the parameters of the case class. Each [[Param]] element has a very useful 115 | * [[CaseClass.Param.typeclass]] field giving the constructed typeclass for the parameter's type. Eg for a `case class Foo(bar: String, 116 | * baz: Int)`, you can obtain `Typeclass[String]`, `Typeclass[Int]`. 117 | */ 118 | abstract class CaseClass[Typeclass[_], Type]( 119 | val typeInfo: TypeInfo, 120 | val isObject: Boolean, 121 | val isValueClass: Boolean, 122 | val parameters: IArray[CaseClass.Param[Typeclass, Type]], 123 | val annotations: IArray[Any], 124 | val inheritedAnnotations: IArray[Any] = IArray.empty[Any], 125 | val typeAnnotations: IArray[Any] 126 | ) extends Serializable: 127 | 128 | // for backward compatibility with v1.0.0 129 | def this( 130 | typeInfo: TypeInfo, 131 | isObject: Boolean, 132 | isValueClass: Boolean, 133 | parameters: IArray[CaseClass.Param[Typeclass, Type]], 134 | annotations: IArray[Any], 135 | typeAnnotations: IArray[Any] 136 | ) = this( 137 | typeInfo, 138 | isObject, 139 | isValueClass, 140 | parameters, 141 | annotations, 142 | IArray.empty[Any], 143 | typeAnnotations 144 | ) 145 | 146 | def params: IArray[CaseClass.Param[Typeclass, Type]] = parameters 147 | 148 | type Param = CaseClass.Param[Typeclass, Type] 149 | 150 | override def toString: String = 151 | s"CaseClass(${typeInfo.full}, ${parameters.mkString(",")})" 152 | def construct[PType](makeParam: Param => PType)(using ClassTag[PType]): Type 153 | def constructMonadic[Monad[_]: Monadic, PType: ClassTag]( 154 | make: Param => Monad[PType] 155 | ): Monad[Type] 156 | def constructEither[Err, PType: ClassTag]( 157 | makeParam: Param => Either[Err, PType] 158 | ): Either[List[Err], Type] 159 | def rawConstruct(fieldValues: Seq[Any]): Type 160 | def rawConstruct(fieldValues: Array[Any]): Type = rawConstruct(ArraySeq.unsafeWrapArray(fieldValues)) 161 | 162 | def param[P]( 163 | name: String, 164 | idx: Int, 165 | repeated: Boolean, 166 | cbn: CallByNeed[Typeclass[P]], 167 | defaultVal: CallByNeed[Option[P]], 168 | annotations: IArray[Any], 169 | inheritedAnns: IArray[Any], 170 | typeAnnotations: IArray[Any] 171 | ): Param = 172 | new CaseClass.Param[Typeclass, Type]( 173 | name, 174 | idx, 175 | repeated, 176 | annotations, 177 | typeAnnotations 178 | ): 179 | type PType = P 180 | def default: Option[PType] = defaultVal.value 181 | override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) 182 | def typeclass = cbn.value 183 | override def inheritedAnnotations = inheritedAnns 184 | def deref(value: Type): P = 185 | value.asInstanceOf[Product].productElement(idx).asInstanceOf[P] 186 | 187 | // for backward compatibility with v1.0.0 188 | def param[P]( 189 | name: String, 190 | idx: Int, 191 | repeated: Boolean, 192 | cbn: CallByNeed[Typeclass[P]], 193 | defaultVal: CallByNeed[Option[P]], 194 | annotations: IArray[Any], 195 | typeAnnotations: IArray[Any] 196 | ): Param = param( 197 | name, 198 | idx, 199 | repeated, 200 | cbn, 201 | defaultVal, 202 | annotations, 203 | IArray.empty[Any], 204 | typeAnnotations 205 | ) 206 | 207 | end CaseClass 208 | 209 | /** Represents a Sealed-Trait or a Scala 3 Enum. 210 | * 211 | * In the terminology of Algebraic Data Types (ADTs), sealed-traits/enums are termed 'sum types'. 212 | */ 213 | case class SealedTrait[Typeclass[_], Type]( 214 | typeInfo: TypeInfo, 215 | subtypes: IArray[SealedTrait.Subtype[Typeclass, Type, _]], 216 | annotations: IArray[Any], 217 | typeAnnotations: IArray[Any], 218 | isEnum: Boolean, 219 | inheritedAnnotations: IArray[Any] 220 | ) extends Serializable: 221 | 222 | // for backward compatibility with v1.0.0 223 | def this( 224 | typeInfo: TypeInfo, 225 | subtypes: IArray[SealedTrait.Subtype[Typeclass, Type, _]], 226 | annotations: IArray[Any], 227 | typeAnnotations: IArray[Any], 228 | isEnum: Boolean 229 | ) = this( 230 | typeInfo, 231 | subtypes, 232 | annotations, 233 | typeAnnotations, 234 | isEnum, 235 | IArray.empty[Any] 236 | ) 237 | 238 | // for backward compatibility with v1.0.0 239 | def copy( 240 | typeInfo: TypeInfo, 241 | subtypes: IArray[SealedTrait.Subtype[Typeclass, Type, _]], 242 | annotations: IArray[Any], 243 | typeAnnotations: IArray[Any], 244 | isEnum: Boolean 245 | ): SealedTrait[Typeclass, Type] = this.copy( 246 | typeInfo, 247 | subtypes, 248 | annotations, 249 | typeAnnotations, 250 | isEnum, 251 | this.inheritedAnnotations 252 | ) 253 | 254 | type Subtype[S] = SealedTrait.SubtypeValue[Typeclass, Type, S] 255 | 256 | override def toString: String = 257 | s"SealedTrait($typeInfo, IArray[${subtypes.mkString(",")}])" 258 | 259 | /** Provides a way to recieve the type info for the explicit subtype that 'value' is an instance of. So if 'Type' is a Sealed Trait or 260 | * Scala 3 Enum like 'Suit', the 'handle' function will be supplied with the type info for the specific subtype of 'value', eg 261 | * 'Diamonds'. 262 | * 263 | * @param value 264 | * must be instance of a subtype of typeInfo 265 | * @param handle 266 | * function that will be passed the Subtype of 'value' 267 | * @tparam Return 268 | * whatever type the 'handle' function wants to return 269 | * @return 270 | * whatever the 'handle' function returned! 271 | */ 272 | def choose[Return](value: Type)(handle: Subtype[_] => Return): Return = 273 | @tailrec def rec(ix: Int): Return = 274 | if ix < subtypes.length then 275 | val sub = subtypes(ix) 276 | if sub.cast.isDefinedAt(value) then handle(SealedTrait.SubtypeValue(sub, value)) 277 | else rec(ix + 1) 278 | else 279 | throw new IllegalArgumentException( 280 | s"The given value `$value` is not a sub type of `$typeInfo`" 281 | ) 282 | 283 | rec(0) 284 | 285 | end SealedTrait 286 | 287 | object SealedTrait: 288 | 289 | // for backward compatibility with v1.0.0 290 | def apply[Typeclass[_], Type]( 291 | typeInfo: TypeInfo, 292 | subtypes: IArray[SealedTrait.Subtype[Typeclass, Type, _]], 293 | annotations: IArray[Any], 294 | typeAnnotations: IArray[Any], 295 | isEnum: Boolean 296 | ) = new SealedTrait[Typeclass, Type]( 297 | typeInfo, 298 | subtypes, 299 | annotations, 300 | typeAnnotations, 301 | isEnum, 302 | IArray.empty[Any] 303 | ) 304 | 305 | /** @tparam Type 306 | * the type of the Sealed Trait or Scala 3 Enum, eg 'Suit' 307 | * @tparam SType 308 | * the type of the subtype, eg 'Diamonds' or 'Clubs' 309 | */ 310 | class Subtype[Typeclass[_], Type, SType]( 311 | val typeInfo: TypeInfo, 312 | val annotations: IArray[Any], 313 | val inheritedAnnotations: IArray[Any], 314 | val typeAnnotations: IArray[Any], 315 | val isObject: Boolean, 316 | val index: Int, 317 | callByNeed: CallByNeed[Typeclass[SType]], 318 | isType: Type => Boolean, 319 | asType: Type => SType & Type 320 | ) extends PartialFunction[Type, SType & Type], 321 | Serializable: 322 | 323 | // for backward compatibility with v1.0.0 324 | def this( 325 | typeInfo: TypeInfo, 326 | annotations: IArray[Any], 327 | typeAnnotations: IArray[Any], 328 | isObject: Boolean, 329 | index: Int, 330 | callByNeed: CallByNeed[Typeclass[SType]], 331 | isType: Type => Boolean, 332 | asType: Type => SType & Type 333 | ) = this( 334 | typeInfo, 335 | annotations, 336 | IArray.empty[Any], 337 | typeAnnotations, 338 | isObject, 339 | index, 340 | callByNeed, 341 | isType, 342 | asType 343 | ) 344 | 345 | /** @return 346 | * the already-constructed typeclass instance for this subtype 347 | */ 348 | def typeclass: Typeclass[SType & Type] = 349 | callByNeed.value.asInstanceOf[Typeclass[SType & Type]] 350 | def cast: PartialFunction[Type, SType & Type] = this 351 | def isDefinedAt(t: Type): Boolean = isType(t) 352 | def apply(t: Type): SType & Type = asType(t) 353 | override def toString: String = s"Subtype(${typeInfo.full})" 354 | 355 | class SubtypeValue[Typeclass[_], Type, S]( 356 | val subtype: Subtype[Typeclass, Type, S], 357 | v: Type 358 | ): 359 | export subtype.{typeclass, typeAnnotations, annotations, inheritedAnnotations, cast, typeInfo} 360 | def value: S & Type = cast(v) 361 | 362 | end SealedTrait 363 | 364 | object CallByNeed: 365 | /** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only 366 | * happen once. 367 | */ 368 | def createLazy[A](a: () => A): CallByNeed[A] = new CallByNeed(a, () => false) 369 | 370 | /** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only 371 | * happen once. 372 | * 373 | * If by-name parameter causes serialization issue, use [[createLazy]]. 374 | */ 375 | def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => false) 376 | 377 | /** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only 378 | * happen once. Evaluation of a value via `.valueEvaluator.map(evaluator => evaluator())` will happen every time the evaluator is called 379 | */ 380 | def createValueEvaluator[A](a: () => A): CallByNeed[A] = new CallByNeed(a, () => true) 381 | 382 | /** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only 383 | * happen once. Evaluation of a value via `.valueEvaluator.map(evaluator => evaluator())` will happen every time the evaluator is called 384 | * 385 | * If by-name parameter causes serialization issue, use [[withValueEvaluator]]. 386 | */ 387 | def withValueEvaluator[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => true) 388 | end CallByNeed 389 | 390 | // Both params are later nullified to reduce overhead and increase performance. 391 | // The supportDynamicValueEvaluation is passed as a function so that it can be nullified. Otherwise, there is no need for the function value. 392 | final class CallByNeed[+A] private (private[this] var eval: () => A, private var supportDynamicValueEvaluation: () => Boolean) 393 | extends Serializable { 394 | 395 | // This second constructor is necessary to support backwards compatibility for v1.3.6 and earlier 396 | def this(eval: () => A) = this(eval, () => false) 397 | 398 | val valueEvaluator: Option[() => A] = { 399 | val finalRes = if (supportDynamicValueEvaluation()) { 400 | val res = Some(eval) 401 | eval = null 402 | res 403 | } else { 404 | None 405 | } 406 | supportDynamicValueEvaluation = null 407 | finalRes 408 | } 409 | 410 | lazy val value: A = { 411 | if (eval == null) { 412 | valueEvaluator.get.apply() 413 | } else { 414 | val result = eval() 415 | eval = null 416 | result 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /core/src/main/scala/magnolia1/impl.scala: -------------------------------------------------------------------------------- 1 | package magnolia1 2 | 3 | import scala.compiletime.* 4 | import scala.deriving.Mirror 5 | import scala.reflect.* 6 | 7 | import Macro.* 8 | 9 | // scala3 lambda generated during derivation reference outer scope 10 | // This fails the typeclass serialization if the outer scope is not serializable 11 | // workaround with this with a serializable fuction 12 | private trait SerializableFunction0[+R] extends Function0[R] with Serializable: 13 | def apply(): R 14 | private trait SerializableFunction1[-T1, +R] extends Function1[T1, R] with Serializable: 15 | def apply(v1: T1): R 16 | 17 | object CaseClassDerivation: 18 | inline def fromMirror[Typeclass[_], A]( 19 | product: Mirror.ProductOf[A] 20 | ): CaseClass[Typeclass, A] = 21 | val params = IArray( 22 | paramsFromMaps[ 23 | Typeclass, 24 | A, 25 | product.MirroredElemLabels, 26 | product.MirroredElemTypes 27 | ]( 28 | paramAnns[A].to(Map), 29 | inheritedParamAnns[A].to(Map), 30 | paramTypeAnns[A].to(Map), 31 | repeated[A].to(Map), 32 | defaultValue[A].to(Map) 33 | )* 34 | ) 35 | ProductCaseClass( 36 | typeInfo[A], 37 | isObject[A], 38 | isValueClass[A], 39 | params, 40 | IArray(anns[A]*), 41 | IArray(inheritedAnns[A]*), 42 | IArray[Any](typeAnns[A]*), 43 | product 44 | ) 45 | 46 | class ProductCaseClass[Typeclass[_], A]( 47 | typeInfo: TypeInfo, 48 | isObject: Boolean, 49 | isValueClass: Boolean, 50 | parameters: IArray[CaseClass.Param[Typeclass, A]], 51 | annotations: IArray[Any], 52 | inheritedAnnotations: IArray[Any], 53 | typeAnnotations: IArray[Any], 54 | product: Mirror.ProductOf[A] 55 | ) extends CaseClass[Typeclass, A]( 56 | typeInfo, 57 | isObject, 58 | isValueClass, 59 | parameters, 60 | annotations, 61 | inheritedAnnotations, 62 | typeAnnotations 63 | ): 64 | def construct[PType: ClassTag](makeParam: Param => PType): A = 65 | product.fromProduct(Tuple.fromIArray(parameters.map(makeParam))) 66 | 67 | override def rawConstruct(fieldValues: Seq[Any]): A = 68 | rawConstruct(fieldValues.toArray) 69 | 70 | override def rawConstruct(fieldValues: Array[Any]): A = 71 | product.fromProduct(Tuple.fromArray(fieldValues)) 72 | 73 | def constructEither[Err, PType: ClassTag]( 74 | makeParam: Param => Either[Err, PType] 75 | ): Either[List[Err], A] = 76 | parameters 77 | .map(makeParam) 78 | .foldLeft[Either[List[Err], Array[PType]]](Right(Array())) { 79 | case (Left(errs), Left(err)) => Left(errs ++ List(err)) 80 | case (Right(acc), Right(param)) => Right(acc ++ Array(param)) 81 | case (errs @ Left(_), _) => errs 82 | case (_, Left(err)) => Left(List(err)) 83 | } 84 | .map { params => product.fromProduct(Tuple.fromArray(params)) } 85 | 86 | def constructMonadic[M[_]: Monadic, PType: ClassTag]( 87 | makeParam: Param => M[PType] 88 | ): M[A] = { 89 | val m = summon[Monadic[M]] 90 | m.map { 91 | parameters.map(makeParam).foldLeft(m.point(Array())) { (accM, paramM) => 92 | m.flatMap(accM) { acc => 93 | m.map(paramM)(acc ++ List(_)) 94 | } 95 | } 96 | } { params => product.fromProduct(Tuple.fromArray(params)) } 97 | } 98 | 99 | inline def paramsFromMapsStep[Typeclass[_], A, l, p]( 100 | annotations: Map[String, List[Any]], 101 | inheritedAnnotations: Map[String, List[Any]], 102 | typeAnnotations: Map[String, List[Any]], 103 | repeated: Map[String, Boolean], 104 | defaults: Map[String, Option[() => Any]], 105 | idx: Int 106 | ): CaseClass.Param[Typeclass, A] = 107 | val label = constValue[l].asInstanceOf[String] 108 | val tc = new SerializableFunction0[Typeclass[p]]: 109 | override def apply(): Typeclass[p] = summonInline[Typeclass[p]] 110 | val d = 111 | defaults.get(label).flatten match { 112 | case Some(evaluator) => 113 | new SerializableFunction0[Option[p]]: 114 | override def apply(): Option[p] = 115 | val v = evaluator() 116 | if ((v: @unchecked).isInstanceOf[p]) new Some(v).asInstanceOf[Option[p]] 117 | else None 118 | case _ => 119 | returningNone.asInstanceOf[SerializableFunction0[Option[p]]] 120 | } 121 | paramFromMaps[Typeclass, A, p]( 122 | label, 123 | CallByNeed.createLazy(tc), 124 | CallByNeed.createValueEvaluator(d), 125 | repeated, 126 | annotations, 127 | inheritedAnnotations, 128 | typeAnnotations, 129 | idx 130 | ) 131 | 132 | /** This method unrolls recursion by 16, 4, 1 steps to increase maximal size of `Labels` and `Params` tuples due to compiler limitation of 133 | * maximal nested inlines. 134 | */ 135 | inline def paramsFromMaps[Typeclass[_], A, Labels <: Tuple, Params <: Tuple]( 136 | annotations: Map[String, List[Any]], 137 | inheritedAnnotations: Map[String, List[Any]], 138 | typeAnnotations: Map[String, List[Any]], 139 | repeated: Map[String, Boolean], 140 | defaults: Map[String, Option[() => Any]], 141 | idx: Int = 0 142 | ): List[CaseClass.Param[Typeclass, A]] = 143 | inline erasedValue[(Labels, Params)] match 144 | case _: (EmptyTuple, EmptyTuple) => 145 | Nil 146 | case _: ( 147 | (l1 *: l2 *: l3 *: l4 *: l5 *: l6 *: l7 *: l8 *: l9 *: l10 *: l11 *: l12 *: l13 *: l14 *: l15 *: l16 *: ltail), 148 | (p1 *: p2 *: p3 *: p4 *: p5 *: p6 *: p7 *: p8 *: p9 *: p10 *: p11 *: p12 *: p13 *: p14 *: p15 *: p16 *: ptail) 149 | ) => 150 | val s1 = paramsFromMapsStep[Typeclass, A, l1, p1](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx) 151 | val s2 = paramsFromMapsStep[Typeclass, A, l2, p2](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 1) 152 | val s3 = paramsFromMapsStep[Typeclass, A, l3, p3](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 2) 153 | val s4 = paramsFromMapsStep[Typeclass, A, l4, p4](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 3) 154 | val s5 = paramsFromMapsStep[Typeclass, A, l5, p5](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 4) 155 | val s6 = paramsFromMapsStep[Typeclass, A, l6, p6](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 5) 156 | val s7 = paramsFromMapsStep[Typeclass, A, l7, p7](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 6) 157 | val s8 = paramsFromMapsStep[Typeclass, A, l8, p8](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 7) 158 | val s9 = paramsFromMapsStep[Typeclass, A, l9, p9](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 8) 159 | val s10 = 160 | paramsFromMapsStep[Typeclass, A, l10, p10](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 9) 161 | val s11 = 162 | paramsFromMapsStep[Typeclass, A, l11, p11](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 10) 163 | val s12 = 164 | paramsFromMapsStep[Typeclass, A, l12, p12](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 11) 165 | val s13 = 166 | paramsFromMapsStep[Typeclass, A, l13, p13](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 12) 167 | val s14 = 168 | paramsFromMapsStep[Typeclass, A, l14, p14](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 13) 169 | val s15 = 170 | paramsFromMapsStep[Typeclass, A, l15, p15](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 14) 171 | val s16 = 172 | paramsFromMapsStep[Typeclass, A, l16, p16](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 15) 173 | 174 | s1 :: s2 :: s3 :: s4 :: s5 :: s6 :: s7 :: s8 :: s9 :: s10 :: s11 :: s12 :: s13 :: s14 :: s15 :: s16 :: 175 | paramsFromMaps[Typeclass, A, ltail, ptail]( 176 | annotations, 177 | inheritedAnnotations, 178 | typeAnnotations, 179 | repeated, 180 | defaults, 181 | idx + 16 182 | ) 183 | case _: ((l1 *: l2 *: l3 *: l4 *: ltail), (p1 *: p2 *: p3 *: p4 *: ptail)) => 184 | val s1 = paramsFromMapsStep[Typeclass, A, l1, p1](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx) 185 | val s2 = paramsFromMapsStep[Typeclass, A, l2, p2](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 1) 186 | val s3 = paramsFromMapsStep[Typeclass, A, l3, p3](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 2) 187 | val s4 = paramsFromMapsStep[Typeclass, A, l4, p4](annotations, inheritedAnnotations, typeAnnotations, repeated, defaults, idx + 3) 188 | 189 | s1 :: s2 :: s3 :: s4 :: 190 | paramsFromMaps[Typeclass, A, ltail, ptail]( 191 | annotations, 192 | inheritedAnnotations, 193 | typeAnnotations, 194 | repeated, 195 | defaults, 196 | idx + 4 197 | ) 198 | case _: ((l *: ltail), (p *: ptail)) => 199 | paramsFromMapsStep[Typeclass, A, l, p]( 200 | annotations, 201 | inheritedAnnotations, 202 | typeAnnotations, 203 | repeated, 204 | defaults, 205 | idx 206 | ) :: 207 | paramsFromMaps[Typeclass, A, ltail, ptail]( 208 | annotations, 209 | inheritedAnnotations, 210 | typeAnnotations, 211 | repeated, 212 | defaults, 213 | idx + 1 214 | ) 215 | 216 | private def paramFromMaps[Typeclass[_], A, p]( 217 | label: String, 218 | tc: CallByNeed[Typeclass[p]], 219 | d: CallByNeed[Option[p]], 220 | repeated: Map[String, Boolean], 221 | annotations: Map[String, List[Any]], 222 | inheritedAnnotations: Map[String, List[Any]], 223 | typeAnnotations: Map[String, List[Any]], 224 | idx: Int 225 | ): CaseClass.Param[Typeclass, A] = 226 | CaseClass.Param[Typeclass, A, p]( 227 | label, 228 | idx, 229 | repeated.getOrElse(label, false), 230 | tc, 231 | d, 232 | IArray.from(annotations.getOrElse(label, List())), 233 | IArray.from(inheritedAnnotations.getOrElse(label, List())), 234 | IArray.from(typeAnnotations.getOrElse(label, List())) 235 | ) 236 | 237 | private val returningNone = 238 | new SerializableFunction0[Option[Any]]: 239 | override def apply(): Option[Any] = None 240 | 241 | end CaseClassDerivation 242 | 243 | trait SealedTraitDerivation: 244 | type Typeclass[T] 245 | 246 | protected inline def deriveSubtype[s](m: Mirror.Of[s]): Typeclass[s] 247 | 248 | protected inline def sealedTraitFromMirror[A]( 249 | m: Mirror.SumOf[A] 250 | ): SealedTrait[Typeclass, A] = 251 | SealedTrait( 252 | typeInfo[A], 253 | IArray(subtypesFromMirror[A, m.MirroredElemTypes](m)*), 254 | IArray.from(anns[A]), 255 | IArray(paramTypeAnns[A]*), 256 | isEnum[A], 257 | IArray.from(inheritedAnns[A]) 258 | ) 259 | 260 | protected inline def subtypesFromMirrorStep[A, s]( 261 | m: Mirror.SumOf[A], 262 | idx: Int 263 | ): List[SealedTrait.Subtype[Typeclass, A, _]] = 264 | summonFrom { 265 | case mm: Mirror.SumOf[`s`] => 266 | subtypesFromMirror[A, mm.MirroredElemTypes]( 267 | mm.asInstanceOf[m.type], 268 | 0, 269 | Nil 270 | ) 271 | case _ => { 272 | val tc = new SerializableFunction0[Typeclass[s]]: 273 | override def apply(): Typeclass[s] = summonFrom { 274 | case tc: Typeclass[`s`] => tc 275 | case _ => deriveSubtype(summonInline[Mirror.Of[s]]) 276 | } 277 | val isType = new SerializableFunction1[A, Boolean]: 278 | override def apply(a: A): Boolean = a.isInstanceOf[s & A] 279 | val asType = new SerializableFunction1[A, s & A]: 280 | override def apply(a: A): s & A = a.asInstanceOf[s & A] 281 | List( 282 | new SealedTrait.Subtype[Typeclass, A, s]( 283 | typeInfo[s], 284 | IArray.from(anns[s]), 285 | IArray.from(inheritedAnns[s]), 286 | IArray.from(paramTypeAnns[A]), 287 | isObject[s], 288 | idx, 289 | CallByNeed.createLazy(tc), 290 | isType, 291 | asType 292 | ) 293 | ) 294 | } 295 | } 296 | 297 | /** This method unrolls recursion by 16, 4, 1 steps to increase maximal size of `SubtypeTuple` tuple due to compiler limitation of maximal 298 | * nested inlines. 299 | */ 300 | protected transparent inline def subtypesFromMirror[A, SubtypeTuple <: Tuple]( 301 | m: Mirror.SumOf[A], 302 | idx: Int = 0, // no longer used, kept for bincompat 303 | result: List[SealedTrait.Subtype[Typeclass, A, _]] = Nil 304 | ): List[SealedTrait.Subtype[Typeclass, A, _]] = 305 | inline erasedValue[SubtypeTuple] match 306 | case _: EmptyTuple => 307 | result.distinctBy(_.typeInfo).sortBy(_.typeInfo.full) 308 | case _: (h1 *: h2 *: h3 *: h4 *: h5 *: h6 *: h7 *: h8 *: h9 *: h10 *: h11 *: h12 *: h13 *: h14 *: h15 *: h16 *: tail) => 309 | val sub1 = subtypesFromMirrorStep[A, h1](m, idx) 310 | val sub2 = subtypesFromMirrorStep[A, h2](m, idx + 1) 311 | val sub3 = subtypesFromMirrorStep[A, h3](m, idx + 2) 312 | val sub4 = subtypesFromMirrorStep[A, h4](m, idx + 3) 313 | val sub5 = subtypesFromMirrorStep[A, h5](m, idx + 4) 314 | val sub6 = subtypesFromMirrorStep[A, h6](m, idx + 5) 315 | val sub7 = subtypesFromMirrorStep[A, h7](m, idx + 6) 316 | val sub8 = subtypesFromMirrorStep[A, h8](m, idx + 7) 317 | val sub9 = subtypesFromMirrorStep[A, h9](m, idx + 8) 318 | val sub10 = subtypesFromMirrorStep[A, h10](m, idx + 9) 319 | val sub11 = subtypesFromMirrorStep[A, h11](m, idx + 10) 320 | val sub12 = subtypesFromMirrorStep[A, h12](m, idx + 11) 321 | val sub13 = subtypesFromMirrorStep[A, h13](m, idx + 12) 322 | val sub14 = subtypesFromMirrorStep[A, h14](m, idx + 13) 323 | val sub15 = subtypesFromMirrorStep[A, h15](m, idx + 14) 324 | val sub16 = subtypesFromMirrorStep[A, h16](m, idx + 15) 325 | 326 | subtypesFromMirror[A, tail]( 327 | m, 328 | idx + 16, 329 | sub1 ::: sub2 ::: sub3 ::: sub4 ::: sub5 ::: sub6 ::: sub7 ::: sub8 ::: sub9 ::: sub10 ::: sub11 ::: sub12 ::: sub13 ::: sub14 ::: sub15 ::: sub16 ::: result 330 | ) 331 | case _: (h1 *: h2 *: h3 *: h4 *: tail) => 332 | val sub1 = subtypesFromMirrorStep[A, h1](m, idx) 333 | val sub2 = subtypesFromMirrorStep[A, h2](m, idx + 1) 334 | val sub3 = subtypesFromMirrorStep[A, h3](m, idx + 2) 335 | val sub4 = subtypesFromMirrorStep[A, h4](m, idx + 3) 336 | subtypesFromMirror[A, tail](m, idx + 4, sub1 ::: sub2 ::: sub3 ::: sub4 ::: result) 337 | case _: (s *: tail) => 338 | val sub = subtypesFromMirrorStep[A, s](m, idx) 339 | subtypesFromMirror[A, tail](m, idx + 1, sub ::: result) 340 | end SealedTraitDerivation 341 | --------------------------------------------------------------------------------