Storing tweets using tweetstream in MongoDB fails with following error - ruby

I have just started to learn ruby and concept of mongodb. This is the script that I am trying to run
require 'rubygems'
require 'tweetstream'
require 'mongo'
TweetStream.configure do |config|
config.consumer_key = '<key>'
config.consumer_secret = '<secret>'
config.oauth_token = '<token>'
config.oauth_token_secret = '<token_secret'
config.auth_method = :oauth
end
connection = Mongo::Connection.new
db = connection.db("tweetsDB")
tweets = db.collection("tweets")
client = TweetStream::Client.new
client.on_error do |message|
puts message
end
client.follow(<user_id>,<user_id>) do |status|
id = tweets.insert(status, :safe => true)
end
NOTE: I have removed all the static private values with in the script above for this post.
Version of Mongo, bson, bson_ext - 1.7.0
error message
NoMethodError: undefined method `has_key?' for #<Twitter::Tweet:0x7f21cd14cf08>
/var/lib/gems/1.8/gems/bson-1.7.0/lib/bson/types/object_id.rb:93:in `create_pk'
/var/lib/gems/1.8/gems/mongo-1.7.0/lib/mongo/collection.rb:360:in `insert'
/var/lib/gems/1.8/gems/mongo-1.7.0/lib/mongo/collection.rb:360:in `collect!'
/var/lib/gems/1.8/gems/mongo-1.7.0/lib/mongo/collection.rb:360:in `insert'
tracker.rb:28
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:525:in `call'
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:525:in `invoke_callback'
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:533:in `yield_message_to'
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:471:in `respond_to'
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:411:in `connect'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:296:in `call'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:296:in `invoke_callback'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:143:in `handle_stream'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:193:in `on_body'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:192:in `each'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:192:in `on_body'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:74:in `<<'
/var/lib/gems/1.8/gems/em-twitter-0.2.1/lib/em-twitter/connection.rb:74:in `receive_data'
/var/lib/gems/1.8/gems/eventmachine-1.0.0/lib/eventmachine.rb:187:in `run_machine'
/var/lib/gems/1.8/gems/eventmachine-1.0.0/lib/eventmachine.rb:187:in `run'
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:385:in `start'
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:128:in `filter'
/var/lib/gems/1.8/gems/tweetstream-2.3.0/lib/tweetstream/client.rb:106:in `follow'
tracker.rb:27

If you want to blindly dump everything into MongoDB then you can just do a :
client.follow(<user_id>,<user_id>) do |status|
data = status.to_hash
id = tweets.insert(data)
end
If you want to be a bit more selective then you can try something like :
# only add the following fields to the database
ONLY = %w{created_at text geo coordinate id_str}
client.follow(<user_id>,<user_id>) do |status|
data = status.to_hash.select{|k,v| ONLY.include?(k.to_s)}
id = tweets.insert(data)
end
Or :
# add everything except the following fields to the database
EXCEPT = %w{entities}
client.follow(<user_id>,<user_id>) do |status|
data = status.to_hash.reject{|k,v| EXCEPT.include?(k.to_s)}
id = tweets.insert(data)
end

It seems looking at the docs that Client.follow just returns a generic ruby object. You have to take the relevant fields from the object and pull them out into json (or, more advisable, a mongoid object) to send to the database.

has_key? is a method available in the Hash superclass. What it's trying to do is make a json string to pass to Mongo. Except that you are passing a string, has_key? is not part of the String class. Just convert it to a hash and you should be good to go.
client.follow(<user_id>,<user_id>) do |status|
id = tweets.insert({:status => status.text}, :safe => true)
end

Related

null validation failed when attribute supplied rom-rb

