Having 'allocator undefined for Data' when saving with ActiveResource
Asked Answered
A

4

7

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 => "[email protected]")

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"=>"[email protected]"}>

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":"[email protected]"}' http://localhost:3000/users

with the response:

{"email":"[email protected]","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}
Armorial answered 5/9, 2011 at 9:11 Comment(0)
V
14

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 => "[email protected]"

def simulate_load_attributes_from_response(response_body)
  puts "Loading #{response_body}.."
  USER.load User.format.decode(response_body)
end

OK = '{"email":"[email protected]","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}'
BORKED = '{"data":{"email":"[email protected]","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":"[email protected]","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}..
Loading {"data":{"email":"[email protected]","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.

Vergievergil answered 5/9, 2011 at 9:45 Comment(2)
I've added the complete stack trace. I find weird that parameters are passed through @attributes, shouldn't be something like @data??Armorial
Its just aesthetics in naming: data is a fairly meaningless name (it is universally applicable), where with attributes you at least get the hint that it is a collection of keyed values.Vergievergil
P
1

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": "[email protected]", 
  "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).

Pair answered 30/9, 2016 at 16:54 Comment(0)
P
0

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
Publish answered 28/8, 2012 at 21:18 Comment(1)
could you add a bit more explanation to this? A big codedump may solve this specific case, but I'd prefer to understand why this works, rather than taking it on faith. Can you add some comments, maybe? Or at least an brief explanation of the THIS IS THE PATCH blocks. Are these the only changes to an existing file?Marshland
M
0

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
Maggoty answered 30/1, 2014 at 13:48 Comment(1)
..."The question is, how did it get there?" This made me laugh because I am here. And it made me realize that I should turn back now and rethink my approach. This is an example of when to just follow "convention over configuration". haha.Boer

© 2022 - 2024 — McMap. All rights reserved.