-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implemented scala 3 basic derivation based on mirrors
- Loading branch information
Георгий Ковалев
committed
Apr 23, 2024
1 parent
0fc4b10
commit 9501a3c
Showing
24 changed files
with
464 additions
and
60 deletions.
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
modules/core/src/main/scala-2.12/tethys/derivation/JsonReaderDerivation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package tethys.derivation | ||
|
||
|
||
private [tethys] trait JsonReaderDerivation { | ||
|
||
} |
6 changes: 6 additions & 0 deletions
6
modules/core/src/main/scala-2.12/tethys/derivation/JsonWriterDerivation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package tethys.derivation | ||
|
||
|
||
private [tethys] trait JsonWriterDerivation { | ||
|
||
} |
6 changes: 6 additions & 0 deletions
6
modules/core/src/main/scala-2.13+/tethys/derivation/JsonReaderDerivation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package tethys.derivation | ||
|
||
|
||
private [tethys] trait JsonReaderDerivation { | ||
|
||
} |
6 changes: 6 additions & 0 deletions
6
modules/core/src/main/scala-2.13+/tethys/derivation/JsonWriterDerivation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
18
modules/core/src/main/scala-3/tethys/OrdinalEnumReader.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
16
modules/core/src/main/scala-3/tethys/OrdinalEnumWriter.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
18
modules/core/src/main/scala-3/tethys/StringEnumReader.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
14
modules/core/src/main/scala-3/tethys/StringEnumWriter.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
31
modules/core/src/main/scala-3/tethys/derivation/Defaults.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) }: _*) } | ||
|
28 changes: 28 additions & 0 deletions
28
modules/core/src/main/scala-3/tethys/derivation/EnumCompanion.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
|
||
|
82 changes: 82 additions & 0 deletions
82
modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
|
64 changes: 64 additions & 0 deletions
64
modules/core/src/main/scala-3/tethys/derivation/JsonWriterDerivation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.