Gerando rotas com parâmetros dinâmicos no Rails de modo fácil

Posted by Rodrigo Panachi on julho 02, 2010

A API de rotas do Rails simplifica consideravelmente o desenvolvimento fornecendo um padrão de geração e utilização de URLs para toda aplicação. Porém algumas necessidades especificas e relativamente simples podem gerar dores de cabeça se forem implementadas incorretamente.

Um caso bastante comum são URLs compostas que sempre apontam para um mesmo recurso. Por exemplo, um blog que possua rotas para seus posts no formato /posts/autor/categoria/permalink provavelmente terá uma rota mapeada como map.post "posts/:author/:category/:permalink" gerando automaticamente os helpers post_path e post_url.

Muito bom, porém para usufruirmos desta facilidade precisamos fornecer os valores dos parâmetros dinâmicos nos controllers e views:

post_path(:author => @post.author, :category => @post.category, :permalink => @post.permalink)

E mesmo que você forneça os parâmetros em um array, vai dar muito trabalho além de deixar muito código repetido pela aplicação.

Entendi! Mas como resolvo este problema?

Para este caso, apenas implementar o método to_param do model não vai servir. Uma solução seria reescrever o método post_path (que é gerado automaticamente) no respectivo helper (posts_helper.rb):

def post_path(post, options = {})
  super(post, :author => post.author, :category => post.category, :permalink => post.permalink)
end

Outra solução seria sobrescrever o método default_url_options no controller para retorna os parâmetros padrões da rota:

def default_url_options(options = {})
  options.merge!(:author => @post.author, :category => @post.category, :permalink => @post.permalink) if options[:action] == "show"
end

A má notícia é que você terá que fazer isto para todos seus controllers e respectivos models.

A terceira solução (e mais elegante) é padronizar a maneira com que os parâmetros opcionais da rota são obtidos a partir do controller e seu respectivo model. Basta adicionar os seguintes métodos no seu ApplicationController:

def default_url_options(options = {})
  if (model = controller_model(options))
    dynamic_route_params(options).each do |param|
      options[param] = model.send(param) if model.respond_to?(param)
    end
  end
  options
end
 
def controller_model(options = {})
  clazz = (options[:controller].singularize.camelize.constantize rescue ActiveRecord)
  options.select { |key, value| value.is_a?(clazz) }.first.second
rescue
  nil
end 
 
def dynamic_route_params(options = {})
  returning [] do |dynamic_params|
    matched_routes = ActionController::Routing::Routes.routes.select do |route|
      route.matches_controller_and_action?(options[:controller], options[:action])
    end
    dynamic_segments = matched_routes.map(&:segments).flatten.each do |segment|
      dynamic_params << segment.key if segment.is_a?(ActionController::Routing::DynamicSegment)
    end
  end
end

Assim, os valores defaults dos parâmetros da rota serão obtidos diretamente do model. Caso queiram contribuir, este código está disponível no github.

Referências
Rails Routing from the Inside Out
Rails Guides: Routing

Active Record em: Como adicionar comportamento as suas associações

Posted by Roger Leite on maio 19, 2010

Qualquer um que comece a desenvolver com Active Record (AR), minha primeira recomendação é, para tudo e leia:  A Guide to Active Record Associations ou O Guia de Associações do Active Record. O guia é bem completo, e descreve muito bem os tipos de associações que estão disponíveis no AR.

Association Proxy, #wtf !

As associações:

  • belongs_to
  • has_one
  • has_many
  • has_and_belongs_to_many

Quando usadas, adicionam alguns métodos (veja Detailed Association Reference). Por exemplo, ao declarar uma associação belongs_to, o model “ganhará” os seguintes métodos:

  • association(force_reload = false)
  • association=(associate)
  • build_association(attributes = {})
  • create_association(attributes = {})

Onde association, será substituído pelo nome da associação. Exemplo retirado do guides:

class Order < ActiveRecord::Base
   belongs_to :customer
end

Cada instância de Order, conterá os métodos:

  • customer
  • customer=
  • build_customer
  • create_customer

