Geo-7
May 11, 2020, 1:12pm
1
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 .
Geo-7
May 11, 2020, 6:10pm
3
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.
Geo-7
May 11, 2020, 6:42pm
5
@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
.
Geo-7
May 11, 2020, 7:15pm
9
@asterite Thank you that is working, but I have two other questions for you.
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 )
second parameter in x.path_node function does not accept XML::Namespace is it ok?
And thank you for this beautiful programming language.
@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:
doc = doc()
doc.xpath("coco()")
end
end
it "returns nil with invalid xpath" do
doc = doc()
doc.xpath_node("//invalid").should be_nil
end
it "finds with namespace" do
doc = XML.parse(%(\
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/">
</feed>
))
nodes = doc.xpath("//atom:feed", namespaces: {"atom" => "http://www.w3.org/2005/Atom"}).as(NodeSet)
nodes.size.should eq(1)
nodes[0].name.should eq("feed")
ns = nodes[0].namespace.not_nil!
ns.href.should eq("http://www.w3.org/2005/Atom")
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?
Geo-7
May 11, 2020, 7:33pm
13
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.
Geo-7
May 14, 2020, 1:33pm
15
Thank U, It makes life easier