Nokogiri Use Ruby-Core Method as XML Node Name - ruby-on-rails

How do I get Nokogiri to accept a ruby-core method as a node name e.g.
xml.hash Digest::SHA256.file form.survey_xml should return something like this
<hash>cde6f0dd030aac1d3aa6d231b7c0cc30a34686a6f6780c468ccc64a4822f01e0</hash>
Instead I am getting an error ArgumentError: wrong number of arguments (1 for 0) in hash of course because hash is a ruby method.
How do I set the node name to hash using the Nokogiri DSL since the API I am interacting with expects that node.
I can just create the xml manually but the answer I am looking for is specifically using nokogiri
More Info
Here is the xml I am trying to create:
<?xml version=\"1.0\"?>
<xforms xmlns=\"http://openrosa.org/xforms/xformsList\">
<xform>
<formID>1</formID>
<name>BLAH BLAH</name>
<version>1</version>
<hash>892734982SDHFK238479823749234934</hash>
<downloadUrl>/Users/me/workspace/dashboard/public/uploads/survey_xml/survey_xml/2/S_1_.xml</downloadUrl>
</xform>
</xforms>
Here is my code:
require 'nokogiri'
require 'digest'
def mine
xml = Nokogiri::XML::Builder.new{ |xml|
xml.xforms xmlns: 'http://openrosa.org/xforms/xformsList' do
#forms.each do |form|
xml.xform do
xml.formID form.id
xml.name form.name
xml.version 1
xml.hash Digest::SHA256.file form.survey_xml.survey_xml.file.file
xml.downloadUrl form.survey_xml.survey_xml.file.file
end
end
end
}.to_xml
end
Based on dimakura's answer:

You can use other Nokogiri methods.
xpath:
node.xpath('hash').first
search:
node.search('hash').first
children:
xml.children.select{|x| x.name == 'hash'}
If you are creating new elements, not getting them. Then you can add them, for example, like this:
xml.add_child '<hash>hash-code</hash>'
Update When working with Nokogiri::XML::Builder special names should be used with underscore (_):
xml.hash_ 'your-hash'

Related

Get the attribute from CSS selector

I'm trying to access the sender attribute of an XML document:
<adi:ADI2 createDateTime="2015-04-10T15:36:03+02:00" docNumber="777"
sender="test" relativePriority="1"...
with the following command:
xml.css('/adi|ADI2[sender]')
But it doesn't work, it gives the exact same result as:
xml.css('/adi|ADI2')
To get the value of the attribute, I'm forced to use:
xml.css('/adi|ADI2[sender]').attribute('sender')
Is there a way of getting the attribute directly from the CSS selector?
You're missing a document root and name-space declaration in your XML sample but here's a simple example of what to do:
require 'nokogiri'
doc = Nokogiri::XML('<root xmlns:adi="http://foo.com"><adi:ADI2 createDateTime="2015-04-10T15:36:03+02:00" docNumber="777" sender="test" relativePriority="1"><root>')
doc.at('adi|ADI2')['sender'] # => "test"
Once we have a pointer to a Node, it can be treated much like a hash. From the Node documentation:
A Nokogiri::XML::Node may be treated similarly to a hash with regard to attributes.
irb(main):004:0> node
=> link
irb(main):005:0> node['href']
=> "#foo"
irb(main):006:0> node.keys
=> ["href", "id"]
irb(main):007:0> node.values
=> ["#foo", "link"]
irb(main):008:0> node['class'] = 'green'
=> "green"
irb(main):009:0> node
=> link
irb(main):010:0>
Your syntax using
xml.css('/adi|ADI2[sender]')
is incorrect.
/adi|ADI2[sender] is an attempt to use mixed CSS/XPath selector it looks like. I'd recommend sticking to CSS as it's simpler and easier to read, unless you need the power of XPath.
Also, instead of using css, you might want to use at. css returns a NodeSet, and you can't return the specific attribute of every Node found using the [attr] syntax unless you iterate over the NodeSet using map. If you'll have multiple instances of that tag, then css, xpath or the generic search will work, otherwise use at, or the language-specific at_css or at_xpath, to find the first such occurrence. at is equivalent to search('...').first.
Nokogiri's "Searching an HTML / XML Document" tutorial covers this.
To get an attribute one could use # selector:
▶ xml = '<tag sender="test">'
#⇒ "<tag sender=\"test\">"
▶ xml = Nokogiri::XML(xml, nil, "UTF-8")
#⇒ #<Nokogiri::XML::Document:0x5ca6f16 name="document" children=...>
# ⇓⇓⇓⇓⇓⇓⇓ attribute
▶ xml.xpath('//tag/#sender').text
#⇒ "test"

