Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Magnolia derivation for Debug typeclass #1371

Merged
merged 2 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ val projectsCommon = List(
experimentalLaws,
experimentalTests,
laws,
macros
macros,
magnolia,
magnoliaTests
)

val projectsJvmOnly = List[ProjectReference](
Expand Down Expand Up @@ -161,6 +163,37 @@ lazy val macros = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.nativeSettings(nativeSettings)
.enablePlugins(BuildInfoPlugin)

lazy val magnolia = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("magnolia"))
.dependsOn(core)
.settings(stdSettings("zio-prelude-magnolia"))
.settings(crossProjectSettings)
.settings(macroDefinitionSettings)
.settings(Compile / console / scalacOptions ~= { _.filterNot(Set("-Xfatal-warnings")) })
.settings(buildInfoSettings("zio.prelude.magnolia"))
.settings(testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")))
.settings(dottySettings)
.settings(magnoliaSettings)
.settings(libraryDependencies += "dev.zio" %%% "zio-test-sbt" % zioVersion % Test)
.jvmSettings(scalaReflectTestSettings)
.jsSettings(jsSettings)
.nativeSettings(nativeSettings)
.enablePlugins(BuildInfoPlugin)

lazy val magnoliaTests = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("magnolia-tests"))
.dependsOn(magnolia)
.settings(stdSettings("zio-prelude-magnolia-tests"))
.settings(crossProjectSettings)
.settings(buildInfoSettings("zio.prelude.magnolia.tests"))
.settings(testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")))
.settings(dottySettings)
.settings(libraryDependencies += "dev.zio" %%% "zio-test-sbt" % zioVersion % Test)
.jvmSettings(scalaReflectTestSettings)
.jsSettings(jsSettings)
.nativeSettings(nativeSettings)
.enablePlugins(BuildInfoPlugin)

lazy val experimental = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("experimental"))
.dependsOn(core)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package zio.debug.magnolia

import zio.Scope
import zio.prelude._
import zio.test.Assertion._
import zio.test.{ZIOSpecDefault, _}

case class Lair(name: String, animal: Animal)

object Lair {
implicit val debug: Debug[Lair] = DeriveDebug.derived[Lair]
}

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

object Animal {
implicit val debug: Debug[Animal] = DeriveDebug.derived[Animal]
}

case class Adult(name: String, age: Int, chidren: List[Child])
case class Child(name: String, age: Int)

object Adult {
implicit val debug: Debug[Adult] = DeriveDebug.derived[Adult]
}

object Child {
implicit val debug: Debug[Child] = DeriveDebug.derived[Child]
}

object Test extends ZIOSpecDefault {

override def spec: Spec[TestEnvironment with Scope, Any] =
suite("DeriveDebug")(
test("should derive Debug for case class") {
val animal = Animal("tiger", 10)

assert(animal.debug.render)(equalTo("Animal(name = \"tiger\", age = 10)"))

},
test("should derive Debug for nested case class") {
val lair = Lair("jungle", Animal("tiger", 10))

assert(lair.debug.render)(equalTo("Lair(name = \"jungle\", animal = Animal(name = \"tiger\", age = 10))"))
},
test("should derive Debug for case class with list") {
val adult = Adult("John", 30, List(Child("Alice", 5), Child("Bob", 10)))

assert(adult.debug.render)(
equalTo(
"Adult(name = \"John\", age = 30, chidren = List(Child(name = \"Alice\", age = 5), Child(name = \"Bob\", age = 10)))"
)
)
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package zio.debug.magnolia
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package name should be zio.prelude.magnolia.


import magnolia1._
import zio.prelude.Debug
import zio.prelude.Debug.Repr

import scala.collection.immutable.ListMap

object DeriveDebug {

type Typeclass[T] = Debug[T]

def join[T](ctx: CaseClass[Debug, T]): Debug[T] =
if (ctx.isValueClass) { (a: T) =>
Repr.VConstructor(
ctx.typeName.owner.split('.').toList,
ctx.typeName.short,
ctx.parameters.map(p => p.typeclass.debug(p.dereference(a))).toList
)
} else if (ctx.isObject) { (_: T) =>
Repr.Object(ctx.typeName.owner.split('.').toList, ctx.typeName.short)
} else { (a: T) =>
Repr.Constructor(
ctx.typeName.owner.split('.').toList,
ctx.typeName.short,
ListMap.apply(ctx.parameters.map(p => p.label -> p.typeclass.debug(p.dereference(a))): _*)
)
}

def split[T](ctx: SealedTrait[Debug, T]): Debug[T] =
new Debug[T] { self =>
def debug(a: T): Repr = ctx.split(a) { sub =>
sub.typeclass.debug(sub.cast(a))
}
}

def derived[T]: Debug[T] = macro Magnolia.gen[T]
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding an extension method on the Debug companion object, so that it also contains the derived method, so that in Scala 3 we can do case class Child(name: String, age: Int) derives Debug?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whhhhhaou that exactly what I did not knew how to do !
Thx, will do it tonight.

Copy link
Contributor Author

@cheleb cheleb Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, I suppose then that I should merge magnolia and core module, right ?
Or AFAIU I run in cyclic dependencies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cyclic dependencies

Nope. Have zio-prelude-magnolia depend on zio-prelude. zio-prelude must not know about Magnolia or ``zio-prelude-magnolia`.

That's why we'll make it an extension method of the Debug companion object. Not an actual method on Debug companion object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:-/
Just pushed the simplest fix.
No idea how to add an extension to an existing companion object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok @sideeffffect I found :S

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package zio.debug.magnolia

import magnolia1._

import scala.collection.immutable.ListMap
import scala.language.experimental.macros
import zio.prelude.Debug
import zio.prelude.Debug.Repr

object DeriveDebug extends AutoDerivation[Debug] {

type Typeclass[T] = Debug[T]

def join[T](ctx: CaseClass[Debug, T]): Debug[T] =
if (ctx.isValueClass) { (a: T) =>
Repr.VConstructor(
ctx.typeInfo.owner.split('.').toList,
ctx.typeInfo.short,
ctx.parameters.map(p => p.typeclass.debug(p.deref(a))).toList
)
} else if (ctx.isObject) { (_: T) =>
Repr.Object(ctx.typeInfo.owner.split('.').toList, ctx.typeInfo.short)
} else { (a: T) =>
Repr.Constructor(
ctx.typeInfo.owner.split('.').toList,
ctx.typeInfo.short,
ListMap.from(ctx.parameters.map(p => p.label -> p.typeclass.debug(p.deref(a))))
)
}

def split[T](ctx: SealedTrait[Debug, T]): Debug[T] =
new Debug[T] { self =>
def debug(a: T): Repr = ctx.choose(a) { sub =>
sub.typeclass.debug(sub.cast(a))
}
}

}
11 changes: 11 additions & 0 deletions project/BuildHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ object BuildHelper {
Test / parallelExecution := false
)

val magnoliaSettings =
libraryDependencies += {
scalaVersion.value match {
case Scala3 =>
"com.softwaremill.magnolia1_3" %% "magnolia" % "1.3.7"
case _ =>
"com.softwaremill.magnolia1_2" %% "magnolia" % "1.1.10"
// libraryDependencies += compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.3" cross CrossVersion.full)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented code in build

}
}

// Keep this consistent with the version in .core-tests/shared/src/test/scala/REPLSpec.scala
val replSettings = makeReplSettings {
"""|import zio._
Expand Down