Nokogiri XML Builder error -> "Document already has a root node" - ruby-on-rails

In my code I'm building an XML request. However, this simple fragment generates an error:
def create_gateways_request
#request_xml = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
xml.gateways(:ua => "#{#plugin_name} #{#version}") {
xml.merchant {
xml.account MSP['merchant']['account_id']
xml.site_id MSP['merchant']['site_id']
xml.site_secure_code MSP['merchant']['site_code']
}
xml.customer {
xml.country #customer[:country]
}
}
end
#request_xml.to_xml
end
The error:
RuntimeError: Document already has a root node
from /Users/scriptdude/.rvm/gems/ruby-1.9.2-p290/gems/nokogiri-1.5.2/lib/nokogiri/xml/document.rb:212:in `add_child'
from /Users/scriptdude/.rvm/gems/ruby-1.9.2-p290/gems/nokogiri-1.5.2/lib/nokogiri/xml/node.rb:549:in `parent='
from /Users/scriptdude/.rvm/gems/ruby-1.9.2-p290/gems/nokogiri-1.5.2/lib/nokogiri/xml/builder.rb:371:in `insert'
from /Users/scriptdude/.rvm/gems/ruby-1.9.2-p290/gems/nokogiri-1.5.2/lib/nokogiri/xml/builder.rb:363:in `method_missing'
from (irb):146
from /Users/scriptdude/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.1.3/lib/rails/commands/console.rb:45:in `start'
from /Users/scriptdude/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.1.3/lib/rails/commands/console.rb:8:in `start'
from /Users/scriptdude/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.1.3/lib/rails/commands.rb:40:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
The root node is <gateways>, right?
What am I doing wrong here?

I cannot reproduce this locally, but you might try this at the end of your method instead:
#request_xml.doc.to_xml
It appears that it thought that you were trying to add a new <to_xml> node to the root of the document, and is complaining because you already have a <gateways> element at the root. I cannot fathom why Nokogiri 1.5.2 would do this, however, as Builder does have a to_xml method.
Here's my simple test that works for me:
require "nokogiri"
def do_it
#builder = Nokogiri::XML::Builder.new{ |x| x.root{ x.kid } }
#builder.to_xml
end
puts do_it
#=> <?xml version="1.0"?>
#=> <root>
#=> <kid/>
#=> </root>
p Nokogiri::VERSION
#=> "1.5.2"

Related

NoMemoryError: failed to allocate memory - Ruby on Rails

