Skip to content

Commit

Permalink
implemented scala 3 basic derivation based on mirrors
Browse files Browse the repository at this point in the history
  • Loading branch information
Георгий Ковалев committed Apr 23, 2024
1 parent 0fc4b10 commit 9501a3c
Show file tree
Hide file tree
Showing 24 changed files with 464 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tethys.derivation


private [tethys] trait JsonReaderDerivation {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tethys.derivation


private [tethys] trait JsonWriterDerivation {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tethys.derivation


private [tethys] trait JsonReaderDerivation {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tethys.derivation


private [tethys] trait JsonWriterDerivation {

}
18 changes: 18 additions & 0 deletions modules/core/src/main/scala-3/tethys/OrdinalEnumReader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tethys

import tethys.readers.{FieldName, ReaderError}
import tethys.readers.tokens.TokenIterator

trait OrdinalEnumReader[A] extends JsonReader[A]

object OrdinalEnumReader:
inline def derived[A <: scala.reflect.Enum]: OrdinalEnumReader[A] =
new OrdinalEnumReader[A]:
def read(it: TokenIterator)(implicit fieldName: FieldName): A =
if it.currentToken().isNumberValue then
val res = it.int()
it.next()
derivation.EnumCompanion.getByOrdinal[A](res)
else
ReaderError.wrongJson(s"Expected int value but found: ${it.currentToken()}")

16 changes: 16 additions & 0 deletions modules/core/src/main/scala-3/tethys/OrdinalEnumWriter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package tethys
import tethys.writers.tokens.TokenWriter


trait OrdinalEnumWriter[A] extends JsonWriter[A]

object OrdinalEnumWriter:
inline def derived[A <: scala.reflect.Enum]: OrdinalEnumWriter[A] =
(value: A, tokenWriter: TokenWriter) => tokenWriter.writeNumber(value.ordinal)

inline def withLabel[A <: scala.reflect.Enum](label: String): JsonObjectWriter[A] =
(value: A, tokenWriter: writers.tokens.TokenWriter) =>
tokenWriter.writeFieldName(label)
tokenWriter.writeNumber(value.ordinal)


18 changes: 18 additions & 0 deletions modules/core/src/main/scala-3/tethys/StringEnumReader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tethys
import tethys.readers.{FieldName, ReaderError}
import tethys.readers.tokens.TokenIterator

trait StringEnumReader[A] extends JsonReader[A]

object StringEnumReader:
inline def derived[A <: scala.reflect.Enum]: StringEnumReader[A] =
new StringEnumReader[A]:
def read(it: TokenIterator)(implicit fieldName: FieldName): A =
if it.currentToken().isStringValue then
val res = it.string()
it.next()
derivation.EnumCompanion.getByName[A](res)
else
ReaderError.wrongJson(s"Expected string value but found: ${it.currentToken()}")


14 changes: 14 additions & 0 deletions modules/core/src/main/scala-3/tethys/StringEnumWriter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package tethys
import tethys.writers.tokens.TokenWriter

trait StringEnumWriter[A] extends JsonWriter[A]

object StringEnumWriter:
inline def derived[A <: scala.reflect.Enum]: StringEnumWriter[A] =
(value: A, tokenWriter: TokenWriter) => tokenWriter.writeString(value.toString)

inline def withLabel[A <: scala.reflect.Enum](label: String): JsonObjectWriter[A] =
(value: A, tokenWriter: writers.tokens.TokenWriter) =>
tokenWriter.writeFieldName(label)
tokenWriter.writeString(value.toString)

31 changes: 31 additions & 0 deletions modules/core/src/main/scala-3/tethys/derivation/Defaults.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tethys.derivation


private[tethys]
object Defaults:
inline def collectFrom[T]: Map[Int, Any] = ${ DefaultsMacro.collect[T] }


private[derivation]
object DefaultsMacro:
import scala.quoted.*

def collect[T: Type](using quotes: Quotes): Expr[Map[Int, Any]] =
import quotes.reflect.*
val typeSymbol = TypeRepr.of[T].typeSymbol

val res = typeSymbol.caseFields.zipWithIndex.flatMap {
case (sym, idx) if sym.flags.is(Flags.HasDefault) =>
val defaultValueMethodSym =
typeSymbol.companionClass
.declaredMethod(s"$$lessinit$$greater$$default$$${idx + 1}")
.headOption
.getOrElse(report.errorAndAbort(s"Error while extracting default value for field '${sym.name}'"))

Some(Expr.ofTuple(Expr(idx) -> Ref(typeSymbol.companionModule).select(defaultValueMethodSym).asExprOf[Any]))
case _ =>
None
}

'{ Map(${ Varargs(res) }: _*) }

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tethys.derivation

private[tethys]
object EnumCompanion:
inline def getByName[T](name: String): T =
${ EnumCompanionMacro.getByName[T]('{ name }) }

inline def getByOrdinal[T](ordinal: Int): T =
${ EnumCompanionMacro.getByOrdinal[T]('{ ordinal }) }


private[derivation]
object EnumCompanionMacro:
import scala.quoted.*
def getByName[T: scala.quoted.Type](name: Expr[String])(using quotes: Quotes): Expr[T] =
import quotes.reflect.*
Select.unique(Ref(TypeRepr.of[T].typeSymbol.companionModule), "valueOf")
.appliedToArgs(List(name.asTerm))
.asExprOf[T]


def getByOrdinal[T: scala.quoted.Type](ordinal: Expr[Int])(using quotes: Quotes): Expr[T] =
import quotes.reflect.*
Select.unique(Ref(TypeRepr.of[T].typeSymbol.companionModule), "fromOrdinal")
.appliedToArgs(List(ordinal.asTerm))
.asExprOf[T]


Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package tethys.derivation

import tethys.readers.{FieldName, ReaderError}
import tethys.readers.tokens.TokenIterator
import tethys.JsonReader

import scala.deriving.Mirror
import scala.compiletime.{erasedValue, summonInline, constValue, constValueTuple, summonFrom}


private [tethys] trait JsonReaderDerivation:
inline def derived[A](using mirror: Mirror.ProductOf[A]): JsonReader[A] =
new JsonReader[A]:
override def read(it: TokenIterator)(implicit fieldName: FieldName) =
if !it.currentToken().isObjectStart then
ReaderError.wrongJson("Expected object start but found: " + it.currentToken().toString)
else
it.nextToken()
val labels = constValueTuple[mirror.MirroredElemLabels].toArray.collect { case s: String => s }
val readersByLabels = labels.zip(summonJsonReaders[A, mirror.MirroredElemTypes].zipWithIndex).toMap
val defaults = getOptionsByIndex[mirror.MirroredElemTypes]().toMap ++ Defaults.collectFrom[A]
val optionalLabels = defaults.keys.map(labels(_))

val collectedValues = scala.collection.mutable.Map.from[Int, Any](defaults)
val missingFields = scala.collection.mutable.Set.from(labels) -- optionalLabels

while (!it.currentToken().isObjectEnd)
val jsonName = it.fieldName()
it.nextToken()
val currentIt = it.collectExpression()
readersByLabels.get(jsonName).foreach { (reader, idx) =>
val value: Any = reader.read(currentIt.copy())(fieldName.appendFieldName(jsonName))
collectedValues += idx -> value
missingFields -= jsonName
}

it.nextToken()

if (missingFields.nonEmpty)
ReaderError.wrongJson("Can not extract fields from json: " + missingFields.mkString(", "))
else
mirror.fromProduct:
new Product:
override def productArity = labels.length
override def productElement(n: Int) = collectedValues(n)
override def canEqual(that: Any) =
that match
case that: Product if that.productArity == productArity => true
case _ => false

private inline def summonJsonReaders[T, Elems <: Tuple]: List[JsonReader[?]] =
inline erasedValue[Elems] match
case _: EmptyTuple =>
Nil
case _: (elem *: elems) =>
deriveOrSummon[T, elem] :: summonJsonReaders[T, elems]

private inline def deriveOrSummon[T, Elem]: JsonReader[Elem] =
inline erasedValue[Elem] match
case _: T =>
deriveRec[T, Elem]
case _ =>
summonInline[JsonReader[Elem]]

private inline def deriveRec[T, Elem]: JsonReader[Elem] =
inline erasedValue[T] match
case _: Elem =>
scala.compiletime.error("Recursive derivation is not possible")
case _ =>
JsonReader.derived[Elem](using summonInline[Mirror.ProductOf[Elem]])


private inline def getOptionsByIndex[Elems <: Tuple](idx: Int = 0): List[(Int, None.type)] =
inline erasedValue[Elems] match
case _: EmptyTuple =>
Nil
case _: (Option[?] *: elems) =>
idx -> None :: getOptionsByIndex[elems](idx + 1)
case _: (_ *: elems) =>
getOptionsByIndex[elems](idx + 1)


Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package tethys.derivation

import tethys.{JsonObjectWriter, JsonWriter}
import tethys.writers.tokens.TokenWriter

import scala.deriving.Mirror
import scala.compiletime.{summonInline, erasedValue, summonFrom}

private[tethys] trait JsonWriterDerivation:
inline def derived[A](using mirror: Mirror.Of[A]): JsonObjectWriter[A] =
new JsonObjectWriter[A]:
override def writeValues(value: A, tokenWriter: TokenWriter): Unit =
inline mirror match
case m: Mirror.ProductOf[A] =>
val product = summonInline[A <:< Product](value)
product.productElementNames
.zip(product.productIterator)
.zip(summonJsonWritersForProduct[A, m.MirroredElemTypes])
.foreach { case ((name, value), writer) =>
writer.write(name, value.asInstanceOf, tokenWriter)
}

case m: Mirror.SumOf[A] =>
summonJsonWritersForSum[A, m.MirroredElemTypes](m.ordinal(value))
.writeValues(value.asInstanceOf, tokenWriter)

private inline def summonJsonWritersForSum[T, Elems <: Tuple]: List[JsonObjectWriter[?]] =
inline erasedValue[Elems] match
case _: EmptyTuple =>
Nil
case _: (elem *: elems) =>
summonOrDeriveJsonWriterForSum[T, elem] :: summonJsonWritersForSum[T, elems]

private inline def summonJsonWritersForProduct[T, Elems <: Tuple]: List[JsonWriter[?]] =
inline erasedValue[Elems] match
case _: EmptyTuple =>
Nil
case _: (elem *: elems) =>
summonOrDeriveJsonWriterForProduct[T, elem] :: summonJsonWritersForProduct[T, elems]

private inline def summonOrDeriveJsonWriterForSum[T, Elem]: JsonObjectWriter[Elem] =
summonFrom[JsonWriter[Elem]] {
case writer: JsonObjectWriter[Elem] =>
writer
case writer: JsonWriter[Elem] =>
scala.compiletime.error("JsonObjectWriter required for the children types, but JsonWriter found")
case _ =>
deriveRec[T, Elem]
}

private inline def summonOrDeriveJsonWriterForProduct[T, Elem]: JsonWriter[Elem] =
summonFrom[JsonWriter[Elem]] {
case writer: JsonWriter[Elem] =>
writer
case _ =>
deriveRec[T, Elem]
}

private inline def deriveRec[T, Elem]: JsonObjectWriter[Elem] =
inline erasedValue[T] match
case _: Elem =>
scala.compiletime.error("Recursive derivation is not possible")
case value =>
JsonWriter.derived[Elem](using summonInline[Mirror.Of[Elem]])
2 changes: 1 addition & 1 deletion modules/core/src/main/scala/tethys/JsonObjectWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ trait JsonObjectWriter[A] extends JsonWriter[A] {
}
}

object JsonObjectWriter extends LowPriorityJsonObjectWriters {
object JsonObjectWriter extends LowPriorityJsonObjectWriters with derivation.JsonWriterDerivation {
def apply[A](implicit jsonObjectWriter: JsonObjectWriter[A]): JsonObjectWriter[A] = jsonObjectWriter
}

Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/main/scala/tethys/JsonReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ trait JsonReader[@specialized(specializations) A] {
}
}

object JsonReader extends AllJsonReaders {
object JsonReader extends AllJsonReaders with derivation.JsonReaderDerivation {
def apply[A](implicit jsonReader: JsonReader[A]): JsonReader[A] = jsonReader

val builder: JsonReaderBuilder.type = JsonReaderBuilder
Expand Down
3 changes: 2 additions & 1 deletion modules/core/src/main/scala/tethys/JsonWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ trait JsonWriter[@specialized(specializations) A] {
}
}

object JsonWriter extends AllJsonWriters {
object JsonWriter extends AllJsonWriters with derivation.JsonWriterDerivation {

def apply[A](implicit jsonWriter: JsonWriter[A]): JsonWriter[A] = jsonWriter

def obj[A]: SimpleJsonObjectWriter[A] = SimpleJsonObjectWriter[A]
Expand Down
Loading

0 comments on commit 9501a3c

Please sign in to comment.