From 88f64bd3abc5d593314af9bf0f0fe31a57849239 Mon Sep 17 00:00:00 2001 From: drewfeelsblue Date: Mon, 14 Apr 2025 19:50:20 +0200 Subject: [PATCH] add-scala3-enum-put-get-derivation --- .../scala-3/doobie/util/GetPlatform.scala | 21 +++++- .../scala-3/doobie/util/PutPlatform.scala | 12 +++- .../main/scala-3/doobie/util/package.scala | 22 ++++++ .../doobie/util/GetSuitePlatform.scala | 9 +++ .../doobie/util/PutSuitePlatform.scala | 7 ++ .../doobie/util/GetSuitePlatform.scala | 67 +++++++++++++++++++ .../doobie/util/PutSuitePlatform.scala | 40 +++++++++++ .../src/test/scala/doobie/util/GetSuite.scala | 22 +++--- .../src/test/scala/doobie/util/PutSuite.scala | 13 +--- 9 files changed, 189 insertions(+), 24 deletions(-) create mode 100644 modules/core/src/main/scala-3/doobie/util/package.scala create mode 100644 modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala create mode 100644 modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala create mode 100644 modules/core/src/test/scala-3/doobie/util/GetSuitePlatform.scala create mode 100644 modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala diff --git a/modules/core/src/main/scala-3/doobie/util/GetPlatform.scala b/modules/core/src/main/scala-3/doobie/util/GetPlatform.scala index 74843014c..b5bf221a6 100644 --- a/modules/core/src/main/scala-3/doobie/util/GetPlatform.scala +++ b/modules/core/src/main/scala-3/doobie/util/GetPlatform.scala @@ -4,4 +4,23 @@ package doobie.util -trait GetPlatform {} +import scala.deriving.Mirror +import scala.compiletime.constValue +import scala.reflect.Enum + +trait GetPlatform { + private def of[A](name: String, cases: List[A], labels: List[String]): Get[A] = + Get[String].temap { caseName => + labels.indexOf(caseName) match { + case -1 => Left(s"enum $name does not contain case: $caseName") + case i => Right(cases(i)) + } + } + + inline final def deriveEnumString[A <: Enum](using mirror: Mirror.SumOf[A]): Get[A] = + of( + constValue[mirror.MirroredLabel], + summonSingletonCases[mirror.MirroredElemTypes, A](constValue[mirror.MirroredLabel]), + summonLabels[mirror.MirroredElemLabels] + ) +} diff --git a/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala b/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala index f39f8a136..0fdaa1025 100644 --- a/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala +++ b/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala @@ -4,4 +4,14 @@ package doobie.util -trait PutPlatform +import scala.compiletime.constValue +import scala.deriving.Mirror +import scala.reflect.Enum + +trait PutPlatform { + inline final def deriveEnumString[A <: Enum](using mirror: Mirror.SumOf[A]): Put[A] = + val _ = summonSingletonCases[mirror.MirroredElemTypes, A](constValue[mirror.MirroredLabel]) + val labels = summonLabels[mirror.MirroredElemLabels] + + Put[String].contramap(a => labels(mirror.ordinal(a))) +} diff --git a/modules/core/src/main/scala-3/doobie/util/package.scala b/modules/core/src/main/scala-3/doobie/util/package.scala new file mode 100644 index 000000000..bea2b6691 --- /dev/null +++ b/modules/core/src/main/scala-3/doobie/util/package.scala @@ -0,0 +1,22 @@ +// Copyright (c) 2013-2020 Rob Norris and Contributors +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package doobie.util + +import scala.compiletime.{constValue, erasedValue, error, summonInline} +import scala.deriving.Mirror + +private[util] inline final def summonLabels[T <: Tuple]: List[String] = + inline erasedValue[T] match + case _: EmptyTuple => Nil + case _: (t *: ts) => constValue[t].asInstanceOf[String] :: summonLabels[ts] + +private[util] inline final def summonSingletonCases[T <: Tuple, A](inline typeName: String): List[A] = + inline erasedValue[T] match + case _: EmptyTuple => Nil + case _: (h *: t) => + inline summonInline[Mirror.Of[h]] match + case m: Mirror.Singleton => m.fromProduct(EmptyTuple).asInstanceOf[A] :: summonSingletonCases[t, A](typeName) + case m: Mirror => + error("Enum " + typeName + " contains non singleton case " + constValue[m.MirroredLabel]) diff --git a/modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala b/modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala new file mode 100644 index 000000000..7f7ceafc1 --- /dev/null +++ b/modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala @@ -0,0 +1,9 @@ +// Copyright (c) 2013-2020 Rob Norris and Contributors +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package doobie.util + +trait GetSuitePlatform {} + +trait GetDBSuitePlatform {} diff --git a/modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala b/modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala new file mode 100644 index 000000000..517fac97c --- /dev/null +++ b/modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala @@ -0,0 +1,7 @@ +// Copyright (c) 2013-2020 Rob Norris and Contributors +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package doobie.util + +trait PutSuitePlatform {} diff --git a/modules/core/src/test/scala-3/doobie/util/GetSuitePlatform.scala b/modules/core/src/test/scala-3/doobie/util/GetSuitePlatform.scala new file mode 100644 index 000000000..4243e61e6 --- /dev/null +++ b/modules/core/src/test/scala-3/doobie/util/GetSuitePlatform.scala @@ -0,0 +1,67 @@ +// Copyright (c) 2013-2020 Rob Norris and Contributors +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package doobie.util + +import Predef.augmentString +import doobie.util.invariant.InvalidValue + +enum EnumWithOnlySingletonCases { + case One, Two, Three +} + +trait GetSuitePlatform { self: munit.FunSuite => + + enum EnumContainsNonSinletonCase { + case One + case Two(i: Int) + } + + sealed trait NotEnum + + case object NotEnum1 extends NotEnum + + test("Get should not be derived for enum with non singleton case") { + val compileError = compileErrors("Get.deriveEnumString[EnumContainsNonSinletonCase]") + + assert(compileError.contains("Enum EnumContainsNonSinletonCase contains non singleton case Two")) + } + + test("Get should be derived for enum with only singleton cases") { + Get.deriveEnumString[EnumWithOnlySingletonCases] + } + + test("Get should not be derived for sealed trait") { + val compileError = compileErrors("Get.deriveEnumString[NotEnum]") + + assert(compileError.contains( + "Type argument GetSuitePlatform.this.NotEnum does not conform to upper bound scala.reflect.Enum")) + } +} + +trait GetDBSuitePlatform { self: munit.CatsEffectSuite & TransactorProvider => + import doobie.syntax.all.* + + given Get[EnumWithOnlySingletonCases] = Get.deriveEnumString + + test("Get should properly read existing value of enum") { + sql"select 'One'".query[EnumWithOnlySingletonCases].unique.transact(xa).attempt.assertEquals( + Right( + EnumWithOnlySingletonCases.One + ) + ) + } + + test("Get should error reading non existing value of enum") { + sql"select 'One1'".query[EnumWithOnlySingletonCases].unique.transact(xa).attempt.assertEquals( + Left( + InvalidValue( + value = "One1", + reason = + "enum EnumWithOnlySingletonCases does not contain case: One1" + ) + ) + ) + } +} diff --git a/modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala b/modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala new file mode 100644 index 000000000..a31562970 --- /dev/null +++ b/modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala @@ -0,0 +1,40 @@ +// Copyright (c) 2013-2020 Rob Norris and Contributors +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package doobie.util + +import Predef.augmentString + +trait PutSuitePlatform { self: munit.FunSuite => + + enum EnumContainsNonSinletonCase { + case One + case Two(i: Int) + } + + enum EnumWithOnlySingletonCases { + case One, Two, Three + } + + sealed trait NotEnum + + case object NotEnum1 extends NotEnum + + test("Put should not be derived for enum with non singleton case") { + val compileError = compileErrors("Put.deriveEnumString[EnumContainsNonSinletonCase]") + + assert(compileError.contains("Enum EnumContainsNonSinletonCase contains non singleton case Two")) + } + + test("Put should be derived for enum with only singleton cases") { + Put.deriveEnumString[EnumWithOnlySingletonCases] + } + + test("Put should not be derived for sealed trait") { + val compileError = compileErrors("Put.deriveEnumString[NotEnum]") + + assert(compileError.contains( + "Type argument PutSuitePlatform.this.NotEnum does not conform to upper bound scala.reflect.Enum")) + } +} diff --git a/modules/core/src/test/scala/doobie/util/GetSuite.scala b/modules/core/src/test/scala/doobie/util/GetSuite.scala index 003e69668..8e2c028b9 100644 --- a/modules/core/src/test/scala/doobie/util/GetSuite.scala +++ b/modules/core/src/test/scala/doobie/util/GetSuite.scala @@ -9,7 +9,17 @@ import doobie.enumerated.JdbcType import doobie.testutils.VoidExtensions import doobie.util.transactor.Transactor -class GetSuite extends munit.CatsEffectSuite { +trait TransactorProvider { + lazy val xa = Transactor.fromDriverManager[IO]( + driver = "org.h2.Driver", + url = "jdbc:h2:mem:queryspec;DB_CLOSE_DELAY=-1", + user = "sa", + password = "", + logHandler = None + ) +} + +class GetSuite extends munit.CatsEffectSuite with GetSuitePlatform { case class X(x: Int) case class Q(x: String) @@ -27,17 +37,9 @@ class GetSuite extends munit.CatsEffectSuite { final case class Foo(s: String) final case class Bar(n: Int) -class GetDBSuite extends munit.CatsEffectSuite { +class GetDBSuite extends munit.CatsEffectSuite with TransactorProvider with GetDBSuitePlatform { import doobie.syntax.all.* - lazy val xa = Transactor.fromDriverManager[IO]( - driver = "org.h2.Driver", - url = "jdbc:h2:mem:queryspec;DB_CLOSE_DELAY=-1", - user = "sa", - password = "", - logHandler = None - ) - // Both of these will fail at runtime if called with a null value, we check that this is // avoided below. implicit val FooMeta: Get[Foo] = Get[String].map(s => Foo(s.toUpperCase)) diff --git a/modules/core/src/test/scala/doobie/util/PutSuite.scala b/modules/core/src/test/scala/doobie/util/PutSuite.scala index fd2f4b0c4..ae1080a97 100644 --- a/modules/core/src/test/scala/doobie/util/PutSuite.scala +++ b/modules/core/src/test/scala/doobie/util/PutSuite.scala @@ -4,11 +4,9 @@ package doobie.util -import cats.effect.IO import doobie.testutils.VoidExtensions -import doobie.util.transactor.Transactor -class PutSuite extends munit.FunSuite { +class PutSuite extends munit.FunSuite with PutSuitePlatform { case class X(x: Int) case class Q(x: String) @@ -17,14 +15,6 @@ class PutSuite extends munit.FunSuite { case class Reg2(x: Int) - val xa = Transactor.fromDriverManager[IO]( - driver = "org.h2.Driver", - url = "jdbc:h2:mem:queryspec;DB_CLOSE_DELAY=-1", - user = "sa", - password = "", - logHandler = None - ) - case class Foo(s: String) case class Bar(n: Int) @@ -33,5 +23,4 @@ class PutSuite extends munit.FunSuite { Put[Int].void Put[String].void } - }