Facing "no implicit conversion of nil into String" error inside eval statement - ruby

Exercise :Creating Classes from CSV Started
Read a csv format file and construct a new class with the name of the file dynamically. So if the csv is persons.csv, the ruby class should be person, if it's places.csv, the ruby class should be places
Also create methods for reading and displaying each value in "csv" file and values in first row of csv file will act as name of the function.
Construct an array of objects and associate each object with the row of a csv file.
For example the content of the csv file could be
name,age,city
gaurav,23,karnal
vilok,23,hissar
My expected output is correct. I am able to print the method name as first row of CSV and remaining lines of code has been evaluated and printed as strings on console.
But along with expected output I'm facing below error.
classes_from_CSV.rb:21:in `eval': no implicit conversion of nil into String (TypeError)
from classes_from_CSV.rb:21:in `block (2 levels) in new_method'
from classes_from_CSV.rb:25:in `call'
from classes_from_CSV.rb:34:in `<main>'
I just want to know how should I remove this error and what is the correct way of evaluating string type user input inside dynamic methods.
require 'csv'
class CsvManipulator
def first_line_csv
CSV.open("Input.csv", 'r') { |csv| csv.first }
end
def remaining_line_csv
text = File.readlines("Input.csv")[1..-1].join()
csv = CSV.parse(text, headers: true)
end
end
class MethodCreator < CsvManipulator
def initialize(class_name)
#klass = Class.new
Object.const_set(class_name, #klass)
end
def new_method(method_name, code_str)
#klass.class_eval do
puts define_method(method_name) { eval(puts"#{code_str}") }
end
end
def call(method_name)
#klass.new.send(method_name)
end
end
class_name = "Input"
obj = MethodCreator.new(class_name)
method_name = (obj.first_line_csv).join(', ')
code_str = obj.remaining_line_csv
obj.new_method(method_name, code_str)
puts obj.call(method_name)
Expected output :
name, age, city
gaurav,23,karnal
vilok,23,hissar
Actual output :
name, age, city
gaurav,23,karnal
vilok,23,hissar
classes_from_CSV.rb:21:in `eval': no implicit conversion of nil into String (TypeError)
from classes_from_CSV.rb:21:in `block (2 levels) in new_method
from classes_from_CSV.rb:25:in `call'
from classes_from_CSV.rb:34:in `<main>'

You are only calling eval in one place, so the error is not hard to find:
eval(puts"#{code_str}")
puts returns nil, so you are effectively calling
eval(nil)
However, eval must be called with a string.

Related

How do I get ruby to print a full backtrace that includes arguments passed to functions?

Sometimes backtrace is enough to diagnose problem. But sometimes reason of crash is not obvious without knowledge what was passed to function.
Getting information what was passed to function that caused crash would be quite useful, especially in cases where reproducing is not obvious because it was caused by for example exception in network connection, weird user input or because program is depends on randomisation or processes data from external sensor.
Lets say that there is following program
def handle_changed_input(changed_input)
raise 'ops' if changed_input =~ /magic/
end
def do_something_with_user_input(input)
input = "#{input.strip}c"
handle_changed_input(input)
end
input = gets
do_something_with_user_input(input)
where user typed "magic" as input. Normally one has
test.rb:2:in `handle_changed_input': ops (RuntimeError)
from test.rb:7:in `do_something_with_user_input'
from test.rb:11:in `<main>'
as output. What one may do to show also what was passed to function? Something like
test.rb:2:in `handle_changed_input("magic")': ops (RuntimeError)
from test.rb:7:in `do_something_with_user_input("magi\n")'
from test.rb:11:in `<main>'
It would be useful in many situations (and not truly useful where parameters are not representable as strings of reasonable legth, there is a good reason why it is not enabled by default).
How one may add this functionality? It is necessary that program works as usually during normal operation and preferably there is no additional output before crash.
I tried for example
def do_something_with_user_input(input)
method(__method__).parameters.map do |_, name|
puts "#{name}=#{binding.local_variable_get(name)}"
end
raise 'ops' if input =~ /magic/
end
input = gets
found in Is there a way to access method arguments in Ruby? but it would print on every single entrance to function what both would flood output and make program significantly slower.
I don't have a complete solution but... But you can get method arguments of all called methods in controlled environment with TracePoint class from Ruby core lib.
Look at the example:
trace = TracePoint.new(:call) do |tp|
puts "===================== #{tp.method_id}"
b_self = tp.binding.eval('self')
names = b_self.method(tp.method_id).parameters.map(&:last)
values = names.map { |name| tp.binding.eval(name.to_s) }
p names.zip(values)
end
trace.enable
def method_a(p1, p2, p3)
end
method_a(1, "foobar", false)
#=> ===================== method_a
#=> [[:p1, 1], [:p2, "foobar"], [:p3, false]]
To print exception backtraces, Ruby uses the C function exc_backtrace from error.c (exc_backtrace on github). Unless you patch Ruby with the functionality you need, I don't think there a way to change exception backtrace outputs.
Here is a snippet (trace.rb) you might find useful:
set_trace_func -> (event, file, line, id, binding, classname) do
if event == 'call' && meth = binding.eval('__method__')
params = binding.method(meth).parameters.select{|e| e[0] != :block}
values = params.map{|_, var| [var, binding.local_variable_get(var)]}
printf "%8s %s:%-2d %15s %8s %s\n", event, file, line, id, classname, values.inspect
else
printf "%8s %s:%-2d %15s %8s\n", event, file, line, id, classname
end
end
def foo(a,b = 0)
bar(a, foo: true)
end
def bar(c, d = {})
puts "!!!buz!!!\n"
end
foo('lol')
The output of that snippet is:
c-return /path/to/trace.rb:1 set_trace_func Kernel
line /path/to/trace.rb:12
c-call /path/to/trace.rb:12 method_added Module
c-return /path/to/trace.rb:12 method_added Module
line /path/to/trace.rb:16
c-call /path/to/trace.rb:16 method_added Module
c-return /path/to/trace.rb:16 method_added Module
line /path/to/trace.rb:20
call /path/to/trace.rb:12 foo Object [[:a, "lol"], [:b, 0]]
line /path/to/trace.rb:13 foo Object
call /path/to/trace.rb:16 bar Object [[:c, "lol"], [:d, {:foo=>true}]]
line /path/to/trace.rb:17 bar Object
c-call /path/to/trace.rb:17 puts Kernel
c-call /path/to/trace.rb:17 puts IO
c-call /path/to/trace.rb:17 write IO
!!!buz!!!
c-return /path/to/trace.rb:17 write IO
c-return /path/to/trace.rb:17 puts IO
c-return /path/to/trace.rb:17 puts Kernel
return /path/to/trace.rb:18 bar Object
return /path/to/trace.rb:14 foo Object
I hope that helps you as much as it helped me.
I think that it is possible. The code below is not perfect and would require some additional work, but it caputers the primary idea of a stacktrace with argument values. Please note, that in order to know the call site, I am zipping the original stacktrace with the entry sites catched by trace function. To distinguishe these entries I use '>' and '<' respectively.
class Reporting
def self.info(arg1)
puts "*** #{arg1} ***"
end
end
def read_byte(arg1)
Reporting.info(arg1)
raise Exception.new("File not found")
end
def read_input(arg1)
read_byte(arg1)
end
def main(arg1)
read_input(arg1)
end
class BetterStacktrace
def self.enable
set_trace_func -> (event, file, line, id, binding, classname) do
case event
when 'call'
receiver_type = binding.eval('self.class')
if receiver_type == Object
meth = binding.eval('__method__')
params = binding.method(meth).parameters.select{|e| e[0] != :block}
values = params.map{|_, var| [var, binding.local_variable_get(var)]}
self.push(event, file, line, id, classname, values)
else
self.push(event, file, line, id, classname)
end
when 'return'
self.pop
when 'raise'
self.push(event, file, line, id, classname)
Thread.current[:_keep_stacktrace] = true
end
end
end
def self.push(event, file, line, id, classname, values=nil)
Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
unless Thread.current[:_keep_stacktrace]
if values
values_msg = values.map(&:last).join(", ")
msg = "%s:%d:in `%s(%s)'" % [file, line, id, values_msg]
else
msg = "%s:%d:in `%s'" % [file, line, id]
end
Thread.current[:_saved_stacktrace] << msg
end
end
def self.pop()
Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
unless Thread.current[:_keep_stacktrace]
value = Thread.current[:_saved_stacktrace].pop
end
end
def self.disable
set_trace_func nil
end
def self.print_stacktrace(calls)
enters = Thread.current[:_saved_stacktrace].reverse
calls.zip(enters).each do |call, enter|
STDERR.puts "> #{enter}"
STDERR.puts "< #{call}"
end
Thread.current[:_saved_stacktrace] = []
end
end
BetterStacktrace.enable
begin
main(10)
rescue Exception => ex
puts "--- Catched ---"
puts ex
BetterStacktrace.print_stacktrace(ex.backtrace)
end
BetterStacktrace.disable
begin
main(10)
rescue Exception
puts "--- Catched ---"
puts ex
puts ex.backtrace
end
The output of the above code is as follows:
*** 10 ***
--- Catched ---
File not found
> work/tracing_with_params.rb:10:in `read_byte'
< work/tracing_with_params.rb:10:in `read_byte'
> work/tracing_with_params.rb:8:in `read_byte(10)'
< work/tracing_with_params.rb:14:in `read_input'
> work/tracing_with_params.rb:13:in `read_input(10)'
< work/tracing_with_params.rb:18:in `main'
> work/tracing_with_params.rb:17:in `main(10)'
< work/tracing_with_params.rb:82:in `<main>'
*** 10 ***
--- Catched ---
File not found
work/tracing_with_params.rb:10:in `read_byte'
work/tracing_with_params.rb:14:in `read_input'
work/tracing_with_params.rb:18:in `main'
work/tracing_with_params.rb:82:in `<main>'
EDIT:
The calls to class functions are not recorded. This has to be fixed in order for the stacktrace printing function not to get invalid output.
Moreover I used the STDERR as output to easily get one or the other output. You can change it if you wish.
MAX_STACK_SIZE = 200
tracer = proc do |event|
if event == 'call' && caller_locations.length > MAX_STACK_SIZE
fail "Probable Stack Overflow"
end
end
set_trace_func(tracer)

ruby filename path to string error

This is my code:
file = File.open('result.txt', 'w+').read
path = Dir[ENV['HOME'] + '/Desktop/Test/*.txt']
file.puts "this is a #{path} test: "
It comes up with an error:
C:/Users/User/RubymineProjects/Comparison/test.rb:5:in `<top (required)>': private method `puts' called for "":String (NoMethodError)
from -e:1:in `load'
from -e:1:in `<main>'
my intended result is:
this is a C:/Users/User/Desktop/Test/new_1.txt test:
i've tried this:
puts "this is a #{path[0]} test: "
which achieves what i want but as soon as i do file.puts it comes up with the same error again.
When you do file.puts here, you're sending a method #puts to a string object that's now stored in the variable file. This is because File#read method returns a string. So, on the first line, file takes the contents of the result.txt and then stores it in the variable file. Then you're callingputson that string. AndString#puts` is a private method, so you can't use it the way you used it in the code above.
If your intention is to write the result this is a C:/Users/User/Desktop/Test/new_1.txt test:, then you need to use the File.open method this way:
File.open('result.txt', 'w+') do |file|
path = Dir[ENV['HOME'] + '/Desktop/Test/*.txt']
file.puts "this is a #{path} test: "
# whatever else needs to be written goes here
end
Or, if you prefer the imperative style instead of the block style:
file = File.new('result.txt', 'w+')
# If you prefer `open`, that works too!
# file = File.open('result.txt', 'w+')
path = Dir[ENV['HOME'] + '/Desktop/Test/*.txt']
file.puts "this is a #{path} test: "
# ensure this is closed, or you'll have memory issues if you do this often
file.close

Can't load bot data because no implicit conversion of nil into String (RuntimeError)

I'm currently going through "Beginning Ruby", Chapter 12, "Beginning Ruby-ChatterBox" where it builds a conversation bot. When running the basic_client.rb file, I get an error message:
: Can't load bot data because no implicit conversion of nil into String (RuntimeError)
from C:/RailsInstaller/Ruby2.1.0/bin/bot.rb:13:in `initialize'
from C:/RailsInstaller/Ruby2.1.0/bin/basic_client.rb:4:in `new'
from C:/RailsInstaller/Ruby2.1.0/bin/basic_client.rb:4:in `<main>'
Similar questions have been asked in the past. I consulted these examples but still can't solve this problem. Please I would appreciate if anyone could help with letting me know what I am doing wrong. Here is an extract of my code files. Please if extra information is needed, I would be glad to let you know.
bot.rb:
require 'yaml'
require_relative 'wordplay'
#A basic implementation of a chatterbox
class Bot
attr_reader :name
#Initialies the bot object, loads in the external YAML data
# file and sets bot's name. Raises an exception if
# the data loading process fails.
def initialize(options)
#name = options[:name] || "Unnamed Bot"
begin
#data = YAML.load(File.read(options[:data_file]))
rescue => e
raise "Can't load bot data because #{e}"
end
end
end
basic_client.rb:
require_relative 'bot'
bot = Bot.new(:name => ARGV[0], :data_file => ARGV[1])
puts bot.greeting
while input = $stdin.gets and input.chomp != 'end'
puts '>> ' + bot.response_to(input)
end
puts bot.farewell
The clue is in your error message:
from C:/RailsInstaller/Ruby2.1.0/bin/bot.rb:13:in `initialize'
Line 13 of bot.rb, in your initialize method.
no implicit conversion of nil into String (RuntimeError)
It can't turn nil into a string.
Your code specifies:
#name = options[:name] || "Unnamed Bot"
Which guards against options[:name] being nil
However
#data = YAML.load(File.read(options[:data_file]))
Specifies that it tries to load a file with a path of options[:data_file]. Which is nil.
So when running your client ensure to pass your name AND the data file where the yaml should be loaded from. OR create a default file and pass that:
#data = YAML.load(File.read(options[:data_file] || 'path/to/default.yml'))
YAML.load(nil)
TypeError: no implicit conversion of nil into String
so you should be careful: the result of File.read(options[:data_file]) is probably nil.
YAML.load(nil.to_s)
=> false

NoMethodError: undefined method `split' for #<Proc: ...> with Faraday

I want to send a get request with a JSON body (for search) using Faraday, but am getting the above error. I thought that self inside the Proc was messing things up, but that had nothing to do with it. I'm following the documentation on the [faraday github page][1] but have gotten stuck on this.
def perform_query
response = self.database.connection.get do |request|
request.url self.path
request.headers['Content-Type'] = 'application/json'
request.body(self.to_json)
end
end
def terms_to_json
terms_array = self.terms.keys.inject([]) do |terms_array, field|
value = self.terms[field]
terms_array.tap do |ary|
if value
ary << "\"#{field}\": \"#{value}\""
end
end
end
"{ #{terms_array.join ','} }"
end
def to_json
"{ \"queryb\" : #{self.terms_to_json} }"
end
Here is the stack trace, with the error coming somewhere in the get Proc in #perform_query :
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/request.rb:60:in `url'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/connection.rb:219:in `block in run_request'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/connection.rb:237:in `block in build_request'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/request.rb:35:in `block in create'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/request.rb:34:in `tap'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/request.rb:34:in `create'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/connection.rb:233:in `build_request'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/connection.rb:218:in `run_request'
from /Users/chrismaddox/.rvm/gems/ruby-1.9.3-p125/gems/faraday-0.8.1/lib/faraday/connection.rb:87:in `get'
from /Users/chrismaddox/Dropbox/LivingSocial/Hungry Academy/Projects/hackchat/search_ruby/elastic.rb:83:in `method_missing'
from /Users/chrismaddox/Dropbox/LivingSocial/Hungry Academy/Projects/hackchat/search_ruby/elastic.rb:112:in `perform_query'
from /Users/chrismaddox/Dropbox/LivingSocial/Hungry Academy/Projects/hackchat/search_ruby/elastic.rb:61:in `send_query'
UPDATE:
the path method returns a string of the path to the search for a given index. Eg /wombats/animals/_search
Elastic::Database#path calls Elastic::Index#index_path:
module Elastic
ELASTIC_URL = "http://localhost:9200"
class Index
attr_reader :index_name, :type_name, :last
def initialize(type)
#index_name = "#{type}-index"
#type_name = type
#last = 0
add_to_elastic
end
def add_to_elastic
index_url = URI.parse "#{ELASTIC_URL}#{index_path}/"
Connection.new(index_url).put()
end
def index_path
"/#{self.index_name}"
end
def search_path
"#{type_path}/_search/"
end
def type_path
"#{self.index_path}/#{type_name}/"
end
end
end
A call to search_path = "#{type_path}/_search/"
A call to type_path = "#{self.index_path}/#{type_name}/"
A call to index_path = "/#{self.index_name}"
So if index name is wombat and type name is animal, search_path evaluates to /wombat/animal//_search
It turns out that this wasn't the problem showing the error, but was caused because Faraday's methods are inconsistent. Faraday::Request#url and Faraday::Request#headers are themselves setter methods, whereas Faraday::Request#body= is the setter method for body.

Having 'allocator undefined for Data' when saving with ActiveResource

What I am missing? I am trying to use a rest service for with Active resource, I have the following:
class User < ActiveResource::Base
self.site = "http://localhost:3000/"
self.element_name = "users"
self.format = :json
end
user = User.new(
:name => "Test",
:email => "test.user#domain.com")
p user
if user.save
puts "success: #{user.uuid}"
else
puts "error: #{user.errors.full_messages.to_sentence}"
end
And the following output for the user:
#<User:0x1011a2d20 #prefix_options={}, #attributes={"name"=>"Test", "email"=>"test.user#domain.com"}>
and this error:
/Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `new': allocator undefined for Data (TypeError)
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `load'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `each'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `load'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1322:in `load_attributes_from_response'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1316:in `create_without_notifications'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1314:in `tap'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1314:in `create_without_notifications'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/observing.rb:11:in `create'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1117:in `save_without_validation'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/validations.rb:87:in `save_without_notifications'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/observing.rb:11:in `save'
from import_rest.rb:22
If I user curl for my rest service it would be like:
curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"test curl", "email":"test#gmail.com"}' http://localhost:3000/users
with the response:
{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}
There is a built-in type named Data, whose purpose is rather mysterious. You appear to be bumping into it:
$ ruby -e 'Data.new'
-e:1:in `new': allocator undefined for Data (TypeError)
from -e:1
The question is, how did it get there? The last stack frame puts us here. So, it appears Data wandered out of a call to find_or_create_resource_for. The code branch here looks likely:
$ irb
>> class C
>> end
=> nil
>> C.const_get('Data')
=> Data
This leads me to suspect you have an attribute or similar floating around named :data or "data", even though you don't mention one above. Do you? Particularly, it seems we have a JSON response with a sub-hash whose key is "data".
Here's a script that can trigger the error for crafted input, but not from the response you posted:
$ cat ./activeresource-oddity.rb
#!/usr/bin/env ruby
require 'rubygems'
gem 'activeresource', '3.0.10'
require 'active_resource'
class User < ActiveResource::Base
self.site = "http://localhost:3000/"
self.element_name = "users"
self.format = :json
end
USER = User.new :name => "Test", :email => "test.user#domain.com"
def simulate_load_attributes_from_response(response_body)
puts "Loading #{response_body}.."
USER.load User.format.decode(response_body)
end
OK = '{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}'
BORKED = '{"data":{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}}'
simulate_load_attributes_from_response OK
simulate_load_attributes_from_response BORKED
produces..
$ ./activeresource-oddity.rb
Loading {"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}..
Loading {"data":{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}}..
/opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `new': allocator undefined for Data (TypeError)
from /opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `load'
from /opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `each'
from /opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `load'
from ./activeresource-oddity.rb:17:in `simulate_load_attributes_from_response'
from ./activeresource-oddity.rb:24
If I were you, I would open /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb, find load_attributes_from_response on line 1320 and temporarily change
load(self.class.format.decode(response.body))
to
load(self.class.format.decode(response.body).tap { |decoded| puts "Decoded: #{decoded.inspect}" })
..and reproduce the error again to see what is really coming out of your json decoder.
I just ran into the same error in the latest version of ActiveResource, and I found a solution that does not require monkey-patching the lib: create a Data class in the same namespace as the ActiveResource object. E.g.:
class User < ActiveResource::Base
self.site = "http://localhost:3000/"
self.element_name = "users"
self.format = :json
class Data < ActiveResource::Base; end
end
Fundamentally, the problem has to do with the way ActiveResource chooses the classes for the objects it instantiates from your API response. It will make an instance of something for every hash in your response. For example, it'll want to create User, Data and Pet objects for the following JSON:
{
"name": "Bob",
"email": "bob#example.com",
"data": {"favorite_color": "purple"},
"pets": [{"name": "Puffball", "type": "cat"}]
}
The class lookup mechanism can be found here. Basically, it checks the resource (User) and its ancestors for a constant matching the name of the sub-resource it wants to instantiate (i.e. Data here). The exception is caused by the fact that this lookup finds the top-level Data constant from the Stdlib; you can therefore avoid it by providing a more specific constant in the resource's namespace (User::Data). Making this class inherit from ActiveResource::Base replicates the behaviour you'd get if the constant was not found at all (see here).
Thanks to phs for his analysis - it got me pointed in the right direction.
I had no choice but to hack into ActiveResource to fix this problem because an external service over which I have no control had published an API where all attributes of the response were tucked away inside a top-level :data attribute.
Here's the hack I ended up putting in config/initializers/active_resource.rb to get this working for me using active resource 3.2.8:
class ActiveResource::Base
def load(attributes, remove_root = false)
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
#prefix_options, attributes = split_options(attributes)
if attributes.keys.size == 1
remove_root = self.class.element_name == attributes.keys.first.to_s
end
# THIS IS THE PATCH
attributes = ActiveResource::Formats.remove_root(attributes) if remove_root
if data = attributes.delete(:data)
attributes.merge!(data)
end
# END PATCH
attributes.each do |key, value|
#attributes[key.to_s] =
case value
when Array
resource = nil
value.map do |attrs|
if attrs.is_a?(Hash)
resource ||= find_or_create_resource_for_collection(key)
resource.new(attrs)
else
attrs.duplicable? ? attrs.dup : attrs
end
end
when Hash
resource = find_or_create_resource_for(key)
resource.new(value)
else
value.duplicable? ? value.dup : value
end
end
self
end
class << self
def find_every(options)
begin
case from = options[:from]
when Symbol
instantiate_collection(get(from, options[:params]))
when String
path = "#{from}#{query_string(options[:params])}"
instantiate_collection(format.decode(connection.get(path, headers).body) || [])
else
prefix_options, query_options = split_options(options[:params])
path = collection_path(prefix_options, query_options)
# THIS IS THE PATCH
body = (format.decode(connection.get(path, headers).body) || [])
body = body['data'] if body['data']
instantiate_collection( body, prefix_options )
# END PATCH
end
rescue ActiveResource::ResourceNotFound
# Swallowing ResourceNotFound exceptions and return nil - as per
# ActiveRecord.
nil
end
end
end
end
I solved this using a monkey-patch approach, that changes "data" to "xdata" before running find_or_create_resource_for (the offending method). This way when the find_or_create_resource_for method runs it won't search for the Data class (which would crash). It searches for the Xdata class instead, which hopefully doesn't exist, and will be created dynamically by the method. This will be a a proper class subclassed from ActiveResource.
Just add a file containig this inside config/initializers
module ActiveResource
class Base
alias_method :_find_or_create_resource_for, :find_or_create_resource_for
def find_or_create_resource_for(name)
name = "xdata" if name.to_s.downcase == "data"
_find_or_create_resource_for(name)
end
end
end

Resources