Giter VIP home page Giter VIP logo

phobos's Introduction

phobos

Maven Central Build Scala Steward Discord

Phobos is an XML data-binding library based on stream parsing. It depends only on aalto-xml for parsing.

Scala 2.12, 2.13 and 3 are supported. See Supported Scala versions for more details.

QuickStart

Add phobos-core to your dependencies:

libraryDependencies += "ru.tinkoff" %% "phobos-core" % "0.21.0"

Then try this code out in sbt console or in a separate source file:

import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.encoding._
import ru.tinkoff.phobos.syntax._
import ru.tinkoff.phobos.derivation.semiauto._

case class TravelPoint(country: String, city: String)
object TravelPoint {
  implicit val travelPointElementEncoder: ElementEncoder[TravelPoint] = deriveElementEncoder
  implicit val travelPointElementDecoder: ElementDecoder[TravelPoint] = deriveElementDecoder
}

case class Price(@attr currency: String, @text value: Double)
object Price {
  implicit val priceElementEncoder: ElementEncoder[Price] = deriveElementEncoder
  implicit val priceElementDecoder: ElementDecoder[Price] = deriveElementDecoder
}

case class Journey(price: Price, departure: TravelPoint, arrival: TravelPoint)
object Journey {
  implicit val journeyXmlEncoder: XmlEncoder[Journey] = deriveXmlEncoder("journey")
  implicit val journeyXmlDecoder: XmlDecoder[Journey] = deriveXmlDecoder("journey")
}


val journey =
  Journey(
    price = Price("EUR", 1000.0),
    departure = TravelPoint("France", "Marcelle"),
    arrival = TravelPoint("Germany", "Munich")
  )

val xml = XmlEncoder[Journey].encode(journey)
println(xml)

val decodedJourney = xml.flatMap(XmlDecoder[Journey].decode(_))
println(decodedJourney)

assert(Right(journey) == decodedJourney)

Please see phobos wiki for explanation of the syntax and more details.

Performance

Performance details can be found out in phobos-benchmark repository.

Modules

There are several additional modules for some specific cases. These modules could be added with command below:

libraryDependencies += "ru.tinkoff" %% "phobos-<module>" % "0.21.0"

Where <module> is module name.

Module name Functionality
core-3-0 Core module compiled for Scala 3.0.
ast Support for parsing XML into AST
akka-http Marshallers and unmarshallers for akka-http
akka-stream Streaming decoding support for akka-stream
cats Cats instances
derevo Separate derivation of encoders and decoders separately using derevo annotations (e.g. @derive(xmlEncoder("foo")))
enumeratum Support for enumeratum enums
fs2 Streaming decoding support (Stream[F, Array[Byte]] => G[A]). Latest fs2 version (fs2 3.x, cats effect 3.x)
fs2-ce2 Streaming decoding support (Stream[F, Array[Byte]] => G[A]). Same as fs2 module, but for fs2 version 2.x using cats effect 2.x
monix Streaming decoding support (Observable[Array[Byte]] => Task[A])
refined Support for refined

Supported Scala versions

Most modules support Scala 2.12, 2.13 and 3. Dependencies for some modules don't support Scala 3, thus these modules support only Scala 2.x.

Detailed information about supported Scala versions is in the table below. Available versions for modules are marked with ✓.

Module name 2.12 2.13 3
core
akka-http
akka-stream
ast
cats
derevo
enumeratum
fs2
fs2-ce2
monix
refined

XSD and WSDL code-generation support

Classes from XSD could be generated using deimos library.

phobos's People

Contributors

amricko0b avatar calvinlfer avatar gregorath avatar ivashin avatar kubukoz avatar phill101 avatar robertgubin avatar scala-steward avatar susliko avatar valentiay avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

phobos's Issues

[ERROR] GC overhead limit exceeded

Good morning,

I have been using this library in my project for a while. However, since Today I am getting the following error GC overhead limit exceeded while compiling my project. This errors comes from the Decoders and encoders.

Revise extra stream parser events

There is a set of parser events which are not encountered during decoding:

PROCESSING_INSTRUCTION=3;
COMMENT=5;
SPACE=6;
ENTITY_REFERENCE=9;
ATTRIBUTE=10;
DTD=11;
NAMESPACE=13;
NOTATION_DECLARATION=14;
ENTITY_DECLARATION=15;

Now these events will either be ignored or decoding will fail. Behavior of decoders must be checked and fixed if needed

Remove dependency on cats

Cats is a heavy dependency, which is used for minor issues in phobos. It should be removed to prevent conflicts in projects, which depend on phobos

Scala 3: derivation for sealed traits with case objects

Problem

An attempt to derive ElementEncoder for sealed trait with case object implementations results in compilation error in Scala 3, while compiles in Scala 2

 sealed trait Foo derives ElementEncoder
   object Foo:
     case object A extends Foo derives ElementEncoder
     case object B extends Foo derives ElementEncoder
[error] 60 |      sealed trait Foo derives ElementEncoder
[error]    |                               ^
[error]    |Exception occurred while executing macro expansion.
[error]    |java.lang.AssertionError: assertion failed
[error]    |    at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:11)
[error]    |    at scala.quoted.runtime.impl.QuotesImpl$reflect$TypeIdent$.apply(QuotesImpl.scala:1093)
[error]    |    at scala.quoted.runtime.impl.QuotesImpl$reflect$TypeIdent$.apply(QuotesImpl.scala:1092)
[error]    |    at ru.tinkoff.phobos.derivation.common$.extractSumTypeChildren$$anonfun$1(common.scala:79)
[error]    |    at scala.collection.immutable.List.map(List.scala:246)
[error]    |    at ru.tinkoff.phobos.derivation.common$.extractSumTypeChildren(common.scala:79)
[error]    |    at ru.tinkoff.phobos.derivation.encoder$.deriveSum$$anonfun$1(encoder.scala:180)
[error]    |    at ru.tinkoff.phobos.derivation.encoder$.deriveSum$$anonfun$adapted$1(encoder.scala:195)
[error]    |    ... dotc frames
[error]    |    at ru.tinkoff.phobos.derivation.encoder$.deriveSum(encoder.scala:195)
[error]    |    at ru.tinkoff.phobos.derivation.encoder$.deriveElementEncoderImpl(encoder.scala:37)
[error]    |
[error]    |----------------------------------------------------------------------------
[error]    |Inline stack trace
[error]    |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]    |This location contains code that was inlined from encoder.scala:19
[error] 19 |    ${deriveElementEncoderImpl('{config})}
[error]    |    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]    |This location contains code that was inlined from encoder.scala:19
[error]  8 |    encoder.deriveElementEncoder[T](ElementCodecConfig.default)
[error]    |    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expected behavior

ElementEncoder is derived and works as follows

implicit val encoder: XmlEncoder[Foo] = XmlEncoder.fromElementEncoder("foo")
assert(XmlEncoder[Foo].encode(Foo.A) == Right(
  """<?xml version='1.0' encoding='UTF-8'?>
        <foo xmlns:ans1="http://www.w3.org/2001/XMLSchema-instance" ans1:type="A"/>
 """))

Decoding issue with Option[Unit]

The encoder for Option[Unit] seems to be bugged:

case class Bar(data: Option[Unit]) derives ElementDecoder
val xml    = "<bar><data/></bar>"
val actual = XmlDecoder.fromElementDecoder[Bar]("bar").decode(xml)
Left(ru.tinkoff.phobos.decoding.DecodingError: Error while decoding XML: Element is already decoded (Most likely it occurred more than once)
	In element 'data'
		in element 'bar'
     )

It may comes from how the decoder for Option works with the one used for Unit:

implicit def optionDecoder[A](implicit decoder: ElementDecoder[A]): ElementDecoder[Option[A]] =
    new ElementDecoder[Option[A]] {
      def decodeAsElement(c: Cursor, localName: String, namespaceUri: Option[String]): ElementDecoder[Option[A]] = {
        if (c.isStartElement) {
          ElementDecoder.errorIfWrongName[Option[A]](c, localName, namespaceUri).getOrElse {
            if (ElementDecoder.isNil(c) || (c.isEmptyElement && c.getAttributeInfo.getAttributeCount == 0)) {
              c.next()
              new ConstDecoder(None)
            } else {
              // If the decoder happens to be a ConstDecoder[Unit], `decodeAsElement` will return a FailedDecoder
              // instead of a ConstDecoder[Unit].
              decoder.map[Option[A]](a => Some(a)).decodeAsElement(c, localName, namespaceUri)
            }
          }
        } else {
          new FailedDecoder[Option[A]](c.error(s"Unexpected event: '${c.getEventType}'"))
        }
      }

Decoding Option[Unit] may be useful to decode empty xml tags acting as flags.

Handle parser exceptions

XmlDecoder methods return either, but if xml document is malformed, parser will throw an exception which will not be handled

Namespace prefix support

Hi there,

I was looking to set up namespaces with a given prefix instead of being automatically determined as ans\d+ in PhobosStreamWriter.

Example of ideal output sampled from w3schools:

<f:table xmlns:f="https://www.w3schools.com/furniture">
  <f:name>African Coffee Table</f:name>
  <f:width>80</f:width>
  <f:length>120</f:length>
</f:table>

where right now from my understanding I wouldn't be able to define the f prefix.

Am I correct in this understanding? If so, is there any straightforward way to accomplish this or work around it if not? Thanks for your insight!

Derivation macros cause inexhaustive match warnings

Using scala 2.13 and the recently added -Xlint:strict-unsealed-patmat compiler option, the code generated by the derivation macros causes inexhaustive match warnings, seemingly related to a match statement using the field names of the given case class. Here's a scastie that reproduces the issue: https://scastie.scala-lang.org/mrdziuban/Ndn1ejJVQJeZ1OBXG9L6Tw/7

I'm able to work around the issue by adding a -Wconf compiler option to target and silence these specific warnings:

scalacOptions += "-Wconf:msg=would fail on the following input.*x.*String forSome x not in.*macro:s"

but I was wondering if there was any way to add a fallback case to the match statement to make it exhaustive? Thanks in advance!

OffsetDateTime instances

Greetings!
It would be nice to have Encoder/Decoder instances for java.time.OffsetDateTime.

I made i tiny PR with them added: #179 .
Feel free to comment or merge/decline :)

Derivation on scala 3 fails for case classes with higher-kinded type parameters

Using scala 3, this code triggers an error at compile-time: https://scastie.scala-lang.org/mrdziuban/pGKWKNNeSCKQWVnM5mUJIw/1

import ru.tinkoff.phobos.decoding.XmlDecoder
import ru.tinkoff.phobos.derivation.semiauto.deriveXmlDecoder

case class Test[F[_]](fi: F[Int])

object Test {
  given xd: XmlDecoder[Test[Option]] = deriveXmlDecoder("Test")
}
Exception occurred while executing macro expansion.
java.lang.AssertionError: assertion failed: TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),HKTypeLambda(List(_$1), List(TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any))), TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any), List()))
  at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
  at dotty.tools.dotc.core.Types$TypeBounds.<init>(Types.scala:5104)
  at dotty.tools.dotc.core.Types$RealTypeBounds.<init>(Types.scala:5180)
  at dotty.tools.dotc.core.Types$TypeBounds$.apply(Types.scala:5224)
  at dotty.tools.dotc.core.Types$TypeBounds.derivedTypeBounds(Types.scala:5112)
  at dotty.tools.dotc.core.ConstraintHandling.addOneBound(ConstraintHandling.scala:267)
  at dotty.tools.dotc.core.ConstraintHandling.addOneBound$(ConstraintHandling.scala:27)
  at dotty.tools.dotc.core.ProperGadtConstraint.addOneBound(GadtConstraint.scala:60)
  at dotty.tools.dotc.core.ConstraintHandling.addBoundTransitively(ConstraintHandling.scala:321)
  at dotty.tools.dotc.core.ConstraintHandling.addBoundTransitively$(ConstraintHandling.scala:27)
  at dotty.tools.dotc.core.ProperGadtConstraint.addBoundTransitively(GadtConstraint.scala:60)
  at dotty.tools.dotc.core.ProperGadtConstraint.addBound(GadtConstraint.scala:167)
  at dotty.tools.dotc.core.TypeComparer.gadtAddLowerBound(TypeComparer.scala:118)
  at dotty.tools.dotc.core.TypeComparer.narrowGADTBounds(TypeComparer.scala:1931)
  at dotty.tools.dotc.core.TypeComparer.compareGADT$1(TypeComparer.scala:523)
  at dotty.tools.dotc.core.TypeComparer.thirdTryNamed$1(TypeComparer.scala:526)
  at dotty.tools.dotc.core.TypeComparer.thirdTry$1(TypeComparer.scala:575)
  at dotty.tools.dotc.core.TypeComparer.secondTry$1(TypeComparer.scala:506)
  at dotty.tools.dotc.core.TypeComparer.compareNamed$1(TypeComparer.scala:306)
  at dotty.tools.dotc.core.TypeComparer.firstTry$1(TypeComparer.scala:312)
  at dotty.tools.dotc.core.TypeComparer.recur(TypeComparer.scala:1336)
  at dotty.tools.dotc.core.TypeComparer.isSubType(TypeComparer.scala:194)
  at dotty.tools.dotc.core.TypeComparer.isSubType(TypeComparer.scala:204)
  at dotty.tools.dotc.core.TypeComparer.topLevelSubType(TypeComparer.scala:128)
  at dotty.tools.dotc.core.TypeComparer$.topLevelSubType(TypeComparer.scala:2752)
  at dotty.tools.dotc.core.Types$Type.$less$colon$less(Types.scala:1044)
  at scala.quoted.runtime.impl.QuoteMatcher$.$eq$qmark$eq(QuoteMatcher.scala:345)
  at scala.quoted.runtime.impl.QuoteMatcher$.treeMatch(QuoteMatcher.scala:126)
  at scala.quoted.runtime.impl.QuotesImpl.scala$quoted$runtime$impl$QuotesImpl$$treeMatch(QuotesImpl.scala:3096)
  at scala.quoted.runtime.impl.QuotesImpl$TypeMatch$.unapply(QuotesImpl.scala:3066)
  at ru.tinkoff.phobos.derivation.decoder$.decodeElementCases$$anonfun$1(decoder.scala:113)
  at scala.collection.immutable.List.map(List.scala:246)
  at ru.tinkoff.phobos.derivation.decoder$.decodeElementCases(decoder.scala:130)
  at ru.tinkoff.phobos.derivation.decoder$.decodeStartElement(decoder.scala:141)
  at ru.tinkoff.phobos.derivation.decoder$.deriveProduct$$anonfun$1(decoder.scala:360)
  at ru.tinkoff.phobos.derivation.decoder$.deriveProduct$$anonfun$adapted$1(decoder.scala:370)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:109)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1452)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformBlock(Trees.scala:1511)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1432)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1434)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformBlock(Trees.scala:1511)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1432)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1440)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform$$anonfun$1(Trees.scala:1513)
  at scala.collection.immutable.List.mapConserve(List.scala:472)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1513)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformSub(Trees.scala:1517)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1438)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1434)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformBlock(Trees.scala:1511)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1432)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1484)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform$$anonfun$1(Trees.scala:1513)
  at scala.collection.immutable.List.mapConserve(List.scala:472)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1513)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformStats(Trees.scala:1509)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformBlock(Trees.scala:1511)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1432)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1484)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform$$anonfun$1(Trees.scala:1513)
  at scala.collection.immutable.List.mapConserve(List.scala:472)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1513)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformStats(Trees.scala:1509)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1488)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1486)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform$$anonfun$1(Trees.scala:1513)
  at scala.collection.immutable.List.mapConserve(List.scala:472)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1513)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformStats(Trees.scala:1509)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformBlock(Trees.scala:1511)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1432)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.quoted.PickledQuotes$.spliceTerms(PickledQuotes.scala:151)
  at dotty.tools.dotc.quoted.PickledQuotes$.unpickleTerm(PickledQuotes.scala:87)
  at scala.quoted.runtime.impl.QuotesImpl.unpickleExprV2(QuotesImpl.scala:3044)
  at ru.tinkoff.phobos.derivation.decoder$.deriveProduct(decoder.scala:370)
  at ru.tinkoff.phobos.derivation.decoder$.deriveElementDecoderImpl(decoder.scala:39)
  at ru.tinkoff.phobos.derivation.decoder$.deriveXmlDecoderImpl$$anonfun$1(decoder.scala:52)
  at ru.tinkoff.phobos.derivation.decoder$.deriveXmlDecoderImpl$$anonfun$adapted$1(decoder.scala:52)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:109)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1452)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform$$anonfun$1(Trees.scala:1513)
  at scala.collection.immutable.List.mapConserve(List.scala:472)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1513)
  at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1418)
  at dotty.tools.dotc.quoted.PickledQuotes$$anon$1.transform(PickledQuotes.scala:134)
  at dotty.tools.dotc.quoted.PickledQuotes$.spliceTerms(PickledQuotes.scala:151)
  at dotty.tools.dotc.quoted.PickledQuotes$.unpickleTerm(PickledQuotes.scala:87)
  at scala.quoted.runtime.impl.QuotesImpl.unpickleExprV2(QuotesImpl.scala:3044)
  at ru.tinkoff.phobos.derivation.decoder$.deriveXmlDecoderImpl(decoder.scala:52)