O association proxy, é o objeto que faz a ligação do objeto que contém a associação, conhecido como owner, e o objeto associado, conhecido como target.

Legal e daí !?!

Graças ao association proxy, ao declarar uma associação, podemos extendê-la e adicionar comportamentos “customizados”. No guia, é citado como Association Extensions. O código de exemplo abaixo, está no github em random-samples.

Para exemplificar, vamos criar um modulo que adiciona o comportamento de uma galeria a qualquer coleção.

module GalleryColletion
 
  def current=(curr = nil)
    @current, @index = nil
 
    if curr.nil?
      @current = collection.first
      @index = 0
    else
      collection.each_with_index do |item, index|
        if item.id.to_i == curr.to_i
          @current = item
          @index = index
        end
      end
    end
    @current
  end
 
  def current
    @current
  end
 
  def position
    @index + 1
  end
 
  def previous?
    return false if @index.nil?
    !!(@index - 1 >= 0)
  end
  def previous
    collection[@index - 1] if previous?
  end
 
  def next?
    return false if @index.nil?
    !!(@index + 1 < collection.size)
  end
  def next
    collection[@index + 1] if next?
  end
 
  private
 
  def collection
    proxy_owner.send(proxy_reflection.name)
  end
 
end

Gallery Collection

Note que o modulo está na pasta lib, logo, a pasta tem que ser adicionada no path via config/environment.rb.

Para extender a associação, declare:

class Article < ActiveRecord::Base
  has_and_belongs_to_many :images, :extend => GalleryColletion
end

Article model, Image model aqui.

Agora para navegar entre as imagens, você pode usar:

a = Article.first
a.images.current = 1 #1 e o Image.id que deseja selecionar
a.images.current
a.images.position
a.images.next?
a.images.next
a.images.previous?
a.images.previous

Caso esteja com coragem, baixe o projeto e veja rodando.

Dúvidas, sugestões, algum “case” de sucesso, comente!

Teste sua aplicação de Linha de Comando com Cucumber! 1

Posted by Roger Leite on maio 14, 2010

É engraçado como tudo é questão de treino e disciplina. Levei um tempo para me acostumar com TDD, Vim e não poderia ser diferente com testes funcionais, sendo mais especifico, Cucumber.

Até o momento, só tinha usado cucumber em projetos web. E quando voltei a desenvolver o rubygems_snapshot, senti falta de algo para testar funcionalmente. Baseado no pik, montei um esquema simples para validar qualquer aplicação de linha de comando.

Como instalar

Basicamente, será necessário (fontes via gist):

  • rake para executar o cucumber
  • env_terminal.rb
  • terminal_steps.rb

Dado que você tem cucumber instalado, com o esquema da pasta “features”.

  • Copie o cucumber.rake para a raiz.
  • Copie o env_terminal.rb para a pasta features.
  • Copie o terminal_steps.rb para a pasta features/step_definitions/terminal_steps.rb.
  • Edite o  env.rb incluindo (pode ser no começo):
require "env_terminal"
  • Dentro do Rakefile, pode ser no final mesmo, adicione:
load "cucumber.rake"

Como usar

Todas features:

rake cucumber

Features com a tag @wip, também conhecida como Work in Progress.

rake cucumber:wip

Informações Extras

Caso precise de mais informações, você tem a opção de ver a saída dos comandos, executando a rake assim:

rake cucumber show_output=true

ou

rake cucumber:wip show_output=true

No caso do snapshot, tive a necessidade de “modificar” o comando gem toda vez que era executado, ou melhor, passar um parâmetro para controlar o ambiente. Dentro do env_terminal.rb, existe o método gsub_command, nele você pode “redefinir” comandos, caso necessite.

Gostei, quero mais!

A solução acima, é bem “caseira”. Para projetos simples com funcionalidades simples, funciona bem.

Caso queira algo mais robusto, você tem a opção da gem Aruba.

Tem este post como introdução:

Aruba – Cucumber Goodness for the Command-Line

