diff --git a/README.md b/README.md index e0ecd56..06fc0c8 100644 --- a/README.md +++ b/README.md @@ -211,20 +211,19 @@ session.asJson == json ``` ## Sealed traits and enums -To derive **JsonReader** you **must** provide **JsonConfig** with discriminator. +To derive **JsonReader** you **must** provide a discriminator. +This can be done via **selector** annotation Discriminator for **JsonWriter** is optional. If you don't need readers/writers for subtypes, you can omit them, they will be derived recursively for your trait/enum. ```scala 3 +import tethys.selector -sealed trait UserAccount(val typ: String) derives JsonReader, JsonObjectWriter +sealed trait UserAccount(@selector val typ: String) derives JsonReader, JsonObjectWriter object UserAccount: - inline given JsonConfig[UserAccount] = - JsonConfig[UserAccount].discriminateBy(_.typ) - case class Customer( id: Long, phone: String diff --git a/modules/core/src/main/scala-3/tethys/JsonConfig.scala b/modules/core/src/main/scala-3/tethys/JsonConfig.scala deleted file mode 100644 index 843587c..0000000 --- a/modules/core/src/main/scala-3/tethys/JsonConfig.scala +++ /dev/null @@ -1,10 +0,0 @@ -package tethys - -sealed trait JsonConfig[A]: - def discriminateBy[Field](select: A => Field): JsonConfig[A] - - -object JsonConfig: - @scala.annotation.compileTimeOnly("Config must be an inlined given or provided directly to 'derived'") - def apply[A]: JsonConfig[A] = - throw IllegalStateException("Config must be an inlined given or provided directly to 'derived'") \ No newline at end of file diff --git a/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala b/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala index 2883958..7267351 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala @@ -1,32 +1,15 @@ package tethys.derivation import tethys.* +import tethys.readers.FieldName import scala.annotation.tailrec import scala.collection.mutable import scala.compiletime.{constValueTuple, summonInline} import scala.deriving.Mirror import scala.quoted.{Expr, FromExpr, Quotes, ToExpr, Type, Varargs} - -private[derivation] inline def searchJsonWriter[Field]: JsonWriter[Field] = - scala.compiletime.summonFrom[JsonWriter[Field]] { - case writer: JsonWriter[Field] => writer - case _ => scala.compiletime.error("JsonWriter not found") - } - -private[derivation] inline def searchJsonObjectWriter[Field]: JsonObjectWriter[Field] = - scala.compiletime.summonFrom[JsonObjectWriter[Field]] { - case writer: JsonObjectWriter[Field] => writer - case _ => scala.compiletime.error("JsonObjectWriter not found") - } - -private[derivation] inline def searchJsonReader[Field]: JsonReader[Field] = - scala.compiletime.summonFrom[JsonReader[Field]] { - case reader: JsonReader[Field] => reader - case _ => scala.compiletime.error("JsonReader not found") - } - - +import tethys.readers.tokens.{TokenIterator, QueueIterator} +import tethys.commons.TokenNode trait ConfigurationMacroUtils: given Quotes = quotes @@ -38,7 +21,9 @@ trait ConfigurationMacroUtils: case success: ImplicitSearchSuccess => success.tree.asExprOf[T] case failure: ImplicitSearchFailure => - report.errorAndAbort(failure.explanation) + // Not sees statements put in a block (e.g. derived instances) + // So we use summonInline + '{summonInline[T]} def lookupOpt[T: Type]: Option[Expr[T]] = Implicits.search(TypeRepr.of[T]) match @@ -47,22 +32,6 @@ trait ConfigurationMacroUtils: case failure: ImplicitSearchFailure => None - def lookupJsonWriter[T: Type]: Expr[JsonWriter[T]] = - Implicits.search(TypeRepr.of[JsonWriter[T]]) match - case success: ImplicitSearchSuccess => - success.tree.asExprOf[JsonWriter[T]] - case failure: ImplicitSearchFailure => - report.errorAndAbort(failure.explanation) - - - def lookupWriterBuilder[T: Type]: Expr[WriterBuilder[T]] = - Implicits.search(TypeRepr.of[WriterBuilder[T]]) match - case success: ImplicitSearchSuccess => - success.tree.asExprOf[WriterBuilder[T]] - case failure: ImplicitSearchFailure => - '{ WriterBuilder[T](using ${lookup[Mirror.ProductOf[T]]}) } - - def prepareWriterProductFields[T: Type]( config: Expr[WriterBuilder[T]] ): List[WriterField] = @@ -395,15 +364,34 @@ trait ConfigurationMacroUtils: .map { case (symbol, idx) => val default = defaults.get(idx).map(_.asExprOf[Any]) macroConfig.extracted.get(symbol.name) match + case Some(field: ReaderField.Basic) => + val updatedDefault = field.extractor match + case None => default + case Some((tpe, lambda)) => + Option.when(tpe.isOption)('{ None }).orElse(default) + .map(default => Apply(Select.unique(lambda, "apply"), List(default.asTerm))).map(_.asExprOf[Any]) + + field.update(idx, updatedDefault, macroConfig.fieldStyle) + case Some(field) => field.update(idx, default, macroConfig.fieldStyle) + case None => ReaderField .Basic(symbol.name, tpe.memberType(symbol), None) .update(idx, default, macroConfig.fieldStyle) } - checkLoops(fields) - (sortDependencies(fields), macroConfig.isStrict) + val existingFieldNames = fields.map(_.name).toSet + val additionalFields = fields.collect { + case field: ReaderField.Extracted => + field.extractors.collect { case (name, tpe) if !existingFieldNames(name) => + ReaderField.Basic(name, tpe, None, -1, Option.when(tpe.isOption)('{None})) + } + }.flatten.distinctBy(_.name) + val allFields = fields ::: additionalFields + checkLoops(allFields) + (sortDependencies(allFields), macroConfig.isStrict) + private def sortDependencies(fields: List[ReaderField]): List[ReaderField] = val known = fields.map(_.name).toSet @@ -528,16 +516,14 @@ trait ConfigurationMacroUtils: } => if acc.extracted.contains(field.name) then exitExtractionAlreadyDefined(field.name) - val lambda = - Typed(fun.asTerm, TypeTree.of[Any => Any]).asExprOf[Any => Any] + loop( config = rest, acc = acc.withExtracted( ReaderField.Basic( name = field.name, - tpe = TypeRepr.of[t1], - extractor = Some((TypeRepr.of[t1], lambda)), - default = Option.when(TypeRepr.of[t1].isOption)('{ None }) + tpe = TypeRepr.of[t], + extractor = Some((TypeRepr.of[t1], Typed(fun.asTerm, TypeTree.of[t1 => t]))) ) ) ) @@ -557,7 +543,7 @@ trait ConfigurationMacroUtils: def loopInner( term: Term, extractors: List[(String, TypeRepr)] = Nil, - lambda: Expr[Any => Any] = '{ identity[Any] } + lambda: Term = '{ identity[Any] }.asTerm ): ReaderBuilderMacroConfig = term match case config @ Apply( @@ -568,15 +554,16 @@ trait ConfigurationMacroUtils: exitExtractionAlreadyDefined(name) loop( config = term.asExprOf[ReaderBuilder[T]], - acc = acc.withExtracted( - ReaderField.Extracted( - name, - tpt.tpe, - extractors, - lambda, - reader = false + acc = acc + .withExtracted( + ReaderField.Extracted( + name, + tpt.tpe, + extractors, + lambda, + reader = false + ) ) - ) ) case config @ Apply( TypeApply(Select(term, "extractReader"), List(tpt)), @@ -600,22 +587,15 @@ trait ConfigurationMacroUtils: loopInner( term = term, extractors = Nil, - lambda = '{ - ${ lambda.asExprOf[Any] }.asInstanceOf[Any => Any] - } + lambda = lambda ) case Apply(Apply(Select(term, "product"), List(mirror)), _) => loopInner( term = term, extractors = Nil, - lambda = '{ - ${ - Select - .unique(mirror, "fromProduct") - .etaExpand(Symbol.spliceOwner) - .asExprOf[Any] - }.asInstanceOf[Any => Any] - } + lambda = Select + .unique(mirror, "fromProduct") + .etaExpand(Symbol.spliceOwner) ) case Apply( TypeApply(Select(term, "from" | "and"), List(tpt)), @@ -646,68 +626,34 @@ trait ConfigurationMacroUtils: loop(traverseTree(config.asTerm).asExprOf[ReaderBuilder[T]]) - def parseSumConfig[T: Type](config: Expr[JsonConfig[T]]) = + def parseSumConfig[T: Type]: SumMacroConfig = val tpe = TypeRepr.of[T] - @scala.annotation.tailrec - def loop( - config: Expr[JsonConfig[T]], - acc: SumMacroConfig = SumMacroConfig() - ): SumMacroConfig = - config match - case '{ JsonConfig[T] } => - acc - - case '{ - ($rest: JsonConfig[T]).discriminateBy[fieldType](${ - SelectedField(field) - }) - } => - val fieldTpe = TypeRepr.of[fieldType] - val symbol = tpe.typeSymbol.fieldMembers - .find(_.name == field.name) - .getOrElse( - report.errorAndAbort( - s"Selector of type ${fieldTpe.show(using Printer.TypeReprShortCode)} not found in ${tpe - .show(using Printer.TypeReprShortCode)}" - ) - ) + val tpt = TypeTree.of[T] + val annotation = TypeRepr.of[selector].typeSymbol + val selectors = tpe.typeSymbol.primaryConstructor.paramSymss.flatten + .filter(_.hasAnnotation(annotation)) + + selectors match + case constructorSymbol :: Nil => + val symbol = tpe.typeSymbol.fieldMembers.find(_.name == constructorSymbol.name) + .getOrElse(report.errorAndAbort(s"Not found symbol corresponding to constructor symbol ${constructorSymbol.name}")) + + val discriminators: List[Term] = getAllChildren(tpe).map { + case tpe: TypeRef => + Select(stub(tpe), symbol) + case tpe: TermRef => + Select(Ref(tpe.termSymbol), symbol) + case tpe => + report.errorAndAbort(s"Unknown tpe: $tpe") + } + SumMacroConfig(Some(DiscriminatorConfig(symbol.name, tpe.memberType(symbol), discriminators))) - tpe.typeSymbol.children - .find(child => - child.caseFields.contains(symbol.overridingSymbol(child)) - ) - .foreach { child => - report.errorAndAbort( - s"Overriding discriminator field '${symbol.name}' in ${child.typeRef - .show(using Printer.TypeReprShortCode)} is prohibited" - ) - } + case Nil => + SumMacroConfig(None) - val discriminators: List[Term] = getAllChildren(tpe).map { - case tpe: TypeRef => - Select(stub(tpe), symbol) - case tpe: TermRef => - Select(Ref(tpe.termSymbol), symbol) - case tpe => - report.errorAndAbort(s"Unknown tpe: $tpe") - } + case multiple => + report.errorAndAbort(s"Only one field can be a selector. Found ${multiple.map(_.name).mkString(", ")}") - loop( - rest, - acc.copy(discriminator = - Some( - DiscriminatorConfig(symbol.name, fieldTpe, discriminators) - ) - ) - ) - - case other => - report.errorAndAbort( - s"Unknown tree. Config must be an inlined given.\nTree: ${other.asTerm - .show(using Printer.TreeStructure)}" - ) - - loop(traverseTree(config.asTerm).asExprOf[JsonConfig[T]]) private def stub(tpe: TypeRepr): Term = import quotes.reflect.* @@ -847,9 +793,92 @@ trait ConfigurationMacroUtils: sealed trait ReaderField { def name: String def tpe: TypeRepr + def reader: Boolean + + def initializeFieldCase( + it: Expr[TokenIterator], + fieldName: Expr[FieldName] + ): Option[CaseDef] = + this match + case _: ReaderField.Basic => + Some( + readerTpe.get.asType match { + case '[t] => + CaseDef( + Literal(StringConstant(name)), + None, + Block( + init { + '{${lookup[JsonReader[t]]}.read(${it})(${fieldName}.appendFieldName(${Expr(name)}))}.asTerm + }, + '{}.asTerm + ) + ) + } + ) + + case _: ReaderField.Extracted => + iteratorRef.map { iteratorRef => + CaseDef( + Literal(StringConstant(name)), + None, + Block( + initIterator('{ ${it}.collectExpression() }.asTerm), + '{}.asTerm + ) + ) + } + + + lazy val (initialize, ref, initRef, iteratorRef) = { + val flags = default.fold(Flags.Deferred | Flags.Mutable)(_ => Flags.Mutable) + val symbol = Symbol.newVal(Symbol.spliceOwner, s"${name}Var", tpe, flags, Symbol.noSymbol) + val initSymbol = Symbol.newVal(Symbol.spliceOwner, s"${name}Init", TypeRepr.of[Boolean], Flags.Mutable, Symbol.noSymbol) + val stat = ValDef(symbol, default.map(_.asTerm)) + val initStat = ValDef(initSymbol, Some('{false}.asTerm)) + val iteratorSymbol = Option.when(reader)(Symbol.newVal(Symbol.spliceOwner, s"${name}Iterator", TypeRepr.of[TokenIterator], Flags.Mutable | Flags.Deferred, Symbol.noSymbol)) + val iteratorStat = iteratorSymbol.map(ValDef(_, None)) + val iteratorRef = iteratorStat.map(stat => Ref(stat.symbol)) + (List(stat, initStat) ++ iteratorStat, Ref(stat.symbol), Ref(initStat.symbol), iteratorRef) + } def idx: Int def default: Option[Expr[Any]] + def readerTpe: Option[TypeRepr] = this match + case ReaderField.Basic(name, tpe, extractor, idx, default) => + Some(extractor.map(_._1).getOrElse(tpe)) + case field: ReaderField.Extracted if field.reader => + None + case field: ReaderField.Extracted => + Some(field.tpe) + + + def init(value: Term): List[Statement] = this match + case ReaderField.Basic(_, _, None, _, _) => + List( + Assign(ref, value), + Assign(initRef, '{true}.asTerm) + ) + + case ReaderField.Basic(_, _, Some((_, lambda)), _, _) => + List( + Assign(ref, Apply(Select.unique(lambda, "apply") , List(value))), + Assign(initRef, '{true}.asTerm) + ) + case extracted: ReaderField.Extracted => + List( + Assign(ref, value), + Assign(initRef, '{ true }.asTerm) + ) + + def initIterator(value: Term): List[Statement] = iteratorRef.map { ref => + List( + Assign(ref, value), + Assign(initRef, '{ true }.asTerm) + ) + }.getOrElse(Nil) + + def update( index: Int, @@ -859,77 +888,58 @@ trait ConfigurationMacroUtils: case field: ReaderField.Basic => field.copy( idx = index, - default = field.default.orElse(default), - name = - fieldStyle.fold(field.name)(FieldStyle.applyStyle(field.name, _)) + default = default, + name = fieldStyle.fold(field.name)(FieldStyle.applyStyle(field.name, _)) ) case field: ReaderField.Extracted => field.copy( idx = index, - default = field.default.orElse(default), - name = - fieldStyle.fold(field.name)(FieldStyle.applyStyle(field.name, _)) + default = default, + name = fieldStyle.fold(field.name)(FieldStyle.applyStyle(field.name, _)) ) - - def defaults(existingFields: Set[String]): List[(String, Expr[Any])] = - this match - case field: ReaderField.Basic => - field.default.map(field.name -> _).toList - case field: ReaderField.Extracted => - field.default.map(field.name -> _).toList ::: - field.extractors - .collect { - case (name, tpe) if !existingFields(name) && tpe.isOption => - name -> '{ (None: Any) } - } - - def requiredLabels(existingFields: Set[String]): List[String] = - this match - case field: ReaderField.Basic => List(field.name) - case field: ReaderField.Extracted => - field.extractors - .collect { - case (name, tpe) if !existingFields(name) && !tpe.isOption => name - } - - def readerTypes(existingFields: Set[String]): List[(String, TypeRepr)] = - this match - case ReaderField.Basic(name, tpe, extractor, idx, default) => - List(name -> tpe) - case ReaderField.Extracted( - name, - tpe, - extractors, - lambda, - reader, - idx, - default - ) => - List(name -> tpe).filterNot(_ => reader) ::: - extractors.collect { - case (name, tpe) if !existingFields(name) => name -> tpe - } - } object ReaderField: case class Basic( name: String, tpe: TypeRepr, - extractor: Option[(TypeRepr, Expr[Any => Any])], + extractor: Option[(TypeRepr, Term)], idx: Int = 0, default: Option[Expr[Any]] = None - ) extends ReaderField + ) extends ReaderField: + def reader = false case class Extracted( name: String, tpe: TypeRepr, extractors: List[(String, TypeRepr)], - lambda: Expr[Any => Any], + lambda: Term, reader: Boolean, idx: Int = 0, default: Option[Expr[Any]] = None - ) extends ReaderField + ) extends ReaderField: + def extract(fields: Map[String, Ref], fieldName: Expr[FieldName]): List[Statement] = + val term = extractors match + case (depName, _) :: Nil => + Apply(Select.unique(lambda, "apply"), List(fields(depName))) + case _ => + val value = extractors + .map((name, _) => fields(name)) + .foldRight[Term]('{ EmptyTuple }.asTerm) { (el, acc) => + Select.unique(acc, "*:") + .appliedToTypes(List(el.tpe, acc.tpe)) + .appliedToArgs(List(el)) + } + Select.unique(lambda, "apply").appliedToArgs(List(value)) + + iteratorRef match + case Some(iteratorRef) => + val reader = Typed(term, TypeTree.of[JsonReader[Any]]).asExprOf[JsonReader[Any]] + val it = '{if ${initRef.asExprOf[Boolean]} then ${iteratorRef.asExprOf[TokenIterator]} else QueueIterator(List(TokenNode.NullValueNode))} + val value = '{${reader}.read(${it})(${fieldName}.appendFieldName(${ Expr(name) }))} + init(value.asTerm) + case None => + init(term) case class ReaderBuilderMacroConfig( extracted: Map[String, ReaderField] = Map.empty, @@ -954,35 +964,6 @@ trait ConfigurationMacroUtils: extension (tpe: TypeRepr) def isOption: Boolean = tpe <:< TypeRepr.of[Option[Any]] - extension [A: Type](exprs: Iterable[A])(using Quotes) - def exprOfMutableMap[K: ToExpr: Type, V: Type](using - ev: A <:< (K, Expr[V]) - ): Expr[mutable.Map[K, V]] = - '{ - mutable.Map(${ - Varargs( - exprs.map(value => Expr.ofTuple(Expr(value._1) -> value._2)).toSeq - ) - }: _*) - } - - def exprOfMap[K: ToExpr: Type, V: Type](using - ev: A <:< (K, Expr[V]) - ): Expr[Map[K, V]] = - '{ - Map(${ - Varargs( - exprs.map(value => Expr.ofTuple(Expr(value._1) -> value._2)).toSeq - ) - }: _*) - } - - def exprOfSet(using to: ToExpr[A]): Expr[Set[A]] = - '{ Set(${ Varargs(exprs.map(value => Expr(value)).toSeq) }: _*) } - - def exprOfMutableSet(using to: ToExpr[A]): Expr[mutable.Set[A]] = - '{ mutable.Set(${ Varargs(exprs.map(value => Expr(value)).toSeq) }: _*) } - given FromExpr[FieldStyle] = new FromExpr[FieldStyle]: override def unapply(x: Expr[FieldStyle])(using Quotes diff --git a/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala b/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala index d01c793..326bea0 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala @@ -2,9 +2,8 @@ package tethys.derivation import tethys.writers.tokens.TokenWriter import tethys.readers.{FieldName, ReaderError} -import tethys.commons.{Token, TokenNode} -import tethys.readers.tokens.{QueueIterator, TokenIterator} -import tethys.{JsonConfig, JsonObjectWriter, JsonReader, JsonWriter, ReaderBuilder, WriterBuilder} +import tethys.readers.tokens.TokenIterator +import tethys.{JsonObjectWriter, JsonReader, JsonWriter, ReaderBuilder, WriterBuilder} import scala.Tuple2 import scala.annotation.tailrec @@ -13,33 +12,35 @@ import scala.quoted.* import scala.collection.mutable import scala.deriving.Mirror +case class RecursiveT(rec: Seq[RecursiveT]) + private[tethys] object Derivation: inline def deriveJsonWriterForProduct[T](inline config: WriterBuilder[T]): JsonObjectWriter[T] = ${ DerivationMacro.deriveJsonWriterForProduct[T]('{config})} - inline def deriveJsonWriterForSum[T](inline config: JsonConfig[T]): JsonObjectWriter[T] = - ${ DerivationMacro.deriveJsonWriterForSum[T]('{config}) } + inline def deriveJsonWriterForSum[T]: JsonObjectWriter[T] = + ${ DerivationMacro.deriveJsonWriterForSum[T] } inline def deriveJsonReaderForProduct[T](inline config: ReaderBuilder[T]): JsonReader[T] = ${ DerivationMacro.deriveJsonReaderForProduct[T]('{config})} - inline def deriveJsonReaderForSum[T](inline config: JsonConfig[T]): JsonReader[T] = - ${ DerivationMacro.deriveJsonReaderForSum[T]('{config})} + inline def deriveJsonReaderForSum[T]: JsonReader[T] = + ${ DerivationMacro.deriveJsonReaderForSum[T] } object DerivationMacro: def deriveJsonWriterForProduct[T: Type](config: Expr[WriterBuilder[T]])(using quotes: Quotes): Expr[JsonObjectWriter[T]] = new DerivationMacro(quotes).deriveJsonWriterForProduct[T](config) - def deriveJsonWriterForSum[T: Type](config: Expr[JsonConfig[T]])(using quotes: Quotes): Expr[JsonObjectWriter[T]] = - new DerivationMacro(quotes).deriveJsonWriterForSum[T](config) + def deriveJsonWriterForSum[T: Type](using quotes: Quotes): Expr[JsonObjectWriter[T]] = + new DerivationMacro(quotes).deriveJsonWriterForSum[T] def deriveJsonReaderForProduct[T: Type](config: Expr[ReaderBuilder[T]])(using quotes: Quotes): Expr[JsonReader[T]] = new DerivationMacro(quotes).deriveJsonReaderForProduct[T](config) - def deriveJsonReaderForSum[T: Type](config: Expr[JsonConfig[T]])(using quotes: Quotes): Expr[JsonReader[T]] = - new DerivationMacro(quotes).deriveJsonReaderForSum(config) + def deriveJsonReaderForSum[T: Type](using quotes: Quotes): Expr[JsonReader[T]] = + new DerivationMacro(quotes).deriveJsonReaderForSum[T] private[derivation] class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: @@ -47,8 +48,8 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: def deriveJsonWriterForProduct[T: Type](config: Expr[WriterBuilder[T]]): Expr[JsonObjectWriter[T]] = val fields = prepareWriterProductFields(config) - Block( - deriveMissingWriters[T](fields.map(_.tpe)), + val writer = Block( + deriveMissingWriters(TypeRepr.of[T], fields.map(_.tpe)), '{ new JsonObjectWriter[T]: override def writeValues(value: T, tokenWriter: TokenWriter): Unit = @@ -57,21 +58,22 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: fields.map { field => field.tpe.asType match case '[f] => '{ - searchJsonWriter[f] + ${lookup[JsonWriter[f]]} .write(${ field.label }, ${ field.value('{ value }.asTerm).asExprOf[f] }, tokenWriter) } }, '{} ) } }.asTerm - ).asExprOf[JsonObjectWriter[T]] + ) + writer.asExprOf[JsonObjectWriter[T]] - def deriveJsonWriterForSum[T: Type](config: Expr[JsonConfig[T]]): Expr[JsonObjectWriter[T]] = + def deriveJsonWriterForSum[T: Type]: Expr[JsonObjectWriter[T]] = val tpe = TypeRepr.of[T] - val parsedConfig = parseSumConfig(config) + val parsedConfig = parseSumConfig[T] val types = getAllChildren(tpe) - Block( - deriveMissingWritersForSum[T](types), + val writer = Block( + deriveMissingWritersForSum(types), '{ new JsonObjectWriter[T]: override def writeValues(value: T, tokenWriter: TokenWriter): Unit = @@ -80,7 +82,7 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: tpe.asType match case '[discriminatorType] => '{ - searchJsonWriter[discriminatorType].write( + ${lookup[JsonWriter[discriminatorType]]}.write( name = ${ Expr(label) }, value = ${ Select.unique('{ value }.asTerm, label).asExprOf[discriminatorType] }, tokenWriter = tokenWriter @@ -93,58 +95,69 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: types = types, write = (ref, tpe) => tpe.asType match case '[t] => - '{ searchJsonObjectWriter[t].writeValues(${ref.asExprOf[t]}, tokenWriter) } + '{ ${lookup[JsonObjectWriter[t]]}.writeValues(${ref.asExprOf[t]}, tokenWriter) } ) } }.asTerm - ).asExprOf[JsonObjectWriter[T]] + ) + writer.asExprOf[JsonObjectWriter[T]] - private def deriveMissingWritersForSum[T: Type](types: List[TypeRepr]): List[ValDef] = - types.zipWithIndex - .flatMap { case (tpe, idx) => - tpe.asType match - case '[t] => - val symbol = Symbol.newVal( - Symbol.spliceOwner, - s"given_jsonWriter_$idx", - TypeRepr.of[JsonObjectWriter[t]], - Flags.Given, - Symbol.noSymbol - ) - Option.when(lookupOpt[JsonObjectWriter[t]].isEmpty)( - ValDef(symbol, Some('{ JsonWriter.derived[t](using ${lookup[Mirror.Of[t]]}) }.asTerm)) - ) - } + private def deriveMissingWritersForSum(types: List[TypeRepr]): List[ValDef] = + types.flatMap { tpe => + tpe.asType match + case '[t] => + val symbol = Symbol.newVal( + Symbol.spliceOwner, + s"given_JsonWriter_${tpe.show(using Printer.TypeReprShortCode)}", + TypeRepr.of[JsonObjectWriter[t]], + Flags.Given, + Symbol.noSymbol + ) + Option.when(lookupOpt[JsonObjectWriter[t]].isEmpty)( + ValDef(symbol, Some('{ JsonObjectWriter.derived[t](using ${lookup[Mirror.Of[t]]}) }.asTerm)) + ) + } - private def deriveMissingWriters[T: Type](tpes: List[TypeRepr]): List[ValDef] = - tpes.distinct.zipWithIndex - .flatMap { (tpe, idx) => + + private def deriveMissingWriters(thisTpe: TypeRepr, tpes: List[TypeRepr]): List[ValDef] = + distinct(tpes).filterNot(isRecursive(thisTpe, _)) + .flatMap { tpe => tpe.asType match case '[t] => - val symbol = Symbol.newVal( - Symbol.spliceOwner, - s"given_jsonWriter_$idx", - TypeRepr.of[JsonWriter[t]], - Flags.Given, - Symbol.noSymbol - ) - tpe match - case or: OrType => - Option.when(lookupOpt[JsonWriter[t]].isEmpty)( - ValDef( - symbol, - Some(deriveOrTypeJsonWriter[t].asTerm) - ) - ) - case _ => - Option.when(lookupOpt[JsonWriter[t]].isEmpty)( - ValDef( - symbol, - Some('{JsonObjectWriter.derived[t](using ${lookup[Mirror.Of[t]]})}.asTerm) + lookupOpt[JsonWriter[t]].map { + _.asTerm match + case ident: Ident => + Left(ident) + case other => + Right(other) + } match + case Some(Left(writer)) => + None + + case other => + val valDef = ValDef( + Symbol.newVal( + Symbol.spliceOwner, + s"given_JsonWriter_${tpe.show(using Printer.TypeReprShortCode)}", + TypeRepr.of[JsonWriter[t]], + Flags.Given, + Symbol.noSymbol + ), + Some( + other.flatMap(_.toOption) + .getOrElse { + tpe match + case or: OrType => + deriveOrTypeJsonWriter[t].asTerm + case _ => + '{ JsonObjectWriter.derived[t](using ${ lookup[scala.deriving.Mirror.Of[t]] }) }.asTerm + } ) ) + Some(valDef) } + private def deriveOrTypeJsonWriter[T: Type]: Expr[JsonWriter[T]] = def collectTypes(tpe: TypeRepr, acc: List[TypeRepr] = Nil): List[TypeRepr] = tpe match @@ -153,7 +166,7 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: val types = collectTypes(TypeRepr.of[T]) val term = Block( - deriveMissingWriters[T](types), + deriveMissingWriters(TypeRepr.of[T], types), '{ new JsonWriter[T]: def write(value: T, tokenWriter: TokenWriter): Unit = @@ -195,173 +208,113 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: def deriveJsonReaderForProduct[T: Type](config: Expr[ReaderBuilder[T]]): Expr[JsonReader[T]] = val tpe = TypeRepr.of[T] val (fields, isStrict) = prepareReaderProductFields[T](config) - val labelsToIndices = fields.map(field => field.name -> field.idx).toMap val existingLabels = fields.map(_.name).toSet - val defaults: Map[String, Expr[Any]] = fields.flatMap(_.defaults(existingLabels)).distinctBy(_._1).toMap - val requiredLabels = fields.flatMap(_.requiredLabels(existingLabels)).toSet -- defaults.keys - val fieldsWithoutReader = fields.collect { case (field: ReaderField.Extracted) if field.reader => field.name } - val fieldNamesWithTypes = fields.flatMap(_.readerTypes(existingLabels)).distinctBy(_._1) - - val sortedFields: List[(String, Int, Option[Expr[Any => Any]], List[(String, Option[Int])], Boolean)] = - fields.map { - case field: ReaderField.Basic => - (field.name, field.idx, field.extractor.map(_._2), Nil, false) - case field: ReaderField.Extracted => - (field.name, field.idx, Some(field.lambda), field.extractors.map((name, _) => (name -> labelsToIndices.get(name))), field.reader) - } + val fieldsWithoutReader = fields.collect { case field: ReaderField.Extracted if field.reader => field.name } - Block( - deriveMissingReaders(fieldNamesWithTypes.map(_._2)), - '{ - new JsonReader[T]: - given JsonReader[T] = this - - def read(it: TokenIterator)(implicit fieldName: FieldName) = - if !it.currentToken().isObjectStart then - ReaderError.wrongJson("Expected object start but found: " + it.currentToken().toString) - - it.nextToken() - val collectedValues: mutable.Map[String, Any] = ${ - if defaults.isEmpty then - '{ mutable.Map.empty } - else - '{ - val fallback = ${ defaults.exprOfMap[String, Any] } - mutable.Map.empty.withDefault(fallback) - } - } + val (basicFields, extractedFields) = fields.partitionMap { + case field: ReaderField.Basic => Left(field) + case field: ReaderField.Extracted => Right(field) + } - val missingFields: mutable.Set[String] = ${ requiredLabels.exprOfMutableSet } - val resultFields = mutable.Map.empty[Int, Any] - lazy val fieldsForExtractedReader = mutable.Map.empty[String, TokenIterator] - lazy val fieldsWithoutReaders: mutable.Set[String] = ${ fieldsWithoutReader.exprOfMutableSet } - while (!it.currentToken().isObjectEnd) - val jsonName = it.fieldName() - it.nextToken() - val currentIt = it.collectExpression() + val expectedFieldNames = basicFields.map(_.name).toSet ++ extractedFields.flatMap(_.extractors.map(_._1)) -- extractedFields.map(_.name) + def failIfNotInitialized(fieldName: Expr[FieldName]): Expr[Unit] = + basicFields.filterNot(_.default.nonEmpty) match + case refs @ head :: tail => + val boolExpr = tail.foldLeft('{!${head.initRef.asExprOf[Boolean]}}) { (acc, el) => + '{${acc} || !${el.initRef.asExprOf[Boolean]}} + } + '{ + if {$boolExpr} then + val uninitializedFields = new scala.collection.mutable.ArrayBuffer[String](${ Expr(refs.size) }) ${ - Match( - '{jsonName}.asTerm, - fieldNamesWithTypes.map { (name, tpe) => - tpe.asType match { - case '[t] => - CaseDef( - Literal(StringConstant(name)), - None, - '{ - collectedValues += ${Expr(name)} -> summonInline[JsonReader[t]] - .read(currentIt.copy())(fieldName.appendFieldName(${Expr(name)})) - missingFields -= ${Expr(name)} - () - }.asTerm - ) - } - } :+ { - def ifFoundFieldForReader: Expr[Unit] = '{ - fieldsForExtractedReader.update(jsonName, currentIt) - missingFields -= jsonName - fieldsWithoutReaders -= jsonName - () - } - - def ifUnknownFieldWhileStrict: Expr[Unit] = '{ - val expectedNames = (collectedValues.keySet ++ missingFields ++ ${ defaults.keySet.exprOfSet }).mkString("'", "', '", "'") - ReaderError.wrongJson(s"unexpected field '$jsonName', expected one of $expectedNames") - () + Expr.block( + refs.map { ref => + '{ + if !${ref.initRef.asExprOf[Boolean]} then + uninitializedFields += ${Expr(ref.name)} } - CaseDef( - Wildcard(), - None, - if fieldsWithoutReader.nonEmpty && isStrict then - '{ - if fieldsWithoutReaders.contains(jsonName) then ${ifFoundFieldForReader} - else if ${ Expr(isStrict) } then ${ifUnknownFieldWhileStrict} - }.asTerm - else if fieldsWithoutReader.nonEmpty && !isStrict then - '{ if fieldsWithoutReaders.contains(jsonName) then ${ ifFoundFieldForReader } else ()}.asTerm - else if fieldsWithoutReader.isEmpty && isStrict then - ifUnknownFieldWhileStrict.asTerm - else '{}.asTerm - ) - } - ).asExprOf[Unit] + }, + '{} + ) } + ReaderError.wrongJson("Can not extract fields from json: " + uninitializedFields.mkString(", "))(${fieldName}) + } - it.nextToken() - - if (missingFields.nonEmpty) - ReaderError.wrongJson("Can not extract fields from json: " + missingFields.mkString(", ")) + case Nil => + '{} + + if tpe.typeSymbol.flags.is(Flags.Module) then + '{JsonReader.const(${Ref(tpe.termSymbol).asExprOf[T]})} + else + val term = Block( + deriveMissingReaders(tpe, basicFields.map(_.tpe)), + '{ + new JsonReader[T]: + given JsonReader[T] = this + override def read(it: TokenIterator)(using fieldName: FieldName) = + if !it.currentToken().isObjectStart then + ReaderError.wrongJson("Expected object start but found: " + it.currentToken().toString) + else ${ + Block( + fields.flatMap(_.initialize), + '{ + @tailrec + def recRead(it: TokenIterator): Unit = + it.currentToken() match + case token if token.isObjectEnd => + it.nextToken() + case token if token.isFieldName => + val jsonName = it.fieldName() + it.nextToken() + ${ + Match( + selector = '{ jsonName }.asTerm, + cases = fields.flatMap(_.initializeFieldCase('{it}, '{fieldName})) :+ + CaseDef( + Wildcard(), + None, + if isStrict then + '{ ReaderError.wrongJson(s"unexpected field '$jsonName', expected one of ${${ Expr(expectedFieldNames.mkString("'", "', '", "'")) }}") }.asTerm + else + '{ it.next() ; () }.asTerm + ) + ).asExprOf[Unit] + } + recRead(it) + + case token => + ReaderError.wrongJson(s"Expect end of object or field name but '$token' found") + + + recRead(it.next()) + + ${ failIfNotInitialized('{ fieldName }) } + + ${ + val allRefs = fields.map(field => field.name -> field.ref).toMap + Expr.block(extractedFields.flatMap(_.extract(allRefs, '{fieldName})).map(_.asExprOf[Unit]), '{}) + } - ${ - Expr.block( - sortedFields.map { (name, idx, function, dependencies, extractReader) => - dependencies match - case Nil => - '{ resultFields += Tuple2(${Expr(idx)}, ${ - function match - case Some(buildField) => - '{ ${buildField}.asInstanceOf[Any => Any].apply(collectedValues(${ Expr(name) })) } - case None => - '{ collectedValues(${ Expr(name) }) } - }) } - - case dependencies => - def fieldsToBuildFrom(depName: String, depIdx: Option[Int]): Expr[Any] = - depIdx.fold('{ collectedValues(${ Expr(depName) }) })(idx => '{ resultFields(${ Expr(idx) }) }) - - val value = (function, dependencies) match - case (None, _) => - report.errorAndAbort("Internal error, function not provided for dependency") - - case (Some(buildField), List((depName, depIdxOpt))) => - '{ ${ buildField }.apply(${fieldsToBuildFrom(depName, depIdxOpt)}) } - - case (Some(buildField), dependencies) => - '{ ${ buildField }.apply(Tuple.fromArray(Array(${ Varargs(dependencies.map(fieldsToBuildFrom)) }: _*))) } - - if (!extractReader) - '{ resultFields += ${Expr(idx)} -> ${ value } } - else - '{ - val read: TokenIterator => Any = ${value}.asInstanceOf[JsonReader[Any]].read(_)(fieldName.appendFieldName(${Expr(name)})) - fieldsForExtractedReader.get(${Expr(name)}) match - case Some(iterator) => - resultFields += ${Expr(idx)} -> read(iterator) - case _ => - try - resultFields += ${Expr(idx)} -> read(QueueIterator(List(TokenNode.NullValueNode))) - fieldsWithoutReaders -= ${Expr(name)} - catch - case _: ReaderError => - } - }, - '{} - ) - } + ${ + New(TypeTree.of[T]) + .select(tpe.classSymbol.get.primaryConstructor) + .appliedToTypes(tpe.typeArgs) + .appliedToArgs(fields.filterNot(_.idx == -1).sortBy(_.idx).map(_.ref)) + .asExprOf[T] + } - ${ if (fieldsWithoutReader.nonEmpty) - '{ - if (fieldsWithoutReaders.nonEmpty) - ReaderError.wrongJson("Can not extract fields from json: " + fieldsWithoutReaders.mkString(", ")) + }.asTerm + ).asExprOf[T] } - else '{} - } - - summonInline[scala.deriving.Mirror.ProductOf[T]].fromProduct: - new Product: - def productArity = resultFields.size - def productElement(n: Int) = resultFields(n) - def canEqual(that: Any) = that match - case that: Product if that.productArity == productArity => true - case _ => false - }.asTerm - ).asExprOf[JsonReader[T]] + }.asTerm + ) + term.asExprOf[JsonReader[T]] - def deriveJsonReaderForSum[T: Type](config: Expr[JsonConfig[T]]): Expr[JsonReader[T]] = + def deriveJsonReaderForSum[T: Type]: Expr[JsonReader[T]] = val tpe = TypeRepr.of[T] - val parsed = parseSumConfig(config) + val parsed = parseSumConfig[T] val children = getAllChildren(tpe) parsed.discriminator match case Some(DiscriminatorConfig(label, tpe, discriminators)) => tpe.asType match @@ -373,13 +326,13 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: ) (stat, Ref(stat.symbol)) ).unzip - Block( - deriveMissingReaders(children) ++ discriminatorStats, + val term = Block( + deriveMissingReadersForSum(children) ++ discriminatorStats, '{ JsonReader.builder .addField[discriminator]( name = ${Expr(label)}, - jsonReader = searchJsonReader[discriminator] + jsonReader = ${lookup[JsonReader[discriminator]]} ) .selectReader[T] { discriminator => ${ @@ -390,7 +343,7 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: CaseDef( branchDiscriminator, None, - Typed('{searchJsonReader[t]}.asTerm, TypeTree.of[JsonReader[? <: T]]) + Typed(lookup[JsonReader[t]].asTerm, TypeTree.of[JsonReader[? <: T]]) ) ) :+ CaseDef(Wildcard(), None, '{ReaderError.wrongJson(s"Unexpected discriminator found: $discriminator")(using FieldName(${Expr(label)})) }.asTerm @@ -399,37 +352,75 @@ class DerivationMacro(val quotes: Quotes) extends ConfigurationMacroUtils: } } }.asTerm - ).asExprOf[JsonReader[T]] + ) + term.asExprOf[JsonReader[T]] case None => - report.errorAndAbort("Discriminator is required to derive JsonReader for sum type. Use JsonConfig[T].discriminateBy(_.field)") + report.errorAndAbort("Discriminator is required to derive JsonReader for sum type. Use @selector annotation") + + private def distinct(tpes: List[TypeRepr]) = + tpes.foldLeft(List.empty[TypeRepr]) { (acc, tpe) => + if (acc.exists(_ =:= tpe)) acc + else tpe :: acc + } + private def isRecursive(tpe: TypeRepr, childTpe: TypeRepr): Boolean = + tpe =:= childTpe || (childTpe match + case AppliedType(_, types) => types.exists(isRecursive(tpe, _)) + case _ => false) - private def deriveMissingReaders(tpes: List[TypeRepr]): List[ValDef] = - tpes.distinct.zipWithIndex.flatMap { (tpe, idx) => - tpe.asType match - case '[t] => - lookupOpt[JsonReader[t]] match - case Some(reader) => - None - case None => - Some( - ValDef( + private def deriveMissingReadersForSum(tpes: List[TypeRepr]): List[ValDef] = + tpes.zipWithIndex + .flatMap { (tpe, idx) => + tpe.asType match + case '[t] => + lookupOpt[JsonReader[t]] match + case Some(reader) => None + case None => + Some( + ValDef( + Symbol.newVal( + Symbol.spliceOwner, + s"given_JsonReader_${tpe.show(using Printer.TypeReprShortCode)}", + TypeRepr.of[JsonReader[t]], + Flags.Given, + Symbol.noSymbol + ), + Some('{ JsonReader.derived[t](using ${ lookup[scala.deriving.Mirror.Of[t]] })}.asTerm) + ) + ) + + } + + + private def deriveMissingReaders(thisTpe: TypeRepr, tpes: List[TypeRepr]): List[ValDef] = + distinct(tpes).filterNot(isRecursive(thisTpe, _)) + .flatMap { tpe => + tpe.asType match + case '[t] => + lookupOpt[JsonReader[t]].map { _.asTerm match + case ident: Ident => + Left(ident) + case other => + Right(other) + } match + case Some(Left(reader)) => + None + + case other => + val valDef = ValDef( Symbol.newVal( Symbol.spliceOwner, - s"given_jsonReader_$idx", + s"given_JsonReader_${tpe.show(using Printer.TypeReprShortCode)}", TypeRepr.of[JsonReader[t]], Flags.Given, Symbol.noSymbol ), - Some('{ JsonReader.derived[t](using ${ lookup[scala.deriving.Mirror.Of[t]] }) }.asTerm) + Some( + other.flatMap(_.toOption) + .getOrElse('{JsonReader.derived[t](using ${ lookup[scala.deriving.Mirror.Of[t]] })}.asTerm) + ) ) - ) - } - - - - - - + Some(valDef) + } diff --git a/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala b/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala index 8ea4292..19ddb9b 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala @@ -1,7 +1,7 @@ package tethys.derivation import scala.deriving.Mirror -import tethys.{JsonConfig, JsonObjectWriter, JsonWriter, WriterBuilder} +import tethys.{JsonObjectWriter, JsonWriter, WriterBuilder} import tethys.writers.tokens.TokenWriter import scala.deriving.Mirror @@ -12,13 +12,10 @@ private[tethys] trait JsonObjectWriterDerivation: inline def derived[A](inline config: WriterBuilder[A])(using mirror: Mirror.ProductOf[A]) = Derivation.deriveJsonWriterForProduct[A](config) - inline def derived[A](inline config: JsonConfig[A])(using m: Mirror.SumOf[A]) = - Derivation.deriveJsonWriterForSum[A](config) - inline def derived[A](using mirror: Mirror.Of[A]): JsonObjectWriter[A] = inline mirror match case given Mirror.ProductOf[A] => - derived( + Derivation.deriveJsonWriterForProduct[A]( summonFrom[WriterBuilder[A]] { case config: WriterBuilder[A] => config @@ -27,12 +24,5 @@ private[tethys] trait JsonObjectWriterDerivation: ) case given Mirror.SumOf[A] => - derived( - summonFrom[JsonConfig[A]] { - case config: JsonConfig[A] => - config - case _ => - JsonConfig[A] - } - ) + Derivation.deriveJsonWriterForSum[A] \ No newline at end of file diff --git a/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala b/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala index c7a4421..dcb0ca0 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala @@ -3,7 +3,7 @@ package tethys.derivation import tethys.commons.TokenNode import tethys.readers.tokens.{QueueIterator, TokenIterator} import tethys.readers.{FieldName, ReaderError} -import tethys.{JsonReader, JsonConfig} +import tethys.JsonReader import tethys.ReaderBuilder import scala.collection.mutable @@ -13,27 +13,28 @@ import scala.compiletime.{constValue, constValueTuple, erasedValue, summonFrom, private [tethys] trait JsonReaderDerivation: + def const[A](value: A): JsonReader[A] = + new JsonReader[A]: + override def read(it: TokenIterator)(implicit fieldName: FieldName): A = + if !it.currentToken().isObjectStart then + ReaderError.wrongJson("Expected object start but found: " + it.currentToken().toString) + else { + it.skipExpression() + value + } inline def derived[A](inline config: ReaderBuilder[A])(using mirror: Mirror.ProductOf[A]): JsonReader[A] = - Derivation.deriveJsonReaderForProduct(config) - - inline def derived[A](inline config: JsonConfig[A])(using mirror: Mirror.SumOf[A]): JsonReader[A] = - Derivation.deriveJsonReaderForSum(config) + Derivation.deriveJsonReaderForProduct[A](config) inline def derived[A](using mirror: Mirror.Of[A]): JsonReader[A] = inline mirror match case given Mirror.ProductOf[A] => - derived( + Derivation.deriveJsonReaderForProduct[A]( summonFrom[ReaderBuilder[A]] { case config: ReaderBuilder[A] => config case _ => ReaderBuilder[A] } ) case given Mirror.SumOf[A] => - derived( - summonFrom[JsonConfig[A]] { - case config: JsonConfig[A] => config - case _ => JsonConfig[A] - } - ) \ No newline at end of file + Derivation.deriveJsonReaderForSum[A] \ No newline at end of file diff --git a/modules/core/src/main/scala-3/tethys/derivation/ReaderBuilderParsed.scala b/modules/core/src/main/scala-3/tethys/derivation/ReaderBuilderParsed.scala deleted file mode 100644 index 0306089..0000000 --- a/modules/core/src/main/scala-3/tethys/derivation/ReaderBuilderParsed.scala +++ /dev/null @@ -1,23 +0,0 @@ -package tethys.derivation - -import tethys.JsonReader - -import scala.quoted.{Quotes, ToExpr} - -private[derivation] case class ReaderBuilderParsed( - defaultValues: Map[String, Any], - fields: List[ReaderBuilderParsed.Field], - readers: Map[String, JsonReader[?]], - requiredLabels: Set[String], - fieldsWithoutReaders: Set[String], - isStrict: Boolean -) - -private[derivation] object ReaderBuilderParsed: - case class Field( - name: String, - idx: Int, - function: Option[Any => Any], - dependencies: List[(String, Option[Int])], - extractReader: Boolean - ) diff --git a/modules/core/src/main/scala-3/tethys/derivation/WriterBuilderParsed.scala b/modules/core/src/main/scala-3/tethys/derivation/WriterBuilderParsed.scala deleted file mode 100644 index faa9f54..0000000 --- a/modules/core/src/main/scala-3/tethys/derivation/WriterBuilderParsed.scala +++ /dev/null @@ -1,7 +0,0 @@ -package tethys.derivation - -import tethys.JsonWriter - -private[derivation] -object WriterBuilderParsed: - case class Field[T, F](label: String, function: T => F, writer: JsonWriter[F]) \ No newline at end of file diff --git a/modules/core/src/main/scala-3/tethys/selector.scala b/modules/core/src/main/scala-3/tethys/selector.scala new file mode 100644 index 0000000..e2b9c84 --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/selector.scala @@ -0,0 +1,3 @@ +package tethys + +class selector extends scala.annotation.StaticAnnotation \ No newline at end of file diff --git a/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala b/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala index 54c5c2b..6912960 100644 --- a/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala +++ b/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala @@ -137,11 +137,9 @@ class DerivationSpec extends AnyFlatSpec with Matchers { enum Disc derives StringEnumJsonWriter, StringEnumJsonReader: case A, B - sealed trait Choose(val discriminator: Disc) derives JsonObjectWriter, JsonReader + sealed trait Choose(@selector val discriminator: Disc) derives JsonObjectWriter, JsonReader object Choose: - inline given JsonConfig[Choose] = JsonConfig[Choose].discriminateBy(_.discriminator) - case class AA() extends Choose(Disc.A) case class BB() extends Choose(Disc.B) @@ -153,14 +151,10 @@ class DerivationSpec extends AnyFlatSpec with Matchers { } it should "write/read sum types with provided json discriminator of simple type" in { - enum Choose(val discriminator: Int) derives JsonObjectWriter, JsonReader: + enum Choose(@selector val discriminator: Int) derives JsonObjectWriter, JsonReader: case AA() extends Choose(0) case BB() extends Choose(1) - object Choose: - inline given JsonConfig[Choose] = JsonConfig[Choose].discriminateBy(_.discriminator) - - (Choose.AA(): Choose).asTokenList shouldBe obj("discriminator" -> 0) (Choose.BB(): Choose).asTokenList shouldBe obj("discriminator" -> 1) @@ -168,33 +162,11 @@ class DerivationSpec extends AnyFlatSpec with Matchers { read[Choose](obj("discriminator" -> 1)) shouldBe Choose.BB() } - it should "write/read json for generic discriminators" in { - enum Disc1 derives StringEnumJsonWriter, StringEnumJsonReader: - case A, B - - enum Disc2 derives StringEnumJsonWriter, StringEnumJsonReader: - case AA, BB - - sealed trait Choose[A](val discriminator: A) derives JsonWriter, JsonReader - - object Choose: - inline given [A]: JsonConfig[Choose[A]] = JsonConfig[Choose[A]].discriminateBy(_.discriminator) - - case class ChooseA() extends Choose[Disc1](Disc1.A) - case class ChooseB() extends Choose[Disc2](Disc2.BB) - - (ChooseA(): Choose[Disc1]).asTokenList shouldBe obj("discriminator" -> "A") - (ChooseB(): Choose[Disc2]).asTokenList shouldBe obj("discriminator" -> "BB") - - read[Choose[Disc1]](obj("discriminator" -> "A")) shouldBe ChooseA() - read[Choose[Disc2]](obj("discriminator" -> "BB")) shouldBe ChooseB() - } - it should "not compile derivation when discriminator override found" in { """ | - | sealed trait Foo(val x: Int) derives JsonReader, JsonObjectWriter + | sealed trait Foo(@selector val x: Int) derives JsonReader, JsonObjectWriter | | object Foo: | given JsonDiscriminator[Foo, Int] = JsonDiscriminator.by(_.x) @@ -401,8 +373,7 @@ class DerivationSpec extends AnyFlatSpec with Matchers { "e" -> 2 )) shouldBe SimpleTypeWithAny(3, "str", 1.0, None) } - - + it should "derive reader for fieldStyle from description" in { given JsonReader[CamelCaseNames] = JsonReader.derived[CamelCaseNames] { @@ -462,7 +433,7 @@ class DerivationSpec extends AnyFlatSpec with Matchers { "not_id_param" -> 2, "simple" -> 3 )) - }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'simple', 'id_param'" + }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'id_param', 'simple'" } @@ -489,7 +460,7 @@ class DerivationSpec extends AnyFlatSpec with Matchers { "not_id_param" -> 2, "simple" -> 3 )) - }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'simple', 'id_param'" + }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'id_param', 'simple'" } @@ -650,10 +621,10 @@ class DerivationSpec extends AnyFlatSpec with Matchers { def write(simpleSealedType: SimpleSealedType): List[TokenNode] = simpleSealedType.asTokenList - write(CaseClass(1)) shouldBe obj("a" -> 1) - write(SimpleClass(2)) shouldBe obj("b" -> 2) - write(JustObject) shouldBe obj("type" -> "JustObject") - write(SubChild(3)) shouldBe obj("c" -> 3) + write(CaseClass(1)) shouldBe obj("__type" -> "CaseClass", "a" -> 1) + write(SimpleClass(2)) shouldBe obj("__type" -> "SimpleClass", "b" -> 2) + write(JustObject) shouldBe obj("__type" -> "JustObject", "type" -> "JustObject") + write(SubChild(3)) shouldBe obj("__type" -> "SubChild", "c" -> 3) } it should "derive reader/writer for simple sealed trait with hierarchy with discriminator" in { @@ -662,8 +633,6 @@ class DerivationSpec extends AnyFlatSpec with Matchers { implicit val justObjectWriter: JsonObjectWriter[JustObject.type] = JsonWriter.obj implicit val subChildWriter: JsonObjectWriter[SubChild] = JsonWriter.derived[SubChild] given JsonReader[SimpleSealedType] = JsonReader.derived[SimpleSealedType] - inline given JsonConfig[SimpleSealedType] = JsonConfig[SimpleSealedType] - .discriminateBy(_.`__type`) implicit val sealedWriter: JsonWriter[SimpleSealedType] = JsonWriter.derived[SimpleSealedType] diff --git a/modules/core/src/test/scala-3/tethys/derivation/SimpleSealedType.scala b/modules/core/src/test/scala-3/tethys/derivation/SimpleSealedType.scala index 0c1282b..4294b6a 100644 --- a/modules/core/src/test/scala-3/tethys/derivation/SimpleSealedType.scala +++ b/modules/core/src/test/scala-3/tethys/derivation/SimpleSealedType.scala @@ -1,6 +1,8 @@ package tethys.derivation -sealed abstract class SimpleSealedType(val `__type`: String) +import tethys.selector + +sealed abstract class SimpleSealedType(@selector val `__type`: String) case class CaseClass(a: Int) extends SimpleSealedType("CaseClass") case class SimpleClass(val b: Int) extends SimpleSealedType("SimpleClass") case object JustObject extends SimpleSealedType("JustObject") 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 3d2847d..ae8734e 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 @@ -115,6 +115,7 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { )) shouldBe SimpleType(2, "str", 1.0) } + it should "derive reader for extract from description with synthetic field" in { implicit val reader: JsonReader[SimpleType] = jsonReader[SimpleType] { describe { @@ -137,6 +138,7 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { )) shouldBe SimpleType(4, "str", 1.0) } + it should "derive reader for extract reader from description" in { implicit val reader: JsonReader[SimpleTypeWithAny] = jsonReader[SimpleTypeWithAny] { describe { @@ -202,12 +204,11 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { "any" -> true )) shouldBe SimpleTypeWithAny(3, "str", 1.0, Some(true)) - //FIXME: figure out how to handle this case - /*read[SimpleTypeWithAny](obj( + read[SimpleTypeWithAny](obj( "s" -> "str", "d" -> 1.0, "e" -> 2 - )) shouldBe SimpleTypeWithAny(3, "str", 1.0, None)*/ + )) shouldBe SimpleTypeWithAny(3, "str", 1.0, None) } it should "derive reader for fieldStyle from description" in { @@ -278,7 +279,7 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { "not_id_param" -> 2, "simple" -> 3 )) - }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'simple', 'id_param'" + }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'id_param', 'simple'" } it should "derive reader for reader config from builder" in { @@ -304,7 +305,7 @@ class SemiautoReaderDerivationTest extends AnyFlatSpec with Matchers { "not_id_param" -> 2, "simple" -> 3 )) - }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'simple', 'id_param'" + }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'not_id_param', expected one of 'some_param', 'id_param', 'simple'" } 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 2d32c98..19a9c10 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 @@ -164,10 +164,10 @@ class SemiautoWriterDerivationTest extends AnyFlatSpec with Matchers { def write(simpleSealedType: SimpleSealedType): List[TokenNode] = simpleSealedType.asTokenList - write(CaseClass(1)) shouldBe obj("a" -> 1) - write(new SimpleClass(2)) shouldBe obj("b" -> 2) - write(JustObject) shouldBe obj("type" -> "JustObject") - write(SubChild(3)) shouldBe obj("c" -> 3) + write(CaseClass(1)) shouldBe obj("__type" -> "CaseClass", "a" -> 1) + write(SimpleClass(2)) shouldBe obj("__type" -> "SimpleClass", "b" -> 2) + write(JustObject) shouldBe obj("__type" -> "JustObject", "type" -> "JustObject") + write(SubChild(3)) shouldBe obj("__type" -> "SubChild", "c" -> 3) } it should "derive writer for simple sealed trait with hierarchy with discriminator" in { @@ -176,12 +176,7 @@ class SemiautoWriterDerivationTest extends AnyFlatSpec with Matchers { implicit val justObjectWriter: JsonObjectWriter[JustObject.type] = JsonWriter.obj implicit val subChildWriter: JsonObjectWriter[SubChild] = jsonWriter[SubChild] - implicit val sealedWriter: JsonWriter[SimpleSealedType] = { - inline given JsonConfig[SimpleSealedType] = - JsonConfig[SimpleSealedType].discriminateBy(_.__type) - - jsonWriter[SimpleSealedType] - } + implicit val sealedWriter: JsonWriter[SimpleSealedType] = jsonWriter[SimpleSealedType] def write(simpleSealedType: SimpleSealedType): List[TokenNode] = simpleSealedType.asTokenList 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 aa8895c..a6652dc 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 @@ -1,6 +1,8 @@ package tethys.derivation -sealed abstract class SimpleSealedType(val __type: String) +import tethys.selector + +sealed abstract class SimpleSealedType(@selector val __type: String) case class CaseClass(a: Int) extends SimpleSealedType("CaseClass") case class SimpleClass(val b: Int) extends SimpleSealedType("SimpleClass") case object JustObject extends SimpleSealedType("JustObject")