diff --git a/README.md b/README.md index 817bd1e..107022a 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ val journey = arrival = TravelPoint("Germany", "Munich") ) -val xml: String = XmlEncoder[Journey].encode(journey) +val xml = XmlEncoder[Journey].encode(journey) println(xml) -val decodedJourney = XmlDecoder[Journey].decode(xml) +val decodedJourney = xml.flatMap(XmlDecoder[Journey].decode(_)) println(decodedJourney) assert(Right(journey) == decodedJourney) diff --git a/build.sbt b/build.sbt index 872669a..29b3397 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ ThisBuild / name := "phobos" -ThisBuild / scalaVersion := "3.1.2" +ThisBuild / scalaVersion := "3.2.1" lazy val commonDependencies = libraryDependencies ++= diff --git a/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/application.scala b/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/application.scala index 5ec6ed6..53e28f3 100644 --- a/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/application.scala +++ b/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/application.scala @@ -9,7 +9,7 @@ import ru.tinkoff.phobos.encoding.XmlEncoder object application { implicit def soapApplicationXmlMarshaller[T](implicit encoder: XmlEncoder[T]): ToEntityMarshaller[T] = Marshaller.withFixedContentType(MediaTypes.`application/xml` withCharset HttpCharsets.`UTF-8`) { body => - HttpEntity(MediaTypes.`application/xml` withCharset HttpCharsets.`UTF-8`, encoder.encode(body)) + HttpEntity(MediaTypes.`application/xml` withCharset HttpCharsets.`UTF-8`, encoder.encodeUnsafe(body)) } implicit def soapApplicationXmlUnmarshaller[T](implicit decoder: XmlDecoder[T]): FromEntityUnmarshaller[T] = diff --git a/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/text.scala b/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/text.scala index b9aeed6..31070e9 100644 --- a/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/text.scala +++ b/modules/akka-http/src/main/scala/ru/tinkoff/phobos/akka_http/marshalling/text.scala @@ -10,7 +10,7 @@ import ru.tinkoff.phobos.encoding.XmlEncoder object text { implicit def soapTextXmlMarshaller[T](implicit encoder: XmlEncoder[T]): ToEntityMarshaller[T] = Marshaller.withFixedContentType(`text/xml(UTF-8)`) { body => - HttpEntity(`text/xml(UTF-8)`, encoder.encode(body)) + HttpEntity(`text/xml(UTF-8)`, encoder.encodeUnsafe(body)) } implicit def soapTextXmlUnmarshaller[T](implicit decoder: XmlDecoder[T]): FromEntityUnmarshaller[T] = diff --git a/modules/akka-stream/src/main/scala/ru/tinkoff/phobos/ops/AkkaStreamOps.scala b/modules/akka-stream/src/main/scala/ru/tinkoff/phobos/ops/AkkaStreamOps.scala index 5dc299a..5490d22 100644 --- a/modules/akka-stream/src/main/scala/ru/tinkoff/phobos/ops/AkkaStreamOps.scala +++ b/modules/akka-stream/src/main/scala/ru/tinkoff/phobos/ops/AkkaStreamOps.scala @@ -39,7 +39,7 @@ private[phobos] trait AkkaStreamOps { } .map { case None => - throw DecodingError("Got an internal error while decoding byte stream", Nil) + throw DecodingError("Got an internal error while decoding byte stream", Nil, None) case Some(SinkDecoderState(_, cursor, elementDecoder)) => elementDecoder.result(cursor.history) diff --git a/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/CodecProperties.scala b/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/CodecProperties.scala index 7e3a10c..bbcc292 100644 --- a/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/CodecProperties.scala +++ b/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/CodecProperties.scala @@ -13,15 +13,12 @@ class CodecProperties extends Properties("Ast codecs") { private val decoder = XmlDecoder.fromElementDecoder[XmlEntry]("test") property("decode(encode(ast)) === ast") = forAll { entry: XmlEntry => - decoder.decode( - encoder.encode(entry), - ) == Right(entry) + encoder.encode(entry).flatMap(decoder.decode(_)) == Right(entry) } property("encode(decode(xmlAst)) === xmlAst") = forAll { entry: XmlEntry => val encoded = encoder.encode(entry) - - decoder.decode(encoded).map(encoder.encode(_)) == Right(encoded) + encoded.flatMap(decoder.decode(_)).map(encoder.encode(_)) == Right(encoded) } } diff --git a/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementDecoderTest.scala b/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementDecoderTest.scala index c3e2386..755b0d4 100644 --- a/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementDecoderTest.scala +++ b/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementDecoderTest.scala @@ -60,15 +60,9 @@ class XmlEntryElementDecoderTest extends AnyWordSpec with Matchers with DiffShou val encoded = ru.tinkoff.phobos.encoding.XmlEncoder.fromElementEncoder[XmlEntry]("ast").encode(n) - val result = XmlDecoder - .fromElementDecoder[XmlEntry]("ast") - .decode( - encoded, - ) + val result = encoded.flatMap(XmlDecoder.fromElementDecoder[XmlEntry]("ast").decode(_)) - result.map(util.AstTransformer.sortNodeValues) shouldMatchTo ( - util.AstTransformer.sortNodeValues(n).asRight[DecodingError] - ) + assert(result.map(util.AstTransformer.sortNodeValues) == Right(util.AstTransformer.sortNodeValues(n))) } } } diff --git a/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementEncoderTest.scala b/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementEncoderTest.scala index 37d952a..7151db2 100644 --- a/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementEncoderTest.scala +++ b/modules/ast/src/test/scala/ru/tinkoff/phobos/ast/XmlEntryElementEncoderTest.scala @@ -16,7 +16,7 @@ class XmlEntryElementEncoderTest extends AnyWordSpec with DiffShouldMatcher with .fromElementEncoder[XmlEntry]("ast") .encode(ast) - assert(result == """bazz""") + assert(result == Right("""bazz""")) } "encodes nested Xml ast correctly" in { case object tinkoff { @@ -46,7 +46,9 @@ class XmlEntryElementEncoderTest extends AnyWordSpec with DiffShouldMatcher with .encode(ast) assert( - result == """bazz11111111111111111111111111122.130.13""", + result == Right( + """bazz11111111111111111111111111122.130.13""", + ), ) } } diff --git a/modules/cats/src/main/scala/ru/tinkoff/phobos/catsInstances.scala b/modules/cats/src/main/scala/ru/tinkoff/phobos/catsInstances.scala index 04e0a46..6aa3358 100644 --- a/modules/cats/src/main/scala/ru/tinkoff/phobos/catsInstances.scala +++ b/modules/cats/src/main/scala/ru/tinkoff/phobos/catsInstances.scala @@ -62,21 +62,25 @@ object catsInstances { listDecoder[A].emap((history, list) => NonEmptyList .fromList(list) - .fold[Either[DecodingError, NonEmptyList[A]]](Left(DecodingError("List is empty", history)))(Right.apply), + .fold[Either[DecodingError, NonEmptyList[A]]](Left(DecodingError("List is empty", history, None)))(Right.apply), ) implicit def nonEmptyVectorElementDecoder[A](implicit decoder: ElementDecoder[A]): ElementDecoder[NonEmptyVector[A]] = vectorDecoder[A].emap((history, vector) => NonEmptyVector .fromVector(vector) - .fold[Either[DecodingError, NonEmptyVector[A]]](Left(DecodingError("Vector is empty", history)))(Right.apply), + .fold[Either[DecodingError, NonEmptyVector[A]]](Left(DecodingError("Vector is empty", history, None)))( + Right.apply, + ), ) implicit def nonEmptyChainElementDecoder[A](implicit decoder: ElementDecoder[A]): ElementDecoder[NonEmptyChain[A]] = chainElementDecoder[A].emap((history, chain) => NonEmptyChain .fromChain(chain) - .fold[Either[DecodingError, NonEmptyChain[A]]](Left(DecodingError("Chain is empty", history)))(Right.apply), + .fold[Either[DecodingError, NonEmptyChain[A]]](Left(DecodingError("Chain is empty", history, None)))( + Right.apply, + ), ) implicit class XmlDecoderCatsOps[A](val xmlDecoder: XmlDecoder[A]) extends AnyVal { diff --git a/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala b/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala index 2dd036c..ccf8440 100644 --- a/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala +++ b/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala @@ -8,6 +8,7 @@ private[decoding] trait AttributeLiteralInstances { decoder .emap((history, a) => if (a == valueOfL.value) Right(valueOfL.value) - else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history)), + else + Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history, None)), ) } diff --git a/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala b/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala index ef4e2f1..5e8bfa9 100644 --- a/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala +++ b/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala @@ -5,6 +5,6 @@ private[decoding] trait ElementLiteralInstances { decoder .emap((history, a) => if (a == valueOfL.value) Right(valueOfL.value) - else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history)), + else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history, None)), ) } diff --git a/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala b/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala index 2466d87..f4f1fc5 100644 --- a/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala +++ b/modules/core/src/main/scala-2.13/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala @@ -5,6 +5,7 @@ private[decoding] trait TextLiteralInstances { decoder .emap((history, a) => if (a == valueOfL.value) Right(valueOfL.value) - else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history)), + else + Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history, None)), ) } diff --git a/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala b/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala index 2dd036c..d0fc926 100644 --- a/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala +++ b/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/AttributeLiteralInstances.scala @@ -8,6 +8,6 @@ private[decoding] trait AttributeLiteralInstances { decoder .emap((history, a) => if (a == valueOfL.value) Right(valueOfL.value) - else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history)), + else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history, None)), ) } diff --git a/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala b/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala index ef4e2f1..5e8bfa9 100644 --- a/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala +++ b/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/ElementLiteralInstances.scala @@ -5,6 +5,6 @@ private[decoding] trait ElementLiteralInstances { decoder .emap((history, a) => if (a == valueOfL.value) Right(valueOfL.value) - else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history)), + else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history, None)), ) } diff --git a/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala b/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala index 2466d87..32a1c61 100644 --- a/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala +++ b/modules/core/src/main/scala-3/ru/tinkoff/phobos/decoding/TextLiteralInstances.scala @@ -5,6 +5,6 @@ private[decoding] trait TextLiteralInstances { decoder .emap((history, a) => if (a == valueOfL.value) Right(valueOfL.value) - else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history)), + else Left(DecodingError(s"Failed to decode literal type. Expected: ${valueOfL.value}, actual: $a", history, None)), ) } diff --git a/modules/core/src/main/scala-3/ru/tinkoff/phobos/derivation/decoder.scala b/modules/core/src/main/scala-3/ru/tinkoff/phobos/derivation/decoder.scala index 47ae111..70c570c 100644 --- a/modules/core/src/main/scala-3/ru/tinkoff/phobos/derivation/decoder.scala +++ b/modules/core/src/main/scala-3/ru/tinkoff/phobos/derivation/decoder.scala @@ -228,7 +228,7 @@ object decoder { $currentFieldStates .getOrElse( ${Expr(field.localName)}, - Left(DecodingError(s"Attribute '${${field.xmlName}}' is missing or invalid", $c.history)) + Left(DecodingError(s"Attribute '${${field.xmlName}}' is missing or invalid", $c.history, None)) ) .asInstanceOf[Either[DecodingError, t]] .flatMap { ${f.asExprOf[t => Either[DecodingError, T]]} } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/AttributeDecoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/AttributeDecoder.scala index 233baf6..137783f 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/AttributeDecoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/AttributeDecoder.scala @@ -64,7 +64,7 @@ object AttributeDecoder extends AttributeLiteralInstances { string match { case "true" | "1" => Right(true) case "false" | "0" => Right(false) - case str => Left(DecodingError(s"Value `$str` is not `true` or `false`", history)) + case str => Left(DecodingError(s"Value `$str` is not `true` or `false`", history, None)) }, ) @@ -73,7 +73,7 @@ object AttributeDecoder extends AttributeLiteralInstances { implicit val charDecoder: AttributeDecoder[Char] = stringDecoder.emap((history, string) => { if (string.length != 1) { - Left(DecodingError("Value too long for char", history)) + Left(DecodingError("Value too long for char", history, None)) } else { Right(string.head) } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/Cursor.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/Cursor.scala index 0c6a08a..b9a3c5e 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/Cursor.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/Cursor.scala @@ -32,7 +32,7 @@ class Cursor(private val sr: XmlStreamReader) { def hasNext: Boolean = sr.hasNext def history: List[String] = historyStack - def error(text: String): DecodingError = DecodingError(text, historyStack) + def error(text: String): DecodingError = DecodingError(text, historyStack, None) var scopeDefaultNamespaceStack: List[String] = Nil def setScopeDefaultNamespace(uri: String): Unit = { diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/DecodingError.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/DecodingError.scala index 2fdca90..e453a82 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/DecodingError.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/DecodingError.scala @@ -1,6 +1,7 @@ package ru.tinkoff.phobos.decoding -case class DecodingError(text: String, history: List[String]) extends Exception { +case class DecodingError(text: String, history: List[String], cause: Option[Throwable]) + extends Exception(cause.orNull) { override def getMessage: String = { val trace = if (history.nonEmpty) { history.mkString("\tIn element '", "'\n\t\tin element '", "'") diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/ElementDecoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/ElementDecoder.scala index 67d1d3a..d7b241e 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/ElementDecoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/ElementDecoder.scala @@ -58,8 +58,8 @@ object ElementDecoder extends ElementLiteralInstances with DerivedElement { def decodingNotCompleteError(history: List[String]): DecodingError = history match { - case element :: others => DecodingError(s"Element '$element' is missing or invalid", others) - case Nil => DecodingError("Root element is missing or invalid", Nil) + case element :: others => DecodingError(s"Element '$element' is missing or invalid", others, None) + case Nil => DecodingError("Root element is missing or invalid", Nil, None) } final class MappedDecoder[A, B](fa: ElementDecoder[A], f: A => B) extends ElementDecoder[B] { @@ -161,7 +161,7 @@ object ElementDecoder extends ElementLiteralInstances with DerivedElement { string match { case "true" | "1" => Right(true) case "false" | "0" => Right(false) - case str => Left(DecodingError(s"Value `$str` is not `true` or `false`", history)) + case str => Left(DecodingError(s"Value `$str` is not `true` or `false`", history, None)) }, ) @@ -170,7 +170,7 @@ object ElementDecoder extends ElementLiteralInstances with DerivedElement { implicit val charDecoder: ElementDecoder[Char] = stringDecoder.emap((history, string) => { if (string.length != 1) { - Left(DecodingError("Value too long for char", history)) + Left(DecodingError("Value too long for char", history, None)) } else { Right(string.head) } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/TextDecoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/TextDecoder.scala index 056b393..f87abab 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/TextDecoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/TextDecoder.scala @@ -113,7 +113,7 @@ object TextDecoder extends TextLiteralInstances { string match { case "true" | "1" => Right(true) case "false" | "0" => Right(false) - case str => Left(DecodingError(s"Value `$str` is not `true` or `false`", history)) + case str => Left(DecodingError(s"Value `$str` is not `true` or `false`", history, None)) }, ) @@ -122,7 +122,7 @@ object TextDecoder extends TextLiteralInstances { implicit val charDecoder: TextDecoder[Char] = stringDecoder.emap((history, string) => { if (string.length != 1) { - Left(DecodingError("Value too long for char", history)) + Left(DecodingError("Value too long for char", history, None)) } else { Right(string.head) } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/XmlDecoder.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/XmlDecoder.scala index cdfca74..8dc3637 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/XmlDecoder.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/XmlDecoder.scala @@ -1,7 +1,7 @@ package ru.tinkoff.phobos.decoding import javax.xml.stream.XMLStreamConstants -import com.fasterxml.aalto.{AsyncByteArrayFeeder, WFCException} +import com.fasterxml.aalto.AsyncByteArrayFeeder import com.fasterxml.aalto.async.{AsyncByteArrayScanner, AsyncStreamReaderImpl} import com.fasterxml.aalto.stax.InputFactoryImpl import ru.tinkoff.phobos.Namespace @@ -42,8 +42,8 @@ trait XmlDecoder[A] extends XmlDecoderIterable[A] { .decodeAsElement(cursor, localname, namespaceuri) .result(cursor.history) } catch { - case e: WFCException => - Left(DecodingError(e.getMessage, cursor.history)) + case e: Throwable => + Left(DecodingError(Option(e.getMessage).getOrElse("No message provided"), cursor.history, Some(e))) } } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/package.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/package.scala index 3cc949a..653ad56 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/package.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/decoding/package.scala @@ -10,7 +10,8 @@ package object decoding { private[decoding] def wrapException[A](f: String => A) = (history: List[String], string: String) => Try(f(string)) match { - case Failure(exception) => Left(DecodingError(exception.getMessage, history)) - case Success(a) => Right(a) + case Failure(exception) => + Left(DecodingError(Option(exception.getMessage).getOrElse("No text provided"), history, Some(exception))) + case Success(a) => Right(a) } } diff --git a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/EncodingError.scala b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/EncodingError.scala new file mode 100644 index 0000000..2710f60 --- /dev/null +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/EncodingError.scala @@ -0,0 +1,5 @@ +package ru.tinkoff.phobos.encoding + +case class EncodingError(text: String, cause: Option[Throwable] = None) extends Exception(text, cause.orNull) { + override def getMessage: String = s"Error while decoding XML: $text" +} 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 index 5fa9e91..f72031c 100644 --- a/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/PhobosStreamWriter.scala +++ b/modules/core/src/main/scala/ru/tinkoff/phobos/encoding/PhobosStreamWriter.scala @@ -1,7 +1,6 @@ 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 @@ -9,7 +8,13 @@ import org.codehaus.stax2.{XMLStreamLocation2, XMLStreamReader2, XMLStreamWriter import org.codehaus.stax2.validation.{ValidationProblemHandler, XMLValidationSchema, XMLValidator} import PhobosStreamWriter.prefixBase -final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { +/** [[PhobosStreamWriter]] implements most methods of [[XMLStreamWriter2]], but it does not extends [[XMLStreamWriter2]] + * + * Unlike [[XMLStreamWriter2]], [[PhobosStreamWriter]] does not throw any exceptions if it is used to write invalid + * characters. Such characters are simply filtered out. However, [[PhobosStreamWriter]] can still throw exceptions if + * it is used to write invalid attribute or element names or namespaces with invalid characters. + */ +final class PhobosStreamWriter(sw: XMLStreamWriter2) { private var discriminatorLocalName: Option[String] = None private var discriminatorNamespace: Option[String] = None @@ -49,9 +54,6 @@ final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { 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) @@ -62,19 +64,10 @@ final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { 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) + sw.writeSpace(text.filter(_.isWhitespace)) 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) + sw.writeRaw(filterXmlText(text)) def copyEventFromReader(r: XMLStreamReader2, preserveEventData: Boolean): Unit = sw.copyEventFromReader(r, preserveEventData) @@ -226,13 +219,13 @@ final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { sw.flush() def writeAttribute(localName: String, value: String): Unit = - sw.writeAttribute(localName, value) + sw.writeAttribute(localName, filterXmlText(value)) def writeAttribute(prefix: String, namespaceURI: String, localName: String, value: String): Unit = - sw.writeAttribute(prefix, namespaceURI, localName, value) + sw.writeAttribute(prefix, namespaceURI, localName, filterXmlText(value)) def writeAttribute(namespaceURI: String, localName: String, value: String): Unit = - sw.writeAttribute(namespaceURI, localName, value) + sw.writeAttribute(namespaceURI, localName, filterXmlText(value)) def writeNamespace(prefix: String, namespaceURI: String): Unit = sw.writeNamespace(prefix, namespaceURI) @@ -264,7 +257,7 @@ final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { sw.writeDefaultNamespace(namespaceURI) def writeComment(data: String): Unit = - sw.writeComment(data) + sw.writeComment(filterXmlText(data)) def writeProcessingInstruction(target: String): Unit = sw.writeProcessingInstruction(target) @@ -273,7 +266,7 @@ final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { sw.writeProcessingInstruction(target, data) def writeCData(data: String): Unit = - sw.writeCData(data) + sw.writeCData(filterXmlText(data)) def writeDTD(dtd: String): Unit = sw.writeDTD(dtd) @@ -291,10 +284,7 @@ final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { 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) + sw.writeCharacters(filterXmlText(text)) def getPrefix(uri: String): String = sw.getPrefix(uri) @@ -326,8 +316,16 @@ final class PhobosStreamWriter(sw: XMLStreamWriter2) extends XMLStreamWriter2 { def setValidationProblemHandler(h: ValidationProblemHandler): ValidationProblemHandler = sw.setValidationProblemHandler(h) + private def filterXmlText(string: String) = + if (string.forall(PhobosStreamWriter.isValidXmlCharacter)) string + else string.filter(PhobosStreamWriter.isValidXmlCharacter) } object PhobosStreamWriter { val prefixBase = "ans" + + def isValidXmlCharacter(char: Char): Boolean = + !(char < ' ' + || 0xd800 <= char && char <= 0xdfff + || char == 0xfffe || char == 0xffff) } 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 b9c2a08..bc65549 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 @@ -28,7 +28,7 @@ object TextEncoder extends TextLiteralInstances { */ implicit val stringEncoder: TextEncoder[String] = new TextEncoder[String] { - def encodeAsText(a: String, sw: PhobosStreamWriter): Unit = sw.writeRaw(a) + def encodeAsText(a: String, sw: PhobosStreamWriter): Unit = sw.writeCharacters(a) } implicit val unitEncoder: TextEncoder[Unit] = 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 23c74ca..400de64 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 @@ -24,10 +24,40 @@ trait XmlEncoder[A] { val preferredNamespacePrefix: Option[String] = None val elementEncoder: ElementEncoder[A] - def encode(a: A, charset: String = "UTF-8"): String = - new String(encodeToBytes(a, charset), charset) + // Methods below previously returned a pure value, now these methods return Either to handle encoding errors. + // Use -Unsafe versions for the old behavior. + // + // Encoding may fail if resulting XML document has illegal characters (https://www.w3.org/TR/xml11/#charsets) + // or if attribute and element names are invalid, for example "1stElement". - def encodeToBytes(a: A, charset: String = "UTF-8"): Array[Byte] = { + def encode(a: A, charset: String = "UTF-8"): Either[EncodingError, String] = + wrapXmlException(encodeUnsafe(a, charset)) + + def encodeToBytes(a: A, charset: String = "UTF-8"): Either[EncodingError, Array[Byte]] = + wrapXmlException(encodeToBytesUnsafe(a, charset)) + + def encodeWithConfig(a: A, config: XmlEncoderConfig): Either[EncodingError, String] = + wrapXmlException(encodeWithConfigUnsafe(a, config)) + + def encodeToBytesWithConfig(a: A, config: XmlEncoderConfig): Either[EncodingError, Array[Byte]] = + wrapXmlException(encodeToBytesWithConfigUnsafe(a, config)) + + def encodePrettyWithConfig(a: A, config: XmlEncoderConfig, ident: Int = 2): Either[EncodingError, String] = + wrapXmlException(encodePrettyWithConfigUnsafe(a, config, ident)) + + def encodePretty(a: A, charset: String = "UTF-8", ident: Int = 2): Either[EncodingError, String] = + wrapXmlException(encodePrettyUnsafe(a, charset, ident)) + + // Methods below can throw exceptions in some rare cases. Use "safe" methods returning Either, + // if you are not sure about the correctness of the data being encoded. + // + // These method may throw if resulting XML document has illegal characters ([[https://www.w3.org/TR/xml11/#charsets]]) + // or if attribute and element names are invalid, for example "1stElement". + + def encodeUnsafe(a: A, charset: String = "UTF-8"): String = + new String(encodeToBytesUnsafe(a, charset), charset) + + def encodeToBytesUnsafe(a: A, charset: String = "UTF-8"): Array[Byte] = { val os = new ByteArrayOutputStream val sw = new PhobosStreamWriter(XmlEncoder.factory.createXMLStreamWriter(os, charset).asInstanceOf[XMLStreamWriter2]) @@ -39,10 +69,10 @@ trait XmlEncoder[A] { os.toByteArray } - def encodeWithConfig(a: A, config: XmlEncoderConfig): String = - new String(encodeToBytesWithConfig(a, config), config.encoding) + def encodeWithConfigUnsafe(a: A, config: XmlEncoderConfig): String = + new String(encodeToBytesWithConfigUnsafe(a, config), config.encoding) - def encodeToBytesWithConfig(a: A, config: XmlEncoderConfig): Array[Byte] = { + def encodeToBytesWithConfigUnsafe(a: A, config: XmlEncoderConfig): Array[Byte] = { val os = new ByteArrayOutputStream val sw = new PhobosStreamWriter( @@ -63,13 +93,17 @@ trait XmlEncoder[A] { /** Warning: Use .encodePrettyWithConfig only for debugging, as it is less performant. For production use * .encodeWithConfig */ - def encodePrettyWithConfig(a: A, config: XmlEncoderConfig, ident: Int = 2): String = - beautifyXml(encodeWithConfig(a, config), ident) + def encodePrettyWithConfigUnsafe(a: A, config: XmlEncoderConfig, ident: Int = 2): String = + beautifyXml(encodeWithConfigUnsafe(a, config), ident) /** Warning: Use .encodePretty only for debugging, as it is less performant. For production use .encode */ - def encodePretty(a: A, charset: String = "UTF-8", ident: Int = 2): String = - beautifyXml(encode(a, charset), ident) + def encodePrettyUnsafe(a: A, charset: String = "UTF-8", ident: Int = 2): String = + beautifyXml(encodeUnsafe(a, charset), ident) + + private def wrapXmlException[B](xml: => B): Either[EncodingError, B] = + try { Right(xml) } + catch { case e: Throwable => Left(EncodingError(Option(e.getMessage).getOrElse("No message provided"), Some(e))) } private def beautifyXml(xml: String, ident: Int): String = { val t = XmlEncoder.transformerFactory.newTransformer() diff --git a/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralDecodingTest.scala b/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralDecodingTest.scala index cfd2847..1b9ea1a 100644 --- a/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralDecodingTest.scala +++ b/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralDecodingTest.scala @@ -41,7 +41,7 @@ class LiteralDecodingTest extends AnyWordSpec with Matchers { """.stripMargin val decoded = XmlDecoder[Foo].decodeFromIterable(toList(string)) decoded should - matchPattern { case Left(DecodingError("Failed to decode literal type. Expected: Ok, actual: Error", _)) => } + matchPattern { case Left(DecodingError("Failed to decode literal type. Expected: Ok, actual: Error", _, _)) => } } "fail on attributes with incorrect value sync" in failOnAttributesWithIncorrectValue(pure) @@ -75,7 +75,7 @@ class LiteralDecodingTest extends AnyWordSpec with Matchers { """.stripMargin val decoded = XmlDecoder[Foo].decodeFromIterable(toList(string)) decoded should - matchPattern { case Left(DecodingError("Failed to decode literal type. Expected: Ok, actual: Error", _)) => } + matchPattern { case Left(DecodingError("Failed to decode literal type. Expected: Ok, actual: Error", _, _)) => } } "fail on elements with incorrect value sync" in failOnElementsWithIncorrectValue(pure) @@ -109,7 +109,7 @@ class LiteralDecodingTest extends AnyWordSpec with Matchers { """.stripMargin val decoded = XmlDecoder[Foo].decodeFromIterable(toList(string)) decoded should - matchPattern { case Left(DecodingError("Failed to decode literal type. Expected: Ok, actual: Error", _)) => } + matchPattern { case Left(DecodingError("Failed to decode literal type. Expected: Ok, actual: Error", _, _)) => } } "fail on elements with incorrect value sync" in failOnElementsWithIncorrectValue(pure) diff --git a/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralEncodingTest.scala b/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralEncodingTest.scala index 9438b18..6b30606 100644 --- a/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralEncodingTest.scala +++ b/modules/core/src/test/scala-2.13/ru/tinkoff/phobos/LiteralEncodingTest.scala @@ -15,9 +15,9 @@ class LiteralEncodingTest extends AnyWordSpec { val string = XmlEncoder[Foo].encode(Foo("Ok")) assert( string == - """ + Right(""" | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -28,9 +28,9 @@ class LiteralEncodingTest extends AnyWordSpec { val string = XmlEncoder[Foo].encode(Foo("Ok")) assert( string == - """ + Right(""" | Ok - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -41,9 +41,9 @@ class LiteralEncodingTest extends AnyWordSpec { val string = XmlEncoder[Foo].encode(Foo("Ok")) assert( string == - """ + Right(""" | Ok - """.stripMargin.minimized, + """.stripMargin.minimized), ) } } diff --git a/modules/core/src/test/scala-3/ru/tinkoff/phobos/DerivationTest.scala b/modules/core/src/test/scala-3/ru/tinkoff/phobos/DerivationTest.scala index 01f19e5..8d16c9c 100644 --- a/modules/core/src/test/scala-3/ru/tinkoff/phobos/DerivationTest.scala +++ b/modules/core/src/test/scala-3/ru/tinkoff/phobos/DerivationTest.scala @@ -27,7 +27,7 @@ class DerivationTest extends AnyWordSpec with Matchers { """.stripMargin.minimized val encoded = XmlEncoder[Bar].encode(bar) - assert(encoded == string) + assert(encoded == Right(string)) } } diff --git a/modules/core/src/test/scala/ru/tinkoff/phobos/AutoDerivationTest.scala b/modules/core/src/test/scala/ru/tinkoff/phobos/AutoDerivationTest.scala index 8e48a67..ea37043 100644 --- a/modules/core/src/test/scala/ru/tinkoff/phobos/AutoDerivationTest.scala +++ b/modules/core/src/test/scala/ru/tinkoff/phobos/AutoDerivationTest.scala @@ -71,7 +71,7 @@ class AutoDerivationTest extends AnyWordSpec with Matchers { |""".stripMargin val encoder = XmlEncoder.fromElementEncoder[Baz]("baz") - assert(encoder.encode(baz) == bazXml.minimized) + assert(encoder.encode(baz) == Right(bazXml.minimized)) val decoder = XmlDecoder.fromElementDecoder[Baz]("baz") assert(decoder.decode(bazXml) == Right(baz)) diff --git a/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationTest.scala b/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationTest.scala index 9e6e879..7972a6d 100644 --- a/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationTest.scala +++ b/modules/core/src/test/scala/ru/tinkoff/phobos/DecoderDerivationTest.scala @@ -161,20 +161,25 @@ class DecoderDerivationTest extends AnyWordSpec with Matchers { val decodedResultInvalidFoo = XmlDecoder[Wrapper].decode(invalidXmlStringAtFoo) val decodedResultInvalidTotal = XmlDecoder[Wrapper].decode(totallyInvalidXml) - assert( - decodedResultInvalidFoo == Left( - DecodingError( - "Unexpected end tag: expected \n at [row,col {unknown-source}]: [3,8]", - List("foo", "foo", "Wrapper"), - ), - ), - ) + decodedResultInvalidFoo should matchPattern { + case Left( + DecodingError( + "Unexpected end tag: expected \n at [row,col {unknown-source}]: [3,8]", + List("foo", "foo", "Wrapper"), + _, + ), + ) => + } - assert( - decodedResultInvalidTotal == Left( - DecodingError("Unexpected character 'L' (code 76) in prolog\n at [row,col {unknown-source}]: [1,2]", Nil), - ), - ) + decodedResultInvalidTotal should matchPattern { + case Left( + DecodingError( + "Unexpected character 'L' (code 76) in prolog\n at [row,col {unknown-source}]: [1,2]", + Nil, + _, + ), + ) => + } } def decodeNilValues(toList: String => List[Array[Byte]]): Assertion = { @@ -813,7 +818,7 @@ class DecoderDerivationTest extends AnyWordSpec with Matchers { barDecoder.decodeFromIterable(toList(string2)) == Right(bar2) && barDecoder.decodeFromIterable(toList(string3)) == Right(bar3) && barDecoder.decodeFromIterable(toList(string4)) == - Left(DecodingError("Unknown type discriminator value: 'Qux'", List("foo", "bar"))), + Left(DecodingError("Unknown type discriminator value: 'Qux'", List("foo", "bar"), None)), ) } @@ -1060,7 +1065,7 @@ class DecoderDerivationTest extends AnyWordSpec with Matchers { animalXmlDecoder.decode(catString) shouldBe Right(Cat("meow")) animalXmlDecoder.decode(dogString) shouldBe Right(Dog(1234)) animalXmlDecoder.decode(robotString) shouldBe - Left(DecodingError("Unknown type discriminator value: 'robot'", List("robot"))) + Left(DecodingError("Unknown type discriminator value: 'robot'", List("robot"), None)) } "override element name with discriminator in xml decoder if configured sync" in @@ -1458,9 +1463,9 @@ class DecoderDerivationTest extends AnyWordSpec with Matchers { val decoded2 = XmlDecoder[Bar].decode(string2) assert( - decoded1 == Left(DecodingError("Invalid local name. Expected 'bar', but found 'wrong'", List("wrong"))) && + decoded1 == Left(DecodingError("Invalid local name. Expected 'bar', but found 'wrong'", List("wrong"), None)) && decoded2 == Left( - DecodingError("Invalid namespace. Expected 'tinkoff.ru', but found 'tcsbank.ru'", List("bar")), + DecodingError("Invalid namespace. Expected 'tinkoff.ru', but found 'tcsbank.ru'", List("bar"), None), ), ) } diff --git a/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationTest.scala b/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationTest.scala index 23386c6..572d1fa 100644 --- a/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationTest.scala +++ b/modules/core/src/test/scala/ru/tinkoff/phobos/EncoderDerivationTest.scala @@ -27,7 +27,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = xmlEncoder.encode(bar) assert( string == - """ + Right(""" | | | d value @@ -38,7 +38,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -53,7 +53,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -62,7 +62,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -87,13 +87,13 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val xml = XmlEncoder[Qux].encode(qux) assert( xml == - """ + Right(""" | | | constant | text | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -108,7 +108,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val xml2 = XmlEncoder[Wrapper].encode(Wrapper(None)) assert( xml1 == - """ + Right(""" | | | @@ -116,12 +116,12 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 2.0 | | - """.stripMargin.minimized && + """.stripMargin.minimized) && xml2 == - """ + Right(""" | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -145,7 +145,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val xml2 = XmlEncoder[Foos].encode(bar2) assert( xml1 == - """ + Right(""" | | | @@ -163,12 +163,12 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 4 | | - """.stripMargin.minimized + """.stripMargin.minimized) && xml2 == - """ + Right(""" | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -181,9 +181,9 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Foo].encode(foo) assert( string == - """ + Right(""" | Zm9vYmFy - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -198,14 +198,14 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value | 3.0 | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -224,7 +224,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Foo].encode(foo) assert( string == - """ + Right(""" | | | @@ -241,7 +241,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | 0 | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -254,10 +254,10 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Foo].encode(foo) assert( string == - """ + Right(""" | | Sending item to 1Buzz - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -270,12 +270,12 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Foo].encode(foo) assert( string == - """ + Right(""" | | | Esca"'<>&pe | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -290,7 +290,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -301,7 +301,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -314,13 +314,13 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Foo].encode(bar) assert( string == - """ + Right(""" | | | 1 | 3.0 | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -335,14 +335,14 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value | 3.0 | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -358,14 +358,14 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value | 3.0 | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -381,14 +381,14 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value | 3.0 | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -417,8 +417,8 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | """.stripMargin.minimized - assert(fooOptionEncoder.encode(fooOption) == fooOptionString) - assert(fooListEncoder.encode(fooList) == fooListString) + assert(fooOptionEncoder.encode(fooOption) == Right(fooOptionString)) + assert(fooListEncoder.encode(fooList) == Right(fooListString)) } "work for nested higher-kinded data" in { @@ -460,8 +460,8 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | """.stripMargin.minimized - assert(barOptionEncoder.encode(barOption) == barOptionString) - assert(barListEncoder.encode(barList) == barListString) + assert(barOptionEncoder.encode(barOption) == Right(barOptionString)) + assert(barListEncoder.encode(barList) == Right(barListString)) } } @@ -483,7 +483,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string1 == - """ + Right(""" | | | d value @@ -492,9 +492,9 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | k | - """.stripMargin.minimized && + """.stripMargin.minimized) && string2 == - """ + Right(""" | | | d value @@ -503,9 +503,9 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized && + """.stripMargin.minimized) && string3 == - """ + Right(""" | | | another one value @@ -514,7 +514,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | v | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -535,7 +535,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string1 == - """ + Right(""" | | | d value @@ -544,9 +544,9 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | k | - """.stripMargin.minimized && + """.stripMargin.minimized) && string2 == - """ + Right(""" | | | d value @@ -555,9 +555,9 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized && + """.stripMargin.minimized) && string3 == - """ + Right(""" | | | another one value @@ -566,7 +566,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | v | - """.stripMargin.minimized, + """.stripMargin.minimized), ) case class Quux(d: String, baz: SealedClasses.Baz, e: Char) @@ -585,7 +585,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string4 == - """ + Right(""" | | | d value @@ -594,9 +594,9 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | k | - """.stripMargin.minimized && + """.stripMargin.minimized) && string5 == - """ + Right(""" | | | d value @@ -605,9 +605,9 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized && + """.stripMargin.minimized) && string6 == - """ + Right(""" | | | another one value @@ -616,7 +616,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | v | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -628,23 +628,23 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( animalEncoder.encode(wolf) == - """ + Right(""" | | | Igor | 0.2 | 20 | - """.stripMargin.minimized && + """.stripMargin.minimized) && animalEncoder.encode(lion) == - """ + Right(""" | | | Sergey | 0.75 | 60.1 | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -656,21 +656,21 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( insectEncoder.encode(hornet) == - """ + Right(""" | | | Anton | 200.123 | - """.stripMargin.minimized && + """.stripMargin.minimized) && insectEncoder.encode(cockroach) == - """ + Right(""" | | | Dmitriy | 5 | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -682,21 +682,21 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( fishEncoder.encode(clownFish) == - """ + Right(""" | | | Nemo | 1 | - """.stripMargin.minimized && + """.stripMargin.minimized) && fishEncoder.encode(whiteShark) == - """ + Right(""" | | | Bill | 20000000000 | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -723,7 +723,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | """.stripMargin.minimized val zoo = Zoo(List(Cow(12.432), Cat("meow"), Dog(1234), Cat("nya"))) - XmlEncoder[Zoo].encode(zoo) shouldBe string + XmlEncoder[Zoo].encode(zoo) shouldBe Right(string) } "override element name with discriminator in xml encoder if configured" in { @@ -743,8 +743,8 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 1234 | |""".stripMargin.minimized - animalXmlEncoder.encode(cat) shouldBe catString - animalXmlEncoder.encode(dog) shouldBe dogString + animalXmlEncoder.encode(cat) shouldBe Right(catString) + animalXmlEncoder.encode(dog) shouldBe Right(dogString) } } @@ -770,7 +770,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -781,7 +781,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -806,7 +806,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -815,7 +815,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -840,7 +840,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -849,7 +849,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -876,7 +876,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -885,7 +885,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -910,7 +910,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -920,7 +920,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -945,7 +945,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | d value @@ -955,7 +955,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -972,13 +972,13 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | d value | 3.0 | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -999,7 +999,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string == - """ + Right(""" | | | 1 @@ -1008,7 +1008,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1031,7 +1031,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string == - """ + Right(""" | | | 1 @@ -1040,7 +1040,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | e | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1060,7 +1060,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string == - """ + Right(""" | | | 1 @@ -1068,7 +1068,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1091,7 +1091,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string == - """ + Right(""" | | | 1 @@ -1099,7 +1099,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1125,13 +1125,13 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string == - """ + Right(""" | | 1 | b value | 3.0 | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1164,7 +1164,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1203,7 +1203,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | | 1 @@ -1211,7 +1211,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 3.0 | | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1238,7 +1238,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { |""".stripMargin.minimized val aquarium = Aquarium(List(SealedClasses.Amphiprion("Marlin", 3), SealedClasses.CarcharodonCarcharias("Jaws", 1234))) - XmlEncoder[Aquarium].encode(aquarium) shouldBe string + XmlEncoder[Aquarium].encode(aquarium) shouldBe Right(string) } "encode sealed traits using element names as discriminators" in { @@ -1261,7 +1261,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | """.stripMargin.minimized val animalShelter = AnimalShelter(List(Cat("meow"), Dog(1234))) - XmlEncoder[AnimalShelter].encode(animalShelter) shouldBe string + XmlEncoder[AnimalShelter].encode(animalShelter) shouldBe Right(string) } "encode namespaces with preferred prefixes" in { @@ -1289,7 +1289,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | """.stripMargin.minimized - XmlEncoder[Foo].encode(foo) shouldBe string + XmlEncoder[Foo].encode(foo) shouldBe Right(string) } "declare namespaces with preferred prefixes from config" in { @@ -1314,13 +1314,13 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { assert( string == - """ + Right(""" | | 1 | b value | 3.0 | - """.stripMargin.minimized, + """.stripMargin.minimized), ) } @@ -1350,7 +1350,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | 123 | b value @@ -1369,7 +1369,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | | | - |""".stripMargin.minimized, + |""".stripMargin.minimized), ) } @@ -1399,7 +1399,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { val string = XmlEncoder[Bar].encode(bar) assert( string == - """ + Right(""" | | 123 | b value @@ -1410,7 +1410,7 @@ class EncoderDerivationTest extends AnyWordSpec with Matchers { | 4.321 | | - |""".stripMargin.minimized, + |""".stripMargin.minimized), ) } } diff --git a/modules/core/src/test/scala/ru/tinkoff/phobos/XmlEncoderTest.scala b/modules/core/src/test/scala/ru/tinkoff/phobos/XmlEncoderTest.scala index 40377b1..cc913ff 100644 --- a/modules/core/src/test/scala/ru/tinkoff/phobos/XmlEncoderTest.scala +++ b/modules/core/src/test/scala/ru/tinkoff/phobos/XmlEncoderTest.scala @@ -12,7 +12,7 @@ class XmlEncoderTest extends AnyWordSpec with Matchers { implicit val fooEncoder: XmlEncoder[Foo] = deriveXmlEncoder("Foo") XmlEncoder[Foo].encodeWithConfig(Foo(1, "abc", 1.0), XmlEncoder.defaultConfig.withoutProlog) shouldBe - "1abc1.0" + Right("1abc1.0") } "not ignore prolog by default" in { @@ -20,7 +20,7 @@ class XmlEncoderTest extends AnyWordSpec with Matchers { implicit val fooEncoder: XmlEncoder[Foo] = deriveXmlEncoder("Foo") XmlEncoder[Foo].encodeWithConfig(Foo(1, "abc", 1.0), XmlEncoder.defaultConfig) shouldBe - "1abc1.0" + Right("1abc1.0") } "overwrite prolog information if configured" in { @@ -28,7 +28,7 @@ class XmlEncoderTest extends AnyWordSpec with Matchers { implicit val fooEncoder: XmlEncoder[Foo] = deriveXmlEncoder("Foo") XmlEncoder[Foo].encodeWithConfig(Foo(1, "abc", 1.0), XmlEncoder.XmlEncoderConfig("UTF-16", "1.1", true)) shouldBe - "1abc1.0" + Right("1abc1.0") } } } diff --git a/modules/enumeratum/src/main/scala/ru/tinkoff/phobos/enumeratum/XmlEnum.scala b/modules/enumeratum/src/main/scala/ru/tinkoff/phobos/enumeratum/XmlEnum.scala index 79e236f..6693689 100644 --- a/modules/enumeratum/src/main/scala/ru/tinkoff/phobos/enumeratum/XmlEnum.scala +++ b/modules/enumeratum/src/main/scala/ru/tinkoff/phobos/enumeratum/XmlEnum.scala @@ -11,7 +11,7 @@ trait XmlEnum[A <: EnumEntry] { this: Enum[A] => def decodeFromString(history: List[String], str: String): Either[DecodingError, A] = this.withNameOption(str) match { case Some(member) => Right(member) - case _ => Left(DecodingError(s"'$str' in not a member of enum $this", history)) + case _ => Left(DecodingError(s"'$str' in not a member of enum $this", history, None)) } implicit val enumElementDecoder: ElementDecoder[A] = ElementDecoder.stringDecoder.emap(decodeFromString) diff --git a/modules/enumeratum/src/test/scala/ru/tinkoff/phobos/enumeratum/EnumeratumTest.scala b/modules/enumeratum/src/test/scala/ru/tinkoff/phobos/enumeratum/EnumeratumTest.scala index de2ccb3..089705f 100644 --- a/modules/enumeratum/src/test/scala/ru/tinkoff/phobos/enumeratum/EnumeratumTest.scala +++ b/modules/enumeratum/src/test/scala/ru/tinkoff/phobos/enumeratum/EnumeratumTest.scala @@ -34,45 +34,49 @@ class EnumeratumTest extends AnyWordSpec with Matchers { val xml2 = XmlEncoder[Bar].encode(bar2) val xml3 = XmlEncoder[Bar].encode(bar3) val xml4 = XmlEncoder[Baz].encode(baz) + val string1 = + """ + | + | + | d value + | + | Foo1 + | + | e + | + """.stripMargin.minimized + val string2 = + """ + | + | + | d value + | + | Foo2 + | + | e + | + """.stripMargin.minimized + val string3 = + """ + | + | + | another one value + | + | Foo3 + | + | v + | + """.stripMargin.minimized + val string4 = + """ + | + | Foo2 + """.stripMargin.minimized assert( - xml1 == - """ - | - | - | d value - | - | Foo1 - | - | e - | - """.stripMargin.minimized && - xml2 == - """ - | - | - | d value - | - | Foo2 - | - | e - | - """.stripMargin.minimized && - xml3 == - """ - | - | - | another one value - | - | Foo3 - | - | v - | - """.stripMargin.minimized && - xml4 == - """ - | - | Foo2 - """.stripMargin.minimized, + xml1 == Right(string1) && + xml2 == Right(string2) && + xml3 == Right(string3) && + xml4 == Right(string4), ) } diff --git a/modules/fs2-ce2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala b/modules/fs2-ce2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala index 3908252..9da5622 100644 --- a/modules/fs2-ce2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala +++ b/modules/fs2-ce2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala @@ -73,12 +73,12 @@ class ParseTest extends AsyncWordSpec with Inspectors { xml.nestedRepetetive -> Vector(1, 2, 3, 4).map(Foo(_)).map(Right(_)), xml.nestedRepetetiveIcnludingOtherTags -> Vector( Right(Foo(1)), - Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"))), + Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"), None)), Right(Foo(2)), Right(Foo(3)), Right(Foo(4)), Right(Foo(5)), - Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"))), + Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"), None)), ), ) diff --git a/modules/fs2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala b/modules/fs2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala index 244e605..a680862 100644 --- a/modules/fs2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala +++ b/modules/fs2/src/test/scala/ru/tinkoff/phobos/test/ParseTest.scala @@ -77,12 +77,12 @@ class ParseTest extends AsyncWordSpec with Inspectors { xml.nestedRepetetive -> Vector(1, 2, 3, 4).map(Foo(_)).map(Right(_)), xml.nestedRepetetiveIcnludingOtherTags -> Vector( Right(Foo(1)), - Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"))), + Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"), None)), Right(Foo(2)), Right(Foo(3)), Right(Foo(4)), Right(Foo(5)), - Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"))), + Left(DecodingError("Invalid local name. Expected 'foo', but found 'bar'", List("bar", "sub", "root"), None)), ), ) diff --git a/modules/refined/src/main/scala/ru/tinkoff/phobos/refined/decoding/DecodingInstances.scala b/modules/refined/src/main/scala/ru/tinkoff/phobos/refined/decoding/DecodingInstances.scala index 34af0c9..00d84b0 100644 --- a/modules/refined/src/main/scala/ru/tinkoff/phobos/refined/decoding/DecodingInstances.scala +++ b/modules/refined/src/main/scala/ru/tinkoff/phobos/refined/decoding/DecodingInstances.scala @@ -53,6 +53,7 @@ trait DecodingInstances { DecodingError( s"Failed to verify $P refinement for value=$rawValue of raw type $T: $error", history, + None, ) } } diff --git a/modules/refined/src/test/scala/ru/tinkoff/phobos/refined/RefinedEncodersTest.scala b/modules/refined/src/test/scala/ru/tinkoff/phobos/refined/RefinedEncodersTest.scala index 8432afd..d2d5b95 100644 --- a/modules/refined/src/test/scala/ru/tinkoff/phobos/refined/RefinedEncodersTest.scala +++ b/modules/refined/src/test/scala/ru/tinkoff/phobos/refined/RefinedEncodersTest.scala @@ -30,7 +30,7 @@ class RefinedEncodersTest extends AnyWordSpec with Matchers { | """.stripMargin.minimized - XmlEncoder[Test].encode(value) shouldEqual expectedResult + XmlEncoder[Test].encode(value) shouldEqual Right(expectedResult) } "encode elements correctly" in { @@ -47,7 +47,7 @@ class RefinedEncodersTest extends AnyWordSpec with Matchers { | """.stripMargin.minimized - XmlEncoder[Test].encode(value) shouldEqual expectedResult + XmlEncoder[Test].encode(value) shouldEqual Right(expectedResult) } "encode text correctly" in { @@ -58,16 +58,15 @@ class RefinedEncodersTest extends AnyWordSpec with Matchers { val qux = Qux("42", Foo(42, NonNegLong(1000L))) val xml = XmlEncoder[Qux].encode(qux) - assert( - xml == - """ + val string = + """ | | | 42 | 1000 | - """.stripMargin.minimized, - ) + """.stripMargin.minimized + assert(xml == Right(string)) } } }