Search code examples
xmlxpathcrystal-lang

Retrieve a value from the first node in an XML::Nodeset using Crystal


I am using Crystal, and am trying to retrieve the ID of a node in an XML document:

<foo ID="bar"></foo>

I am using the following code to get to the ID

require "xml"
file = File.read("path/to/doc.xml")
xml = XML.parse(file)
xpath_context = XML::XPathContext.new(xml)
nodeset = xpath_context.evaluate("//foo/@ID")

If I inspect the nodeset, I get the content I am expecting:

[#<XML::Attribute:0x1287690 name="ID" value="bar">]

And nodeset.class returns XML::NodeSet which has an instance method []. So I believe I should be able to do this to get the value:

node = nodeset[0]
node.value

However, when I call nodeset[0] I get the following error:

undefined method '[]' for Float64 (compile-time type is (String | Float64 | Bool | XML::NodeSet))

    node = nodeset[0]

I do not understand why the [] method is seeing nodeset as a Float64 when both inspect and class are seeing it as an XML::Nodeset.

What am I missing?

Is it a coincidence that String has a [] method, but Float64 does not?


Solution

  • When you execute evaluate the return type is a union type of all the possible values. In this case XML::NodeSet is the runtime type (notice the difference with the compile time type).

    If you can assure that the return type is always a node set, then you can simply do:

    nodeset = xpath_context.evaluate("//foo/@ID") as XML::NodeSet
    

    But that will raise an exception if the result has a different type. Another option is do it conditionally:

    if nodeset.is_a?(XML::NodeSet)
        # use nodeset here without casting, the compiler will restrict the type
    end
    

    Or even with a case statement:

    case nodeset
    when XML::NodeSet
      # ...
    end