Rubygems Snapshot! Importando/Exportando gems com velocidade!

Posted by Roger Leite on maio 10, 2010

Nova versão do Rubygems_Snapshot no ar!

Fico feliz em dizer que este é o meu primeiro projeto que passou dos dez “watchers” ! :D E que realmente ajudou alguns developers mundo afora.

A primeira versão foi muito focada no uso pessoal, como “quebra-galho” mesmo. Resolvi investir algum tempo e praticamente refaze-lá, pois ao usar no dia-a-dia percebi algumas falhas e dificuldades de uso.

Basicamente, nesta nova versão:

- É possível trabalhar com mais de um formato. Yml e Tar, que é o padrão.

- Ao exportar, os arquivos “.gem” serão exportados.

- Ao importar, ocorre praticamente uma instalação “offlline”, muito mais rápido e livre de problemas com “sources” do rubygem !

- Usar o Snapshot como API !

Como tive um trabalhão para deixar o Readme legal, não pretendo escrever mais que isso! :D

Marcos, valeu pelo incentivo! Cara, forka lá e manda bala!

Importando e exportando suas gems com Rubygems Snapshot 2

Posted by Roger Leite on dezembro 22, 2009

Final de ano está rendendo.
O rubygems_snapshot nasceu da necessidade de “migrar” as gems instaladas de uma máquina para outra, aliado ao rvm (veja este post-guia-rápido), permite mudar e/ou criar diferentes ambientes em minutos. Assim você pode fugir do famoso “gem hell”.

Veja como é difícil usar:

Instalação:

sudo gem install rubygems_snapshot

Para exportar as gems instaladas:

gem snapshot export projeto-exemplo.yml

Supondo que esteja em outra máquina, para importar as gems, use:

[sudo] gem snapshot import projeto-exemplo.yml

Afinal, o que tem de legal nisso?

Vamos supor que você acaba de entrar numa nova equipe e tem que montar o ambiente de desenvolvimento (por sinal, um ambiente complicado de configurar). O gem snapshot aliada ao rvm, foi feito para facilitar isto, vamos a um exemplo rápido:

Com o rvm, você pode criar um “novo ambiente”:

rvm use 1.8.7%projeto_exemplo
gem list

Deve retornar vazio.

gem install rubygems_snapshot
gem snapshot import projeto-exemplo.yml

Instalará as gems necessárias para o projeto e pronto!

ToDo:

Esta é uma versão bem básica, onde o “import” somente lê as gem e version e manda instalar sem requerir dependências. Está previsto de colocar um aviso no final das gems que deram erro, geralmente devido a dependências de “build nativos”, mas por enquanto estamos usando aqui na equipe com sucesso.

Como faço para criar um rubygems plugin também?

Bom, logo de cara posso te garantir que não é difícil (apesar da pouca documentação na internet), mas deixarei os detalhes para um outro post. Por enquanto, a minha recomendação é: clone o projeto, analise os dois rb do projeto :D e crie o seu!

Caso algum corajoso for usar, estou a disposição para ajudar, é só deixar um comentário aê!
Valeu e sucesso!

Ruby, Rubygems e $LOAD_PATH ou Como funciona o require de gems

Posted by Roger Leite on dezembro 17, 2009

Na madrugada passada, andei “brincando” com o fonte do Rubygems. Logo de cara posso te dizer que não consegui fazer o que queria, e pra amenizar o sentimento de “perda de tempo”, resolvi postar alguns truques aprendidos.

Baixei o fonte do rubygems, como faço pra rodá-lo sem alterar o meu sistema?

Foi a primeira pergunta que fiz. Percebi que com o google não iria encontrar a resposta, mas consegui uma dica importante: $LOAD_PATH.

$ irb
irb(main):001:0> $LOAD_PATH

No meu Ubuntu, obtive:

