Skip to content
Open
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
2 changes: 2 additions & 0 deletions modules/core/shared/src/main/scala/neotype/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ abstract class Newtype[A] extends TypeWrapper[A]:
object Newtype:
type WithType[A, B] = Newtype[A] { type Type = B }

given canEqual[A, B](using Newtype.WithType[A, B], CanEqual[A, A]): CanEqual[B, B] = CanEqual.derived

extension [A, B](value: B)(using
newtype: Newtype.WithType[A, B]
) //
Expand Down
44 changes: 44 additions & 0 deletions modules/core/shared/src/test/scala/neotype/NewtypeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ object NewtypeSpec extends ZIOSpecDefault:
assertTrue(res.message contains "Neotype Error")
}

test("comparison between types that don't have a CanEqual should be possible when strictEquality is not enabled"){
val x: Right[Nothing, NoCanEqualNewtype] = Right(NoCanEqualNewtype(NoCanEqual(10)))
val y: Either[Throwable, NoCanEqualSubtype] = Right(NoCanEqualSubtype(NoCanEqual(10)))

assertTrue(x == y)
}

suiteAll("makeOrThrow") {
test("newtype success") {
val res = PositiveIntNewtype.makeOrThrow(1)
Expand Down Expand Up @@ -285,4 +292,41 @@ object NewtypeSpec extends ZIOSpecDefault:
assertTrue(error.getMessage == "Must be the secret string!")
}
}

suiteAll("CanEqual") {
import scala.language.strictEquality

test("newtypes should be able to be compared to their own types")(
assertTrue(PositiveIntNewtype(10) == PositiveIntNewtype(10))
)

test("newtypes should not be able to be compared to their underlying types") {

val res = typeCheckErrors("""PositiveIntNewtype(10) == 10""").head

assertTrue(res.message contains "Values of types neotype.PositiveIntNewtype.Type and Int cannot be compared with == or !=.")
}

test("newtypes should not be able to be if their underlying type is not comparable") {

val res = typeCheckErrors("""NoCanEqualNewtype(NoCanEqual(10)) == NoCanEqualNewtype(NoCanEqual(10))""").head

assertTrue(res.message contains "Values of types neotype.NoCanEqualNewtype.Type and neotype.NoCanEqualNewtype.Type cannot be compared with == or !=.")
}

test("subtypes should be able to be compared to their own types")(
assertTrue(PositiveIntSubtype(10) == PositiveIntSubtype(10))
)

test("subtypes should be able to be compared to their underlying types") (
assertTrue(PositiveIntSubtype(10) == 10)
)

test("subtypes should not be able to be if their underlying type is not comparable") {

val res = typeCheckErrors("""NoCanEqualSubtype(NoCanEqual(10)) == NoCanEqual(10)""").head

assertTrue(res.message contains "Values of types neotype.NoCanEqualSubtype.Type and neotype.NoCanEqual cannot be compared with == or !=")
}
}
}
8 changes: 8 additions & 0 deletions modules/core/shared/src/test/scala/neotype/TestNewtypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,11 @@ type NonNegativeCents = NonNegativeCents.Type
object NonNegativeCents extends Subtype[Cents]:
override inline def validate(input: Cents): Boolean =
input.unwrap >= 0

case class NoCanEqual(value: Int)

type NoCanEqualNewtype = NoCanEqualNewtype.Type
object NoCanEqualNewtype extends Newtype[NoCanEqual]

type NoCanEqualSubtype = NoCanEqualSubtype.Type
object NoCanEqualSubtype extends Subtype[NoCanEqual]
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ object CalibanSpec extends ZIOSpecDefault:
},
test("ArgBuilder success") {
val argBuilder = summonInline[ArgBuilder[SimpleSubtype]]
assertTrue(argBuilder.build(IntValue(123)) == Right(SimpleNewtype(123)))
assertTrue(argBuilder.build(IntValue(123)) == Right(SimpleSubtype(123)))
}
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ object ChimneySpec extends ZIOSpecDefault:
test("partially transform success") {
val string = "hello world"
val result = string.transformIntoPartial[ValidatedSubtype]
assertTrue(result.asEither == Right(ValidatedNewtype("hello world")))
assertTrue(result.asEither == Right(ValidatedSubtype("hello world")))
},
test("partially transform failure") {
val string = "hello"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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))
read.map(value => wrappedType.makeOrThrow(value))

given write[A, B](using wrappedType: WrappedType[A, B], write: Write[A]): Write[B] =
write.contramap(wrappedType.unwrap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import _root_.doobie.util.transactor.Transactor
import cats.Show
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import neotype.{Newtype, Subtype}
import neotype.Newtype
import neotype.Subtype
import neotype.common.NonEmptyString
import neotype.interop.doobie.given
import neotype.test.definitions.*
Expand Down