valentiay / phobos

Efficient and expressive XML data-binding library for Scala
Apache License 2.0
20 stars 5 forks source link

`withRemoveNamespaces` not work for element attributes if a namespace is defined #19

Open geny200 opened 6 months ago

geny200 commented 6 months ago

Hi, addition of a new flag withRemoveNamespaces ( issue https://github.com/valentiay/phobos/issues/3 , pr https://github.com/valentiay/phobos/pull/5 ) is a really useful thing, but there is one bug, this config does not work with attributes if namespace is set. Here is an example with a comparison of this flag for an "element" and for "attribute". (it works correctly for "element", it doesn't work for "attribute").

import phobos.Namespace
import phobos.configured.ElementCodecConfig
import phobos.decoding.XmlDecoder
import phobos.derivation.semiauto.{deriveXmlDecoderConfigured, deriveXmlEncoderConfigured}
import phobos.encoding.XmlEncoder
import phobos.syntax.attr

object MyNamespace {
  type ns = MyNamespace.type
  implicit val ns: Namespace[ns] = Namespace.mkInstance[ns]("http://example.com", Some("example"))
}

case class BarAttr(@attr foo: String)

object BarAttr {
  val configEnc: ElementCodecConfig = ElementCodecConfig.default.withAttributesDefaultNamespace(MyNamespace)
  val configDec: ElementCodecConfig = ElementCodecConfig.default.withRemoveNamespaces

  implicit val barXmlEncoder: XmlEncoder[BarAttr] =
    deriveXmlEncoderConfigured[BarAttr]("BarAttr", configEnc)
  implicit val barXmlDecoder: XmlDecoder[BarAttr] =
    deriveXmlDecoderConfigured[BarAttr]("BarAttr", configDec)
}

case class BarElem(foo: String)

object BarElem {
  val configEnc: ElementCodecConfig = ElementCodecConfig.default.withElementsDefaultNamespace(MyNamespace)
  val configDec: ElementCodecConfig = ElementCodecConfig.default.withRemoveNamespaces

  implicit val barXmlEncoder: XmlEncoder[BarElem] =
    deriveXmlEncoderConfigured[BarElem]("BarElem", configEnc)
  implicit val barXmlDecoder: XmlDecoder[BarElem] =
    deriveXmlDecoderConfigured[BarElem]("BarElem", configDec)
}

object App {
  def main(args: Array[String]): Unit = {
    println(XmlEncoder[BarAttr].encode(BarAttr("qwerty")))
    // <?xml version='1.0' encoding='UTF-8'?><BarAttr xmlns:ans1="http://example.com" ans1:foo="qwerty"/>

    println(XmlEncoder[BarElem].encode(BarElem("qwerty")))
    // <?xml version='1.0' encoding='UTF-8'?><BarElem><ans1:foo xmlns:ans1="http://example.com">qwerty</ans1:foo></BarElem>

    val xmlAttr =
      """<?xml version='1.0' encoding='UTF-8'?><BarAttr xmlns:ans1="http://example.com" ans1:foo="qwerty"/>"""
    println(XmlDecoder[BarAttr].decode(xmlAttr))
    // {{{
    // phobos.decoding.DecodingError: Error while decoding XML: Missing 'foo' attribute
    //  In element 'BarAttr'
    // }}}

    val xmlElem =
      """<?xml version='1.0' encoding='UTF-8'?><BarElem><ans1:foo xmlns:ans1="http://example.com">qwerty</ans1:foo></BarElem>"""
    println(XmlDecoder[BarElem].decode(xmlElem))
    // Ok

    // Check law for element
    val y: BarElem = BarElem("qwerty")
    assert(
      XmlEncoder[BarElem]
        .encode(y)
        .toOption
        .flatMap(XmlDecoder[BarElem].decode(_).toOption)
        .contains(y),
    ) // Ok

    // Check law for attribute
    val x: BarAttr = BarAttr("qwerty")
    assert(
      XmlEncoder[BarAttr]
        .encode(x)
        .toOption
        .flatMap(XmlDecoder[BarAttr].decode(_).toOption)
        .contains(x),
    ) // assertion failed
  }
}
valentiay commented 5 months ago

Hi! Thank you for reporting this issue. This is a tricky one. I'll need some more time to investigate why does it work that way. I'm surprised, that it does not break the law for elements