Allow manual namespace definition

In some cases xml encoder may define namespaces in not optimal way, for example:

  @XmlnsDef("http://example.com")
  case object ex

  @XmlCodec("foo")
  final case class Foo(@xmlns(ex) a: Int, @xmlns(ex) b: Int, @xmlns(ex) ...)

  val foo = Foo(1, 2, ...)

  XmlEncoder[Foo].encode(foo)
}

results into

<?xml version='1.0' encoding='UTF-8'?>
<foo>
  <ans1:a xmlns:ans1="http://example.com">1</ans1:a>
  <ans2:b xmlns:ans2="http://example.com">2</ans2:b>
  ...
</foo>

This result into large overhead, if there is a lot of children elements with http://example.com namespace. It is necessary to allow to define namespaces manually, so user could help encoder with defining namespaces in an optimal way:

<?xml version='1.0' encoding='UTF-8'?>
<foo xmlns:ans1="http://example.com">
  <ans1:a>1</ans1:a>
  <ans1:b>2</ans1:b>
  ...
</foo>

Text decoder fails on mixed content if child element has same name as parent

@ElementCodec
case class Element2(@text text: String)
@ElementCodec
case class Element1(element: Element2, @text text: String)
@XmlCodec("document")
case class Document(element: Element1)

println(
  XmlDecoder[Document].decode(
    "<?xml version='1.0' encoding='UTF-8'?>" +
      "<document><element>text1<element>text2</element></element></document>"
  )
)

