├── 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 | 
2 |
3 | [
](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 |
.
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 |
--------------------------------------------------------------------------------