Skip to content

Safer encoding #273

Merged
merged 2 commits into from
Feb 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

ThisBuild / name := "phobos"

ThisBuild / scalaVersion := "3.1.2"
ThisBuild / scalaVersion := "3.2.1"

lazy val commonDependencies =
libraryDependencies ++=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class XmlEntryElementEncoderTest extends AnyWordSpec with DiffShouldMatcher with
.fromElementEncoder[XmlEntry]("ast")
.encode(ast)

assert(result == """<?xml version='1.0' encoding='UTF-8'?><ast foo="5"><bar>bazz</bar></ast>""")
assert(result == Right("""<?xml version='1.0' encoding='UTF-8'?><ast foo="5"><bar>bazz</bar></ast>"""))
}
"encodes nested Xml ast correctly" in {
case object tinkoff {
Expand Down Expand Up @@ -46,7 +46,9 @@ class XmlEntryElementEncoderTest extends AnyWordSpec with DiffShouldMatcher with
.encode(ast)

assert(
result == """<?xml version='1.0' encoding='UTF-8'?><ans1:ast xmlns:ans1="https://tinkoff.ru" foo="5"><bar>bazz</bar><array foo2="true" foo3="false"><elem>11111111111111</elem><elem>11111111111112</elem></array><nested><scala>2.13</scala><dotty>0.13</dotty><scala-4/></nested></ans1:ast>""",
result == Right(
"""<?xml version='1.0' encoding='UTF-8'?><ans1:ast xmlns:ans1="https://tinkoff.ru" foo="5"><bar>bazz</bar><array foo2="true" foo3="false"><elem>11111111111111</elem><elem>11111111111112</elem></array><nested><scala>2.13</scala><dotty>0.13</dotty><scala-4/></nested></ans1:ast>""",
),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]]} }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
},
)

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 '", "'")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down Expand Up @@ -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))
},
)

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
},
)

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
Loading