results to

Left(ru.tinkoff.phobos.decoding.DecodingError: Error while decoding XML: Decoding not complete
	In element 'element'
		in element 'document'
     )

when it must be

Right(Document(Element1(Element2(text2), text1)))

How to parse xml that has "xmlns" instead of "xmls:ans1"

Hey 👋 😄 ,

I am trying to read the following xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo xmlns="foo.com">
    <isFoo>true</isFoo>
</foo>

With the following code:

import scala.io.Source

import ru.tinkoff.phobos.annotations.{XmlCodec, XmlnsDef}
import ru.tinkoff.phobos.configured.ElementCodecConfig
import ru.tinkoff.phobos.decoding._

@XmlnsDef("foo.com")
case object foo

object FooDefinitions {

  val config: ElementCodecConfig = ElementCodecConfig.default.withNamespaceDefined(foo)
}

@XmlCodec("foo", FooDefinitions.config)
case class Foo(isFoo: Boolean)

object FooMain extends App {

  val input = Source.fromResource("foo.xml").getLines().mkString("\n")

  val decoded = XmlDecoder[Foo].decode(input)
  println(decoded)
}

However if I run FooMain I get:

Left(ru.tinkoff.phobos.decoding.DecodingError: Error while decoding XML: Invalid namespace. Expected no namespace, but found 'foo.com'
	In element 'foo'
     )

