phobos icon indicating copy to clipboard operation
phobos copied to clipboard

Decoding issue with Option[Unit]

Open FrancisToth opened this issue 3 years ago • 1 comments

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.

FrancisToth avatar Sep 27 '22 19:09 FrancisToth

I've done some investigation and it seems there's an issue with the Decoder derivation. For some reasons I don't get yet, the decodeAsElement is called recursively until a FailedDecoder is returned. The result function therefore returns a Left(...).

I've managed to fix this with this:

given ElementDecoder[Unit] = ElementDecoder.stringDecoder.map(_ => ())
given ElementEncoder[Unit] = ElementEncoder.stringEncoder.contramap(_ => "")

Does this solution make sense?

FrancisToth avatar Sep 28 '22 13:09 FrancisToth

It's certainly a bug. Decoder for Unit does not move the decoding "cursor" from <data/> to the next XML tag, hence the derived decoder tries to decode <data/> once again, and the decoding process fails.

Your fix makes sense, as it will move the "cursor" out of the <data/> element. Encoder change is also useful, as it adds some sense to Unit elements

valentiay avatar Nov 13 '22 10:11 valentiay

Will publish this change in 0.17.0

valentiay avatar Nov 13 '22 11:11 valentiay