diff --git a/README.md b/README.md index 6398d58..61a7d9a 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,13 @@ It depends on [cats-core](https://github.com/typelevel/cats) and Scala 2.12 and 2.13 are supported. Support for Scala 2.11 may be implemented on demand. +:fire: Sealed traits enconding and decoding was implemented in 0.5.0 + ## QuickStart Add phobos-core to your dependencies: ``` -libraryDependencies += "ru.tinkoff" %% "phobos-core" % "0.4.0" +libraryDependencies += "ru.tinkoff" %% "phobos-core" % "0.5.0" ``` Then try this code out in `sbt console` or in a separate source file: @@ -65,7 +67,7 @@ Performance details can be found out in [phobos-benchmark repository](https://gi There are several additional modules for some specific cases. These modules could be added with command below: ``` -libraryDependencies += "ru.tinkoff" %% "phobos-" % "0.4.0" +libraryDependencies += "ru.tinkoff" %% "phobos-" % "0.5.0" ``` Where `` is module name. diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/configured/ElementCodecConfig.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/configured/ElementCodecConfig.scala index dd928df..a167cc2 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/configured/ElementCodecConfig.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/configured/ElementCodecConfig.scala @@ -1,12 +1,29 @@ package ru.tinkoff.phobos.configured -final case class ElementCodecConfig(transformAttributeNames: String => String, - transformElementNames: String => String) { - def withElementsRenamed(transform: String => String): ElementCodecConfig = copy(transformElementNames = transform) - def withAttributesRenamed(transform: String => String): ElementCodecConfig = copy(transformAttributeNames = transform) - def withStyle(transform: String => String): ElementCodecConfig = copy(transformElementNames = transform, transformAttributeNames = transform) +final case class ElementCodecConfig( + transformAttributeNames: String => String, + transformElementNames: String => String, + transformConstructorNames: String => String, + discriminatorLocalName: String, + discriminatorNamespace: Option[String] +) { + def withElementsRenamed(transform: String => String): ElementCodecConfig = + copy(transformElementNames = transform) + + def withAttributesRenamed(transform: String => String): ElementCodecConfig = + copy(transformAttributeNames = transform) + + def withConstructorsRenamed(transform: String => String): ElementCodecConfig = + copy(transformConstructorNames = transform) + + def withStyle(transform: String => String): ElementCodecConfig = + copy(transformElementNames = transform, transformAttributeNames = transform) + + def withDiscriminator(localName: String, namespace: Option[String]): ElementCodecConfig = + copy(discriminatorLocalName = localName, discriminatorNamespace = namespace) } object ElementCodecConfig { - val default: ElementCodecConfig = ElementCodecConfig(identity, identity) + val default: ElementCodecConfig = + ElementCodecConfig(identity, identity, identity, "type", Some("http://www.w3.org/2001/XMLSchema-instance")) } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/DecoderDerivation.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/DecoderDerivation.scala index 56cc4f8..d61cdac 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/DecoderDerivation.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/DecoderDerivation.scala @@ -3,7 +3,8 @@ package ru.tinkoff.phobos.derivation import ru.tinkoff.phobos.Namespace import ru.tinkoff.phobos.configured.ElementCodecConfig import ru.tinkoff.phobos.decoding.{AttributeDecoder, ElementDecoder, TextDecoder} -import ru.tinkoff.phobos.derivation.CompileTimeState.{ProductType, Stack} +import ru.tinkoff.phobos.derivation.CompileTimeState.{CoproductType, ProductType, Stack} + import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.reflect.macros.blackbox @@ -13,12 +14,77 @@ class DecoderDerivation(ctx: blackbox.Context) extends Derivation(ctx) { def searchType[T: c.WeakTypeTag]: Type = appliedType(c.typeOf[ElementDecoder[_]], c.weakTypeOf[T]) - def deriveProductCodec[T: c.WeakTypeTag](stack: Stack[c.type])(params: IndexedSeq[CaseClassParam]): Tree = { + val derivationPkg = q"_root_.ru.tinkoff.phobos.derivation" + val decodingPkg = q"_root_.ru.tinkoff.phobos.decoding" + val scalaPkg = q"_root_.scala" + val javaPkg = q"_root_.java.lang" + + def deriveCoproductCodec[T: c.WeakTypeTag](stack: Stack[c.type])( + config: Expr[ElementCodecConfig], + subtypes: Iterable[SealedTraitSubtype] + ): Tree = { + val assignedName = TermName(c.freshName(s"ElementDecoderTypeclass")).encodedName.toTermName + + val elementDecoderType = typeOf[ElementDecoder[_]] + val preAssignments = new ListBuffer[Tree] + val classType = c.weakTypeOf[T] + + val alternatives = subtypes.map { subtype => + val requiredImplicit = appliedType(elementDecoderType, subtype.subtypeType) + val path = CoproductType(weakTypeOf[T].toString) + val frame = stack.Frame(path, appliedType(elementDecoderType, weakTypeOf[T]), assignedName) + val derivedImplicit = stack.recurse(frame, requiredImplicit) { + typeclassTree(stack)(subtype.subtypeType, elementDecoderType) + } + + val ref = TermName(c.freshName("paramTypeclass")) + val assigned = deferredVal(ref, requiredImplicit, derivedImplicit) - val derivationPkg = q"_root_.ru.tinkoff.phobos.derivation" - val decodingPkg = q"_root_.ru.tinkoff.phobos.decoding" - val scalaPkg = q"_root_.scala" - val javaPkg = q"_root_.java.lang" + preAssignments.append(assigned) + + cq""" + discriminator if discriminator == `${subtype.constructorName}` => + $ref.decodeAsElement(cursor, localName, namespaceUri).map(d => d: $classType) + """ + }.toBuffer :+ + cq""" unknown => + new $decodingPkg.ElementDecoder.FailedDecoder[$classType]( + cursor.error(s"Unknown type discriminator value: '$${unknown}'") + )""" + + q""" + ..$preAssignments + new $decodingPkg.ElementDecoder[$classType] { + def decodeAsElement( + cursor : $decodingPkg.Cursor, + localName: $javaPkg.String, + namespaceUri: $scalaPkg.Option[$javaPkg.String], + ): $decodingPkg.ElementDecoder[$classType] = { + if (cursor.getEventType == _root_.com.fasterxml.aalto.AsyncXMLStreamReader.EVENT_INCOMPLETE) { + this + } else { + val discriminatorIdx = cursor.getAttributeIndex($config.discriminatorNamespace.getOrElse(null), $config.discriminatorLocalName) + if (discriminatorIdx > -1) { + cursor.getAttributeValue(discriminatorIdx) match { + case ..$alternatives + } + } else { + new $decodingPkg.ElementDecoder.FailedDecoder[$classType]( + cursor.error(s"No type discriminator '$${$config.discriminatorNamespace.fold("")(_ + ":")}$${$config.discriminatorLocalName}' found") + ) + } + } + } + + val isCompleted: $scalaPkg.Boolean = false + + def result(history: $scalaPkg.List[$javaPkg.String]): $scalaPkg.Either[$decodingPkg.DecodingError, $classType] = + $scalaPkg.Left($decodingPkg.DecodingError("Decoding not complete", history)) + } + """ + } + + def deriveProductCodec[T: c.WeakTypeTag](stack: Stack[c.type])(params: IndexedSeq[CaseClassParam]): Tree = { val decoderStateObj = q"$derivationPkg.DecoderDerivation.DecoderState" diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/Derivation.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/Derivation.scala index ad3ecc8..bd63399 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/Derivation.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/Derivation.scala @@ -5,21 +5,34 @@ import ru.tinkoff.phobos.configured.ElementCodecConfig import ru.tinkoff.phobos.derivation.CompileTimeState.{ChainedImplicit, Stack} import ru.tinkoff.phobos.derivation.Derivation.DirectlyReentrantException import ru.tinkoff.phobos.derivation.auto.Exported -import ru.tinkoff.phobos.syntax.{attr, renamed, text, xmlns} +import ru.tinkoff.phobos.syntax.{attr, discriminator, renamed, text, xmlns} + import scala.reflect.macros.blackbox private[phobos] abstract class Derivation(val c: blackbox.Context) { import c.universe._ - final case class CaseClassParam(localName: String, - xmlName: Tree, - namespaceUri: Tree, - paramType: Type, - category: ParamCategory) + final case class CaseClassParam( + localName: String, + xmlName: Tree, + namespaceUri: Tree, + paramType: Type, + category: ParamCategory + ) + + final case class SealedTraitSubtype( + constructorName: Tree, + subtypeType: Type + ) def searchType[T: c.WeakTypeTag]: Type + def deriveCoproductCodec[T: c.WeakTypeTag](stack: Stack[c.type])( + config: Expr[ElementCodecConfig], + subtypes: Iterable[SealedTraitSubtype] + ): Tree + def deriveProductCodec[T: c.WeakTypeTag](stack: Stack[c.type])(params: IndexedSeq[CaseClassParam]): Tree def error(msg: String): Nothing = c.abort(c.enclosingPosition, msg) @@ -30,8 +43,8 @@ private[phobos] abstract class Derivation(val c: blackbox.Context) { def exportedTypecclass(searchType: Type): Option[Tree] = Option(c.inferImplicitValue(appliedType(typeOf[Exported[_]], searchType))) - .map(exported => q"$exported.value") .filterNot(_.isEmpty) + .map(exported => q"$exported.value") def typeclassTree(stack: Stack[c.type])(genericType: Type, typeConstructor: Type): Tree = { val prefixType = c.prefix.tree.tpe @@ -68,12 +81,13 @@ private[phobos] abstract class Derivation(val c: blackbox.Context) { val classType = weakTypeOf[T] val typeSymbol = classType.typeSymbol if (!typeSymbol.isClass) error("Don't know how to work with not classes") - val classSymbol = typeSymbol.asClass - val namespaceType = typeOf[Namespace[_]] - val attrType = typeOf[attr] - val textType = typeOf[text] - val xmlnsType = weakTypeOf[xmlns[_]] - val renamedType = typeOf[renamed] + val classSymbol = typeSymbol.asClass + val namespaceType = typeOf[Namespace[_]] + val attrType = typeOf[attr] + val textType = typeOf[text] + val xmlnsType = weakTypeOf[xmlns[_]] + val renamedType = typeOf[renamed] + val discriminatorType = typeOf[discriminator] val expandDeferred = new Transformer { override def transform(tree: Tree) = tree match { @@ -86,7 +100,19 @@ private[phobos] abstract class Derivation(val c: blackbox.Context) { def inferCodec: Tree = { if (classSymbol.isSealed) { - error("Sealed traits support is not implemented yet") + val sealedTraitSubtypes = classType.typeSymbol.asClass.knownDirectSubclasses.map { symbol => + val constructorName = q"""$config.transformConstructorNames(`${symbol.name.decodedName.toString}`)""" + val discriminatorValue = symbol.annotations.collectFirst { + case annot if annot.tree.tpe =:= discriminatorType => + annot.tree.children.tail.collectFirst { + case t @ Literal(Constant(_: String)) => t + }.getOrElse { + error("@discriminator is only allowed to be used with string literals") + } + }.getOrElse(constructorName) + SealedTraitSubtype(discriminatorValue, symbol.asType.toType) + } + deriveCoproductCodec(stack)(config, sealedTraitSubtypes) } else if (classSymbol.isCaseClass) { def fetchGroup(param: TermSymbol): ParamCategory = { @@ -143,7 +169,7 @@ private[phobos] abstract class Derivation(val c: blackbox.Context) { val xmlName: Tree = param.annotations.collectFirst { case annotation if annotation.tree.tpe =:= renamedType => annotation.tree.children.tail.collectFirst { - case t@Literal(Constant(_: String)) => t + case t @ Literal(Constant(_: String)) => t }.getOrElse { error("@renamed is only allowed to be used with string literals") } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/EncoderDerivation.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/EncoderDerivation.scala index 094bfb0..3ca0826 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/EncoderDerivation.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/derivation/EncoderDerivation.scala @@ -2,8 +2,9 @@ package ru.tinkoff.phobos.derivation import ru.tinkoff.phobos.Namespace import ru.tinkoff.phobos.configured.ElementCodecConfig -import ru.tinkoff.phobos.derivation.CompileTimeState.{ProductType, Stack} +import ru.tinkoff.phobos.derivation.CompileTimeState.{CoproductType, ProductType, Stack} import ru.tinkoff.phobos.encoding.{AttributeEncoder, ElementEncoder, TextEncoder} + import scala.collection.mutable.ListBuffer import scala.reflect.macros.blackbox @@ -13,6 +14,55 @@ class EncoderDerivation(ctx: blackbox.Context) extends Derivation(ctx) { def searchType[T: c.WeakTypeTag]: Type = appliedType(c.typeOf[ElementEncoder[_]], c.weakTypeOf[T]) + def deriveCoproductCodec[T: c.WeakTypeTag](stack: Stack[c.type])( + config: Expr[ElementCodecConfig], + subtypes: Iterable[SealedTraitSubtype] + ): Tree = { + val assignedName = TermName(c.freshName(s"ElementEncoderTypeclass")).encodedName.toTermName + + val preAssignments = new ListBuffer[Tree] + val classType = c.weakTypeOf[T] + val scalaPkg = q"_root_.scala" + val javaPkg = q"_root_.java.lang" + val elementEncoderType = typeOf[ElementEncoder[_]] + + val alternatives = subtypes.map { subtype => + val requiredImplicit = appliedType(elementEncoderType, subtype.subtypeType) + val path = CoproductType(weakTypeOf[T].toString) + val frame = stack.Frame(path, appliedType(elementEncoderType, weakTypeOf[T]), assignedName) + val derivedImplicit = stack.recurse(frame, requiredImplicit) { + typeclassTree(stack)(subtype.subtypeType, elementEncoderType) + } + + val ref = TermName(c.freshName("paramTypeclass")) + val assigned = deferredVal(ref, requiredImplicit, derivedImplicit) + + preAssignments.append(assigned) + + cq"""sub: ${subtype.subtypeType.resultType} => + sw.memorizeDiscriminator($config.discriminatorNamespace, $config.discriminatorLocalName, ${subtype.constructorName}) + $ref.encodeAsElement(sub, sw, localName, namespaceUri) + """ + } + + q""" + ..$preAssignments + + new _root_.ru.tinkoff.phobos.encoding.ElementEncoder[$classType] { + def encodeAsElement( + a: $classType, + sw: _root_.ru.tinkoff.phobos.encoding.PhobosStreamWriter, + localName: $javaPkg.String, + namespaceUri: $scalaPkg.Option[$javaPkg.String] + ): $scalaPkg.Unit = { + a match { + case ..$alternatives + } + } + } + """ + } + def deriveProductCodec[T: c.WeakTypeTag](stack: Stack[c.type])(params: IndexedSeq[CaseClassParam]): Tree = { val assignedName = TermName(c.freshName(s"ElementEncoderTypeclass")).encodedName.toTermName @@ -71,7 +121,7 @@ class EncoderDerivation(ctx: blackbox.Context) extends Derivation(ctx) { new _root_.ru.tinkoff.phobos.encoding.ElementEncoder[$classType] { def encodeAsElement( a: $classType, - sw: _root_.org.codehaus.stax2.XMLStreamWriter2, + sw: _root_.ru.tinkoff.phobos.encoding.PhobosStreamWriter, localName: $javaPkg.String, namespaceUri: $scalaPkg.Option[$javaPkg.String] ): $scalaPkg.Unit = { @@ -97,7 +147,9 @@ class EncoderDerivation(ctx: blackbox.Context) extends Derivation(ctx) { def xmlNs[T: c.WeakTypeTag, NS: c.WeakTypeTag](localName: Tree, ns: Tree): Tree = xmlNsConfigured[T, NS](localName, ns, defaultConfig) - def xmlNsConfigured[T: c.WeakTypeTag, NS: c.WeakTypeTag](localName: Tree, ns: Tree, config: Expr[ElementCodecConfig]): Tree = { + def xmlNsConfigured[T: c.WeakTypeTag, NS: c.WeakTypeTag](localName: Tree, + ns: Tree, + config: Expr[ElementCodecConfig]): Tree = { val nsInstance = Option(c.inferImplicitValue(appliedType(weakTypeOf[Namespace[_]], weakTypeOf[NS]))) .filter(_.nonEmpty) .getOrElse(error(s"Could not find Namespace instance for $ns")) diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/AttributeEncoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/AttributeEncoder.scala index 9e6e11e..dbb0445 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/AttributeEncoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/AttributeEncoder.scala @@ -4,7 +4,6 @@ import java.time.{LocalDate, LocalDateTime, LocalTime, ZonedDateTime} import java.util.{Base64, UUID} import cats.Contravariant -import org.codehaus.stax2.XMLStreamWriter2 /** * Warning! This is an internal API which may change in future. @@ -18,11 +17,11 @@ import org.codehaus.stax2.XMLStreamWriter2 * To create new instance use .contramap method of existing instance. */ trait AttributeEncoder[A] { self => - def encodeAsAttribute(a: A, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit + def encodeAsAttribute(a: A, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit def contramap[B](f: B => A): AttributeEncoder[B] = new AttributeEncoder[B] { - def encodeAsAttribute(b: B, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = + def encodeAsAttribute(b: B, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = self.encodeAsAttribute(f(b), sw, localName, namespaceUri) } } @@ -38,13 +37,13 @@ object AttributeEncoder { */ implicit val stringEncoder: AttributeEncoder[String] = new AttributeEncoder[String] { - def encodeAsAttribute(a: String, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = + def encodeAsAttribute(a: String, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = namespaceUri.fold(sw.writeAttribute(localName, a))(ns => sw.writeAttribute(ns, localName, a)) } implicit val unitEncoder: AttributeEncoder[Unit] = new AttributeEncoder[Unit] { - def encodeAsAttribute(a: Unit, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = () + def encodeAsAttribute(a: Unit, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = () } implicit val booleanEncoder: AttributeEncoder[Boolean] = stringEncoder.contramap(_.toString) @@ -74,7 +73,7 @@ object AttributeEncoder { implicit def optionEncoder[A](implicit encoder: AttributeEncoder[A]): AttributeEncoder[Option[A]] = new AttributeEncoder[Option[A]] { - def encodeAsAttribute(a: Option[A], sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = + def encodeAsAttribute(a: Option[A], sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = a.foreach(encoder.encodeAsAttribute(_, sw, localName, namespaceUri)) } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/ElementEncoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/ElementEncoder.scala index 812faae..486d49e 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/ElementEncoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/ElementEncoder.scala @@ -5,7 +5,6 @@ import java.util.{Base64, UUID} import cats.{Contravariant, Foldable} import cats.data.{Chain, NonEmptyChain, NonEmptyList, NonEmptySet, NonEmptyVector} -import org.codehaus.stax2.XMLStreamWriter2 /** * Warning! This is an internal API which may change in future. @@ -24,11 +23,11 @@ import org.codehaus.stax2.XMLStreamWriter2 * not defined in typeclass, it should be passed in encodeAsElement method. */ trait ElementEncoder[A] { self => - def encodeAsElement(a: A, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit + def encodeAsElement(a: A, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit def contramap[B](f: B => A): ElementEncoder[B] = new ElementEncoder[B] { - def encodeAsElement(b: B, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = + def encodeAsElement(b: B, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = self.encodeAsElement(f(b), sw, localName, namespaceUri) } } @@ -44,7 +43,7 @@ object ElementEncoder { */ implicit val stringEncoder: ElementEncoder[String] = new ElementEncoder[String] { - def encodeAsElement(a: String, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = { + def encodeAsElement(a: String, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = { namespaceUri.fold(sw.writeStartElement(localName))(ns => sw.writeStartElement(ns, localName)) sw.writeCharacters(a) sw.writeEndElement() @@ -53,7 +52,7 @@ object ElementEncoder { implicit val unitEncoder: ElementEncoder[Unit] = new ElementEncoder[Unit] { - def encodeAsElement(a: Unit, sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = () + def encodeAsElement(a: Unit, sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = () } implicit val booleanEncoder: ElementEncoder[Boolean] = stringEncoder.contramap(_.toString) @@ -83,7 +82,7 @@ object ElementEncoder { implicit def optionEncoder[A](implicit encoder: ElementEncoder[A]): ElementEncoder[Option[A]] = new ElementEncoder[Option[A]] { - def encodeAsElement(a: Option[A], sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = + def encodeAsElement(a: Option[A], sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = a.foreach(encoder.encodeAsElement(_, sw, localName, namespaceUri)) } @@ -93,14 +92,14 @@ object ElementEncoder { implicit def foldableEncoder[F[_]: Foldable, A](implicit encoder: ElementEncoder[A]): ElementEncoder[F[A]] = new ElementEncoder[F[A]] { - def encodeAsElement(as: F[A], sw: XMLStreamWriter2, localName: String, namespaceUri: Option[String]): Unit = + def encodeAsElement(as: F[A], sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = Foldable[F].foldLeft(as, ())((_, a) => encoder.encodeAsElement(a, sw, localName, namespaceUri)) } implicit def iteratorEncoder[A](implicit encoder: ElementEncoder[A]): ElementEncoder[Iterator[A]] = new ElementEncoder[Iterator[A]] { def encodeAsElement(as: Iterator[A], - sw: XMLStreamWriter2, + sw: PhobosStreamWriter, localName: String, namespaceUri: Option[String]): Unit = as.foreach(a => encoder.encodeAsElement(a, sw, localName, namespaceUri)) diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/PhobosStreamWriter.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/PhobosStreamWriter.scala new file mode 100644 index 0000000..5b023be --- /dev/null +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/PhobosStreamWriter.scala @@ -0,0 +1,297 @@ +package ru.tinkoff.phobos.encoding + +import java.math.BigInteger + +import javax.xml.namespace.{NamespaceContext, QName} +import javax.xml.stream.XMLStreamException +import org.codehaus.stax2.typed.Base64Variant +import org.codehaus.stax2.{XMLStreamLocation2, XMLStreamReader2, XMLStreamWriter2} +import org.codehaus.stax2.validation.{ValidationProblemHandler, XMLValidationSchema, XMLValidator} + +final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { + + private var discriminatorLocalName: Option[String] = None + private var discriminatorNamespace: Option[String] = None + private var discriminatorValue: Option[String] = None + + /** + * Writes type-discriminator attribute inside next start element + * + * Following code + * + * sw.memoizeDiscriminator(Some("http://www.w3.org/2001/XMLSchema-instance"), "type", "dog") + * sw.writeStartElement("GoodBoy") + * + * will result to something like + * + * + * + * + * This API extension is required to keep ElementEncoder API simple. + * This method overrides old discriminator if it was already memorized + * + * @param namespaceUri namespace uri of type discriminator + * @param localName local name of type discriminator + * @param value value of type discriminator + */ + def memorizeDiscriminator(namespaceUri: Option[String], localName: String, value: String): Unit = { + discriminatorNamespace = namespaceUri + discriminatorLocalName = Some(localName) + discriminatorValue = Some(value) + } + + def isPropertySupported(name: String): Boolean = + sw.isPropertySupported(name) + + def setProperty(name: String, value: Any): Boolean = + sw.setProperty(name, value) + + def getLocation: XMLStreamLocation2 = + sw.getLocation + + def getEncoding: String = + sw.getEncoding + + def writeCData(text: Array[Char], start: Int, len: Int): Unit = + sw.writeCData(text, start, len) + + def writeDTD(rootName: String, systemId: String, publicId: String, internalSubset: String): Unit = + sw.writeDTD(rootName, systemId, publicId, internalSubset) + + def writeFullEndElement(): Unit = + sw.writeFullEndElement() + + def writeStartDocument(version: String, encoding: String, standAlone: Boolean): Unit = + sw.writeStartDocument(version, encoding, standAlone) + + def writeSpace(text: String): Unit = + sw.writeSpace(text) + + def writeSpace(text: Array[Char], offset: Int, length: Int): Unit = + sw.writeSpace(text, offset, length) + + def writeRaw(text: String): Unit = + sw.writeRaw(text) + + def writeRaw(text: String, offset: Int, length: Int): Unit = + sw.writeRaw(text, offset, length) + + def writeRaw(text: Array[Char], offset: Int, length: Int): Unit = + sw.writeRaw(text, offset, length) + + def copyEventFromReader(r: XMLStreamReader2, preserveEventData: Boolean): Unit = + sw.copyEventFromReader(r, preserveEventData) + + def closeCompletely(): Unit = + sw.closeCompletely() + + def writeBoolean(value: Boolean): Unit = + sw.writeBoolean(value) + + def writeInt(value: Int): Unit = + sw.writeInt(value) + + def writeLong(value: Long): Unit = + sw.writeLong(value) + + def writeFloat(value: Float): Unit = + sw.writeFloat(value) + + def writeDouble(value: Double): Unit = + sw.writeDouble(value) + + def writeInteger(value: BigInteger): Unit = + sw.writeInteger(value) + + def writeDecimal(value: java.math.BigDecimal): Unit = + sw.writeDecimal(value) + + def writeQName(value: QName): Unit = + sw.writeQName(value) + + def writeBinary(value: Array[Byte], from: Int, length: Int): Unit = + sw.writeBinary(value, from, length) + + def writeBinary(variant: Base64Variant, value: Array[Byte], from: Int, length: Int): Unit = + sw.writeBinary(variant, value, from, length) + + def writeIntArray(value: Array[Int], from: Int, length: Int): Unit = + sw.writeIntArray(value, from, length) + + def writeLongArray(value: Array[Long], from: Int, length: Int): Unit = + sw.writeLongArray(value, from, length) + + def writeFloatArray(value: Array[Float], from: Int, length: Int): Unit = + sw.writeFloatArray(value, from ,length) + + def writeDoubleArray(value: Array[Double], from: Int, length: Int): Unit = + sw.writeDoubleArray(value, from, length) + + def writeBooleanAttribute(prefix: String, namespaceURI: String, localName: String, value: Boolean): Unit = + sw.writeBooleanAttribute(prefix, namespaceURI, localName, value) + + def writeIntAttribute(prefix: String, namespaceURI: String, localName: String, value: Int): Unit = + sw.writeIntAttribute(prefix, namespaceURI, localName, value) + + def writeLongAttribute(prefix: String, namespaceURI: String, localName: String, value: Long): Unit = + sw.writeLongAttribute(prefix, namespaceURI, localName, value) + + def writeFloatAttribute(prefix: String, namespaceURI: String, localName: String, value: Float): Unit = + sw.writeFloatAttribute(prefix, namespaceURI, localName, value) + + def writeDoubleAttribute(prefix: String, namespaceURI: String, localName: String, value: Double): Unit = + sw.writeDoubleAttribute(prefix, namespaceURI, localName, value) + + def writeIntegerAttribute(prefix: String, namespaceURI: String, localName: String, value: BigInteger): Unit = + sw.writeIntegerAttribute(prefix, namespaceURI, localName, value) + + def writeDecimalAttribute(prefix: String, namespaceURI: String, localName: String, value: java.math.BigDecimal): Unit = + sw.writeDecimalAttribute(prefix, namespaceURI, localName, value) + + def writeQNameAttribute(prefix: String, namespaceURI: String, localName: String, value: QName): Unit = + sw.writeQNameAttribute(prefix, namespaceURI, localName, value) + + def writeBinaryAttribute(prefix: String, namespaceURI: String, localName: String, value: Array[Byte]): Unit = + sw.writeBinaryAttribute(prefix, namespaceURI, localName, value) + + def writeBinaryAttribute(variant: Base64Variant, prefix: String, namespaceURI: String, localName: String, value: Array[Byte]): Unit = + sw.writeBinaryAttribute(variant, prefix, namespaceURI, localName, value) + + def writeIntArrayAttribute(prefix: String, namespaceURI: String, localName: String, value: Array[Int]): Unit = + sw.writeIntArrayAttribute(prefix, namespaceURI, localName, value) + + def writeLongArrayAttribute(prefix: String, namespaceURI: String, localName: String, value: Array[Long]): Unit = + sw.writeLongArrayAttribute(prefix, namespaceURI, localName, value) + + def writeFloatArrayAttribute(prefix: String, namespaceURI: String, localName: String, value: Array[Float]): Unit = + sw.writeFloatArrayAttribute(prefix, namespaceURI, localName, value) + + def writeDoubleArrayAttribute(prefix: String, namespaceURI: String, localName: String, value: Array[Double]): Unit = + sw.writeDoubleArrayAttribute(prefix, namespaceURI, localName, value) + + private def maybeWriteDiscriminator(): Unit = { + (discriminatorNamespace, discriminatorLocalName, discriminatorValue) match { + case (None, None, None) => + case (None, Some(dLocalName), Some(dValue)) => sw.writeAttribute(dLocalName, dValue) + case (Some(dNamespace), Some(dLocalName), Some(dValue)) => sw.writeAttribute(dNamespace, dLocalName, dValue) + case state => throw new XMLStreamException(s"Unexpected discriminator names state: $state") + } + discriminatorNamespace = None + discriminatorLocalName = None + discriminatorValue = None + } + + def writeStartElement(localName: String): Unit = { + sw.writeStartElement(localName: String) + maybeWriteDiscriminator() + } + + def writeStartElement(namespaceURI: String, localName: String): Unit = { + sw.writeStartElement(namespaceURI, localName) + maybeWriteDiscriminator() + } + + def writeStartElement(prefix: String, localName: String, namespaceURI: String): Unit = { + sw.writeStartElement(prefix, localName, namespaceURI) + maybeWriteDiscriminator() + } + + def writeEmptyElement(namespaceURI: String, localName: String): Unit = + sw.writeEmptyElement(namespaceURI, localName) + + def writeEmptyElement(prefix: String, localName: String, namespaceURI: String): Unit = + sw.writeEmptyElement(prefix, localName, namespaceURI) + + def writeEmptyElement(localName: String): Unit = + sw.writeEmptyElement(localName) + + def writeEndElement(): Unit = + sw.writeEndElement() + + def writeEndDocument(): Unit = + sw.writeEndDocument() + + def close(): Unit = + sw.close() + + def flush(): Unit = + sw.flush() + + def writeAttribute(localName: String, value: String): Unit = + sw.writeAttribute(localName, value) + + def writeAttribute(prefix: String, namespaceURI: String, localName: String, value: String): Unit = + sw.writeAttribute(prefix, namespaceURI, localName, value) + + def writeAttribute(namespaceURI: String, localName: String, value: String): Unit = + sw.writeAttribute(namespaceURI, localName, value) + + def writeNamespace(prefix: String, namespaceURI: String): Unit = + sw.writeNamespace(prefix, namespaceURI) + + def writeDefaultNamespace(namespaceURI: String): Unit = + sw.writeDefaultNamespace(namespaceURI) + + def writeComment(data: String): Unit = + sw.writeComment(data) + + def writeProcessingInstruction(target: String): Unit = + sw.writeProcessingInstruction(target) + + def writeProcessingInstruction(target: String, data: String): Unit = + sw.writeProcessingInstruction(target, data) + + def writeCData(data: String): Unit = + sw.writeCData(data) + + def writeDTD(dtd: String): Unit = + sw.writeDTD(dtd) + + def writeEntityRef(name: String): Unit = + sw.writeEntityRef(name) + + def writeStartDocument(): Unit = + sw.writeStartDocument() + + def writeStartDocument(version: String): Unit = + sw.writeStartDocument(version) + + def writeStartDocument(encoding: String, version: String): Unit = + sw.writeStartDocument(encoding, version) + + def writeCharacters(text: String): Unit = + sw.writeCharacters(text) + + def writeCharacters(text: Array[Char], start: Int, len: Int): Unit = + sw.writeCharacters(text, start, len) + + def getPrefix(uri: String): String = + sw.getPrefix(uri) + + def setPrefix(prefix: String, uri: String): Unit = + sw.setPrefix(prefix, uri) + + def setDefaultNamespace(uri: String): Unit = + sw.setDefaultNamespace(uri) + + def setNamespaceContext(context: NamespaceContext): Unit = + sw.setNamespaceContext(context) + + def getNamespaceContext: NamespaceContext = + sw.getNamespaceContext + + def getProperty(name: String): AnyRef = + sw.getProperty(name) + + def validateAgainst(schema: XMLValidationSchema): XMLValidator = + sw.validateAgainst(schema) + + def stopValidatingAgainst(schema: XMLValidationSchema): XMLValidator = + sw.stopValidatingAgainst(schema) + + def stopValidatingAgainst(validator: XMLValidator): XMLValidator = + sw.stopValidatingAgainst(validator) + + def setValidationProblemHandler(h: ValidationProblemHandler): ValidationProblemHandler = + sw.setValidationProblemHandler(h) +} diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/TextEncoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/TextEncoder.scala index 98d9d8a..a11b0eb 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/TextEncoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/TextEncoder.scala @@ -4,7 +4,6 @@ import java.time.{LocalDate, LocalDateTime, LocalTime, ZonedDateTime} import java.util.{Base64, UUID} import cats.Contravariant -import org.codehaus.stax2.XMLStreamWriter2 /** * Use XmlEncoder for encoding XML documents. @@ -16,11 +15,11 @@ import org.codehaus.stax2.XMLStreamWriter2 */ trait TextEncoder[A] { self => - def encodeAsText(a: A, sw: XMLStreamWriter2): Unit + def encodeAsText(a: A, sw: PhobosStreamWriter): Unit def contramap[B](f: B => A): TextEncoder[B] = new TextEncoder[B] { - def encodeAsText(b: B, sw: XMLStreamWriter2): Unit = self.encodeAsText(f(b), sw) + def encodeAsText(b: B, sw: PhobosStreamWriter): Unit = self.encodeAsText(f(b), sw) } } @@ -35,12 +34,12 @@ object TextEncoder { */ implicit val stringEncoder: TextEncoder[String] = new TextEncoder[String] { - def encodeAsText(a: String, sw: XMLStreamWriter2): Unit = sw.writeRaw(a) + def encodeAsText(a: String, sw: PhobosStreamWriter): Unit = sw.writeRaw(a) } implicit val unitEncoder: TextEncoder[Unit] = new TextEncoder[Unit] { - def encodeAsText(a: Unit, sw: XMLStreamWriter2): Unit = () + def encodeAsText(a: Unit, sw: PhobosStreamWriter): Unit = () } implicit val booleanEncoder: TextEncoder[Boolean] = stringEncoder.contramap(_.toString) diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/XmlEncoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/XmlEncoder.scala index 074c1c7..2901087 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/XmlEncoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/XmlEncoder.scala @@ -2,9 +2,9 @@ package ru.tinkoff.phobos.encoding import java.io.ByteArrayOutputStream -import org.codehaus.stax2.XMLStreamWriter2 import cats.syntax.option._ import com.fasterxml.aalto.stax.OutputFactoryImpl +import org.codehaus.stax2.XMLStreamWriter2 import ru.tinkoff.phobos.Namespace import ru.tinkoff.phobos.encoding.XmlEncoder.XmlEncoderConfig @@ -31,7 +31,7 @@ trait XmlEncoder[A] { val os = new ByteArrayOutputStream val factory = new OutputFactoryImpl factory.setProperty("javax.xml.stream.isRepairingNamespaces", true) - val sw = factory.createXMLStreamWriter(os, charset).asInstanceOf[XMLStreamWriter2] + val sw = new PhobosStreamWriter(factory.createXMLStreamWriter(os, charset).asInstanceOf[XMLStreamWriter2]) sw.writeStartDocument() elementencoder.encodeAsElement(a, sw, localname, namespaceuri) sw.writeEndDocument() @@ -47,7 +47,7 @@ trait XmlEncoder[A] { val os = new ByteArrayOutputStream val factory = new OutputFactoryImpl factory.setProperty("javax.xml.stream.isRepairingNamespaces", true) - val sw = factory.createXMLStreamWriter(os, config.encoding).asInstanceOf[XMLStreamWriter2] + val sw = new PhobosStreamWriter(factory.createXMLStreamWriter(os, config.encoding).asInstanceOf[XMLStreamWriter2]) if (config.writeProlog) { sw.writeStartDocument(config.encoding, config.version) } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/syntax.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/syntax.scala index 4ca8f8d..f4caa16 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/syntax.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/syntax.scala @@ -2,27 +2,32 @@ package ru.tinkoff.phobos import scala.annotation.StaticAnnotation /** - * Syntax annotations for case class params. See ru.tinkoff.derivation.semiato docs for more explanation. - */ + * Syntax annotations for case class params. See ru.tinkoff.derivation.semiato docs for more explanation. + */ object syntax { /** - * Case class params with @attr annotation are treated as element attributes. - */ + * Case class params with @attr annotation are treated as element attributes. + */ final class attr() extends StaticAnnotation /** - * Case class params with @text annotation are treated as text inside elements. - */ + * Case class params with @text annotation are treated as text inside elements. + */ final class text() extends StaticAnnotation /** - * Annotation @xmlns adds namespace to case class parameter if implicit Namespace[T] exists. - */ + * Annotation @xmlns adds namespace to case class parameter if implicit Namespace[T] exists. + */ final class xmlns[T](ns: T) extends StaticAnnotation /** - * Allows to rename xml tag or attribute name while encoding and decoding - */ + * Allows to rename xml tag or attribute name while encoding and decoding + */ final class renamed(to: String) extends StaticAnnotation + + /** + * Allows to define custom type discriminator value for sealed trait instance + */ + final class discriminator(value: String) extends StaticAnnotation } diff --git a/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationSuit.scala b/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationSuit.scala index 4bde2dc..2d50ce1 100644 --- a/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationSuit.scala +++ b/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationSuit.scala @@ -559,6 +559,241 @@ class DecoderDerivationSuit extends WordSpec with Matchers { "decode with @renamed having priority over naming async" in decodeRenamedPriority(fromIterable) } + "Decoder derivation for sealed traits" should { + def decodeSealedTraits(toList: String => List[Array[Byte]]): Assertion = { + @ElementCodec + case class Bar(d: String, foo: SealedClasses.Foo, e: Char) + + val bar1 = Bar("d value", SealedClasses.Foo1("string"), 'k') + val bar2 = Bar("d value", SealedClasses.Foo2(1), 'e') + val bar3 = Bar("another one value", SealedClasses.Foo3(1.1234), 'v') + + val barDecoder = XmlDecoder.fromElementDecoder[Bar]("bar") + + val string1 = """ + | + | d value + | + | string + | + | k + | + """.stripMargin + val string2 = """ + | + | d value + | + | 1 + | + | e + | + """.stripMargin + val string3 = """ + | + | another one value + | + | 1.1234 + | + | v + | + """.stripMargin + + assert( + barDecoder.decodeFromFoldable(toList(string1)) == Right(bar1) && + barDecoder.decodeFromFoldable(toList(string2)) == Right(bar2) && + barDecoder.decodeFromFoldable(toList(string3)) == Right(bar3) + ) + } + + "decode sealed traits sync" in decodeSealedTraits(pure) + "decode sealed traits async" in decodeSealedTraits(fromIterable) + + def decodeSealedTraitsWithCustomDiscriminator(toList: String => List[Array[Byte]]): Assertion = { + @ElementCodec + case class Qux(d: String, bar: SealedClasses.Bar, e: Char) + + val qux1 = Qux("d value", SealedClasses.Bar1("string"), 'k') + val qux2 = Qux("d value", SealedClasses.Bar2(1), 'e') + val qux3 = Qux("another one value", SealedClasses.Bar3(1.1234), 'v') + + val quxDecoder = XmlDecoder.fromElementDecoder[Qux]("qux") + + val string1 = """ + | + | d value + | + | string + | + | k + | + """.stripMargin + val string2 = """ + | + | d value + | + | 1 + | + | e + | + """.stripMargin + val string3 = """ + | + | another one value + | + | 1.1234 + | + | v + | + """.stripMargin + + assert( + quxDecoder.decodeFromFoldable(toList(string1)) == Right(qux1) && + quxDecoder.decodeFromFoldable(toList(string2)) == Right(qux2) && + quxDecoder.decodeFromFoldable(toList(string3)) == Right(qux3) + ) + + @ElementCodec + case class Quux(d: String, baz: SealedClasses.Baz, e: Char) + + val quux1 = Quux("d value", SealedClasses.Baz1("string"), 'k') + val quux2 = Quux("d value", SealedClasses.Baz2(1), 'e') + val quux3 = Quux("another one value", SealedClasses.Baz3(1.1234), 'v') + + val quuxDecoder = XmlDecoder.fromElementDecoder[Quux]("quux") + + val string4 = """ + | + | d value + | + | string + | + | k + | + """.stripMargin + val string5 = """ + | + | d value + | + | 1 + | + | e + | + """.stripMargin + val string6 = """ + | + | another one value + | + | 1.1234 + | + | v + | + """.stripMargin + + assert( + quuxDecoder.decodeFromFoldable(toList(string4)) == Right(quux1) && + quuxDecoder.decodeFromFoldable(toList(string5)) == Right(quux2) && + quuxDecoder.decodeFromFoldable(toList(string6)) == Right(quux3) + ) + } + + "decode sealed traits with custom discriminator sync" in decodeSealedTraitsWithCustomDiscriminator(pure) + "decode sealed traits with custom discriminator async" in decodeSealedTraitsWithCustomDiscriminator(fromIterable) + + def decodeSealedTraitsWithConstructorNamesTransformed(toList: String => List[Array[Byte]]) = { + val wolf = SealedClasses.CanisLupus("Igor", 0.2, 20) + val lion = SealedClasses.PantheraLeo("Sergey", 0.75, 60.1) + + val animalDecoder = XmlDecoder.fromElementDecoder[SealedClasses.Mammalia]("animal") + + val string1 = """ + | + | Igor + | 0.2 + | 20 + | + """.stripMargin + + val string2 = """ + | + | Sergey + | 0.75 + | 60.1 + | + """.stripMargin + + assert( + animalDecoder.decodeFromFoldable(toList(string1)) == Right(wolf) && + animalDecoder.decodeFromFoldable(toList(string2)) == Right(lion) + ) + } + + "decode sealed traits with constructor names transformed sync" in + decodeSealedTraitsWithConstructorNamesTransformed(pure) + "decode sealed traits with constructor names transformed async" in + decodeSealedTraitsWithConstructorNamesTransformed(fromIterable) + + def decodeSealedTraitsWithCustomDiscriminatorValues(toList: String => List[Array[Byte]]): Assertion = { + val hornet = SealedClasses.Vespa("Anton", 200.123) + val cockroach = SealedClasses.Blattodea("Dmitriy", 5) + + val insectDecoder = XmlDecoder.fromElementDecoder[SealedClasses.Insecta]("insect") + + val string1 = """ + | + | Anton + | 200.123 + | + """.stripMargin + + val string2 = """ + | + | Dmitriy + | 5 + | + """.stripMargin + + assert( + insectDecoder.decodeFromFoldable(toList(string1)) == Right(hornet) && + insectDecoder.decodeFromFoldable(toList(string2)) == Right(cockroach) + ) + } + + "decode sealed traits with custom discriminator values sync" in + decodeSealedTraitsWithCustomDiscriminatorValues(pure) + "decode sealed traits with custom discriminator values async" in + decodeSealedTraitsWithCustomDiscriminatorValues(fromIterable) + + def notTransformCustomDiscriminatorValues(toList: String => List[Array[Byte]]): Assertion = { + val clownFish = SealedClasses.Amphiprion("Nemo", 1) + val whiteShark = SealedClasses.CarcharodonCarcharias("Bill", 20000000000L) + + val fishDecoder = XmlDecoder.fromElementDecoder[SealedClasses.Pisces]("fish") + + val string1 = """ + | + | Nemo + | 1 + | + """.stripMargin + + val string2 = """ + | + | Bill + | 20000000000 + | + """.stripMargin + + assert( + fishDecoder.decodeFromFoldable(toList(string1)) == Right(clownFish) && + fishDecoder.decodeFromFoldable(toList(string2)) == Right(whiteShark) + ) + } + "not transform custom discriminator values sync" in + notTransformCustomDiscriminatorValues(pure) + "not transform custom discriminator values async" in + notTransformCustomDiscriminatorValues(fromIterable) + } + "Decoder derivation with namespaces" should { def decodeSimpleCaseClasses(toList: String => List[Array[Byte]]): Assertion = { diff --git a/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationSuit.scala b/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationSuit.scala index 3247945..daaebfc 100644 --- a/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationSuit.scala +++ b/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationSuit.scala @@ -213,67 +213,6 @@ class EncoderDerivationSuit extends WordSpec with Matchers { """.stripMargin.minimized) } - // "encode sealed traits" in { - // @ElementCodec - // sealed trait Foo - // object Foo { - // @ElementCodec - // case class Foo1(a: String) extends Foo - // @ElementCodec - // case class Foo2(b: Int) extends Foo - // @ElementCodec - // case class Foo3(c: Char) extends Foo - // } - // import Foo._ - // @ElementCodec - // case class Bar(d: String, foo: Foo, e: Char) - // - // val bar1 = Bar("d value", Foo1("string"), 'e') - // val bar2 = Bar("d value", Foo2(1), 'e') - // val bar3 = Bar("another one value", Foo3('c'), 'v') - // val barEncoder = XmlEncoder.fromTagEncoder[Bar]("bar") - // (for { - // xml1 <- barEncoder.encode(bar1).firstL - // xml2 <- barEncoder.encode(bar2).firstL - // xml3 <- barEncoder.encode(bar3).firstL - // } yield { - // assert( - // xml1 == - // """ - // | - // | - // | d value - // | - // | string - // | - // | e - // | - // """.stripMargin.minimized && - // xml2 == - // """ - // | - // | - // | d value - // | - // | 1 - // | - // | e - // | - // """.stripMargin.minimized && - // xml3 == - // """ - // | - // | - // | another one value - // | - // | c - // | - // | v - // | - // """.stripMargin.minimized) - // }).runToFuture - // } - "encode mixed content" in { @XmlCodec("foo") case class Foo(count: Int, buz: String, @text text: String) @@ -408,6 +347,235 @@ class EncoderDerivationSuit extends WordSpec with Matchers { } } + "Encoder derivation for sealed traits" should { + "encode simple sealed traits" in { + @ElementCodec + case class Bar(d: String, foo: SealedClasses.Foo, e: Char) + + val bar1 = Bar("d value", SealedClasses.Foo1("string"), 'k') + val bar2 = Bar("d value", SealedClasses.Foo2(1), 'e') + val bar3 = Bar("another one value", SealedClasses.Foo3(1.1234), 'v') + + val barEncoder = XmlEncoder.fromElementEncoder[Bar]("bar") + + val string1 = barEncoder.encode(bar1) + val string2 = barEncoder.encode(bar2) + val string3 = barEncoder.encode(bar3) + + assert( + string1 == + """ + | + | + | d value + | + | string + | + | k + | + """.stripMargin.minimized && + string2 == + """ + | + | + | d value + | + | 1 + | + | e + | + """.stripMargin.minimized && + string3 == + """ + | + | + | another one value + | + | 1.1234 + | + | v + | + """.stripMargin.minimized) + } + + "encode sealed traits with custom discriminator" in { + @ElementCodec + case class Qux(d: String, bar: SealedClasses.Bar, e: Char) + + val qux1 = Qux("d value", SealedClasses.Bar1("string"), 'k') + val qux2 = Qux("d value", SealedClasses.Bar2(1), 'e') + val qux3 = Qux("another one value", SealedClasses.Bar3(1.1234), 'v') + + val quxEncoder = XmlEncoder.fromElementEncoder[Qux]("qux") + + val string1 = quxEncoder.encode(qux1) + val string2 = quxEncoder.encode(qux2) + val string3 = quxEncoder.encode(qux3) + + assert( + string1 == + """ + | + | + | d value + | + | string + | + | k + | + """.stripMargin.minimized && + string2 == + """ + | + | + | d value + | + | 1 + | + | e + | + """.stripMargin.minimized && + string3 == + """ + | + | + | another one value + | + | 1.1234 + | + | v + | + """.stripMargin.minimized) + + @ElementCodec + case class Quux(d: String, baz: SealedClasses.Baz, e: Char) + + val quux1 = Quux("d value", SealedClasses.Baz1("string"), 'k') + val quux2 = Quux("d value", SealedClasses.Baz2(1), 'e') + val quux3 = Quux("another one value", SealedClasses.Baz3(1.1234), 'v') + + val quuxEncoder = XmlEncoder.fromElementEncoder[Quux]("quux") + + val string4 = quuxEncoder.encode(quux1) + val string5 = quuxEncoder.encode(quux2) + val string6 = quuxEncoder.encode(quux3) + + assert( + string4 == + """ + | + | + | d value + | + | string + | + | k + | + """.stripMargin.minimized && + string5 == + """ + | + | + | d value + | + | 1 + | + | e + | + """.stripMargin.minimized && + string6 == + """ + | + | + | another one value + | + | 1.1234 + | + | v + | + """.stripMargin.minimized) + } + + "encode sealed traits with constructor names transformed" in { + val wolf = SealedClasses.CanisLupus("Igor", 0.2, 20) + val lion = SealedClasses.PantheraLeo("Sergey", 0.75, 60.1) + + val animalEncoder = XmlEncoder.fromElementEncoder[SealedClasses.Mammalia]("animal") + + assert( + animalEncoder.encode(wolf) == + """ + | + | + | Igor + | 0.2 + | 20 + | + """.stripMargin.minimized && + animalEncoder.encode(lion) == + """ + | + | + | Sergey + | 0.75 + | 60.1 + | + """.stripMargin.minimized + ) + } + + "encode sealed traits with custom discriminator values" in { + val hornet = SealedClasses.Vespa("Anton", 200.123) + val cockroach = SealedClasses.Blattodea("Dmitriy", 5) + + val insectEncoder = XmlEncoder.fromElementEncoder[SealedClasses.Insecta]("insect") + + assert( + insectEncoder.encode(hornet) == + """ + | + | + | Anton + | 200.123 + | + """.stripMargin.minimized && + insectEncoder.encode(cockroach) == + """ + | + | + | Dmitriy + | 5 + | + """.stripMargin.minimized) + } + + "not transform custom discriminator values" in { + val clownFish = SealedClasses.Amphiprion("Nemo", 1) + val whiteShark = SealedClasses.CarcharodonCarcharias("Bill", 20000000000L) + + val fishEncoder = XmlEncoder.fromElementEncoder[SealedClasses.Pisces]("fish") + + assert( + fishEncoder.encode(clownFish) == + """ + | + | + | Nemo + | 1 + | + """.stripMargin.minimized && + fishEncoder.encode(whiteShark) == + """ + | + | + | Bill + | 20000000000 + | + """.stripMargin.minimized + ) + } + } + "Encoder derivation with namespaces" should { "encode simple case classes" in { @XmlnsDef("tinkoff.ru") diff --git a/modules/core/src/test/scala/ru/tinkoff/phobos/SealedClasses.scala b/modules/core/src/test/scala/ru/tinkoff/phobos/SealedClasses.scala new file mode 100644 index 0000000..4fc1ed1 --- /dev/null +++ b/modules/core/src/test/scala/ru/tinkoff/phobos/SealedClasses.scala @@ -0,0 +1,76 @@ +package ru.tinkoff.phobos + +import ru.tinkoff.phobos.annotations.ElementCodec +import ru.tinkoff.phobos.configured.ElementCodecConfig +import ru.tinkoff.phobos.configured.naming._ +import ru.tinkoff.phobos.syntax.discriminator + +object SealedClasses { + @ElementCodec + sealed trait Foo + + + @ElementCodec + case class Foo1(a: String) extends Foo + @ElementCodec + case class Foo2(b: Int) extends Foo + @ElementCodec + case class Foo3(c: Double) extends Foo + + + @ElementCodec(ElementCodecConfig.default.withDiscriminator("discriminator", None)) + sealed trait Bar + + @ElementCodec + case class Bar1(a: String) extends Bar + @ElementCodec + case class Bar2(b: Int) extends Bar + @ElementCodec + case class Bar3(c: Double) extends Bar + + + @ElementCodec(ElementCodecConfig.default.withDiscriminator("discriminator", Some("https://tinkoff.ru"))) + sealed trait Baz + + @ElementCodec + case class Baz1(a: String) extends Baz + @ElementCodec + case class Baz2(b: Int) extends Baz + @ElementCodec + case class Baz3(c: Double) extends Baz + + + @ElementCodec(ElementCodecConfig.default.withConstructorsRenamed(snakeCase)) + sealed trait Mammalia { + def name: String + def strength: Double + } + + @ElementCodec + case class CanisLupus(name: String, strength: Double, age: Int) extends Mammalia + @ElementCodec + case class PantheraLeo(name: String, strength: Double, speed: Double) extends Mammalia + + + @ElementCodec + sealed trait Insecta + + @ElementCodec + @discriminator("hornet") + case class Vespa(name: String, damage: Double) extends Insecta + + @ElementCodec + @discriminator("cockroach") + case class Blattodea(name: String, legsNumber: Int) extends Insecta + + + @ElementCodec(ElementCodecConfig.default.withConstructorsRenamed(snakeCase)) + sealed trait Pisces + + @ElementCodec + @discriminator("ClownFish") + case class Amphiprion(name: String, finNumber: Int) extends Pisces + + @ElementCodec + case class CarcharodonCarcharias(name: String, teethNumber: Long) extends Pisces +} diff --git a/publish.sbt b/publish.sbt index 2220606..dc66998 100644 --- a/publish.sbt +++ b/publish.sbt @@ -1,6 +1,6 @@ import Publish._ -publishVersion := "0.4.0" +publishVersion := "0.5.0" ThisBuild / organization := "ru.tinkoff" ThisBuild / version := {