Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions modules/core/shared/src/main/scala/neotype/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand All @@ -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]

Expand All @@ -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]

Expand All @@ -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],
Expand All @@ -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]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down