2011-09-05 19 views
6

¿Qué me falta? Estoy tratando de utilizar un servicio de descanso para con el recurso de Active, tengo el siguiente:Tener 'asignador indefinido para datos' al guardar con ActiveResource

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 

Y la siguiente salida para el usuario:

#<User:0x1011a2d20 @prefix_options={}, @attributes={"name"=>"Test", "email"=>"[email protected]"}> 

y este 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 

Si usuario curl para mi servicio de descanso sería como:

curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"test curl", "email":"[email protected]"}' http://localhost:3000/users 

con la respuesta:

{"email":"[email protected].com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5} 

Respuesta

12

Hay un tipo integrado que se denomina Data, cuyo propósito es rather mysterious. Usted parece toparse con él:

$ ruby -e 'Data.new' 
-e:1:in `new': allocator undefined for Data (TypeError) 
    from -e:1 

La pregunta es, ¿cómo llegó allí? El último marco de pila nos pone here. Por lo tanto, aparece Data vagando fuera de una llamada al find_or_create_resource_for. La rama de código here parece probable:

$ irb 
>> class C 
>> end 
=> nil 
>> C.const_get('Data') 
=> Data 

Esto me lleva a sospechar que tiene un atributo o similares que flotan alrededor nombrados :data o "data", a pesar de que usted no menciona arriba. ¿Vos si? Particularmente, parece que tenemos una respuesta JSON con un sub-hash cuya clave es "datos".

Aquí hay un script que puede desencadenar el error de entrada diseñado, pero no de la respuesta que envió:

$ 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 

produce ..

$ ./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 

Si yo fuera usted, me gustaría abrir /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb , encontrar load_attributes_from_response en la línea 1320 y cambiar temporalmente

load(self.class.format.decode(response.body)) 

a

load(self.class.format.decode(response.body).tap { |decoded| puts "Decoded: #{decoded.inspect}" }) 

..y reproduzca el error nuevamente para ver lo que realmente está saliendo de su decodificador json.

+0

He agregado el seguimiento completo de la pila. Me parece extraño que los parámetros se pasen a través de @attributes, ¿no debería ser algo como @data ?? – aletapool

+0

Es solo una estética al nombrar: 'data' es un nombre bastante sin sentido (es universalmente aplicable), donde con' attributes' al menos se obtiene la sugerencia de que es una colección de valores con clave. – phs

0

Gracias a phs por su análisis, me apuntó en la dirección correcta.

No tuve más remedio que hackear ActiveResource para solucionar este problema porque un servicio externo sobre el que no tengo control había publicado una API donde todos los atributos de la respuesta estaban dentro de un atributo de datos de nivel superior.

Aquí está el truco que terminé poniendo en config/initializers/active_resource.rb para que esto funcione para mí utilizando el recurso activo 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 
+0

¿podría agregar un poco más de explicación a esto? Un gran error puede resolver este caso específico, pero preferiría entender por qué funciona, en lugar de tomarlo con fe. ¿Puedes agregar algunos comentarios, tal vez? O al menos una breve explicación de los bloques 'THIS IS THE PATCH'. ¿Son estos los únicos cambios en un archivo existente? –

0

I resolvieron este utilizando un enfoque mono-patch, que cambia de "datos" a "XDATA" antes de ejecutar find_or_create_resource_for (el método infractor). De esta forma, cuando se ejecuta el método find_or_create_resource_for, no buscará la clase Data (que se bloqueará). En su lugar, busca la clase Xdata, que con suerte no existe, y se creará dinámicamente por el método. Esta será una clase adecuada subclasificada desde ActiveResource.

Sólo añadir un archivo containig esto dentro 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 
+0

... "La pregunta es, ¿cómo llegó allí?" Esto me hizo reír porque estoy aquí. Y me hizo darme cuenta de que debería volver ahora y repensar mi enfoque. Este es un ejemplo de cuándo simplemente seguir "convención sobre configuración". jaja. – nil

1

simplemente me encontré con el mismo error en la última versión de ActiveResource, y he encontrado una solución que no requiere mono-parche en el lib: crear una Data clase en el mismo espacio de nombres que el objeto ActiveResource. Ej .:

class User < ActiveResource::Base 
    self.site = "http://localhost:3000/" 
    self.element_name = "users" 
    self.format = :json 

    class Data < ActiveResource::Base; end 
    end 

Fundamentalmente, el problema tiene que ver con la forma ActiveResource elige las clases de los objetos que crea instancias de su respuesta de la API. Hará una instancia de algo por cada hash en su respuesta. Por ejemplo, se querrá crear User, Data y Pet objetos para el siguiente JSON: mecanismo de búsqueda

{ 
    "name": "Bob", 
    "email": "[email protected]", 
    "data": {"favorite_color": "purple"}, 
    "pets": [{"name": "Puffball", "type": "cat"}] 
} 

La clase se puede encontrar here. Básicamente, comprueba el recurso (User) y sus antecesores para una constante que coincida con el nombre del sub-recurso que quiere crear una instancia (es decir, Data aquí). La excepción se debe al hecho de que esta búsqueda encuentra la constante de nivel superior Data de Stdlib; por lo tanto, puede evitarlo proporcionando una constante más específica en el espacio de nombres del recurso (User::Data). Hacer que esta clase herede de ActiveResource::Base replica el comportamiento que obtendría si la constante no se encontrara en absoluto (see here).

Cuestiones relacionadas