geirolz / cats-xml   0.0.8

Apache License 2.0 Website GitHub

A functional library to work with XML in Scala using Cats.

Scala versions: 2.13 3.x

cats-xml

Build Status codecov Codacy Badge Sonatype Nexus (Releases) javadoc.io Scala Steward badge GitHub license

A functional library to work with XML in Scala using cats core.

libraryDependencies += "com.github.geirolz" %% "cats-xml" % "0.0.7"

This library is not production ready yet. There is a lot of work to do to complete it:

  • Macros to derive Encoder and Decoder for Scala 2
  • Reach a good code coverage with the tests (using munit) above 60%
  • Support XPath
  • Decoder and Encoder for primitives with error accumulating
  • Good error handling and messaging
  • Integration with standard scala xml library
  • Integration with cats-effect to load files effectfully
  • Macros to derive Encoder and Decoder for Scala 3
  • Performance benchmarks
  • Integration with Tapir and Http4s
  • Literal macros to check XML strings at compile time

Contributions are more than welcome 💪

Modules

Example

Given

case class Foo(
    foo: Option[String], 
    bar: Int, 
    text: Boolean
)

Plain creation

import cats.xml.XmlNode
import cats.xml.implicits.*
import cats.implicits.*

val optNode: Option[XmlNode] = None
// optNode: Option[XmlNode] = None
val node: XmlNode = 
  XmlNode("Wrapper")
    .withAttributes(
      "a" := 1,
      "b" := "test",
      "c" := Some(2),
      "d" := None,
    )
    .withChildren(
      XmlNode("Root").withChildren(
        XmlNode.group(
          XmlNode("A").withText(1),
          XmlNode("B").withText("2"),
          XmlNode("C").withText(Some(3)),
          XmlNode("D").withText(None),
          optNode.orXmlNull
        )
      )
    )
// node: XmlNode = <Wrapper a="1" b="test" c="2" >
//  <Root>
//   <A>1</A>
//   <B>2</B>
//   <C>3</C>
//   <D/>
//  </Root>
// </Wrapper>

Decoding

import cats.xml.codec.Decoder
import cats.xml.implicits.*
import cats.implicits.*

val decoder: Decoder[Foo] =
  Decoder.fromCursor(c =>
    (
      c.attr("name").as[Option[String]],
      c.attr("bar").as[Int],
      c.text.as[Boolean]
    ).mapN(Foo.apply)
  )

Encoding

import cats.xml.XmlNode
import cats.xml.codec.Encoder

val encoder: Encoder[Foo] = Encoder.of(t =>
  XmlNode("Foo")
    .withAttributes(
      "foo"  := t.foo.getOrElse("ERROR"),
      "bar"  := t.bar
    )
    .withText(t.text)
)

Navigating

import cats.xml.XmlNode
import cats.xml.cursor.Cursor
import cats.xml.cursor.FreeCursor
import cats.xml.implicits.*

val node = xml"""
     <wrapper>
         <root>
           <foo>1</foo>
           <baz>2</baz>
           <bar>3</bar>
         </root>
     </wrapper>"""
// node: XmlNode = <wrapper>
//  <root>
//   <foo>1</foo>
//   <baz>2</baz>
//   <bar>3</bar>
//  </root>
// </wrapper>

val fooNode: Cursor.Result[XmlNode] = node.focus(_.root.foo)
// fooNode: Cursor.Result[XmlNode] = Right(value = <foo>1</foo>)
val fooTextValue: FreeCursor.Result[Int] = node.focus(_.root.foo.text.as[Int])
// fooTextValue: FreeCursor.Result[Int] = Valid(a = 1)

Modifying

import cats.xml.XmlNode
import cats.xml.modifier.Modifier
import cats.xml.implicits.*

val node = xml"""
     <wrapper>
         <root>
           <foo>
             <baz>
               <bar>
                 <value>1</value>
               </bar>
             </baz>
           </foo>
         </root>
       </wrapper>"""
// node: XmlNode = <wrapper>
//  <root>
//   <foo>
//    <baz>
//     <bar>
//      <value>2</value>
//     </bar>
//    </baz>
//   </foo>
//  </root>
// </wrapper>

val result: Modifier.Result[XmlNode] = node.modify(_.root.foo.baz.bar.value.modifyIfNode(_.withText(2)))
// result: Modifier.Result[XmlNode] = Right(
//   value = <wrapper>
//  <root>
//   <foo>
//    <baz>
//     <bar>
//      <value>2</value>
//     </bar>
//    </baz>
//   </foo>
//  </root>
// </wrapper>
// )