Skip to content
Draft
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
12 changes: 12 additions & 0 deletions upickle/implicits/src-3/upickle/implicits/Readers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,17 @@ trait ReadersVersionSpecific
else new HugeCaseObjectContext[T](paramCount) with ObjectContext
}

inline given derivedStringBasedUnionEnumerationReader[T <: String](using IsUnionOf[String, T]): Reader[T] =
lazy val values = UnionDerivation.constValueUnionTuple[String, T]

lazy val valuesList = values.toList.asInstanceOf[List[T]]
// Codec.string.validate(validator.asInstanceOf[Validator[String]]).map(_.asInstanceOf[T])(_.asInstanceOf[String])
new SimpleReader[T] {
override def expectedMsg = s"expected oneOf ${valuesList.asInstanceOf[List[T]].mkString(", ")}"
override def visitString(s: CharSequence, index: Int) =
val v = s.toString
if (valuesList.contains(v)) v.asInstanceOf[T]
else throw new Abort(expectedMsg)
}

end ReadersVersionSpecific
60 changes: 60 additions & 0 deletions upickle/implicits/src-3/upickle/implicits/UnionDerivation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package upickle.implicits

import scala.compiletime.*
import scala.deriving.*
import scala.quoted.*

@scala.annotation.implicitNotFound("${A} is not a union of ${T}")
private[implicits] sealed trait IsUnionOf[T, A]

private[implicits] object IsUnionOf:

private val singleton: IsUnionOf[Any, Any] = new IsUnionOf[Any, Any] {}

transparent inline given derived[T, A]: IsUnionOf[T, A] = ${ deriveImpl[T, A] }

private def deriveImpl[T, A](using quotes: Quotes, t: Type[T], a: Type[A]): Expr[IsUnionOf[T, A]] =
import quotes.reflect.*
val tpe: TypeRepr = TypeRepr.of[A]
val bound: TypeRepr = TypeRepr.of[T]

def validateTypes(tpe: TypeRepr): Unit =
tpe.dealias match
case o: OrType =>
validateTypes(o.left)
validateTypes(o.right)
case o =>
if o <:< bound then ()
else report.errorAndAbort(s"${o.show} is not a subtype of ${bound.show}")

tpe.dealias match
case o: OrType =>
validateTypes(o)
('{ IsUnionOf.singleton.asInstanceOf[IsUnionOf[T, A]] }).asExprOf[IsUnionOf[T, A]]
case o =>
if o <:< bound then ('{ IsUnionOf.singleton.asInstanceOf[IsUnionOf[T, A]] }).asExprOf[IsUnionOf[T, A]]
else report.errorAndAbort(s"${tpe.show} is not a Union")

private[implicits] object UnionDerivation:
transparent inline def constValueUnionTuple[T, A](using IsUnionOf[T, A]): Tuple = ${ constValueUnionTupleImpl[T, A] }

private def constValueUnionTupleImpl[T: Type, A: Type](using Quotes): Expr[Tuple] =
Expr.ofTupleFromSeq(constTypes[T, A])

private def constTypes[T: Type, A: Type](using Quotes): List[Expr[Any]] =
import quotes.reflect.*
val tpe: TypeRepr = TypeRepr.of[A]
val bound: TypeRepr = TypeRepr.of[T]

def transformTypes(tpe: TypeRepr): List[TypeRepr] =
tpe.dealias match
case o: OrType =>
transformTypes(o.left) ::: transformTypes(o.right)
case o: Constant if o <:< bound && o.isSingleton =>
o :: Nil
case o =>
report.errorAndAbort(s"${o.show} is not a subtype of ${bound.show}")

transformTypes(tpe).distinct.map(_.asType match
case '[t] => '{ constValue[t] })

7 changes: 6 additions & 1 deletion upickle/implicits/src-3/upickle/implicits/Writers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@ trait WritersVersionSpecific
inline def derived[T](using Mirror.Of[T], ClassTag[T]): Writer[T] = macroWAll[T]
end WriterExtension


inline given derivedStringBasedUnionEnumerationWriter[T <: String](using IsUnionOf[String, T]): Writer[T] =
new Writer[T] {
override def isJsonDictKey = true
def writeString(v: T): String = v
def write0[R](out: Visitor[_, R], v: T): R = out.visitString(writeString(v), -1)
}
29 changes: 29 additions & 0 deletions upickle/test/src-3/upickletest/UnionTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package upickletest

import ujson.ParseException
import upickletest.TestUtil.rw
import upickle.core.AbortException

import scala.language.implicitConversions
import utest.{assert, intercept, *}
import upickle.default.*

type AorB = "A" | "B"
type AorBorC = AorB | "C"

object UnionTests extends TestSuite {


val tests = Tests {
test("literal union"){
test("strings"){
rw[AorB]("A", "\"A\"")
rw[AorBorC]("C", "\"C\"")
compileError("""rw[AorBorC]("D", "\"D\"")""")
compileError("""rw[AorBorC]("A": String, "\"A\"")""")
}
}
}
}