["/usr/local/lib/site_ruby/1.8", "/usr/local/lib/site_ruby/1.8/i486-linux", "/usr/local/lib/site_ruby/1.8/i386-linux", "/usr/local/lib/site_ruby", "/usr/lib/ruby/vendor_ruby/1.8", "/usr/lib/ruby/vendor_ruby/1.8/i486-linux", "/usr/lib/ruby/vendor_ruby", "/usr/lib/ruby/1.8", "/usr/lib/ruby/1.8/i486-linux", "/usr/lib/ruby/1.8/i386-linux", "."]
$ ls /usr/local/lib/site_ruby/1.8/

Exatamente nesta pasta que se encontra o rubygems.rb. Bingo!
Para rodar o fonte do rubygems, só é necessário adicionar ao $LOAD_PATH a pasta lib do projeto. Dado que estou na raiz do projeto rubygems baixado, execute:

~/rubygems$ ruby -I $PWD/lib ./bin/gem -v

O paramêtro -I permite adicionar diretório ao $LOAD_PATH. Simples e prático. Primeiro problema resolvido, comecei a programar.

Afinal, como funciona o “require de gems”?

Bom, já sabemos que o require “rubygems” fuciona pois encontra-se no $LOAD_PATH do ruby, no caso do meu Ubuntu em “/usr/local/lib/site_ruby/1.8″.

Basicamente (e muito), o Rubygems faz duas coisas no Kernel do Ruby.

  • Adiciona o metodo Kernel#gem.
  • Faz um Monkey Patch no Kernel#require

Kernel#gem

Permite “acionar” uma versão específica de gem. Note que este acionar, traduz-se para, adicionar a lib da gem no $LOAD_PATH. Segue um trecho do comentário do Kernel#gem:

##

# Use Kernel#gem to activate a specific version of +gem_name+.

#

# +version_requirements+ is a list of version requirements that the

# specified gem must match, most commonly “= example.version.number”.  See

# Gem::Requirement for how to specify a version requirement.

#

# If you will be activating the latest version of a gem, there is no need to

# call Kernel#gem, Kernel#require will do the right thing for you.

#

# Kernel#gem returns true if the gem was activated, otherwise false.  If the

# gem could not be found, didn’t match the version requirements, or a

# different version was already activated, an exception will be raised.
[...]

Kernel#require

No final do rubygems.rb encontramos:

if RUBY_VERSION < ‘1.9′ then

require ‘rubygems/custom_require’

end

Não consegui descobrir o que acontece com o ruby 1.9, mas no 1.8, o monkey patch executa os seguintes passos:

  • Chama o “original” require;
  • Em caso de LoadError;
    • Executa o “Gem.searcher.find(path)”;
    • Se true
      • Chama o activate (novamente traduz-se para adiciona a gem no $LOAD_PATH)
      • Executa o “original” require novamente;

Exemplos com o IRB

Para finalizar legal e comprovar tudo isso, fiz alguns testes:

$ gem list json

*** LOCAL GEMS ***

json (1.2.0, 1.1.9)

json_pure (1.2.0)

$ irb
irb(main):001:0> $LOAD_PATH

=> ["/usr/local/lib/site_ruby/1.8", "/usr/local/lib/site_ruby/1.8/i486-linux", "/usr/local/lib/site_ruby/1.8/i386-linux", "/usr/local/lib/site_ruby", "/usr/lib/ruby/vendor_ruby/1.8", "/usr/lib/ruby/vendor_ruby/1.8/i486-linux", "/usr/lib/ruby/vendor_ruby", "/usr/lib/ruby/1.8", "/usr/lib/ruby/1.8/i486-linux", "/usr/lib/ruby/1.8/i386-linux", "."]

irb(main):004:0> require "json"

LoadError: no such file to load — json

from (irb):4:in `require’

from (irb):4

from :0

irb(main):005:0> gem "json", "= 1.2.0"

NoMethodError: undefined method `gem’ for main:Object

from (irb):5
from :0

O require “json” por si só, carrega a versão mais atual da gem.

irb(main):006:0> require "rubygems"

=> true

irb(main):007:0> require "json"

=> true

irb(main):008:0> JSON::VERSION

=> “1.2.0″

