Skip to the content.

XmlZoom

This feature allows users read/obtain nodes or attributes.

We can see zoom as a function that traverse the xml and select one or more elements. This feature core is written with tagless final and all methods returns an output value wrapped in F[_].

Given

import advxml.implicits.*
import advxml.transform.*
import advxml.transform.XmlZoom.{$, root}

How to build

Zoom has two types, we can have XmlZoom(unbinded) or BindedXmlZoom. The differences between Unbinded and Binded is that Unbinded doesn’t know the target and contains only the list of action to do, while Binded contains both actions and target.

It is possible to convert an XmlZoom(Unbinded) to BindedXmlZoom and vice-versa using bind and unbind idempotent methods.

val zoom: XmlZoom = XmlZoom.root //unbinded
// zoom: XmlZoom = Unbinded(List()) //unbinded
val binded: BindedXmlZoom = zoom.bind(<foo/>) //binded
// binded: BindedXmlZoom = Binded(<foo/>,List()) //binded
val unbinded: XmlZoom = zoom.unbind() //unbinded again
// unbinded: XmlZoom = Unbinded(List())
XmlZoom(unbinded)
BindedXmlZoom

Once created we can append actions using the following methods:

XmlZoom and BindedXmlZoom both extends Dynamic so you can use dot notation instead of immediateChild thanks to selectDynamic. We can even combine immediateChild and atIndex using applyDynamic

//this equals to root.down("foo").down("bar").down("text")
//this equals to root / "foo" / "bar" / "text"
val selectDynamicZoom: XmlZoom = root.foo.bar.test
// selectDynamicZoom: XmlZoom = Unbinded(
//   actions = List(Down(value = "foo"), Down(value = "bar"), Down(value = "test"))
// )

//this equals to root.down("foo").down("bar").down("text").atIndex(1)
//this equals to root / "foo" / "bar" / "text" atIndex(1)
val applyDynamicZoom: XmlZoom = root.foo.bar.test(1)
// applyDynamicZoom: XmlZoom = Unbinded(
//   actions = List(
//     Down(value = "foo"),
//     Down(value = "bar"),
//     Down(value = "test"),
//     AtIndex(idx = 1)
//   )
// )

How to run

To run a zoom we need to have a “target”, a NodeSeq instance. So if we have an unbinded XmlZoom conceptually we have to pass a NodeSeq instance as target when we invoke the zoom, while BindedXmlZoom already has this information so is not necessary specify the zoom target when you need to run it.

To run a zoom we can use two method, run and detailed.

import scala.util.Try
import scala.xml.NodeSeq

//XmlZoom
val unbinded: XmlZoom = XmlZoom.root
// unbinded: XmlZoom = Unbinded(List())
val unbindedRun: Try[NodeSeq] = unbinded.run[Try](<foo/>)
// unbindedRun: Try[NodeSeq] = Success(<foo/>)
val unbindedDetailed: Try[XmlZoomResult] = unbinded.detailed[Try](<foo/>)
// unbindedDetailed: Try[XmlZoomResult] = Success(Result(<foo/>,List()))

//BindedXmlZoom
val binded: BindedXmlZoom = unbinded.bind(<foo/>)
// binded: BindedXmlZoom = Binded(<foo/>,List())
val bindedRun: Try[NodeSeq] = binded.run[Try]
// bindedRun: Try[NodeSeq] = Success(<foo/>)
val bindedsDetailed: Try[XmlZoomResult] = binded.detailed[Try]
// bindedsDetailed: Try[XmlZoomResult] = Success(Result(<foo/>,List()))

Attributes and Text

Advxml also provides an XmlContetZoom to read attributes and text from a NodeSeq instance. XmlContetZoom syntax is added to NodeSeq, F[NodeSeq], XmlZoom and BindedXmlZoom using implicit class.

We can use attr to get an attribute value or content to get the node content.

import advxml.transform.XmlZoom.root
import cats.instances.try_.*

import scala.util.Try
import scala.xml.Elem

val doc: Elem = <foo T1='1'>TEXT</foo>
// doc: Elem = <foo T1="1">TEXT</foo>
val zoom: XmlZoom = XmlZoom.root
// zoom: XmlZoom = Unbinded(List())
val bindedZoom: BindedXmlZoom = XmlZoom.root(<foo T1='1'>TEXT</foo>)
// bindedZoom: BindedXmlZoom = Binded(<foo T1="1">TEXT</foo>,List())

