Semana passada fui apresentar os recursos de consumo de APIs REST usando ActiveResource como client.

Em resumo, é uma ótima biblioteca, mas não é perfeita ainda, ela deve melhorar nas próximas versões. E por outro lado, a maioria das APIs ditas ‘REST’ não são REST de verdade. Flickr e Youtube são dois exemplos. Veja este link para aprender a conversar com o Twitter. Este outro link para aprender como estender o ActiveResource para APIs não-REST e este link para entender como consumir o YouTube.

Mas além disso encontrei uma pequena surpresa: o ActiveResource tem uma documentação afirmando ter suporte a validações porém ele não as implementa! Então resolvi ver o que seria necessário para isso.

Explicando: na API REST você pode dar um POST à URL para chamar o método ‘create’. Porém não existe algo semelhante para chamar um ‘valid?’. Uma das maneiras de checar seu objeto ActiveResource antes de dar o POST é implementar validations na classe que estende o ActiveResource::Base.

O trecho que interessa é mais ou menos a partir da linha 219 deste arquivo na versão 2.0.2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person < ActiveResource::Base
  self.site = "http://www.localhost.com:3000/"

  protected

  def validate
    errors.add_on_empty %w( first_name last_name )
    errors.add("phone_number", 
      "has invalid format") unless phone_number =~ /[0-9]*/
  end

  # is only run the first time a new object is saved
  def validate_on_create 
    unless valid_member?(self)
      errors.add("membership_discount", 
        "has expired")
    end
  end

  def validate_on_update
    errors.add_to_base("No changes 
      have occurred") if unchanged_attributes?
  end
end

person = Person.new("first_name" => "Jim", 
  "phone_number" => "I will not tell you.")
person.save                         
# => false (and doesn't do the save)
person.errors.empty?                
# => false
person.errors.count                 
# => 2
person.errors.on "last_name"        
# => "can't be empty"
person.attributes = { "last_name" => "Halpert", 
  "phone_number" => "555-5555" }
person.save                         
# => true (and person is now saved to the remote service)

Como podemos ver é uma simplicação do que teríamos num ActiveRecord::Base, mas serve. Porém veja como logo abaixo neste arquivo é implementado o método que deveria chamar as validações:

1
2
3
4
5
6
7
def save_with_validation
  save_without_validation
  true
rescue ResourceInvalid => error
  errors.from_xml(error.response.body)
  false
end

Entenderam? Ele não chama a validação! Simplesmente chama novamente o método original (ver documentação sobre alias_method_chain para entender).

Não está claro porque isso está dessa maneira porque o módulo validation.rb parece ter tudo pronto para que as coisas funcionem conforme está na documentação. O ticket 10985 no Trac diz a mesma coisa mas ainda não foi solucionado.

Portanto, se você precisar dessa funcionalidade, não é difícil conseguir que funcione. Para tanto precisaremos de um pequeno monkey-patching. No arquivo config/environment.rb (ou algum outro dentro de config/initializers) coloque o seguinte:

1
require 'active_resource_validation'

E crie o arquivo lib/active_resource_validation.rb com o seguinte:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module ActiveResource::RealValidation  
  def self.included(base) # :nodoc:
    
    base.class_eval { alias_method_chain :save, :real_validation }  
  end  
  def save_with_real_validation # :nodoc:
    
    validate if respond_to? :validate    
    validate_on_create if new?  && respond_to?(:validate_on_create)    
    validate_on_update if !new? && respond_to?(:validate_on_update)    
    valid? ? save_without_validation : false  
  rescue ResourceInvalid => error    
    errors.from_xml(error.response.body)    
    false  
  end
end

# solves http://dev.rubyonrails.org/ticket/10985

ActiveResource::Base.send :include, ActiveResource::RealValidation

Pronto, agora as validations devem funcionar conforme está documentado. Eu chequei no trunk do Rails e até hoje isso ainda não está implementado.

comentários deste blog disponibilizados por Disqus