From 7931aa96f31d3df54baa680d1998b40d6e5aab96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D0=BE=D1=80=D0=B3=D0=B8=D0=B9=20=D0=9A=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=D0=B5=D0=B2?= Date: Sun, 21 Apr 2024 14:17:51 +0500 Subject: [PATCH] implemented scala 3 basic derivation based on mirrors --- .../derivation/JsonReaderDerivation.scala | 6 + .../derivation/JsonWriterDerivation.scala | 6 + .../derivation/JsonReaderDerivation.scala | 6 + .../derivation/JsonWriterDerivation.scala | 6 + .../scala-3/tethys/OrdinalEnumReader.scala | 18 +++ .../scala-3/tethys/OrdinalEnumWriter.scala | 16 +++ .../scala-3/tethys/StringEnumReader.scala | 18 +++ .../scala-3/tethys/StringEnumWriter.scala | 14 +++ .../scala-3/tethys/derivation/Defaults.scala | 31 +++++ .../tethys/derivation/EnumCompanion.scala | 28 +++++ .../derivation/JsonReaderDerivation.scala | 90 ++++++++++++++ .../derivation/JsonWriterDerivation.scala | 59 +++++++++ .../main/scala/tethys/JsonObjectWriter.scala | 2 +- .../src/main/scala/tethys/JsonReader.scala | 2 +- .../src/main/scala/tethys/JsonWriter.scala | 3 +- .../test/scala-3/tethys/DerivationSpec.scala | 117 ++++++++++++++++++ .../tethys/derivation/AutoDerivation.scala | 2 + .../derivation/SemiautoDerivation.scala | 6 +- .../derivation/SemiautoDerivationMacro.scala | 30 ++++- .../scala-3/tethys/derivation/package.scala | 24 ---- .../derivation/AutoWriterDerivationTest.scala | 4 +- .../SemiautoReaderDerivationTest.scala | 16 +-- .../SemiautoWriterDerivationTest.scala | 27 ++-- .../tethys/derivation/SimpleSealedType.scala | 4 +- 24 files changed, 473 insertions(+), 62 deletions(-) create mode 100644 modules/core/src/main/scala-2.12/tethys/derivation/JsonReaderDerivation.scala create mode 100644 modules/core/src/main/scala-2.12/tethys/derivation/JsonWriterDerivation.scala create mode 100644 modules/core/src/main/scala-2.13+/tethys/derivation/JsonReaderDerivation.scala create mode 100644 modules/core/src/main/scala-2.13+/tethys/derivation/JsonWriterDerivation.scala create mode 100644 modules/core/src/main/scala-3/tethys/OrdinalEnumReader.scala create mode 100644 modules/core/src/main/scala-3/tethys/OrdinalEnumWriter.scala create mode 100644 modules/core/src/main/scala-3/tethys/StringEnumReader.scala create mode 100644 modules/core/src/main/scala-3/tethys/StringEnumWriter.scala create mode 100644 modules/core/src/main/scala-3/tethys/derivation/Defaults.scala create mode 100644 modules/core/src/main/scala-3/tethys/derivation/EnumCompanion.scala create mode 100644 modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala create mode 100644 modules/core/src/main/scala-3/tethys/derivation/JsonWriterDerivation.scala create mode 100644 modules/core/src/test/scala-3/tethys/DerivationSpec.scala delete mode 100644 modules/macro-derivation/src/main/scala-3/tethys/derivation/package.scala diff --git a/modules/core/src/main/scala-2.12/tethys/derivation/JsonReaderDerivation.scala b/modules/core/src/main/scala-2.12/tethys/derivation/JsonReaderDerivation.scala new file mode 100644 index 00000000..b9cd2274 --- /dev/null +++ b/modules/core/src/main/scala-2.12/tethys/derivation/JsonReaderDerivation.scala @@ -0,0 +1,6 @@ +package tethys.derivation + + +private [tethys] trait JsonReaderDerivation { + +} \ No newline at end of file diff --git a/modules/core/src/main/scala-2.12/tethys/derivation/JsonWriterDerivation.scala b/modules/core/src/main/scala-2.12/tethys/derivation/JsonWriterDerivation.scala new file mode 100644 index 00000000..65680ab5 --- /dev/null +++ b/modules/core/src/main/scala-2.12/tethys/derivation/JsonWriterDerivation.scala @@ -0,0 +1,6 @@ +package tethys.derivation + + +private [tethys] trait JsonWriterDerivation { + +} \ No newline at end of file diff --git a/modules/core/src/main/scala-2.13+/tethys/derivation/JsonReaderDerivation.scala b/modules/core/src/main/scala-2.13+/tethys/derivation/JsonReaderDerivation.scala new file mode 100644 index 00000000..b9cd2274 --- /dev/null +++ b/modules/core/src/main/scala-2.13+/tethys/derivation/JsonReaderDerivation.scala @@ -0,0 +1,6 @@ +package tethys.derivation + + +private [tethys] trait JsonReaderDerivation { + +} \ No newline at end of file diff --git a/modules/core/src/main/scala-2.13+/tethys/derivation/JsonWriterDerivation.scala b/modules/core/src/main/scala-2.13+/tethys/derivation/JsonWriterDerivation.scala new file mode 100644 index 00000000..65680ab5 --- /dev/null +++ b/modules/core/src/main/scala-2.13+/tethys/derivation/JsonWriterDerivation.scala @@ -0,0 +1,6 @@ +package tethys.derivation + + +private [tethys] trait JsonWriterDerivation { + +} \ No newline at end of file diff --git a/modules/core/src/main/scala-3/tethys/OrdinalEnumReader.scala b/modules/core/src/main/scala-3/tethys/OrdinalEnumReader.scala new file mode 100644 index 00000000..4d6b9573 --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/OrdinalEnumReader.scala @@ -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()}") + diff --git a/modules/core/src/main/scala-3/tethys/OrdinalEnumWriter.scala b/modules/core/src/main/scala-3/tethys/OrdinalEnumWriter.scala new file mode 100644 index 00000000..cceb2deb --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/OrdinalEnumWriter.scala @@ -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) + + diff --git a/modules/core/src/main/scala-3/tethys/StringEnumReader.scala b/modules/core/src/main/scala-3/tethys/StringEnumReader.scala new file mode 100644 index 00000000..987f2697 --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/StringEnumReader.scala @@ -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()}") + + diff --git a/modules/core/src/main/scala-3/tethys/StringEnumWriter.scala b/modules/core/src/main/scala-3/tethys/StringEnumWriter.scala new file mode 100644 index 00000000..2c54183e --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/StringEnumWriter.scala @@ -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) + diff --git a/modules/core/src/main/scala-3/tethys/derivation/Defaults.scala b/modules/core/src/main/scala-3/tethys/derivation/Defaults.scala new file mode 100644 index 00000000..9f33abcd --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/derivation/Defaults.scala @@ -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) }: _*) } + diff --git a/modules/core/src/main/scala-3/tethys/derivation/EnumCompanion.scala b/modules/core/src/main/scala-3/tethys/derivation/EnumCompanion.scala new file mode 100644 index 00000000..2b977f86 --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/derivation/EnumCompanion.scala @@ -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] + + diff --git a/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala b/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala new file mode 100644 index 00000000..5cc0a1ef --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala @@ -0,0 +1,90 @@ +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](0).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 value => + JsonReader.derived[Elem](using summonInline[Mirror.ProductOf[Elem]]) + + + private inline def getLabels[T <: Tuple]: List[String] = + inline erasedValue[T] match + case EmptyTuple => Nil + case _: (t *: ts) => + constValue[t].asInstanceOf[String] :: getLabels[ts] + + + private inline def getOptionsByIndex[Elems <: Tuple](idx: Int): List[(Int, Option[?])] = + inline erasedValue[Elems] match + case _: EmptyTuple => + Nil + case _: (elem *: elems) => + inline erasedValue[elem] match + case _: Option[_] => + idx -> None :: getOptionsByIndex[elems](idx + 1) + case _ => + getOptionsByIndex[elems](idx + 1) + diff --git a/modules/core/src/main/scala-3/tethys/derivation/JsonWriterDerivation.scala b/modules/core/src/main/scala-3/tethys/derivation/JsonWriterDerivation.scala new file mode 100644 index 00000000..0abb4e97 --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/derivation/JsonWriterDerivation.scala @@ -0,0 +1,59 @@ +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(summonJsonWriters[A, m.MirroredElemTypes]) + .foreach { case ((name, value), writer) => + writer.write(name, value.asInstanceOf, tokenWriter) + } + + case m: Mirror.SumOf[A] => + summonJsonWriters[A, m.MirroredElemTypes](m.ordinal(value)) + .asInstanceOf[JsonObjectWriter[A]] + .writeValues(value, tokenWriter) + + + private inline def summonJsonWriters[T, Elems <: Tuple]: List[JsonWriter[?]] = + inline erasedValue[Elems] match + case _: EmptyTuple => + Nil + case _: (elem *: elems) => + deriveOrSummon[T, elem] :: summonJsonWriters[T, elems] + + private inline def deriveOrSummon[T, Elem]: JsonWriter[Elem] = + summonFrom[scala.util.NotGiven[Elem =:= T]] { + case found: scala.util.NotGiven[_] => + summonFrom[JsonWriter[Elem]] { + case writer: JsonWriter[Elem] => + writer + case _ => + deriveRec[T, Elem] + } + case _ => + inline erasedValue[Elem] match + case _: T => + deriveRec[T, Elem] + case _ => + summonInline[JsonWriter[Elem]] + + } + + private inline def deriveRec[T, Elem]: JsonWriter[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]]) \ No newline at end of file diff --git a/modules/core/src/main/scala/tethys/JsonObjectWriter.scala b/modules/core/src/main/scala/tethys/JsonObjectWriter.scala index ad8fbc72..42ce0100 100644 --- a/modules/core/src/main/scala/tethys/JsonObjectWriter.scala +++ b/modules/core/src/main/scala/tethys/JsonObjectWriter.scala @@ -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 } diff --git a/modules/core/src/main/scala/tethys/JsonReader.scala b/modules/core/src/main/scala/tethys/JsonReader.scala index 0fe0b174..14d99b90 100644 --- a/modules/core/src/main/scala/tethys/JsonReader.scala +++ b/modules/core/src/main/scala/tethys/JsonReader.scala @@ -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 diff --git a/modules/core/src/main/scala/tethys/JsonWriter.scala b/modules/core/src/main/scala/tethys/JsonWriter.scala index 205263a7..1474ddea 100644 --- a/modules/core/src/main/scala/tethys/JsonWriter.scala +++ b/modules/core/src/main/scala/tethys/JsonWriter.scala @@ -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] diff --git a/modules/core/src/test/scala-3/tethys/DerivationSpec.scala b/modules/core/src/test/scala-3/tethys/DerivationSpec.scala new file mode 100644 index 00000000..78e8df4d --- /dev/null +++ b/modules/core/src/test/scala-3/tethys/DerivationSpec.scala @@ -0,0 +1,117 @@ +package tethys + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import tethys.commons.TokenNode.obj +import tethys.commons.{Token, TokenNode} +import tethys.readers.tokens.QueueIterator +import tethys.writers.tokens.SimpleTokenWriter.SimpleTokenWriterOps + +class DerivationSpec extends AnyFlatSpec with Matchers { + def read[A: JsonReader](nodes: List[TokenNode]): A = { + val it = QueueIterator(nodes) + val res = it.readJson[A].fold(throw _, identity) + it.currentToken() shouldBe Token.Empty + res + } + + it should "compile and correctly write and read product" in { + case class Person(id: Int, name: String, phone: Option[String], default: String = "") derives JsonWriter, JsonReader + + case class Wrapper(person: Person) derives JsonWriter, JsonReader + + Person(2, "Peter", None).asTokenList shouldBe obj( + "id" -> 2, + "name" -> "Peter", + "default" -> "" + ) + + Wrapper(Person(3, "Parker", None, "abc")).asTokenList shouldBe obj( + "person" -> obj("id" -> 3, "name" -> "Parker", "default" -> "abc") + ) + + read[Person](obj("id" -> 1, "name" -> "abc")) shouldBe Person(1, "abc", None) + + read[Person]( + obj( + "abd" -> 3, + "name" -> "abc", + "id" -> 1, + "default" -> "abc" + ) + ) shouldBe Person(1, "abc", None, "abc") + + read[Wrapper]( + obj( + "abc" -> 5, + "person" -> obj("id" -> 3, "name" -> "Parker", "phone" -> "123") + ) + ) shouldBe Wrapper(Person(3, "Parker", Some("123"))) + } + + it should "compile and correctly write sum" in { + sealed trait A derives JsonWriter + + case class B(b: Int, i: String) extends A derives JsonWriter + + case class C(c: String) extends A derives JsonWriter + + + (B(2, "abc"): A).asTokenList shouldBe obj( + "b" -> 2, + "i" -> "abc" + ) + } + + it should "compile and correctly read/write enum with StringEnumWriter" in { + enum A derives StringEnumWriter, StringEnumReader: + case B, C + + A.B.asTokenList shouldBe TokenNode.value("B") + A.C.asTokenList shouldBe TokenNode.value("C") + + read[A]( + TokenNode.value("B") + ) shouldBe A.B + + read[A]( + TokenNode.value("C") + ) shouldBe A.C + } + + it should "compile and correctly read/write enum with OrdinalEnumWriter" in { + enum A derives OrdinalEnumWriter, OrdinalEnumReader: + case B, C + + A.B.asTokenList shouldBe TokenNode.value(0) + A.C.asTokenList shouldBe TokenNode.value(1) + + read[A]( + TokenNode.value(0) + ) shouldBe A.B + + read[A]( + TokenNode.value(1) + ) shouldBe A.C + } + + it should "compile and correcly write enum obj with discriminator" in { + enum A: + case B, C + + { + given JsonWriter[A] = StringEnumWriter.withLabel("__type") + A.B.asTokenList shouldBe obj("__type" -> "B") + A.C.asTokenList shouldBe obj("__type" -> "C") + } + + { + given JsonWriter[A] = OrdinalEnumWriter.withLabel("__type") + + A.B.asTokenList shouldBe obj("__type" -> 0) + A.C.asTokenList shouldBe obj("__type" -> 1) + } + } + + +} diff --git a/modules/macro-derivation/src/main/scala-3/tethys/derivation/AutoDerivation.scala b/modules/macro-derivation/src/main/scala-3/tethys/derivation/AutoDerivation.scala index 59eead6d..f4803b6e 100644 --- a/modules/macro-derivation/src/main/scala-3/tethys/derivation/AutoDerivation.scala +++ b/modules/macro-derivation/src/main/scala-3/tethys/derivation/AutoDerivation.scala @@ -7,6 +7,7 @@ import tethys.commons.LowPriorityInstance import tethys.derivation.impl.derivation.AutoDerivationMacro import scala.annotation.experimental +@deprecated("Auto derivation is deprecated and will be removed in future versions. Use `derives` instead") trait AutoDerivation { implicit inline def jsonWriterMaterializer[T]: LowPriorityInstance[JsonObjectWriter[T]] = ${ AutoDerivation.jsonWriterMaterializer[T] } @@ -16,6 +17,7 @@ trait AutoDerivation { } private[this] object AutoDerivation { + @experimental def jsonWriterMaterializer[T: Type](using Quotes): Expr[LowPriorityInstance[JsonObjectWriter[T]]] = new AutoDerivationMacro(quotes).simpleJsonWriter[T] diff --git a/modules/macro-derivation/src/main/scala-3/tethys/derivation/SemiautoDerivation.scala b/modules/macro-derivation/src/main/scala-3/tethys/derivation/SemiautoDerivation.scala index db68b630..7b629c99 100644 --- a/modules/macro-derivation/src/main/scala-3/tethys/derivation/SemiautoDerivation.scala +++ b/modules/macro-derivation/src/main/scala-3/tethys/derivation/SemiautoDerivation.scala @@ -18,8 +18,9 @@ import scala.annotation.compileTimeOnly import scala.annotation.experimental trait SemiautoDerivation { + @deprecated("Use JsonObjectWriter.derived or JsonWriter.derived instead") inline def jsonWriter[T]: JsonObjectWriter[T] = - ${ SemiautoDerivation.jsonWriter[T] } + JsonWriter.derived[T](using scala.compiletime.summonInline[scala.deriving.Mirror.Of[T]]) inline def jsonWriter[T](inline description: WriterDescription[T]): JsonObjectWriter[T] = ${ SemiautoDerivation.jsonWriterWithDescription[T]('description) } @@ -33,8 +34,9 @@ trait SemiautoDerivation { inline def describe[T <: Product](inline builder: => WriterBuilder[T]): WriterDescription[T] = ${ SemiautoDerivation.describeWriter[T]('builder) } + @deprecated("Use JsonReader.derived instead") inline def jsonReader[T]: JsonReader[T] = - ${ SemiautoDerivation.jsonReader[T] } + JsonReader.derived[T](using scala.compiletime.summonInline[scala.deriving.Mirror.ProductOf[T]]) inline def jsonReader[T](inline description: ReaderDescription[T]): JsonReader[T] = ${ SemiautoDerivation.jsonReaderWithDescription[T]('description) } diff --git a/modules/macro-derivation/src/main/scala-3/tethys/derivation/impl/derivation/SemiautoDerivationMacro.scala b/modules/macro-derivation/src/main/scala-3/tethys/derivation/impl/derivation/SemiautoDerivationMacro.scala index a5535094..26960288 100644 --- a/modules/macro-derivation/src/main/scala-3/tethys/derivation/impl/derivation/SemiautoDerivationMacro.scala +++ b/modules/macro-derivation/src/main/scala-3/tethys/derivation/impl/derivation/SemiautoDerivationMacro.scala @@ -40,6 +40,24 @@ class SemiautoDerivationMacro(val quotes: Quotes) extends WriterDerivation with if (tpeSym.isClassDef && tpeSym.flags.is(Flags.Case)) deriveCaseClassWriter[T](description) + else if tpeSym.flags.is(Flags.Enum) then + report.errorAndAbort( + s""" + |Old Enum derivation is not supported anymore + | + |Use JsonWriter.derived for complex enums like this: + | enum ComplexEnum: + | case A(x: B) + | case B + | + |Use StringEnumWriter.derived or OrdinalEnumWriter.derived for basic enums like this: + | enum BasicEnum: + | case A, B + | + |Use StringEnumWriter.withDiscriminator("__type") or OrdinalEnumWriter.withDiscriminator("__type") if you want write an object for BasicEnum like + | { "__type": A } + |""".stripMargin + ) else if (tpeSym.flags.is(Flags.Enum) || (tpeSym.flags.is(Flags.Sealed) && (tpeSym.flags.is(Flags.Trait) || tpeSym.flags.is(Flags.Abstract)))) deriveSealedClassWriter[T](description.config) else @@ -90,7 +108,17 @@ class SemiautoDerivationMacro(val quotes: Quotes) extends WriterDerivation with if (tpe.termSymbol.isNoSymbol) { if (tpeSym.isClassDef && tpeSym.flags.is(Flags.Case)) deriveCaseClassReader[T](description) - else if (tpeSym.flags.is(Flags.Enum | Flags.Abstract)) + else if (tpeSym.flags.is(Flags.Enum)) + report.errorAndAbort( + s""" + |Old Enum derivation is not supported anymore + | + |Use StringEnumReader.derived or OrdinalEnumReader.derived for basic enums like this: + | enum BasicEnum: + | case A, B + |""".stripMargin + ) + else if (tpeSym.flags.is(Flags.Abstract)) deriveEnumReader[T] else report.errorAndAbort(s"Can't derive json reader! '${tpe.show}' isn't a Case Class") diff --git a/modules/macro-derivation/src/main/scala-3/tethys/derivation/package.scala b/modules/macro-derivation/src/main/scala-3/tethys/derivation/package.scala deleted file mode 100644 index 178dd440..00000000 --- a/modules/macro-derivation/src/main/scala-3/tethys/derivation/package.scala +++ /dev/null @@ -1,24 +0,0 @@ -package tethys - -import scala.deriving.Mirror - -import tethys.{JsonObjectWriter, JsonReader, JsonWriter} -import tethys.derivation.semiauto.{jsonReader, jsonWriter} -import scala.annotation.experimental - -package object derivation { - extension (underlying: JsonReader.type) { - @experimental - inline def derived[T](using Mirror.Of[T]): JsonReader[T] = jsonReader[T] - } - - extension (underlying: JsonWriter.type) { - @experimental - inline def derived[T](using Mirror.Of[T]): JsonWriter[T] = jsonWriter[T] - } - - extension (underlying: JsonObjectWriter.type) { - @experimental - inline def derived[T](using Mirror.Of[T]): JsonObjectWriter[T] = jsonWriter[T] - } -} diff --git a/modules/macro-derivation/src/test/scala-3/tethys/derivation/AutoWriterDerivationTest.scala b/modules/macro-derivation/src/test/scala-3/tethys/derivation/AutoWriterDerivationTest.scala index 3cbbf3ea..f3293f88 100644 --- a/modules/macro-derivation/src/test/scala-3/tethys/derivation/AutoWriterDerivationTest.scala +++ b/modules/macro-derivation/src/test/scala-3/tethys/derivation/AutoWriterDerivationTest.scala @@ -83,7 +83,7 @@ class AutoWriterDerivationTest extends AnyFlatSpec with Matchers { case _: ADTWithTypeA[B] => "ADTWithTypeA" case _: ADTWithTypeB[B] => "ADTWithTypeB" } - simpleJsonObjectWriter ++ jsonWriter[ADTWithType[B]] + simpleJsonObjectWriter ++ auto.jsonWriterMaterializer[ADTWithType[B]].instance } (ADTWithTypeB[Int](1, ADTWithTypeA[Int](2)): ADTWithType[Int]).asTokenList shouldBe obj( @@ -104,7 +104,7 @@ class AutoWriterDerivationTest extends AnyFlatSpec with Matchers { implicit val simpleClassWriter: JsonObjectWriter[SimpleClass] = JsonWriter.obj[SimpleClass].addField("b")(_.b) implicit val justObjectWriter: JsonObjectWriter[JustObject.type] = JsonWriter.obj.addField("type")(_ => "JustObject") - implicit val sealedWriter: JsonWriter[SimpleSealedType] = jsonWriter[SimpleSealedType] + implicit val sealedWriter: JsonWriter[SimpleSealedType] = auto.jsonWriterMaterializer[SimpleSealedType].instance def write(simpleSealedType: SimpleSealedType): List[TokenNode] = simpleSealedType.asTokenList diff --git a/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoReaderDerivationTest.scala b/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoReaderDerivationTest.scala index 45480ffe..8200c616 100644 --- a/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoReaderDerivationTest.scala +++ b/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoReaderDerivationTest.scala @@ -2,16 +2,14 @@ package tethys.derivation import org.scalatest.matchers.should.Matchers import org.scalatest.flatspec.AnyFlatSpec -import tethys.JsonReader -import tethys.commons.TokenNode.{value => token, *} +import tethys.{JsonReader, StringEnumReader, TokenIteratorOps} +import tethys.commons.TokenNode.{value as token, *} import tethys.commons.{Token, TokenNode} import tethys.derivation.builder.{FieldStyle, ReaderBuilder, ReaderDerivationConfig} import tethys.derivation.semiauto.* import tethys.readers.ReaderError import tethys.readers.tokens.QueueIterator -import tethys.TokenIteratorOps - class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { def read[A: JsonReader](nodes: List[TokenNode]): A = { @@ -44,7 +42,7 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { } it should "derive reader for recursive type" in { - implicit val recursiveReader: JsonReader[RecursiveType] = jsonReader[RecursiveType] + implicit lazy val recursiveReader: JsonReader[RecursiveType] = jsonReader[RecursiveType] read[RecursiveType](obj( "a" -> 1, @@ -326,9 +324,7 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { it should "derive reader for simple enum" in { - implicit val oneReader: JsonReader[SimpleEnum.ONE.type] = jsonReader[SimpleEnum.ONE.type] - implicit val twoReader: JsonReader[SimpleEnum.TWO.type] = jsonReader[SimpleEnum.TWO.type] - implicit val simpleEnumReader: JsonReader[SimpleEnum] = jsonReader[SimpleEnum] + implicit val simpleEnumReader: JsonReader[SimpleEnum] = StringEnumReader.derived read[SimpleEnum]( token(SimpleEnum.ONE.toString) @@ -340,9 +336,7 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { } it should "derive reader for parametrized enum" in { - implicit val oneReader: JsonReader[ParametrizedEnum.ONE.type] = jsonReader[ParametrizedEnum.ONE.type] - implicit val twoReader: JsonReader[ParametrizedEnum.TWO.type] = jsonReader[ParametrizedEnum.TWO.type] - implicit val parametrizedEnumReader: JsonReader[ParametrizedEnum] = jsonReader[ParametrizedEnum] + implicit val parametrizedEnumReader: JsonReader[ParametrizedEnum] = StringEnumReader.derived read[ParametrizedEnum]( token(ParametrizedEnum.ONE.toString) diff --git a/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoWriterDerivationTest.scala b/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoWriterDerivationTest.scala index f092517e..e62cd63a 100644 --- a/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoWriterDerivationTest.scala +++ b/modules/macro-derivation/src/test/scala-3/tethys/derivation/SemiautoWriterDerivationTest.scala @@ -3,7 +3,7 @@ package tethys.derivation import org.scalatest.matchers.should.Matchers import org.scalatest.flatspec.AnyFlatSpec import tethys.commons.TokenNode -import tethys.{JsonObjectWriter, JsonWriter} +import tethys.{JsonObjectWriter, JsonWriter, StringEnumWriter} import tethys.derivation.builder.{FieldStyle, WriterBuilder, WriterDerivationConfig} import tethys.writers.tokens.SimpleTokenWriter.* import tethys.commons.TokenNode.{value as token, *} @@ -99,6 +99,10 @@ class SemiautoWriterDerivationTest extends AnyFlatSpec with Matchers { ) } + enum A derives JsonObjectWriter: + case B(a: Int) + case C(b: String) + it should "derive writer for A => B => A cycle" in { implicit lazy val testWriter1: JsonWriter[ComplexRecursionA] = jsonWriter[ComplexRecursionA] implicit lazy val testWriter2: JsonWriter[ComplexRecursionB] = jsonWriter[ComplexRecursionB] @@ -164,6 +168,7 @@ class SemiautoWriterDerivationTest extends AnyFlatSpec with Matchers { implicit val justObjectWriter: JsonObjectWriter[JustObject.type] = JsonWriter.obj.addField("type")(_ => "JustObject") implicit val subChildWriter: JsonObjectWriter[SubChild] = jsonWriter[SubChild] + implicit val sealedSubWriter: JsonWriter[SimpleSealedTypeSub] = jsonWriter[SimpleSealedTypeSub] implicit val sealedWriter: JsonWriter[SimpleSealedType] = jsonWriter[SimpleSealedType] def write(simpleSealedType: SimpleSealedType): List[TokenNode] = simpleSealedType.asTokenList @@ -193,40 +198,28 @@ class SemiautoWriterDerivationTest extends AnyFlatSpec with Matchers { } it should "derive writer for simple enum" in { - implicit val oneWriter: JsonObjectWriter[SimpleEnum.ONE.type] = jsonWriter[SimpleEnum.ONE.type] - implicit val twoWriter: JsonObjectWriter[SimpleEnum.TWO.type] = jsonWriter[SimpleEnum.TWO.type] - implicit val simpleEnumWriter: JsonWriter[SimpleEnum] = jsonWriter[SimpleEnum] + implicit val simpleEnumWriter: JsonWriter[SimpleEnum] = StringEnumWriter.derived SimpleEnum.ONE.asTokenList shouldBe token("ONE") SimpleEnum.TWO.asTokenList shouldBe token("TWO") } it should "derive writer for parametrized enum" in { - implicit val oneWriter: JsonObjectWriter[ParametrizedEnum.ONE.type] = jsonWriter[ParametrizedEnum.ONE.type] - implicit val twoWriter: JsonObjectWriter[ParametrizedEnum.TWO.type] = jsonWriter[ParametrizedEnum.TWO.type] - implicit val parametrizedEnumWriter: JsonWriter[ParametrizedEnum] = jsonWriter[ParametrizedEnum] + implicit val parametrizedEnumWriter: JsonWriter[ParametrizedEnum] = StringEnumWriter.derived ParametrizedEnum.ONE.asTokenList shouldBe token("ONE") ParametrizedEnum.TWO.asTokenList shouldBe token("TWO") } it should "derive writer with discriminator for simple enum" in { - implicit val oneWriter: JsonObjectWriter[SimpleEnum.ONE.type] = jsonWriter[SimpleEnum.ONE.type] - implicit val twoWriter: JsonObjectWriter[SimpleEnum.TWO.type] = jsonWriter[SimpleEnum.TWO.type] - implicit val simpleEnumWriter: JsonWriter[SimpleEnum] = jsonWriter[SimpleEnum]( - WriterDerivationConfig.empty.withDiscriminator("__type") - ) + implicit val simpleEnumWriter: JsonWriter[SimpleEnum] = StringEnumWriter.withLabel("__type") SimpleEnum.ONE.asTokenList shouldBe obj("__type" -> "ONE") SimpleEnum.TWO.asTokenList shouldBe obj("__type" -> "TWO") } it should "derive writer with discriminator for parametrized enum" in { - implicit val oneWriter: JsonObjectWriter[ParametrizedEnum.ONE.type] = jsonWriter[ParametrizedEnum.ONE.type] - implicit val twoWriter: JsonObjectWriter[ParametrizedEnum.TWO.type] = jsonWriter[ParametrizedEnum.TWO.type] - implicit val simpleEnumWriter: JsonWriter[ParametrizedEnum] = jsonWriter[ParametrizedEnum]( - WriterDerivationConfig.empty.withDiscriminator("__type") - ) + implicit val simpleEnumWriter: JsonWriter[ParametrizedEnum] = StringEnumWriter.withLabel("__type") ParametrizedEnum.ONE.asTokenList shouldBe obj ("__type" -> "ONE") ParametrizedEnum.TWO.asTokenList shouldBe obj ("__type" -> "TWO") diff --git a/modules/macro-derivation/src/test/scala-3/tethys/derivation/SimpleSealedType.scala b/modules/macro-derivation/src/test/scala-3/tethys/derivation/SimpleSealedType.scala index 2a5d6d76..3e2e7416 100644 --- a/modules/macro-derivation/src/test/scala-3/tethys/derivation/SimpleSealedType.scala +++ b/modules/macro-derivation/src/test/scala-3/tethys/derivation/SimpleSealedType.scala @@ -2,8 +2,8 @@ package tethys.derivation sealed abstract class SimpleSealedType case class CaseClass(a: Int) extends SimpleSealedType -class SimpleClass(val b: Int) extends SimpleSealedType -object JustObject extends SimpleSealedType +case class SimpleClass(val b: Int) extends SimpleSealedType +case object JustObject extends SimpleSealedType sealed abstract class SimpleSealedTypeSub extends SimpleSealedType case class SubChild(c: Int) extends SimpleSealedTypeSub