It works if I change the xml to have xmls:ans1 instead of xmls, however I don't have control over the received XML.

Question

How can I use phobos to parse the above XML?

Namespaces are ignored when using element names as discriminators

When a sealed trait is encoded using element names as discriminators, defined namespaces are ignored and are missing from the resulting xml.

Namespaces are not ignored when type attributes are used as discriminators, however are validated only when @default attribute is absent.

In the example below original and re-encoded xmls should be equal:

import ru.tinkoff.phobos.annotations._
import ru.tinkoff.phobos.configured._
import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.encoding._
import ru.tinkoff.phobos.syntax._

@XmlnsDef("http://example.com") case object Ns

object Foo {
  val config = ElementCodecConfig.default.usingElementNamesAsDiscriminator
}

@ElementCodec(Foo.config) sealed trait Foo
@ElementCodec(Foo.config) case class Bar() extends Foo
@ElementCodec(Foo.config) case class Baz() extends Foo

@XmlCodec("Wrapper", ElementCodecConfig.default.withNamespaceDefined(Ns))
case class Wrapper(@xmlns(Ns) @default items: List[Foo])

val original =
  """<?xml version='1.0' encoding='UTF-8'?><Wrapper xmlns:ans1="http://example.com"><ans1:Bar/><ans1:Baz/></Wrapper>"""