I generated a simple script that on other occasions has worked for me, but this is because the amount of information in the loop generates a NoMemoryError error.
I have 16 GB of memory and also a lot of virtual memory available. When I perform the test, the RAM memory is completely filled.
The script is:
require 'rest-client'
require 'json'
require 'open-uri'
require 'csv'
def self.qos7705egressdiscard_summary
xml = File.read("#{Rails.root}/public/Discard_qos7705egress.xml")
data = RestClient.post("http://10.140.255.1:8080/xmlapi/invoke", xml,{:"Content-Type" => 'application/soap+xml'})
data_parsed = Hash.from_xml(data)
return data_parsed
end
def self.samqos7705egressdiscardtotal_table
tabletotal = Hash.new
data_stats = qos7705egressdiscard_summary['Envelope']['Body']['findResponse']['result']['service.SapEgrQosQueueStatsLogRecord']
data_stats.map do |qosdiscard_device|
#devicetotal = qosdiscard_device["monitoredObjectSiteName"]
#servicetotal = qosdiscard_device["monitoredObjectPointer"]
#porttotal = qosdiscard_device["displayedName"]
#queueIdtotal = qosdiscard_device["queueId"]
#discardinproftotal = qosdiscard_device["droppedInProfOctets"].to_i
#discardoutproftotal = qosdiscard_device["droppedOutProfOctets"].to_i
time_unixtotal = (qosdiscard_device["timeCaptured"]).to_i/1000
#timeCapturedtotal = Time.at(time_unixtotal).strftime("%B %e, %Y at %I:%M %p")
#discardtotal = #discardinproftotal + #discardoutproftotal
#device_int_stats_total = (#devicetotal+#porttotal+#queueIdtotal).to_s
hash = Hash[devicetotal: #devicetotal, servicetotal: #servicetotal, porttotal: #porttotal, queueIdtotal: #queueIdtotal, discardtotal: #discardtotal, device_int_stats_total: #device_int_stats_total, timeCapturedtotal: #timeCapturedtotal, time_unixtotal: time_unixtotal]
tabletotal << hash
#tabletotal.write(hash)
end
end
The exact error is:
NoMemoryError: failed to allocate memory
from /usr/local/lib/ruby/2.2.0/irb/inspector.rb:108:in `inspect'
from /usr/local/lib/ruby/2.2.0/irb/inspector.rb:108:in `block in <module:IRB>'
from /usr/local/lib/ruby/2.2.0/irb/inspector.rb:101:in `call'
from /usr/local/lib/ruby/2.2.0/irb/inspector.rb:101:in `inspect_value'
from /usr/local/lib/ruby/2.2.0/irb/context.rb:383:in `inspect_last_value'
from /usr/local/lib/ruby/2.2.0/irb.rb:661:in `output_value'
from /usr/local/lib/ruby/2.2.0/irb.rb:490:in `block (2 levels) in eval_input'
from /usr/local/lib/ruby/2.2.0/irb.rb:623:in `signal_status'
from /usr/local/lib/ruby/2.2.0/irb.rb:486:in `block in eval_input'
from /usr/local/lib/ruby/2.2.0/irb/ruby-lex.rb:245:in `block (2 levels) in each_top_level_statement'
from /usr/local/lib/ruby/2.2.0/irb/ruby-lex.rb:231:in `loop'
from /usr/local/lib/ruby/2.2.0/irb/ruby-lex.rb:231:in `block in each_top_level_statement'
from /usr/local/lib/ruby/2.2.0/irb/ruby-lex.rb:230:in `catch'
from /usr/local/lib/ruby/2.2.0/irb/ruby-lex.rb:230:in `each_top_level_statement'
from /usr/local/lib/ruby/2.2.0/irb.rb:485:in `eval_input'
from /usr/local/lib/ruby/2.2.0/irb.rb:395:in `block in start'
from /usr/local/lib/ruby/2.2.0/irb.rb:394:in `catch'
from /usr/local/lib/ruby/2.2.0/irb.rb:394:in `start'
from /usr/local/lib/ruby/gems/2.2.0/gems/railties-4.2.0/lib/rails/commands/console.rb:110:in `start'
from /usr/local/lib/ruby/gems/2.2.0/gems/railties-4.2.0/lib/rails/commands/console.rb:9:in `start'
from /usr/local/lib/ruby/gems/2.2.0/gems/railties-4.2.0/lib/rails/commands/commands_tasks.rb:68:in `console'
from /usr/local/lib/ruby/gems/2.2.0/gems/railties-4.2.0/lib/rails/commands/commands_tasks.rb:39:in `run_command!'
from /usr/local/lib/ruby/gems/2.2.0/gems/railties-4.2.0/lib/rails/commands.rb:17:in `<top (required)>'
from bin/rails:4:in `require'
from bin/rails:4:in `<main>'Maybe IRB bug!
On line 25 I added this tabletotal.write (hash) to be able to write it on disk and not in memory but I got the following error:
NoMethodError: undefined method `write' for {}:Hash
What is the problem here? Also how can I fix it?
Start by making it less cr*p:
def extract_data(input)
{
devicetotal: input["monitoredObjectSiteName"],
servicetotal: input["monitoredObjectPointer"],
porttotal: input["displayedName"],
queueIdtotal: input["queueId"],
discardinproftotal: input["droppedInProfOctets"].to_i,
discardoutproftotal: input["droppedOutProfOctets"].to_i,
time_unixtotal: input["timeCaptured"].to_i/1000
}.tap do |h|
h[:timeCapturedtotal] = Time.at(h[:time_unixtotal]).strftime("%B %e, %Y at %I:%M %p"),
h[:discardtotal] = h[:discardinproftotal] + h[:discardoutproftotal]
h[:device_int_stats_total] =(h[:devicetotal]+h[:porttotal]+h[:queueIdtotal]).to_s
end
end
This method is really easy to test since you just insert input and write assertions about the output.
If you want to map and apply this to the input array you would do:
data_stats.map(&:extract_data)
Your code tries to output a hash but uses the shovel operator like on a array. You need to decide if the appropiate output is an array or a hash.
I need all those variables because I use them in a html in table
format.
This won't work - the instance variables will only contain the values from the last element as they get overwritten in each iteration.
You instead need to iterate over an array of hashes or objects in the view:
<% #stats.each do |s| %>
<tr>
<td><%= s[:devicetotal] %></td>
# ...
</tr>
<% end %>
If you really want to use instance varibles you need to create a object instance for each element:
class DataStat
include ActiveModel::AttributeAssignment
attr_accessor :devicetotal, :servicetotal # ...
def self.from_import(input)
self.new(
devicetotal: input["monitoredObjectSiteName"],
servicetotal: input["monitoredObjectPointer"],
# ...
)
end
end
#stats = data_stats.map { |ds| DataStat.from_import(ds) }
You also need to deal with the issue that you´re running out of memory since you are just slop converting the whole XML document into a Ruby hash. You need to parse it with Nokogiri instead and extract what you actually need.

JSON Rest-Client Authorization in Rails

For some reason when I run this in my Rails console it is not working.
RestClient.post 'http://localhost/WebService/AuthenticateLogin', :content_type => :json, {:params => {:RuntimeEnvironment => 1, 'Email' => 'someone#example.com', 'Password' => 'Pa$$worD'}}
The odd part is when I do the same with cURL it works perfectly fine:
curl -H "Content-Type: application/json" -d '{"RuntimeEnvironment":1,"Email":"someone#example.com","Password":"Pa$$worD"}' -X POST http://localhost/WebService/AuthenticateLogin
Here is my stacktrace:
SyntaxError: (irb):33: syntax error, unexpected '\n', expecting =>
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/console.rb:110:in `start'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/console.rb:9:in `start'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/commands_tasks.rb:68:in `console'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/commands_tasks.rb:39:in `run_command!
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands.rb:17:in `<top (required)>'
from bin/rails:4:in `require'
from bin/rails:4:in `<main>'
And because I think it may be relevant, I used this statement as well:
RestClient.post 'http://localhost/WebService/AuthenticateLogin', :content_type => :json, :params => {:RuntimeEnvironment => 1, 'Email' => 'someone#example.com', 'Password' => 'Pa$$worD'}
And got this as my stacktrace:
RestClient.post "http://localhost/WebService/AuthenticateLogin", "content_type=json&params[RuntimeEnvironment]=1&params[Email]=someone%40example.com&params[Password]=Pa$$worD", "Accept"=>"*/*; q=0.5, application/
xml", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"114", "Content-Type"=>"application/x-www-form-urlencoded"
# => 400 BadRequest | text/html 2738 bytes
RestClient::BadRequest: 400 Bad Request
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/rest-client-1.6.7/lib/restclient/abstract_response.rb:48:in `return!'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/rest-client-1.6.7/lib/restclient/request.rb:230:in `process_result'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/rest-client-1.6.7/lib/restclient/request.rb:178:in `block in transmit'
from C:/Ruby200-x64/lib/ruby/2.0.0/net/http.rb:852:in `start'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/rest-client-1.6.7/lib/restclient/request.rb:172:in `transmit'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/rest-client-1.6.7/lib/restclient/request.rb:64:in `execute'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/rest-client-1.6.7/lib/restclient/request.rb:33:in `execute'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/rest-client-1.6.7/lib/restclient.rb:72:in `post'
from (irb):34
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/console.rb:110:in `start'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/console.rb:9:in `start'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/commands_tasks.rb:68:in `console'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands/commands_tasks.rb:39:in `run_command!'
from C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/railties-4.2.0/lib/rails/commands.rb:17:in `<top (required)>'
from bin/rails:4:in `require'
from bin/rails:4:in `<main>'
I have scoured Stack and tried different ways of listing out the params, moving/removing the content_type, etc. Nothing is working for me.
You don't need to nest the params like that. Just do:
RestClient.post 'http://localhost/WebService/AuthenticateLogin', {"RuntimeEnvironment" => 1, 'Email' => 'someone#example.com', 'Password' => 'Pa$$worD'}, :content_type => :json
I noticed my content_type was not being updated, searched it up and StackOverflow came through for me.
How do I make Ruby's RestClient gem respect content_type on post?
I basically just changed my request to be:
RestClient.post 'http://localhost/WebService/AuthenticateLogin', '{"RuntimeEnvironment":1, "Email":"someone#example.com", "Password":"Pa$$worD"}', :content_type => "json"
I merely added quotes around my params like so: '{params}'
And instead of using => in the params, I changed to colons just like the actual request I used in cURL and everything worked like a charm.
Also note I quoted the "json" content_type.
Thanks #rlarcombe for your help.

Can't set timezone using abbreviation

I can't set timezone on Rails using its abbreviation, for example:
>> Time.zone = 'BRT'
ArgumentError: Invalid Timezone: BRT
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-3.2.21/lib/active_support/core_ext/time/zones.rb:61:in `rescue in find_zone!'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-3.2.21/lib/active_support/core_ext/time/zones.rb:53:in `find_zone!'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-3.2.21/lib/active_support/core_ext/time/zones.rb:37:in `zone='
from (irb):14
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/railties-3.2.21/lib/rails/commands/console.rb:47:in `start'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/railties-3.2.21/lib/rails/commands/console.rb:8:in `start'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/railties-3.2.21/lib/rails/commands.rb:41:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
This is necessary as some systems (android and some browsers) report timezone using the abbreviation.
The list of abbreviations can be found at http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
jstimezone was reporting timezone using abbreviations. It is also quite buggy and unmaintained (https://bitbucket.org/pellepim/jstimezonedetect/issues?status=new&status=open). It is simpler to just use standard javascript:
var offset = - new Date().getTimezoneOffset()/60
Then call on document ready:
$.cookie("browser.tzoffset", offset, { expires: 30, path: '/' })
Then in rails use around_filter in ApplicationController:
def set_time_zone
return yield unless (utc_offset = cookies['browser.tzoffset']).present?
utc_offset = utc_offset.to_i
gmt_offset = if utc_offset == 0 then nil elsif utc_offset > 0 then -utc_offset else "+#{-utc_offset}" end
Time.use_zone("Etc/GMT#{gmt_offset}"){ yield }
rescue ArgumentError
yield
end
This localizes all dates for users, independently where he/she is. In Brazil we have multiple timezones, for example.
PS: ActiveSupport::TimeZone[utc_offset.to_i] can't be used as it uses DST, see https://github.com/rails/rails/issues/20504
PS: You can also use moment: moment.parseZone(Date.now()).utcOffset()/60 or moment().format('zz')
You don't have to use around_filter.
Put this in before_action
Time.zone = "Etc/GMT#{gmt_offset}"
(Time.zone is thread local. It's safe to change.)

GET method with json body

I have to consume methods from an external API provided, and one method is a GET with a json body. I'm making the connection through rails using rest-client gem that makes that job.
I know that RestClient.post accepts json:
RestClient.post "http://example.com/resource", { 'x' => 1 }.to_json, :content_type => :json, :accept => :json
And also for my case works perfectly. But if i change the .post to .get, i get this error:
ArgumentError: wrong number of arguments (3 for 2)
from /Users/toptierlabs/.rvm/gems/ruby-1.9.3-p429#rails3tutorial2ndEd/gems/rest-client-1.6.7/lib/restclient.rb:67:in `get'
from /Users/toptierlabs/Desktop/Proyectos/AppraisalLane/app/models/autoniq.rb:8:in `getCircleInventory'
from (irb):3
from /Users/toptierlabs/.rvm/gems/ruby-1.9.3-p429#rails3tutorial2ndEd/gems/railties-4.0.2/lib/rails/commands/console.rb:90:in `start'
from /Users/toptierlabs/.rvm/gems/ruby-1.9.3-p429#rails3tutorial2ndEd/gems/railties-4.0.2/lib/rails/commands/console.rb:9:in `start'
from /Users/toptierlabs/.rvm/gems/ruby-1.9.3-p429#rails3tutorial2ndEd/gems/railties-4.0.2/lib/rails/commands.rb:62:in `<top (required)>'
from bin/rails:4:in `require'
from bin/rails:4:in `<main>'
Is there any way in Rails to do a GET with a json body? Or any possible solution to translate a "get with a json body" to something else? Is not necessary to use the rest-client gem, if any one have another solution is welcome too.
Thanks!
Solved:
RestClient.get "http://example.com/resource", params: {'json' => { 'x' => 1 }.to_json},:content_type => :json, :accept => :json

Ruby Newbie strftime Time.Now

I have the following written in ruby
t = Time.now
t.strftime("%Y-%d-%m")
SCHEDULER.every '1m', :first_in => 0 do |job|
send_event('gmail_gh', {current: gmail.inbox.count(:after => Date.parse(t)), older: gmail.inbox.count})
But i get this error
scheduler caught exception:
can't convert Time into String
/var/dashing/cdmdash/jobs/gmail_gh.rb:21:in `parse'
/var/dashing/cdmdash/jobs/gmail_gh.rb:21:in `block in <top (required)>'
/usr/local/rvm/gems/ruby-1.9.3-p327/gems/rufus-scheduler-2.0.17/lib/rufus/sc/jobs.rb:231:in `call'
/usr/local/rvm/gems/ruby-1.9.3-p327/gems/rufus-scheduler-2.0.17/lib/rufus/sc/jobs.rb:231:in `trigger_block'
/usr/local/rvm/gems/ruby-1.9.3-p327/gems/rufus-scheduler-2.0.17/lib/rufus/sc/jobs.rb:191:in `block in trigger'
/usr/local/rvm/gems/ruby-1.9.3-p327/gems/rufus-scheduler-2.0.17/lib/rufus/sc/scheduler.rb:416:in `call'
/usr/local/rvm/gems/ruby-1.9.3-p327/gems/rufus-scheduler-2.0.17/lib/rufus/sc/scheduler.rb:416:in `block in trigger_job'
I think it has something to do with the t variable and it not being a truing, I am new to Ruby so I am abit stuck
If you look at the gem documentation, you will see that the :after and :before params take in a date in the format of YYYY-MM-DD.
From the gem Readme:
gmail.inbox.count(:after => Date.parse("2010-02-20"), :before => Date.parse("2010-03-20"))
gmail.inbox.count(:on => Date.parse("2010-04-15"))
Your code is passing in YYYY-DD-MM which is likely causing the error.
Edit
When you call strftime on a datetime object, it doesn't modify the object - only returns the string notation based on format you give.
As a result, the Date.parse(t) is still getting Time.now was a parameter.
Try this:
t = Time.now.strftime("%Y-%m-%d")
Date.parse(t)

Resources