I'm trying to get to grips with the rom-rb persistence library, using sqlite3.
I ran the following migration, which includes a NOT NULL constraint:
ROM::SQL.migration do
change do
create_table :users do
primary_key :id
column :name, String, null: false
column :age, Integer
column :is_admin, TrueClass
end
end
end
Here's my simple app.rb:
require 'rom'
rom = ROM.container(:sql, 'sqlite://db/my-db-file.db') do |config|
class Users < ROM::Relation[:sql]
schema(infer: true)
end
config.relation(:users)
end
users = rom.relations[:users]
puts users.to_a.inspect # => []
create_user = users.command(:create)
create_user.call( name: 'Rob', age: 30, is_admin: true )
puts users.to_a.inspect # never reached
Trying to run this script produced the following output:
Roberts-MacBook-Pro:my-rom-demo Rob$ ruby app.rb
[]
/Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/statement.rb:108:in `step': SQLite3::ConstraintException: NOT NULL constraint failed: users.name (ROM::SQL::NotNullConstraintError)
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/statement.rb:108:in `block in each'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/statement.rb:107:in `loop'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/statement.rb:107:in `each'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/database.rb:156:in `to_a'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/database.rb:156:in `block in execute'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/database.rb:95:in `prepare'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sqlite3-1.3.13/lib/sqlite3/database.rb:137:in `execute'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/adapters/sqlite.rb:189:in `block (2 levels) in _execute'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/database/logging.rb:38:in `log_connection_yield'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/adapters/sqlite.rb:189:in `block in _execute'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/database/connecting.rb:253:in `block in synchronize'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/connection_pool/threaded.rb:91:in `hold'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/database/connecting.rb:253:in `synchronize'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/adapters/sqlite.rb:180:in `_execute'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/adapters/sqlite.rb:146:in `execute_insert'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/dataset/actions.rb:1099:in `execute_insert'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/sequel-5.11.0/lib/sequel/dataset/actions.rb:399:in `insert'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-sql-2.5.0/lib/rom/sql/relation/writing.rb:39:in `insert'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-sql-2.5.0/lib/rom/sql/commands/create.rb:46:in `block in insert'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-sql-2.5.0/lib/rom/sql/commands/create.rb:46:in `map'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-sql-2.5.0/lib/rom/sql/commands/create.rb:46:in `insert'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-sql-2.5.0/lib/rom/sql/commands/create.rb:31:in `execute'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-core-4.2.1/lib/rom/command.rb:280:in `call'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-sql-2.5.0/lib/rom/sql/commands/error_wrapper.rb:16:in `call'
from /Rob.rvm/gems/ruby-2.4.0#learn-rails/gems/rom-core-4.2.1/lib/rom/commands/composite.rb:17:in `call'
from app.rb:15:in `<main>'
Why does it think my name attribute is null when I'm providing it?
NOTE: I revised my answer after some testing and learning about ops gem versions
The reason you're getting a NULL CONSTRAINT error is because ROM does not
have a schema loaded for the the users table.
When you defined the container below
rom = ROM.container(:sql, 'sqlite://db/my-db-file.db') do |config|
class Users < ROM::Relation[:sql]
schema(infer: true)
end
config.relation(:users)
end
you defined two things, a relation class bound to a constant called Users and an auto generated relation with the same name but is actually registered inside the ROM container. Effectively the Users constant relation is being ignored. The reason this is important is because the auto generated relation isn't automatically inferring the schema from the database so when you go to write data out, the schema forces all of the unknown keys to be removed causing the error. All you're sending to the db is {}.
To fix the error just tell the relation to infer the schema - an example can be seen below.
require 'rom'
require 'rom/sql'
require 'sqlite3'
puts "ROM Version #{ROM::Core::VERSION}" # 4.2.1
puts "ROM Version #{ROM::SQL::VERSION}" # 2.5.0
puts "Sequel Version #{Sequel::VERSION}" # 5.11.0
puts "SQLite3 Gem Version #{SQLite3::VERSION}" # 1.3.13
opts = {
adapter: :sqlite,
database: 'c:/mydb.db'
}
rom = ROM.container(:sql, opts) do |c|
# Just another way to write the same users table
# c.gateways[:default].create_table(:users) do
# column :id, :integer, primary_key: true
# column :name, :string, null: false
# column :age, :integer
# column :is_admin, :bool
# end
c.gateways[:default].create_table :users do
primary_key :id
column :name, String, null: false
column :age, Integer
column :is_admin, TrueClass
end
c.relation(:users) do
schema(infer: true)
end
end
users = rom.relations[:users]
puts users.to_a.inspect # => []
create_user = users.command(:create)
create_user.call(name: 'Rob', age: 30, is_admin: true)
puts users.to_a.inspect # never reached
# Uncomment if you want to see the users schema
# puts users.dataset.db.schema(:users)
If you want to use standalone relation classes instead of the container config dsl then I suggest reading up on the Auto Registration system.
DATABASE CREATION ISSUE
There is a whole host of things that could be going on which could prevent a sqlite database from being created.
It could be a permissions issue
The directory structure might not exist
Sqlite might not be compiled to handle URI's (only matters if you are using file:// in your paths) [see sqlite docs]
My advice here is when working with sqlite and ROM, use the opts hash example from the script above and try and use a relative path from the current working directory. That seems to always work.

Should I be using EM::Synchrony::Multi or EM::Synchrony::FiberIterator with Goliath?

Maybe this is the wrong approach, but I'm trying to parallelize em-hiredis puts and lookups in Goliath with EM::Synchrony::Multi or EM::Synchrony::FiberIterator. However, I can't seem to access basic values initialized in the config. I keep getting method_missing errors.
Here's the basic watered down version of what I'm trying to do:
/lib/config/try.rb
config['redisUri'] = 'redis://localhost:6379/0'
config['redis_db'] ||= EM::Hiredis.connect
config['user_agent'] = "MyCrawler Mozilla/5.0 Compat etc."
Here's the basic Goliath Setup
/try.rb
require "goliath"
require "em-hiredis"
require "em-synchrony/fiber_iterator"
require "em-synchrony/em-hiredis"
require "em-synchrony/em-multi"
class Try < Goliath::API
use Goliath::Rack::Params
use Goliath::Rack::DefaultMimeType
def response(env)
case env['REQUEST_PATH']
when "/start" then
start_crawl()
body = "STARTING"
[200, {}, body]
end
end
def start_crawl
urls = ["http://www.example.com/",
"http://www.example.com/photos/",
"http://www.example.com/video/",
]
EM::Synchrony::FiberIterator.new(urls, 3).each do |url|
p "#{user_agent}"
redis_db.sadd 'test_queue', url
end
# multi = EM::Synchrony::Multi.new
# urls.each_with_index do |url, index|
# p "#{user_agent}"
# multi.add index, redis_db.sadd('test_queue', url)
# end
end
end
However, I keep getting errors where Goliath doesn't know what user_agent is or redis_db which were initialized in the config.
[936:INFO] 2012-09-21 23:47:10 :: Starting server on 0.0.0.0:9000 in development mode. Watch out for stones.
/Users/ewu/.rvm/gems/ruby-1.9.3-p194#crawler/gems/goliath-1.0.0/lib/goliath/api.rb:143:in `method_missing': undefined local variable or method `user_agent' for #<Try:0x007ff5a431c4e0 #opts={}> (NameError)
from ./lib/try.rb:27:in `block in start_crawl'
from /Users/ewu/.rvm/gems/ruby-1.9.3-p194#crawler/gems/em-synchrony-1.0.2/lib/em-synchrony/fiber_iterator.rb:10:in `call'
from /Users/ewu/.rvm/gems/ruby-1.9.3-p194#crawler/gems/em-synchrony-1.0.2/lib/em-synchrony/fiber_iterator.rb:10:in `block (2 levels) in each'
...
...
...
Ideally I'd be able to get FiberIterator working, because I have additional conditionals to check for:
EM::Synchrony::FiberIterator.new(urls, 3).each do |new_url}
is_member = redis_db.sismember('crawled_urls', new_url)
is_member += redis_db.sismember('queued_urls', new_url)
if is_member == 0
redis_db.lpush 'crawl_queue', new_url
redis_db.sadd 'queued_urls', new_url
end
end
I don't think your config file is getting loaded. The name of try.rb needs to match the name of the robojin.rb file in the config directory.

