diff --git a/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala new file mode 100644 index 000000000..17f96e66c --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala @@ -0,0 +1,17 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.Castable.<:> +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} + +private[opaque] trait BaseOpaque[From] extends Castable.Ops { + trait Tag + type Type + + def apply(value: From): Type + + + implicit protected final val castable: From <:> Type = new Castable[From, Type] + implicit final def transparentCodec(implicit fromCodec: GenCodec[From]): GenCodec[Type] = wrapF(fromCodec) + implicit final def transparentKeyCodec(implicit fromKeyCodec: GenKeyCodec[From]): GenKeyCodec[Type] = wrapF(fromKeyCodec) +} diff --git a/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala b/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala new file mode 100644 index 000000000..919dccd2e --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala @@ -0,0 +1,15 @@ +package com.avsystem.commons +package opaque + +private[opaque] final class Castable[A, B] +private[opaque] object Castable { + type <:>[A, B] = Castable[A, B] + def apply[A, B](implicit ev: A <:> B): Castable[A, B] = ev + + private[opaque] trait Ops { + @inline final def wrap[From, To](value: From)(implicit ev: From <:> To): To = value.asInstanceOf[To] + @inline final def unwrap[From, To](value: To)(implicit ev: From <:> To): From = value.asInstanceOf[From] + @inline final def wrapF[From, To, F[_]](value: F[From])(implicit ev: From <:> To): F[To] = value.asInstanceOf[F[To]] + @inline final def unwrapF[From, To, F[_]](value: F[To])(implicit ev: From <:> To): F[From] = value.asInstanceOf[F[From]] + } +} diff --git a/core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala new file mode 100644 index 000000000..f1a8569a7 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala @@ -0,0 +1,19 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.Opaque.Hidden + +trait Opaque[From] extends BaseOpaque[From] { + + final type Type = Hidden[From, Tag] +} + +object Opaque { + + type Hidden[From, Tag] + @inline implicit def classTag[From, Tag](implicit base: ClassTag[From]): ClassTag[Hidden[From, Tag]] = ClassTag(base.runtimeClass) + + trait Default[From] extends Opaque[From] { + override final def apply(value: From): Type = wrap(value) + } +} \ No newline at end of file diff --git a/core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala new file mode 100644 index 000000000..961091981 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala @@ -0,0 +1,15 @@ +package com.avsystem.commons +package opaque + +trait Subopaque[From] extends BaseOpaque[From] { + + final type Type = From & Tag + def apply(value: From): Type +} + +object Subopaque { + + trait Default[From] extends Subopaque[From] { + override final def apply(value: From): Type = wrap(value) + } +} diff --git a/core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala b/core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala new file mode 100644 index 000000000..2280f6540 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala @@ -0,0 +1,43 @@ +package com.avsystem.commons +package opaque + +import org.scalatest.funsuite.AnyFunSuiteLike + +final class CastableTest extends AnyFunSuiteLike { + test("cast only works for compatible types") { + assertCompiles( + //language=scala + """ + |new Castable.Ops { + | implicit val intToLong: Castable[Int, Long] = new Castable[Int, Long] + | + | wrap[Int, Long](42) + | unwrap[Int, Long](42L) + | wrapF[Int, Long, List](List(42)) + | unwrapF[Int, Long, List](List(42L)) + | + | wrap(42) + | unwrap(42L) + | wrapF(List(42)) + | unwrapF(List(42L)) + | } + |""".stripMargin, + ) + assertDoesNotCompile( + //language=scala + """ + |new Castable.Ops { + | wrap[Int, Long](42) + | unwrap[Int, Long](42L) + | wrapF[Int, Long, List](List(42)) + | unwrapF[Int, Long, List](List(42L)) + | + | wrap(42) + | unwrap(42L) + | wrapF(List(42)) + | unwrapF(List(42L)) + | } + |""".stripMargin, + ) + } +} diff --git a/core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala b/core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala new file mode 100644 index 000000000..d716081b6 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala @@ -0,0 +1,52 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.OpaqueTest.* +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +final class OpaqueTest extends AnyFlatSpec with Matchers { + + "Opaque" should "create a type with no runtime overhead" in { + PosInt(1) shouldEqual 1 + PosInt(-1) shouldEqual 0 + } + + it should "not be a subtype of its Repr" in { + type Foo = Foo.Type + object Foo extends Opaque.Default[Int] + assertCompiles("Foo(1): Foo") + assertDoesNotCompile("Foo(1): Int") + } + + it should "support user ops" in { + (PosInt(3) -- PosInt(1)) shouldEqual PosInt(2) + } + + it should "work in Arrays" in { + object Foo extends Opaque.Default[Int] + + val foo = Foo(42) + Array(foo).apply(0) shouldEqual foo + } + + "Opaque.Default" should "automatically create an apply method" in { + object PersonId extends Opaque.Default[Int] + PersonId(1) shouldEqual 1 + } +} + +object OpaqueTest { + object PosInt extends Opaque[Int] { + def apply(value: Int): Type = wrap { + if (value < 0) 0 else value + } + + implicit final class Ops(private val me: PosInt.Type) extends AnyVal { + def --(other: PosInt.Type): PosInt.Type = wrap { + val result = unwrap(me) - unwrap(other) + if (result < 0) 0 else result + } + } + } +} \ No newline at end of file diff --git a/core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala b/core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala new file mode 100644 index 000000000..f699c80b6 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala @@ -0,0 +1,55 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.Subopaque.* +import com.avsystem.commons.opaque.SubopaqueTest.* +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +final class SubopaqueTest extends AnyFlatSpec with Matchers { + + "SubOpaque" should "create a type with no runtime overhead" in { + PosInt(1) shouldEqual 1 + PosInt(-1) shouldEqual 0 + } + + it should "be a subtype of its Repr" in { + type Foo = Foo.Type + object Foo extends Subopaque.Default[Int] + assertCompiles("Foo(1): Foo") + assertCompiles("Foo(1): Int") + + Foo(1) - Foo(0) shouldEqual Foo(1) + } + + it should "support user ops" in { + (PosInt(3) -- PosInt(1)) shouldEqual PosInt(2) + } + + it should "work in Arrays" in { + object Foo extends Subopaque.Default[Int] + + val foo = Foo(42) + Array(foo).apply(0) shouldEqual foo + } + + "Subopaque.Default" should "automatically create an apply method" in { + object PersonId extends Subopaque.Default[Int] + PersonId(1) shouldEqual 1 + } +} + +object SubopaqueTest { + object PosInt extends Subopaque[Int] { + def apply(value: Int): Type = wrap { + if (value < 0) 0 else value + } + + implicit final class Ops(private val me: PosInt.Type) extends AnyVal { + def --(other: PosInt.Type): PosInt.Type = wrap { + val result = unwrap(me) - unwrap(other) + if (result < 0) 0 else result + } + } + } +} \ No newline at end of file diff --git a/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala b/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala index 7b3c94240..ac34d474a 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala @@ -4,6 +4,7 @@ package serialization import com.avsystem.commons.annotation.AnnotationAggregate import com.avsystem.commons.meta.{AutoOptionalParams, MacroInstances} import com.avsystem.commons.misc.{AutoNamedEnum, NamedEnumCompanion, TypedKey} +import com.avsystem.commons.opaque.{Opaque, Subopaque} import scala.annotation.meta.getter @@ -134,6 +135,9 @@ object CodecTestData { @transparent case class StringId(id: String) object StringId extends TransparentWrapperCompanion[String, StringId] + object SomeOpaque extends Opaque.Default[Int] + object SomeSubopaque extends Subopaque.Default[Int] + trait HasSomeStr { @name("some.str") def str: String @generated def someStrLen: Int = str.length diff --git a/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala index 27ba19dac..8dc26c1d6 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala @@ -72,6 +72,11 @@ abstract class GenCodecRoundtripTest extends AbstractCodecTest { testRoundtrip(StringId("lolfuu"), "lolfuu") } + test("opaque and subopaque") { + testRoundtrip(SomeOpaque(42)) + testRoundtrip(SomeSubopaque(42)) + } + test("case class") { testRoundtrip(SomeCaseClass("dafuq", List(1, 2, 3))) }