XmlDecoder[Wrapper].decode(original).map { decoded =>
  val encoded = XmlEncoder[Wrapper].encode(decoded)
  println(s"Original: $original")
  println(s"Encoded:  $encoded")
  assert(original == encoded)
}

Default value whenever a value cannot be decoded

I'd like to provide some way to set a default value whenever some field cannot be decoded (because it does not have the right type or else). Here's an attempt to do this:

Could anyone look at this and tell me if I am doing it the right way?

final class OrElseElement[A, B <: A](decoder: ElementDecoder[A], default: String => B) extends ElementDecoder[A] {
  override def decodeAsElement(c: Cursor, localName: String, namespaceUri: Option[String]): ElementDecoder[A] =
    OrElseElement(decoder.decodeAsElement(c, localName, namespaceUri), _ => default(localName))

  override def result(history: => List[String]): Either[DecodingError, A] =
    Right(decoder.result(history).fold(e => default(history.headOption.getOrElse("root")), identity))

  override def isCompleted: Boolean = decoder.isCompleted
}

Here's the same idea for attribute decoding:

final class OrElseAttribute[A, B <: A](decoder: AttributeDecoder[A], onError: (String, String) => B) extends AttributeDecoder[A] {
  override def decodeAsAttribute(c: Cursor, localName: String, namespaceUri: Option[String]): Either[DecodingError, A] =
    Right(decoder.decodeAsAttribute(c, localName, namespaceUri).fold(e => onError(localName, e.getMessage), identity))
}

Here's how this could be used:

enum Field[+A] { self =>
  case Present[A](value: A) extends Field[A]
  case Missing extends Field[Nothing]
  case Unparsable(error: String) extends Field[Nothing]

given [A: ElementDecoder]ElementDecoder[Field[A]] = {
  new OrElseElement(
    ElementDecoder[Option[A]].emap { (_, oa) =>
      Right(oa.fold(Field.Missing)(Field.Found(_)))
    },
    Field.UnParsable(_)
  )
}

Add FromRequestUnmarshaller in akka-http module

Hello!
It seems that in order to have an Akka-http endpoint that accepts a request with xml in the body and decodes it we need a FromRequestUnmarshaller available, as it is expected from entity and as in the example bellow

(post & path("request") & entity(as[Request])){ req =>
      complete(OK, req)
}

It look like that the implementation can be the same as for the FromResponseUnmarshaller.

Can we add this? I can open a PR if that is okay

Lack of consistency when decoding optional lists.

Hi, I've noticed that in some cases a decoder may not produce the same result:

import ru.tinkoff.phobos.decoding.*

case class Foo(i: Int)                  derives ElementDecoder
case class Bar(foo: List[Foo] = Nil)    derives ElementDecoder
case class Qux(bar: Option[Bar] = None) derives ElementDecoder

println(XmlDecoder.fromElementDecoder[Qux]("qux").decode("<qux><bar></bar></qux>"))
println(XmlDecoder.fromElementDecoder[Qux]("qux").decode("<qux><bar/></qux>"))
println(XmlDecoder.fromElementDecoder[Bar]("bar").decode("<bar></bar>"))
println(XmlDecoder.fromElementDecoder[Bar]("bar").decode("<bar/>"))

outputs

Right(Qux(Some(Bar(List())))) // <qux><bar></bar></qux>
Right(Qux(None))              // <qux><bar/></qux>
Right(Bar(List()))            // <bar></bar>
Right(Bar(List()))            // <bar/>

Is there any reason why a decoder outputs different result depending on if the value is optional or not? I would have expected "<qux><bar/></qux>" to be decoded like "<qux><bar></bar></qux>". Is there anything I can do at the decoder level to make this behavior consistent? I am using 15.1.

Derivation is not working for generic sealed traits

Following example should compile and work correctly, but it fails with compile error Don't know how to work with not classes

import ru.tinkoff.phobos.derivation.semiauto._
import ru.tinkoff.phobos.syntax._
import ru.tinkoff.phobos.decoding._

sealed trait Animal[B] {
  def body: B
}
object Animal {
  implicit def xmlDecoder[B: ElementDecoder]: XmlDecoder[Animal[B]] = deriveXmlDecoder("animal")
}
case class Dog[B](@attr breed: String, body: B) extends Animal[B]
object Dog {
  implicit def dogDecoder[B: ElementDecoder]: ElementDecoder[Dog[B]] = deriveElementDecoder
}
case class Cat[B](@attr age: Int, body: B) extends Animal[B]
object Cat {
  implicit def catDecoder[B: ElementDecoder]: ElementDecoder[Cat[B]] = deriveElementDecoder
}

Testkit module

Hi! It would be nice to have a testing module that'd let you do something like Circe's codec laws.

I'm building something like this for work, I'll see if I can contribute it back here later.

Things I'd like to see in this kind of thing:

  • def codingTest[A: XmlEncoder: XmlDecoder](value: A, expectedEncoding: String): Unit
  • def roundtripTest[A: Arbitrary: XmlEncoder: XmlDecoder]: Assertion = forAll(...)

It would probably be better to compare the AST instead of strings, though. I suppose currently I can defer to aalto's parser. These would probably help (especially in test land):

  • Must have: function that parses String to a XML AST with an Either in the result
  • Nice to have: XML interpolator (doesn't need to support symbol interpolations in the beginning, just calling the parser at compile-time would be nice)
  • Nice to have: type alias / newtype for the AST type, so that users don't need to find the right import from aalto

Update: I also just realised codingTest would need a way to check equality (in the "value encodes back as expectedEncoding" test) ignoring the missing fields...

More detailed error messages

In current version error message in most cases is "Decoding not complete". This message is confusing and lacks details. It should be more verbose, when required XML element is missing or invalid.

Messages like

Error while decoding XML: Decoding not complete
    In element 'bar'
        in element 'foo'

should be changed to something like

Error while decoding XML: Element 'buz' is missing or invalid
    In element 'bar'
        in element 'foo'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.