Seguindo a seqüencia que eu iniciei aqui, mas alterando a ordem proposta, Vamos conversar um pouco sobre os Geradores de código do Rails.
Generators são uma das coisas mais legais do Rails!
Sim, você ja viu eles, lembra do primeiro screencast sobre rails que você viu? Sim, aquele mesmo em que um cara cria um CRUD em 1 minuto …
Generators são a “forma rails” de gerar código
Mas o código gerado não precisa ser apenas aquele CRUD, ele pode ser o que você quiser, e o Rails ja tem um suporte excelente para isto, pois este mesmo suporte ja é utilizado na base do Rails.
Beleza, mas por que eu iria querer escrever um gerador de código?
Pense sobre o projeto que você estaria começando hoje, lembre de todos aqueles formulários para CRUD que você precisara criar, ou aquela parte quase igual de todas as páginas da aplicação …
Agora pense em todas as aplicações da empresa que você trabalho …
Entendeu agora?
Sim, eu concordo que utilizar o “scaffold” padrão não é realmente útil, mas se esta geração de código inicial for feita seguindo os padrões da sua empresa, ela pode aumentar muito a velocidade de desenvolvimento inicial das aplicações …
Agora eu imagino que você esteja pensando: Legal, generators são show de bola, mas para de falar besteira e me mostre o código!
Então, vamos brincar um pouco …
Vamos iniciar criando um novo projeto rails: rails plugins102
agora de dentro do diretório do projeto execute: script\generate plugin my_generator
(Se você esta pensando que você ja viu isto antes, provavelmente você leu o post anterior sobre escrita de plugins para o rails
)
Agora que você a criou a estrutura para um novo plugin, vamos escrever algum código!
Para criar um novo generator, você precisa criar um diretório de nome “generators” dentro do diretório do plugin, um diretório com o nome do seu generator dentro deste, e um diretório de nome “templates” dentro do diretório do seu generator, por exemplo a minha estrutura ficou assim:
Na verdade, você não precisa de um plugin para criar um generator, você pode colocar o seu novo generator dentro de qualquer um dos seguintes diretórios:
Mas eu acho que um plugin é a orma mais fácil de começar, e vai ser mais fácil de utilizar nas suas aplicações também, mas claro que se você for utilizar isto em diversas aplicações um GEM seria melhor pois você teria apenas uma cópia do código para todo o servidor, mas como eu nunca criei um GEM vou continuar com o plugin pelo menos para este exemplo …
para o código do generator nos vamos criar um arquivo no diretório do generator de nome [nome_do_generator]_generator.rb, no meu caso o nome do arquivo ficou: my_generator/generators/test_gen/test_gen_generator.rb
Agora dentro deste arquivo precisamos criar a classe que define o generator, esta classe deve ter o nome de acordo com o nome do arquivo (seguindo os padrões do ruby), no meu caso o nome da classe precisa ser TestGenGenerator, e ela precisa extender uma das classes padrão para generators do rails (na verdade eu poderia não extender nada e implementar um monte de métodos, mas assim é bem mais fácil
), estas classes são:
Rails::Generator::Base ou Rails::Generator::NamedBase, eu vou utilizar NamedBase (A base para o controller generator do Rails), e vou criar um modelo de generator para uma migration apenas para mostrar como isto funciona …
NamedBase é uma ótima base para geradores que esperam parametros no formato: Nome [parametro1] [parametro2] …
Para todos os outros Base é uma melhor pedida …
O nosso código inicial é este:
1 2 3 4 5 6 7 | class TestGenGenerator < Rails::Generator::NamedBase def manifest record do |m| end end end |
Nesta classe precisamos configurar o manifesto do generator e configurar quaisquer variáveis locais que utilizemos nos templates.
Mas com o código que nos temos, você ja pode executar: ruby script\generate test_gen asdas_dasda asd:ash (letras aleatórias como parâmetros por hora).
NamedBase ja vai configurar algumas variáveis para nós quando executarmos este comando:
Claro que por enquanto não estamos gerando absolutamente nada, por que o manifesto do gerador esta em branco, então vamos criar um exemplo simples para ver como isto realmente funciona …
Vamos criar um diretório de nome “dummy” dentro do diretório “template” e um arquivo em branco de nome “log.log” dentro deste, e vamos fazer as seguintes alterações no manifesto:
1 2 3 4 5 | def manifest record do |m| m.file 'dummy/log.log', "log/#{file_path}.log" end end |
Este código vai dizer para o gerador copiar o nosso arquivo em branco para $APP_ROOT/log/asdas_dasda.log se executarmos o gerador com os mesmos parâmetros que antes …
Mas apenas copiar arquivos de um lugar para outro não é uma coisa muito divertida, então vamos brincar um pouco com o ERB, e vamos criar uma migration para o nosso plugin, para isto vamos alterar novamente o manifesto como no seguinte exemplo:
1 2 3 4 5 6 7 8 | def manifest @migration_name = "Create#{class_name}" @migration_action = "add" record do |m| m.file 'dummy/log.log', "log/#{file_path}.log" m.migration_template 'lib/mymigration.rb',"db/migrate", :migration_file_name => "create_#{file_path}" end end |
Como você pode ver no código, estou informando ao gerador que eu tenho um arquivo de nome mymigration.rb dentro do diretório templates/lib, este template vai ser processado pelo ERB e o código gerado vai ser colocado no diretório “db/migrate”e vai ser chamado XXX_create_asdas_dasda.rb (XXX vai ser o número da migration gerado automaticamente pelo rails)
O conteúdo do arquivo mymigration.rb é o seguinte:
1 2 3 4 5 6 7 8 9 10 11 | class <%= migration_name.underscore.camelize %> < ActiveRecord::Migration def self.up<% attributes.each do |attribute| %> <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end -%> <%- end %> end def self.down<% attributes.reverse.each do |attribute| %> <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end -%> <%- end %> end end |
este é um template ERB que vai gerar código ruby.
Agora que ja temos um plugin razoavelmente útil , vamos ver o que mais é possível fazer …
para responder esta pergunta, vou copiar parte da documentação da classe Rails::Generator::Commands::Create, ou seja, todas as possibilidades de comandos para o manifesto do gerador:
Acho que era isto, você pode fazer perguntas nos comentários se quiser, vou tentar responder todas (se é que alguem vai perguntar
)
Para mais documentação sobre generators do rails siga os seguintes links:
http://wiki.rubyonrails.org/rails/pages/UnderstandingGenerators
http://www.aidanf.net/node/33
http://api.rubyonrails.org/classes/Rails/Generator/Base.html
http://api.rubyonrails.org/classes/Rails/Generator/NamedBase.html
http://api.rubyonrails.org/classes/Rails/Generator/Commands/Create.html
Espero que este passo a passo ajude alguem!
O próximo vai ser sobre testar o seu plugin! e o quarto eu ainda não pensei sobre o que vai ser, sugestões são bem vindas
Tags: generator, howto, passo a passo, plugin, rails, Ruby, step by step
Uma das coisas mais legais no Rails é o suporte a Plugins …
O Ruby On Rails em si já é um excelente framework, mas a combinação dos Plugins do Rails com as classes abertas do Ruby é uma combinação explosiva!
Esta combinação é o que permite a criação de “tags” customizadas para utilizar nas suas aplicações RoR nas views e nos layouts.
E é realmente fácil de criar estes “view helpers” no Rails.
A forma padrão (sem plugins) de se criar estas “tags” é simplesmente criar um método em uma das classes Helper (as que ficam em app/helpers), por exemplo, se todos os forms na sua aplicação ficam dentro de tabelas, com uma coluna para o label e uma para o campo real, você pode criar um método helper para diminuir bastante a quantidade de código a ser digitada seguindo estes passos:
O formulário padrão, bastante simples e feio, para o model “Example” que criamos, nesta aplicação seria algo parecido com:
1 2 3 4 5 6 7 | <% form_for(@example) do |f| %>
<table>
<tr><td><label for="example_name">Name</label></td><td><%= f.text_field :name %></td></tr>
<tr><td><label for="example_name">Url</label></td><td><%= f.text_field :url %></td></tr>
<tr><td colspan="2"><%= f.submit "Update" %></td></tr>
</table>
<% end %> |
mas se editarmos o arquivo app/helpers/application_helper.rb e adicionarmos o seguinte método:
1 2 3 | def textfield label, object, property, options = {} %Q{<tr><td><label for="#{object.to_s}_#{property.to_s}">#{label}</label></td><td>#{text_field object, property, options}</td></tr>} end |
o código da view ficaria muito mais simples como podemos ver abaixo.
1 2 3 4 5 6 7 | <% form_for(@example) do |f| %>
<table>
<%= textfield "Name", :example, :name %>
<%= textfield "Url", :example, :url %>
<tr><td colspan="2"><%= f.submit "Update" %></td></tr>
</table>
<% end %> |
Se você pensar apenas neste pequeno formulário, pode parecer muita complicação para pouca coisa, mas pensando na aplicação inteira isto poupa bastante trabalho
Agora pensando um pouco maior, imagine que este padrão de layout (bem feio por sinal) que eu defini para esta aplicação, seja o padrão de todas as aplicações de toda a empresa!
e todos os desenvolvedores estão trabalhando da primeira forma que eu descrevi, codificando o HTML direto em todas as páginas.
Um belo dia, um designer novo, contratado pelo dono da empresa, diz que em todos os TR de todos os forms, é necessário adicionar uma classe CSS.
Nesta situação, o negócio é sentar e chorar …
Mas se você escolheu trabalhar da segunda forma, utilizando um helper, no máximo você precisara alterar uma linha de código por aplicação!
Você pode se considerar um herói! Certo?
Mas considere a opção de você ser ainda mais esperto do que isto! Que tal criar um plugin que vai conter estas tags para facilitar o trabalho, e utilizar este plugin em todas as aplicações da empresa?
Desta forma você precisaria alterar apenas uma linha de código, testar apenas uma vez, e todas as aplicações da empresa ja estariam corrigidas!
E como sempre, com o Rails, esta é uma tarefa muito fácil de ser completada ![]()
Apenas siga estes passos simples:
1 2 3 4 5 6 | # LifeSaver module LifeSaver def textfield label, object, property, options = {} %Q{<tr><td><label for=#{object.to_s}_#{property.to_s}>#{label}</label></td><td>#{text_field object, property, options}</td></tr>} end end |
1 2 | # Include hook code here ActionView::Base.send :include, LifeSaver |
Ok, você acabou de criar o seu primeiro plugin para o Rails!
E sim, o código é exatamente o mesmo utilizado no application_helper.rb, o único truque esta no init.rb, aquela linha de código, inclui todos os métodos do module “LifeSaver” na classe base de todas as views do Rails, a ActionView::Base.
Agora, se você escolheu esta terceira opção, va falar com o seu chefe, conte uma historia parecida com a que eu contei aqui, e peça um aumento, por que você merece, você acabou de poupar diversas horas de trabalho de umas 3 pessoas pelo menos
Espero que este pequeno passo a passo ajude alguem
Este é o primeiro post de uma série sobre escrita de plugins para RoR, o próximo vai ser sobre testes unitários para o código dos seus plugins, o terceiro vai falar de plugins com generators, e o quarto vocês vão ter que voltar aqui para descobrir sobre o que vai ser
Tags: howto, passo a passo, plugin, rails, Ruby, step by step
Trabalhando com Flex com um backend em Java, cheguei a conclusão que a melhor alternativa para o projeto seria simplesmente fazer posts de dados do flex para o java e receber XML de volta (o Flex trabalha muito bem com XML).
Mas tem um problema nisto, passar um ou dois valores desta forma é bastante fácil, mas quando eu precisava passar objetos complexos era muito chato passar milhares de parâmetros para uma URL, então tive a idéia de mandar também XML de volta para o Java …
desta forma, o único problema que continuou foi eu precisar escrever manualmente o XML para cada post, sendo que na maioria das vezes eu ja tinha um objeto Flex …
Solucionei isto com uma pequena função utilitária que transforma qualquer objeto em XML, tentei procurar alguma coisa na API padrão, mas não encontrei nada, então escrevi isto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package teste { import mx.utils.ObjectUtil; public class Utils { public static function objectToXml(obj : Object, name : String) : XML{ var result : XML; var info:Object = ObjectUtil.getClassInfo(obj); if(name==null) name = info.name; result = new XML("<" + name + "></"+ name + ">"); for each (var qn : QName in info.properties){ var val : Object = obj[qn.toString()]; if(ObjectUtil.isSimple(val)) result[qn.toString()] = val; else result.appendChild(objectToXml(val,qn.toString())); } return result; } } } |
Para usar também é bem fácil:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="appInit()"> <mx:Script> <![CDATA[ import teste.Utils; import mx.utils.ObjectUtil; import mx.controls.Alert; public function appInit() : void { var obj : Object = {name:'teste',address:{street:'rua',number:20}}; Alert.show(Utils.objectToXml(obj,'teste').toXMLString()); } ]]> </mx:Script> </mx:Application> |
e o XML gerado no exemplo é este:
1 2 3 4 5 6 7 | <teste> <address> <number>20</number> <street>rua</street> </address> <name>teste</name> </teste> |
Bom, não sei se esta é a melhor forma de se trabalhar, mas até que gostei da solução
Tags: actionscript, dica, flex, howto
English Version here
Antes de iniciar a implementação do chat, nos precisamos instalar as dependências do juggernaut, ele precisa das gems json e eventmachine instaladas, para instalar ambas basta executar o seguinte comando:
Agora estamos prontos para começar a brincar …
Primeiro, vamos criar uma aplicação rails e instalar o plugin juggernaut com os seguitnes comandos:
O Juggernaut usa um servidor externo implementando Flash XML Push, e precisamos configurar este servidor, então vamos editar o arquivo: config/juggernaut.yml
Coloquei aqui no blog apenas as configurações que precisei alterar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | PUSH_PORT: 8080 ... DEFAULT_CHANNELS: - "chat" ... PUSH_HELPER_HOST: "localhost" ... SECRET: "481516232342edededededed" ... LOGIN_GET_URL: "http://localhost:3000/session/login" LOGOUT_GET_URL: "http://localhost:3000/session/logout" ... SESSION_ID: "_chattest_session_id" ... BASE64: true |
Precisei alterar PUSH_PORT Por que no linux apenas o root pode ouvir em portas inferiores a 1024, e mesmo assim eu não acho que a porta 443 padrão seja uma boa escolha, ja que esta é a porta padrão do protocolo HTTPS.
A linhe com PUSH_HELPER_HOST precisa ser alterada para o mesmo nome do servidor que for utilizado para a acessar a aplicação, em desenvolvimento, localhost deve resolver, mas não esqueça de alterar esta configuração quando for fazer o deploy do seu chat.
LOGIN_GET_URL e LOGOUT_GET_URL são utilizadas para notificar a aplicação quando um usuário conecta ou desconecta, apenas o desconectar é realmente importante para nós.
A SESSION_ID precisa ser igual ao nome do cookie utilizado como marcador de sessão da aplicação, no rails 1.2.x fica no application controller, no rails 2.0 esta configuração vai para o enviroment.rb
e BASE64 é a configuração mágica que habilita o uso dos helpers do rails para geração do Javascript que vai ser utilizado.
Agora vamos começar a criar o layout da aplicação.
Crie um arquivo de nome pubic/stylesheets/public.css com o seguinte conteúdo:
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 | body { background-color: white; } #users { float: left; width: 200px; height: 400px; border-style: inset; overflow: auto; color: white; background-color: gray; } #dasd { height: 400px; margin-left: 5px; border-style: inset; overflow: auto; color: white; background-color: gray; } #controls { clear: both; padding: 0 0 0 0; height: 55px; vertical-align: top; border-style: inset; overflow: auto; color: white; background-color: gray; } |
e o arquivo: app/views/layouts/application.rhtml com o seguinte conteúdo.
1 2 3 4 5 6 7 8 9 10 | <html> <head> <title>Chat Test</title> <%= stylesheet_link_tag 'public' %> <%= javascript_include_tag :defaults %> </head> <body> <%= yield %> </body> </html> |
Com o layout pronto (sim, esta bem feio, mas eu não sou designer eu sou um desenvolvedor, então solicite ao designer da sua equipe a criação de um novo layout para este chat
) vamos criar o código fonte necessário e o banco de dados com estes 4 comandos:
Layout pronto, arquivos criados, agora só precisamos escrever o código da aplicação ![]()
Vamos começar editando o model OnlineUser (app/model/online_user.rb) para adicionar algumas validações, elas não são realmente necessárias, são resquícios da primeira tentativa de implementação do chat, mas mesmo assim mantive elas no código
1 2 3 4 | class OnlineUser < ActiveRecord::Base validates_presence_of :username, :session_id, :last_seen validates_uniqueness_of :username, :if => Proc.new {|user| user.online } end |
Agora a view principal da aplicação no arquivo:app/views/chat/index.rhtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!-- Register with Juggernault --> <%= listen_to_juggernaut_channels [:generic],session.session_id %> <!-- The Users List --> <div id="users"> <ul id="users_list"></ul> </div> <!-- The messages pane --> <div id="dasd"></div> <!-- The controls pane (login and send messages) --> <div id="controls"><%= render :partial => 'login' %></div> <!-- An util javascript to scroll the messages window --> <script type="text/javascript"> function scrollMessages(){ $('dasd').scrollTop = $('dasd').scrollHeight; } </script> |
Como podem ver é bem simples, apenas 3 DIVs, um javascript para fazer o scroll da tela de mensagens e um comando para inicializar o juggernaut no canal “generic” e utilizando o session_id como identificador individual do usuário.
A DIV de mensagens esta com o id dasd por que eu estava testando algumas possibilidades de conflito e esqueci de mudar o ID depois
Como pode ser visto na página index, precisaremos de um partial de nome “login” e vamos também precisar de um de nome “controls”, então vamos começar codificando o partial “controls”: app/views/chat/_controls.rhtml
1 2 3 4 5 6 | <% form_remote_tag( :url => { :action => :say }, :complete => "$('message').value = '';$('message').focus();" ) do %> <%= text_field_tag( 'message', '', { :size => 90, :id => 'message'} ) %> <%= submit_tag "Send" %> <% end %> |
Bastante simples, possui apenas uma tag remote_form_tag que fara um post para a action “say”, um campo de texto para a mensagem e um botão de submit, quando o formulário é enviado o foco volta para o campo message para que o usuário possa digitar a próxima mensagem …
E o partial de login: app/views/chat/_login.rhtml
1 2 3 4 5 6 7 | <%= "#{@message}<br/>" if @message %><% form_remote_tag( :url => { :action => :login }, :complete => "$('username').value = ''", :after => "$('login').disabled = true" ) do %> <%= text_field_tag( 'username', '', { :size => 90, :id => 'username'} ) %> <%= submit_tag "Join", :id => 'login' %> <% end %> |
Bastante parecido com o anterior, mas faz o post para uma action de nome “login”, e o javascript agora desabilita o botão quando o formulário é enviado, e também possui uma mensagem que deve aparecer para o usuário informando que o apelido ja esta sendo utilizado.
Agora todas as views ja estão prontas, podemos passar para a codificação da lógica da aplicação.
Como podemos ver nas views até agora, precisaremos de um controller de nome “chat” com dois métodos: login e say
vamos analisar um pouco o código do chat controller: app/controllers/chat_controller.rb
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | class ChatController < ApplicationController #this method does not need to exist, but I like to see it here, it only needs to render the index.rhtml view def index end def login #creates a new OnlineUser record, this is used to store who are the users that are online now @user = OnlineUser.new @user.username = Juggernaut.html_and_string_escape params[:username] @user.session_id = session.session_id @user.online = true @user.last_seen = Time.now #if we can save, it means that there is no other user with the same nick online, so this user can join the chat if @user.save #let's save the username in the session for future reference session[:username] = @user.username #if there are online users, fill the users box for the new user know who is online @users = OnlineUser.find(:all, :conditions => ["online = true and id != ?", @user.id]) if @users.size >0 data = render_to_string(:update) do |page| @users.each {|u| page.insert_html :bottom, :users_list, %Q{<li id="user_#{u.username}">#{u.username}</li>} } end #send the javascript only to the new user Juggernaut.send_to(@user.session_id, data) end #create a javascript call to add the new user to the end of the online users list data = render_to_string(:update) do |page| page.insert_html :bottom, :users_list, %Q{<li id="user_#{@user.username}">#{@user.username}</li>} page.insert_html :bottom, :dasd, "<b>user #{@user.username} just joined the chat</b><br/>" end #add the new user to the chat channel Juggernaut.add_channel(@user.session_id, 'chat') #send the javascript to all users in the chat channel Juggernaut.send_data(data, 'chat') render(:update) do |page| page.replace_html 'controls', :partial => "controls" end else @message = 'This nick name is already in use, please choose another' render(:update) do |page| page.replace_html 'controls', :partial => "login" end end end def say #escape the message, that way the user can not harm others sending HTML ot JavaScript commands message = "#{session[:username]}: #{Juggernaut.html_and_string_escape(params[:message])}" #create a javascript to add the new message to the end of the messages screen and scroll the div to the bottom data = render_to_string(:update) do |page| page.insert_html :bottom, :dasd, "#{message}<br/>" page.call "scrollMessages" end #send the message to all users Juggernaut.send_data(data, 'chat') render :nothing => true end end |
O método say é bastante simples, então vamos começar analisando este:
Primeiro a mensagem é reconstruída adicionando o nome do usuário no inicio da mensagem enviada removendo todos os possíveis códigos HTML ou Javascript para garantir a segurança dos outros usuários.
Então é construido um Javascript utilizando o JavaScriptBuilder do rails e o método render_to_string para adicionar a nova mensagem no final da div “dasd” e fazer o scroll caso necessário.
Então este Javascript é enviado para todos os clientes que estão ouvindo o canal “chat”.
Agora um pouco sobre o método login:
Esta é quase toda a lógica que precisamos, a única parte que falta é remover um usuário da lista de usuários online de todos os participantes quando este fechar o browser ou deixar o chat por qualquer motivo, para isto vamos ao método logout do controller session: app/controllers/session_controller.rb
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 | class SessionController < ApplicationController #Called when a user disconnect (a refresh in the browser causes this to be called too) def logout #search for the user record using the session_id @u = OnlineUser.find_by_session_id(session.session_id) reset_session #if a user was found if @u username = @u.username #remove it from the database @u.destroy #remove from the online users list from all users, and tell others this user left the chat data = render_to_string(:update) do |page| page.remove "user_#{username}" page.insert_html :bottom, :dasd, "<b>User #{username} left the chat</b><br/>" end Juggernaut.send_data(data,'chat') end render :nothing => true end def login render :nothing => true end end |
O método login não faz nada, pois não precisamos desta notificação para o chat, então vamos a descrição do método logout …
E isto é tudo pessoal! (isto só fica engraçado em ingles
)
Agora só falta executar a aplicação.
Para executar esta aplicação, precisamos iniciar o servidor do Rails como padrão, e então iniciar o push_server, para fazer isto basta executar os seguintes dois comandos.
E acessar o seu chat novo em folha no endereço: http://localhost:3000/chat
Eu criei este exemplo emquanto estudava um pouco sobre o Juggernaut, é possível que existam maneiras mais fáceis de implementar isto, mas eu acredito que seja um bom ponto de partida.
Quaisquer dicas para melhorar o exemplo são muito bem vindas.
Tags: ajax, chat, example, flash, howto, push, rails, reverse, Ruby, simple, xml