val docAttr: Try[String] = doc.attr("T1").extract[Try] //Success("1")
// docAttr: Try[String] = Success(1) //Success("1")
val docMissingAttr: Try[String] = doc.attr("MISSING").extract[Try] //Failure(ValidationRule.Error)
// docMissingAttr: Try[String] = Failure(java.lang.RuntimeException: 
// # Value: foo /@ MISSING => ""
// - FAILED RULES(1)
// [ ] NonEmpty: Empty value.
// -------------------------------------
// 
// ) //Failure(ValidationRule.Error)
val docText: Try[String] = doc.content.extract[Try] //Success("TEXT")
// docText: Try[String] = Success(TEXT) //Success("TEXT")

val zoomAttr: Try[String] = zoom.attr(<foo T1='1'>TEXT</foo>, "T1").extract[Try] //Success("1")
// zoomAttr: Try[String] = Success(1) //Success("1")
val zoomText: Try[String] = zoom.content(<foo T1='1'>TEXT</foo>).extract[Try] //Success("TEXT")
// zoomText: Try[String] = Success(TEXT) //Success("TEXT")

val bindedZoomAttr: Try[String] = bindedZoom.attr("T1").extract[Try] //Success("1")
// bindedZoomAttr: Try[String] = Success(1) //Success("1")
val bindedZoomText: Try[String] = bindedZoom.content.extract[Try] //Success("TEXT")
// bindedZoomText: Try[String] = Success(TEXT)

This feature combined to converter feature allows we to convert attributes data from String to another type.

import advxml.transform.XmlZoom.root
import cats.instances.try_.*

import scala.util.Try
import scala.xml.Elem

val doc: Elem = <foo T1='1'>100</foo>
// doc: Elem = <foo T1="1">100</foo>
val docAttr: Try[Int] = doc.attr("T1").as[Try[Int]] //Success(1)
// docAttr: Try[Int] = Success(1) //Success(1)
val docText: Try[Int] = doc.content.as[Try[Int]] //Success(100)
// docText: Try[Int] = Success(100)

Full Example

import advxml.transform.XmlZoom.{$, root}
import advxml.implicits.*
import scala.xml.*
import scala.util.Try
import cats.instances.try_.*
import cats.instances.option.*

val doc: Elem =
  <Persons>
    <Person Name="Mimmo">
      <Cars>
        <Car Brand="Fiat">
          <Price>10000</Price>
        </Car>
      </Cars>
    </Person>
  </Persons>
// doc: Elem = <Persons>
//     <Person Name="Mimmo">
//       <Cars>
//         <Car Brand="Fiat">
//           <Price>10000€</Price>
//         </Car>
//       </Cars>
//     </Person>
//   </Persons>

//Immediate Child with XmlZoom - Binded
val mandatoryNode: Try[NodeSeq] = $(doc).Person.Cars.run[Try]
// mandatoryNode: Try[NodeSeq] = Success(<Cars>
//         <Car Brand="Fiat">
//           <Price>10000€</Price>
//         </Car>
//       </Cars>)
val optionalNode: Option[NodeSeq] = $(doc).Person.Cars.run[Option]
// optionalNode: Option[NodeSeq] = Some(<Cars>
//         <Car Brand="Fiat">
//           <Price>10000€</Price>
//         </Car>
//       </Cars>)

//Immediate Child with XmlZoom - Unbinded
val mandatoryNodeB: Try[NodeSeq] = root.Person.Cars.run[Try](doc)
// mandatoryNodeB: Try[NodeSeq] = Success(<Cars>
//         <Car Brand="Fiat">
//           <Price>10000€</Price>
//         </Car>
//       </Cars>)
val optionalNodeB: Option[NodeSeq] = root.Person.Cars.run[Option](doc)
// optionalNodeB: Option[NodeSeq] = Some(<Cars>
//         <Car Brand="Fiat">
//           <Price>10000€</Price>
//         </Car>
//       </Cars>)

//Attributes
val mandatoryAttr: Try[String] = $(doc).Person.Cars.attr("Brand").as[Try[String]]
// mandatoryAttr: Try[String] = Failure(java.lang.RuntimeException: 
// # Value:  /@ Brand => ""
// - FAILED RULES(1)
// [ ] NonEmpty: Empty value.
// -------------------------------------
// 
// )
val optionalAttr: Option[String] = $(doc).Person.Cars.attr("Brand").as[Option[String]]
// optionalAttr: Option[String] = None

//Content
val mandatoryText: Try[String] = $(doc).Person.Cars.content.as[Try[String]]
// mandatoryText: Try[String] = Success(
//         
//           10000€
//         
//       )
val optionalText: Option[String] = $(doc).Person.Cars.content.as[Option[String]]
// optionalText: Option[String] = Some(
//         
//           10000€
//         
//       )