How to open, parse and process XML file with Ox gem like with Nokogiri gem?

I want to open an external XML file, parse it and use the data to store in my database. I do this with Nokogiri quite easy:
file = '...external.xml'
xml = Nokogiri::XML(open(file))
xml.xpath('//Element').each do |element|
# process elements and save to Database e.g.:
#data = Model.new(:attr => element.at('foo').text)
#data.save
end
Now I want to try the (maybe faster) Ox gem (https://github.com/ohler55/ox) - but I do not get how to open and process a file from the documentary.
Any equivalent code examples for the above code would be awesome! Thank you!
You can't use XPath to locate nodes in Ox, but Ox does provide a locate method. You can use it like so:
xml = Ox.parse(%Q{
<root>
<Element>
<foo>ex1</foo>
</Element>
<Element>
<foo>ex2</foo>
</Element>
</root>
}.strip)
xml.locate('Element/foo/^Text').each do |t|
#data = Model.new(:attr => t)
#data.save
end
# or if you need to do other stuff with the element first
xml.locate('Element').each do |elem|
# do stuff
#data = Model.new(:attr => elem.locate('foo/^Text').first)
#data.save
end
If your query doesn't find any matches, it will return an empty array. For a brief description of the locate query parameter, see the source code at element.rb.
From the documentation:
doc2 = Ox.parse(xml)
To read the contents of a file in Ruby you can use xml = IO.read('filename.xml') (among others). So:
doc = Ox.parse(IO.read(filename))
If your XML file is UTF-8 encoded, then alternatively:
doc = Ox.parse( File.open(filename,"r:UTF-8",&:read) )

Testing Nokogiri XML for Attributes

Using RSpec How could/should I test to ensure elements exists and have specified values
In my example I'm looking to ensure I have an EnvelopeVersion with a value 1.0, i would also like to see a test just to ensure EnvelopeVersion exists
def self.xml_header
builder = Nokogiri::XML::Builder.new do |xml|
xml.Root{
xml.EnvelopeVersion "1.0"
}
end
builder.to_xml
end
I've tried this, but it failed undefined method `has_node?' for #
it 'should create valid header' do
doc = GEM::xml_header
doc.should have_node("EnvelopeVersion ")
end
Your solution can be simplified:
doc = Nokogiri::XML::Document.parse(GEM::xml_header)
doc.xpath('//Root/EnvelopeVersion').text.should eq("1.0")
can be simplified to:
doc = Nokogiri::XML(GEM::xml_header)
doc.at('EnvelopeVersion').text.should eq("1.0")
Nokogiri has two main "find-it" methods: search and at. They are generic, in that they accept both XPath and CSS accessors. search returns a NodeSet of all matching nodes, and at returns the first Node that matches.
There are also the methods at_xpath and at_css if you want something a bit more mnemonic, along with xpath and css.
I ended up using nokogiri in my tests to parse the generated xml and query it
require 'nokogiri'
describe 'function' do
describe '.xml_header' do
it 'should create valid header' do
doc = Nokogiri::XML::Document.parse(GEM::xml_header)
doc.xpath('//Root/EnvelopeVersion').text.should eq("1.0")
end
end
end

Rails3-jquery-autocomplete distinct values

I'm using the rails3-jquery-autocomplete gem on a field with non-unique values, but I want the results it retrieves to be duplicate-free. Any ideas on how to accomplish this?
I had the same problem in my project https://github.com/marciomr/Terra-Livre and I solved it doing the following:
I installed rails3-jquery-autocomplete as a plugin in vendor/plugin directory
I changed the file helpers.rb like this:
def json_for_autocomplete(items, method, extra_data)
json = items.collect do |item| # here I put the result in a variable
hash = {"label" => item.send(method), "value" => item.send(method)} #here I removed the id
extra_data.each do |datum|
hash[datum] = item.send(datum)
end if extra_data
hash
end
json.uniq # this line is new
end
I removed the id from the json file and then retrieved uniq values.
Since I didn't need the id it worked fine for me. I think if I need the id I can put it in extra_data, but I am not sure.
I have just forked the project with this alteration: git://github.com/marciomr/rails3-jquery-autocomplete.git
Since I ran into this myself, I thought I would record my own solution for posterity, since it does not require editing the gem's source. This is for the officially maintained fork of the gem: https://github.com/bigtunacan/rails-jquery-autocomplete.
You can handle the json encoding directly via the autocomplete block in the controller, which we can leverage to change the array of records.
Here is an example in which we get a unique list of schools that students go to:
autocomplete :student, :school do |items|
ActiveSupport::JSON.encode( items.uniq{ |i| i["value"] } )
end
"items" is an array of hashes, which by default contain an id, a label, and a value, so this passes only unique values into the json encoder (of your choice).

Whats the best way to put a small ruby app online?

I have a small ruby application I wrote that's an anagram searcher. It's for learning ruby, but I would like to put it up online for personal use. I have some experience with Rails, and many here have recommended Sinatra. I'm fine with either, but I cannot find any information on how to use a text file instead of a database.
The application is quite simple, validates against a text file of a word list, then finds all anagrams. I have been assuming that this should be quite simple, but I'm stuck on importing that textfile into Rails (or Sinatra if i choose that way). In the Rails project, I have placed the textfile in the lib directory.
Unfortunately, even though the path appears to be correct in Rails, I get an error:
no such file to load -- /Users/court/Sites/cvtest/lib/english.txt
(cvtest is the name of the rails project)
Here is the code. It works great by itself:
file_path = '/Users/court/Sites/anagram/dictionary/english.txt'
input_string = gets.chomp
# validate input to list
if File.foreach(file_path) {|x| break x if x.chomp == input_string}
#break down the word
word = input_string.split(//).sort
# match word
anagrams = IO.readlines(file_path).partition{
|line| line.strip!
(line.size == word.size && line.split(//).sort == word)
}[0]
#list all words except the original
anagrams.each{ |matched_word| puts matched_word unless matched_word == input_string }
#display error if
else
puts "This word cannot be found in the dictionary"
end
Factor the actual functionality (finding the anagrams) into a method. Call that method from your Web app.
In Rails, you'd create a controller action that calls that method instead of ActiveRecord. In Sinatra, you'd just create a route that calls the method. Here's a Sinatra example:
get '/word/:input'
anagrams = find_anagrams(params[:input])
anagrams.join(", ")
end
Then, when you access the http://yourapp.com/word/pool, it will print "loop, polo".
I know the question is marked as answered, but I prefer the following, as it uses query parameters rather than path based parameters, which means you can pass the parameters in using a regular GET form submission:
require 'rubygems'
require 'sinatra'
def find_anagrams word
# your anagram method here
end
get '/anagram' do
#word = params['word']
#anagrams = find_anagrams #word if #word
haml :anagram
end
And the following haml (you could use whatever template language you prefer). This will give you an input form, and show the list of anagrams if a word has been provided and an anagram list has been generated:
%h1
Enter a word
%form{:action => "anagram"}
%input{:type => "text", :name => "word"}
%input{:type => "submit"}
- if #word
%h1
Anagrams of
&= #word
- if #anagrams
%ul
- #anagrams.each do |word|
%li&= word
- else
%p No anagrams found
With sinatra, you can do anything. These examples doesn't even require sinatra, you could roll your own rack interface thing.
require 'rubygems'
require 'sinatra'
require 'yaml'
documents = YAML::load_file("your_data.yml")
Or:
require 'rubygems'
require 'sinatra'
content = Dir[File.join(__DIR__, "content/*.textile)].map {|path|
content = RedCloth(File.read(path)).to_html
}
Etcetera.

Resources