From b04c7732871fe558d8f713c3bf6dbb53779ecb7d Mon Sep 17 00:00:00 2001 From: felix Date: Thu, 22 Jan 2026 10:13:47 +0100 Subject: [PATCH 1/2] Added an emap method --- modules/core/src/main/scala/doobie/util/read.scala | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/scala/doobie/util/read.scala b/modules/core/src/main/scala/doobie/util/read.scala index 42c43f80d..5d7463002 100644 --- a/modules/core/src/main/scala/doobie/util/read.scala +++ b/modules/core/src/main/scala/doobie/util/read.scala @@ -4,11 +4,12 @@ package doobie.util -import cats.Applicative +import cats.{Applicative, Show} import doobie.ResultSetIO import doobie.enumerated.Nullability import doobie.enumerated.Nullability.{NoNulls, NullabilityKnown} import doobie.free.resultset as IFRS +import doobie.util.invariant.InvalidValue import java.sql.ResultSet import scala.annotation.implicitNotFound @@ -58,6 +59,16 @@ trait Read[A] { final def ap[B](ff: Read[A => B]): Read[B] = { new Read.Composite[B, A => B, A](ff, this, (f, a) => f(a)) } + + /** Equivalent to `map`, but allows the conversion to fail with an error message. + */ + final def emap[B](f: A => Either[String, B])(implicit sA: Show[A]): Read[B] = + map { a => + f(a) match { + case Left(reason) => throw InvalidValue[A, B](a, reason) + case Right(b) => b + } + } } object Read extends ReadPlatform { From d4cddc3989507752787538ee501f80a275889692 Mon Sep 17 00:00:00 2001 From: felix Date: Mon, 26 Jan 2026 09:26:31 +0100 Subject: [PATCH 2/2] Added emap tests --- .../test/scala/doobie/util/ReadSuite.scala | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/core/src/test/scala/doobie/util/ReadSuite.scala b/modules/core/src/test/scala/doobie/util/ReadSuite.scala index e98554352..3387e83f8 100644 --- a/modules/core/src/test/scala/doobie/util/ReadSuite.scala +++ b/modules/core/src/test/scala/doobie/util/ReadSuite.scala @@ -4,6 +4,7 @@ package doobie.util +import cats.Show import cats.effect.IO import doobie.util.TestTypes.* import doobie.util.transactor.Transactor @@ -155,6 +156,32 @@ class ReadSuite extends munit.CatsEffectSuite with ReadSuitePlatform { insertTuple3AndCheckRead((1, "s1", "s2"), WrappedSimpleCaseClass(SimpleCaseClass(Some(1), "custom", Some("s2")))) } + test(".emap should correctly transform the value") { + import doobie.implicits.* + implicit val s: Show[SimpleCaseClass] = _.toString + implicit val r: Read[WrappedSimpleCaseClass] = Read[SimpleCaseClass].emap(s => + Right(WrappedSimpleCaseClass( + s.copy(s = "custom") + ))) + + insertTuple3AndCheckRead((1, "s1", "s2"), WrappedSimpleCaseClass(SimpleCaseClass(Some(1), "custom", Some("s2")))) + } + + test(".emap should fail a transform") { + import doobie.implicits.* + implicit val s: Show[SimpleCaseClass] = _.toString + implicit val r: Read[WrappedSimpleCaseClass] = Read[SimpleCaseClass].emap(_ => + Left("Invalid transformation") + ) + + sql"SELECT 1,'a','b'".query[WrappedSimpleCaseClass].unique.transact(xa).attempt.assertEquals( + Left(doobie.util.invariant.InvalidValue[SimpleCaseClass, WrappedSimpleCaseClass]( + SimpleCaseClass(Some(1), "a", Some("b")), + "Invalid transformation") + ) + ) + } + /* case class with nested Option case class field */