Sinatra, Mongoid, Heroku, MongoHQ: connecting to Mongodb

Trying to get Mongoid up and running with Sinatra on Heroku (MongoHQ). Previous experience with Rails but first time with the stack and Sinatra.
Started with one of the simple examples on the web (app.rb):
require 'rubygems'
require 'sinatra'
require 'mongo'
require 'mongoid'
configure do
Mongoid.load!('mongoid.yml')
Mongoid.configure do |config|
if ENV['MONGOHQ_URL']
conn = Mongo::Connection.from_uri(ENV['MONGOHQ_URL'])
uri = URI.parse(ENV['MONGOHQ_URL'])
# problem happens here
config.master = conn.db(uri.path.gsub(/^\//, ''))
else
config.master = Mongo::Connection.from_uri("mongodb://localhost:27017").db('test')
end
end
end
# Models
class Counter
include Mongoid::Document
field :count, :type => Integer
def self.increment
c = first || new({:count => 0})
c.inc(:count, 1)
c.save
c.count
end
end
# Controllers
get '/' do
"Hello visitor n" + Counter.increment.to_s
end
For reference, mongoid.yml looks like:
development:
sessions:
default:
database: localhost
production:
sessions:
default:
uri: <%= ENV['MONGOHQ_URL'] %>
As per app.rb (# problem happens here), my logs say:
/app/app.rb:15:in `block (2 levels) in <top (required)>': undefined method `master=' for Mongoid::Config:Module (NoMethodError)
from /app/vendor/bundle/ruby/1.9.1/gems/mongoid-3.0.3/lib/mongoid.rb:112:in `configure'
from /app/app.rb:11:in `block in <top (required)>'
from /app/vendor/bundle/ruby/1.9.1/gems/sinatra-1.3.2/lib/sinatra/base.rb:1273:in `configure'
from /app/app.rb:8:in `<top (required)>'
I have also tried variants, including:
config.master = Mongo::Connection.from_uri(ENV['MONGOHQ_URL']).db('appXXXXX')
Mongoid.database = Mongo::Connection.from_uri(ENV['MONGOHQ_URL']).db('appXXXXXXX')
But get the same error:
undefined method `master` for Mongoid::Config:Module (NoMethodError)
or:
undefined method `database=` for Mongoid::Config:Module (NoMethodError)
What am I missing?
Shouldn't be
configure do
Mongoid.load!('mongoid.yml')
end
enough?
That's what the mongid docs are saying. The MONGOHQ_URL environment variable already contains every information to initialize the connection to the db.
So was using Mongoid 3.x ... which:
Doesn't use 10gen driver: uses Moped
Doesn't use config.master
The canonical sample code above which is all over the web works out of the box with Mongoid 2.x so have dropped back to that for the time being.
Thanks!

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