The most interesting part of xml-lens
is API for XML transformations. It’s divided into two layers:
Optics API (also called low-level API) and DSL (also called high-level API).
Optics API (low-level API)
Purpose of Optics API
Optics API is the heart of xml-lens
. Optics offers way of constructing immutable, principled APIs on any ADTs.
Therefore, they promise a good solution for operating on XML. circe-optics
was also an inspiration for such solution.
The main purpose of Optics API is to provide set of lawful Optics to modify and traverse xml-lens
AST.
The focus of that API is more on completeness rather than on ease of use.
When to use Optics API
You should use Optics API if high-level API does not provide functionality you need. Otherwise use high-level API which is easier to use.
When utilizing Optics API you will operate directly on Monocle
types. There is some theory behind them and therefore
some basic knowledge of it is neccessary to comfortably use this kind of API. Understanding what is Lens
, Prism
and
Traversal
will be particularly helpful. If you don’t know where to start educating about Optics you may find
section resources relevant.
How to use Optics API
In order to explain how to use Optics API it will be presented some motivational example, followed by analysis how it works and suggestions how to explore Optics API by yourself.
Exemplary usage
In this example we will modify all text nodes in path b/c
:
import pl.msitko.xml.parsing.XmlParser
import pl.msitko.xml.printing.XmlPrinter
import pl.msitko.xml.optics.ElementOptics._
import pl.msitko.xml.optics.LabeledElementOptics._
import pl.msitko.xml.optics.XmlDocumentOptics._
val exampleXml = """<?xml version="1.0" encoding="UTF-8"?>
<a>
<b>
<c>item1</c>
<d>item2</d>
</b>
<b>
<c>item1</c>
<e>item2</e>
<c>item3</c>
</b>
</a>"""
val parsed = XmlParser.parse(exampleXml).right.get
val traversal = rootLens.composeTraversal(deep("b").composeTraversal(deeper("c")).composeOptional(hasTextOnly))
val res = traversal.modify(_.toUpperCase)(parsed)
XmlPrinter.print(res)
// res4: String =
// <?xml version="1.0" encoding="UTF-8"?>
// <a>
// <b>
// <c>ITEM1</c>
// <d>item2</d>
// </b>
// <b>
// <c>ITEM1</c>
// <e>item2</e>
// <c>ITEM3</c>
// </b>
// </a>
You can find DSL equivalent here. The result is as expected, now let’s analyze API more deeply.
Usually you’ll start constructing any transformation with:
import pl.msitko.xml.optics.XmlDocumentOptics
// import pl.msitko.xml.optics.XmlDocumentOptics
XmlDocumentOptics.rootLens
// res5: monocle.Lens[pl.msitko.xml.entities.XmlDocument,pl.msitko.xml.entities.LabeledElement] = monocle.PLens$$anon$7@118612c7
which as you see is simply monocle.Lens
. Then to compose it with other optics you can use its compose...
methods.
You man wonder how can you know what to import from import pl.msitko.xml.optics
. The general rule is that optics
which source is of type A
are defined in pl.msitko.xml.optics.A
Example: You started defining definition with rootLens
of type Lens[XmlDocument, LabeledElement]
. Now you need to
compose it with optics which source type is LabeledElement
. Then you need to import pl.msitko.xml.optics.LabeledElement
.
Unfortunately xml-lens
cannot define package object that would define all optics and would let you to include all optics
with one import. The reason for that is that some names of optics are the same between different source type, e.g.
LabeledElementOptics.children
and ElementOptics.children
.
DSL (high-level API)
Purpose of DSL
The goal of DSL is to provide a set of convenient combinators for the most common operations. Those combinators are implemented on top of Optics API.
How to use DSL
Firstly you need to import pl.msitko.xml.dsl._
. It will bring root
into scope. root
represents
the root element of XML document. Therefore, it serves as starting point for defining any transformations.
Exemplary usage
We will rewrite the same transformation as described here, this time with DSL.
import pl.msitko.xml.parsing.XmlParser
import pl.msitko.xml.printing.XmlPrinter
import pl.msitko.xml.dsl._
val exampleXml = """<?xml version="1.0" encoding="UTF-8"?>
<a>
<b>
<c>item1</c>
<d>item2</d>
</b>
<b>
<c>item1</c>
<e>item2</e>
<c>item3</c>
</b>
</a>"""
val parsed = XmlParser.parse(exampleXml).right.get
val transformation = (root \ "b" \ "c").hasTextOnly.modify(_.toUpperCase)
val res = transformation(parsed)
XmlPrinter.print(res)
// res11: String =
// <?xml version="1.0" encoding="UTF-8"?>
// <a>
// <b>
// <c>ITEM1</c>
// <d>item2</d>
// </b>
// <b>
// <c>ITEM1</c>
// <e>item2</e>
// <c>ITEM3</c>
// </b>
// </a>
Resources on Optics
Articles
- A presentation of the most important types of optics: link
- Monocle documentation
- List of references from Monocle documentation
- Scala Exercises page
Talks
- Ilan Godik’s talk - great introductory talk into Optics in Scala using Monocle by one of its maintainers. Short and does not require any specific knowledge upfront. Also introduces Van Laarhoven Lenses
- Julien Truffaut’s talk - Julien is an author of Monocle, in this talk he provides great overview and intuitions about various types of Optics
- another talk by Julien Truffaut - this
one is about
JsonPath
- concept already mentioned in this article in section coveringcirce-optics
- Brian McKenna’s talk - Brian goes through Optics libraries in a few different languages: PureScript, Haskell, Scala and Java. Mentions nice examples of applications including representing web pages as Optics which allows to navigate between state and UI in Halogen, working with Kinesis records in Haskell, handling errors with Prisms in Scala