diff --git a/modules/core/shared/src/main/scala/neotype/package.scala b/modules/core/shared/src/main/scala/neotype/package.scala index af99cc4..da77274 100644 --- a/modules/core/shared/src/main/scala/neotype/package.scala +++ b/modules/core/shared/src/main/scala/neotype/package.scala @@ -187,6 +187,7 @@ trait WrappedType[Underlying, Wrapped]: type Wrapper <: TypeWrapper[Underlying] { type Type = Wrapped } def unwrap(wrapped: Wrapped): Underlying def make(underlying: Underlying): Either[String, Wrapped] + def makeOrThrow(input: Underlying): Wrapped inline def unsafeMake(inline underlying: Underlying): Wrapped = underlying.asInstanceOf[Wrapped] inline def unsafeMakeF[F[_]](inline underlying: F[Underlying]): F[Wrapped] = underlying.asInstanceOf[F[Wrapped]] @@ -198,11 +199,13 @@ object WrappedType: type Wrapper = nt.type inline def unwrap(wrapped: B): A = nt.unwrap(wrapped) inline def make(underlying: A): Either[String, B] = nt.make(underlying) + inline def makeOrThrow(underlying: A): B = nt.makeOrThrow(underlying) given subtypeWrappedType[A, B <: A](using st: Subtype.WithType[A, B]): WrappedType[A, B] with type Wrapper = st.type inline def unwrap(wrapped: B): A = wrapped inline def make(underlying: A): Either[String, B] = st.make(underlying) + inline def makeOrThrow(underlying: A): B = st.makeOrThrow(underlying) trait SimpleWrappedType[Underlying, Wrapped] extends WrappedType[Underlying, Wrapped] @@ -213,12 +216,14 @@ object SimpleWrappedType: type Wrapper = nt.type inline def unwrap(wrapped: B): A = nt.unwrap(wrapped) inline def make(underlying: A): Either[String, B] = nt.make(underlying) + inline def makeOrThrow(underlying: A): B = nt.makeOrThrow(underlying) given subtypeSimple[A, B <: A](using st: Subtype.WithType[A, B], ev: IsSimpleType[st.type]): SimpleWrappedType[A, B] with type Wrapper = st.type inline def unwrap(wrapped: B): A = wrapped inline def make(underlying: A): Either[String, B] = st.make(underlying) + inline def makeOrThrow(underlying: A): B = st.makeOrThrow(underlying) trait ValidatedWrappedType[Underlying, Wrapped] extends WrappedType[Underlying, Wrapped] @@ -232,6 +237,7 @@ object ValidatedWrappedType: type Wrapper = nt.type inline def unwrap(wrapped: B): A = nt.unwrap(wrapped) inline def make(underlying: A): Either[String, B] = nt.make(underlying) + inline def makeOrThrow(underlying: A): B = nt.makeOrThrow(underlying) given subtypeValidated[A, B <: A](using st: Subtype.WithType[A, B], @@ -240,6 +246,7 @@ object ValidatedWrappedType: type Wrapper = st.type inline def unwrap(wrapped: B): A = wrapped inline def make(underlying: A): Either[String, B] = st.make(underlying) + inline def makeOrThrow(underlying: A): B = st.makeOrThrow(underlying) /** Typeclass for wrapping an underlying value with validation. */ trait Wrappable[Underlying, Wrapped]: diff --git a/modules/neotype-doobie/shared/src/main/scala/neotype/interop/doobie/DoobieInstances.scala b/modules/neotype-doobie/shared/src/main/scala/neotype/interop/doobie/DoobieInstances.scala index 204c8f5..9a47b12 100644 --- a/modules/neotype-doobie/shared/src/main/scala/neotype/interop/doobie/DoobieInstances.scala +++ b/modules/neotype-doobie/shared/src/main/scala/neotype/interop/doobie/DoobieInstances.scala @@ -16,6 +16,12 @@ given get[A, B](using nt: WrappedType[A, B], get: Get[A], show: Show[A]): Get[B] given put[A, B](using nt: WrappedType[A, B], put: Put[A]): Put[B] = nt.unsafeMakeF(put) +given read[A, B](using wrappedType: WrappedType[A, B], read: Read[A]): Read[B] = + read.map(value => wrappedType.makeOrThrow(value)) + +given write[A, B](using wrappedType: WrappedType[A, B], write: Write[A]): Write[B] = + write.contramap(wrappedType.unwrap) + given arrayGet[A, B: ClassTag](using nt: WrappedType[A, B], get: Get[Array[A]], show: Show[Array[A]]): Get[Array[B]] = get.temap { arr => arr.foldRight[Either[String, Array[B]]](Right(Array.empty[B])) { (elem, acc) => diff --git a/modules/neotype-doobie/shared/src/test/scala/neotype/interop/doobie/DoobieInstancesSpec.scala b/modules/neotype-doobie/shared/src/test/scala/neotype/interop/doobie/DoobieInstancesSpec.scala index 278f04c..97e2a1a 100644 --- a/modules/neotype-doobie/shared/src/test/scala/neotype/interop/doobie/DoobieInstancesSpec.scala +++ b/modules/neotype-doobie/shared/src/test/scala/neotype/interop/doobie/DoobieInstancesSpec.scala @@ -7,8 +7,7 @@ import _root_.doobie.util.transactor.Transactor import cats.Show import cats.effect.IO import cats.effect.unsafe.implicits.global -import neotype.Newtype -import neotype.Subtype +import neotype.{Newtype, Subtype} import neotype.common.NonEmptyString import neotype.interop.doobie.given import neotype.test.definitions.* @@ -17,6 +16,10 @@ import zio.test.* object DoobieInstancesSpec extends ZIOSpecDefault: + type CompositeNewtype = CompositeNewtype.Type + + object CompositeNewtype extends Newtype[Composite] + val transactor = Transactor.fromDriverManager[IO]( driver = "org.h2.Driver", // driver classname url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", // connect URL (memory database) @@ -90,19 +93,43 @@ object DoobieInstancesSpec extends ZIOSpecDefault: ), suite("Composite")( test("composite success") { - val result = sql"SELECT 'Hello', 42, 'Hello World!', 100" + val expectedValue = + Composite( + ValidatedNewtype("Hello"), + SimpleNewtype(42), + ValidatedSubtype("Hello World!"), + SimpleSubtype(100) + ) + + val result = sql"SELECT $expectedValue" .query[Composite] .unique .transact(transactor) .unsafeRunSync() + assertTrue( - result == Composite( + result == expectedValue + ) + }, + test("composite newtype success") { + val expectedValue: CompositeNewtype = CompositeNewtype( + Composite( ValidatedNewtype("Hello"), SimpleNewtype(42), ValidatedSubtype("Hello World!"), SimpleSubtype(100) ) ) + + val result = sql"SELECT $expectedValue" + .query[CompositeNewtype] + .unique + .transact(transactor) + .unsafeRunSync() + + assertTrue( + result == expectedValue + ) }, test("composite fail") { val result = scala.util.Try(