xml-lens icon indicating copy to clipboard operation
xml-lens copied to clipboard

How would I focus on only the instances of an element which have a particular attribute value?

Open howyp opened this issue 7 years ago • 2 comments

This lib looks great!

But I'm a bit stuck on how to do something that I'd expected to be straight forward. Maybe I just don't know enough about optics. What I want is to select only the elements that have an attribute with a given value.

So given this example:

val xml =
  """
    |<a>
    |  <b>
    |    <c example="1">1234</c>
    |    <c example="2">5678</c>
    |    <c example="3">9123</c>
    |  </b>
    |</a>
  """.stripMargin

I'd be after only the <c> that has a example attribute with value 2. Using this expression gives me all of the <c> elements:

val c = root \ "b" \ "c"
println(pl.msitko.xml.parsing.XmlParser.parse(xml).map(c.getAll))

//prints:
//Right(List(Element(Vector(Attribute(ResolvedName(,,example),1)),List(Text(1234)),Vector()), Element(Vector(Attribute(ResolvedName(,,example),2)),List(Text(5678)),Vector()), Element(Vector(Attribute(ResolvedName(,,example),3)),List(Text(9123)),Vector())))

I've tried using having, something like this:

val c = root \ "b" \ "c" having {
  case LabeledElement(_, Element(attr, _, _)) => attr.find(_.key.localName == "example").exists(_.value == "1")
}

but that seems to only pass child elements of <c> to the partial function, which doesn't give me a chance to inspect the attributes.

howyp avatar Jun 01 '18 19:06 howyp

Hey @howyp, thank you for your interest in xml-lens and reporting your problem. It's especially appreciated taking into account that this is early version of the lib.

I managed to make it work for your case. First let's define a general Prism[Element, Element] out of given predicate:

def being(predicate: Element => Boolean): Prism[Element, Element] =
    Prism[Element, Element]{ el =>
      if (predicate(el)) {
        Some(el)
      } else {
        None
      }
    }(identity)

Then you can define your query as:

val havingAttrValPredicate = ElementOptics.being(el => ElementOptics.attribute("example")
        .getOption(el)
        .exists(_ == "2"))
val fetch = (root \ "b" \ "c").composePrism(havingAttrValPredicate)

Now you should be able to use such defined fetch on concrete XmlDocument:

fetch.getAll(XmlParser.parse(xml).right.get)

Does it work? Yes. Is it comfortable? No, it's embarrassingly complicated and I will work to make it easier in future. In particular being method should be part of library. The general idea of improving xml-lens is first by defining a set of general lenses (namely pl.msitko.xml.optics package) and then to try to build on top of them a bunch of useful combinators (namely pl.msitko.xml.dsl package). I find the problem you reported very useful for both of objectives.

You can track the progress on this ticket: https://github.com/note/xml-lens/pull/39 if you have any other suggestion you can comment there.

Will close that ticket after above mentioned PR is merged.

note avatar Jun 04 '18 23:06 note

I had a similar need and created a fork to work it into the app that I was creating. Here is a link to the pull request: https://github.com/note/xml-lens/pull/44

prgsmall avatar Oct 02 '19 04:10 prgsmall