irb(main):009:0> $LOAD_PATH

=> ["/usr/lib/ruby/gems/1.8/gems/gemcutter-0.1.8/lib", "/usr/lib/ruby/gems/1.8/gems/json-1.2.0/bin", "/usr/lib/ruby/gems/1.8/gems/json-1.2.0/ext/json/ext", "/usr/lib/ruby/gems/1.8/gems/json-1.2.0/ext", "/usr/lib/ruby/gems/1.8/gems/json-1.2.0/lib", "/usr/local/lib/site_ruby/1.8", "/usr/local/lib/site_ruby/1.8/i486-linux", "/usr/local/lib/site_ruby/1.8/i386-linux", "/usr/local/lib/site_ruby", "/usr/lib/ruby/vendor_ruby/1.8", "/usr/lib/ruby/vendor_ruby/1.8/i486-linux", "/usr/lib/ruby/vendor_ruby", "/usr/lib/ruby/1.8", "/usr/lib/ruby/1.8/i486-linux", "/usr/lib/ruby/1.8/i386-linux", "."]

irb(main):010:0> quit

Após o require “json”, as pastas foram adicionadas no $LOAD_PATH.

"/usr/lib/ruby/gems/1.8/gems/json-1.2.0/bin", "/usr/lib/ruby/gems/1.8/gems/json-1.2.0/ext/json/ext", "/usr/lib/ruby/gems/1.8/gems/json-1.2.0/ext", "/usr/lib/ruby/gems/1.8/gems/json-1.2.0/lib"

Agora olhe que interessante este último teste:

$ irb

irb(main):001:0> require "rubygems"

=> true

irb(main):002:0> $LOAD_PATH

=> ["/usr/lib/ruby/gems/1.8/gems/gemcutter-0.1.8/lib", "/usr/local/lib/site_ruby/1.8", "/usr/local/lib/site_ruby/1.8/i486-linux", "/usr/local/lib/site_ruby/1.8/i386-linux", "/usr/local/lib/site_ruby", "/usr/lib/ruby/vendor_ruby/1.8", "/usr/lib/ruby/vendor_ruby/1.8/i486-linux", "/usr/lib/ruby/vendor_ruby", "/usr/lib/ruby/1.8", "/usr/lib/ruby/1.8/i486-linux", "/usr/lib/ruby/1.8/i386-linux", "."]

irb(main):003:0> gem "json", "= 1.1.9"

=> true

irb(main):004:0> $LOAD_PATH

=> ["/usr/lib/ruby/gems/1.8/gems/gemcutter-0.1.8/lib", "/usr/lib/ruby/gems/1.8/gems/json-1.1.9/bin", "/usr/lib/ruby/gems/1.8/gems/json-1.1.9/ext/json/ext", "/usr/lib/ruby/gems/1.8/gems/json-1.1.9/ext", "/usr/lib/ruby/gems/1.8/gems/json-1.1.9/lib", "/usr/local/lib/site_ruby/1.8", "/usr/local/lib/site_ruby/1.8/i486-linux", "/usr/local/lib/site_ruby/1.8/i386-linux", "/usr/local/lib/site_ruby", "/usr/lib/ruby/vendor_ruby/1.8", "/usr/lib/ruby/vendor_ruby/1.8/i486-linux", "/usr/lib/ruby/vendor_ruby", "/usr/lib/ruby/1.8", "/usr/lib/ruby/1.8/i486-linux", "/usr/lib/ruby/1.8/i386-linux", "."]

irb(main):005:0> JSON

NameError: uninitialized constant JSON
from (irb):5

irb(main):006:0> require "json"

=> true

irb(main):007:0> JSON::VERSION

=> “1.1.9″

irb(main):008:0> quit

Note que após o gem “json”, “= 1.1.9″ … a versao 1.1.9 foi adicionada no $LOAD_PATH mas não foi carregada. Ao executar o require “json”, como este já estava no $LOAD_PATH, a versão 1.1.9 é usada.

Espero que com estas explicações, você use com mais segurança o rubygems.