How to set namespace in XML::Node xpath_node?

Hi

puts x.first_element_child.not_nil!.namespace

returns

#<XML::Namespace:0x7f50d27ad000 prefix=“soap” href=“http://www.w3.org/2003/05/soap-envelope”>

but

puts x.xpath_node(“/Envelope/Body”).not_nil!.name

returns

Unhandled exception: Nil assertion failed (NilAssertionError)

and

puts x.xpath_node(“/Envelope/Body”,“soap”).not_nil!.name

return compile time error

namespaces.each do |prefix, uri|
^—
Error: undefined method ‘each’ for String

and

x.xpath_node(“/Envelope/Body”,{“prefix”=>“soap”})
x.xpath_node(“/Envelope/Body”,{“soap”=>“http://www.w3.org/2003/05/soap-envelope”})

are also nill.

and

x.xpath_node(“/Envelope/Body”,x.first_element_child.not_nil!.namespace).not_nil!.name

return compile time error

In /usr/share/crystal/src/xml/xpath_context.cr:39:16

39 | namespaces.each do |prefix, uri|
^—
Error: undefined method ‘each’ for XML::Namespace

Do you have the code to reproduce this? Like a full example. Can use https://play.crystal-lang.org/#/cr.

@Blacksmoke16
I rewrote the code here :
https://play.crystal-lang.org/#/r/91qo

:+1: thanks. Yea this seems to be a bug. https://github.com/crystal-lang/crystal/blob/9b12792c4d532feb2a8875983e72ad19d0a249b9/src/xml/node.cr#L486 calls into a method that wants to iterate over the namespaces https://github.com/crystal-lang/crystal/blob/master/src/xml/xpath_context.cr#L39.

However there isn’t a .each defined on XML::Namespace. I’d make an issue with that playground link. Guess we didn’t have any specs on this code and this is the first time that someone tried using xpath with namespaces since Ary implemented it 5 years ago ha.

@Blacksmoke16
Thanks, Can you share a link of that issue here?

Sorry, I was suggesting you make an issue about it. I don’t think there is one already.

This works:

require "xml"
str_xml = <<-XML
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Body>
    <NumberToWordsResponse xmlns="http://www.dataaccess.com/webservicesserver/">
      <NumberToWordsResult>string</NumberToWordsResult>
    </NumberToWordsResponse>
  </soap:Body>
</soap:Envelope>
XML
x = XML.parse str_xml
puts x.first_element_child.not_nil!.namespace
p x.xpath_node("/soap:Envelope/soap:Body", {"soap" => "http://www.w3.org/2003/05/soap-envelope"})

I think that’s the way to use it? Maybe we need more API examples in the docs.

Given #namespace has the name/url within it, probably could also add #each to it. So the user wouldn’t have to construct a hash like {namespace.prefix => namespace.href}.

EDIT: Or at least some overloads to accept either a XML::Namespace, or Hash | NamedTuple.

@asterite Thank you that is working, but I have two other questions for you.

  1. If it is the way that we should use it what is the point of defining namespace?
    (I don not know how it is defined in other programing languages but I think namespace is for avoiding name conflicts and this is the first time that I make and parse WSDL without library :slight_smile: )
  2. second parameter in x.path_node function does not accept XML::Namespace is it ok?

And thank you for this beautiful programming language.

:slight_smile: my bad

@Geo-7 I don’t know much about XML and namespaces, I just copied the behavior from Nokogiri:

Another alternative is to remove all namespaces as suggested by that answer, but I don’t think there’s such thing in Crystal.

I think that if a doc has namespaces, you’ll have to use them. Otherwise there’s no way to tell where “foo” belongs.

Also, if docs are missing, it’s always a good idea to check the specs:

If Nokogiri accepts an XML::Namespace as an argument, we probably should too.

I just checked, nokogiri just accepts a hash.

There’s an XML::Node#namespaces method, you can just call that and pass it to the xpath_node as a second argument. Something like:

x = XML.parse str_xml
p x.xpath_node("/soap:Envelope/soap:Body", x.root.not_nil!.namespaces)

Unfortunately that doesn’t work but probably should. Care to report a bug?

Thanks, I’ll report a bug.

In the next version you’ll be able to do:

x.xpath_node("/soap:Envelope/soap:Body")

and it will work out of the box.

Thank U, It makes life easier :slight_smile: