Criando seu primeiro projeto com Merb e DataMapper


Leia em 17 minutos

No começo do mês foi lançada a versão 1.0 do Merb. Esta versão trouxe a facilidade de instalação que faltava nas versões anteriores, permitindo configurar o ambiente de maneira bastante simples.

Merb é um framework web para Ruby totalmente agnóstico. Você escolher o seu ORM de preferência, seu framework de testes, bibliotecas Javascript. Totalmente modular, permite carregar somente o que será necessário para criar sua aplicação.

Eu já tinha tentado escrever sobre ele em ocasiões anteriores, sem sucesso. Agora, resolvi colocar a coisa para funcionar e, para minha surpresa, foi mais simples do que eu esperava, como você pode conferir.

Todo o código está no Github: http://github.com/fnando/merb-contacts-app; divirta-se!

Instalando o Merb

A maneira mais fácil de instalar o Merb é utilizando o pacote completo.

sudo gem install merb

Esta gem irá instalar muitas dependências e pode demorar um pouco!

Gerando a estrutura do aplicativo

Para criar um novo aplicativo, você utilizar o comando merb-gen.

$ merb-gen app contacts
Generating with app generator:
     [ADDED]  tasks/merb.thor
     [ADDED]  .gitignore
     [ADDED]  public/.htaccess
     [ADDED]  tasks/doc.thor
     [ADDED]  public/javascripts/jquery.js
     [ADDED]  doc/rdoc/generators/merb_generator.rb
     [ADDED]  doc/rdoc/generators/template/merb/api_grease.js
     [ADDED]  doc/rdoc/generators/template/merb/index.html.erb
     [ADDED]  doc/rdoc/generators/template/merb/merb.css
     [ADDED]  doc/rdoc/generators/template/merb/merb.rb
     [ADDED]  doc/rdoc/generators/template/merb/merb_doc_styles.css
     [ADDED]  doc/rdoc/generators/template/merb/prototype.js
     [ADDED]  public/favicon.ico
     [ADDED]  public/images/merb.jpg
     [ADDED]  public/merb.fcgi
     [ADDED]  public/robots.txt
     [ADDED]  Rakefile
     [ADDED]  app/controllers/application.rb
     [ADDED]  app/controllers/exceptions.rb
     [ADDED]  app/helpers/global_helpers.rb
     [ADDED]  app/models/user.rb
     [ADDED]  app/views/exceptions/not_acceptable.html.erb
     [ADDED]  app/views/exceptions/not_found.html.erb
     [ADDED]  autotest/discover.rb
     [ADDED]  autotest/merb.rb
     [ADDED]  autotest/merb_rspec.rb
     [ADDED]  config/database.yml
     [ADDED]  config/dependencies.rb
     [ADDED]  config/environments/development.rb
     [ADDED]  config/environments/production.rb
     [ADDED]  config/environments/rake.rb
     [ADDED]  config/environments/staging.rb
     [ADDED]  config/environments/test.rb
     [ADDED]  config/init.rb
     [ADDED]  config/rack.rb
     [ADDED]  config/router.rb
     [ADDED]  public/javascripts/application.js
     [ADDED]  public/stylesheets/master.css
     [ADDED]  merb/merb-auth/setup.rb
     [ADDED]  merb/merb-auth/strategies.rb
     [ADDED]  merb/session/session.rb
     [ADDED]  spec
     [ADDED]  gems
     [ADDED]  app/views/layout/application.html.erb

Por padrão, o Merb vem configurado para utilizar o RSpec como framework de testes e o DataMapper como ORM. Ao contrário do Rails, você pode utilizar outros frameworks transparentemente.

Nota sobre o DataMapper: Se você armazena objetos no Memcache, o DataMapper pode não ser o ORM mais adequado. Acontece que seus objetos não podem passar pelo Marshal, devido à maneira como o DataMapper foi implementado. Para acompanhar os detalhes deste problema, veja este ticket no Lighthouse.

Para iniciar o servidor de desenvolvimento, basta digitar o comando merb à partir da raíz de seu projeto. Ele responderá na porta 4000.

$ merb
Loading init file from /Users/fnando/Sites/contacts/config/init.rb
Loading /Users/fnando/Sites/contacts/config/environments/development.rb
 ~ Connecting to database...
 ~ Loaded slice 'MerbAuthSlicePassword' ...
 ~ Parent pid: 21182
 ~ Compiling routes...
 ~ Activating slice 'MerbAuthSlicePassword' ...
merb : worker (port 4000) ~ Starting Mongrel at port 4000
merb : worker (port 4000) ~ Successfully bound to port 4000

Ao acessar o endereço http://localhost:4000, você verá uma página como esta:

Página inicial do Merb

Agora que já geramos a estrutura de nosso projeto, vamos fazer algo real!

Criando nosso aplicativo

Já deu para perceber que quase todos os artigos de frameworks web envolvem blogs e listas de tarefas? Para provar esta regra, seremos a exceção. Vamos criar um aplicativo onde os usuários podem se cadastrar e adicionar seus contatos. Será uma versão simplificada de uma agenda de contatos.

O primeiro passo é criar a área de registro e autenticação de usuários. O Merb possui um pacote chamado MerbAuth, um framework de autenticação simples, porém completo. Ele permite criar estratégias de autenticação, visto que cada aplicação pode possuir diferentes tipos de autententicação (HTTP, formulário, OpenID, Google Account, etc).

A classe User é criada por padrão quando um aplicativo é gerado. Abra o arquivo "app/models/user.rb".

class User
  include DataMapper::Resource

  property :id,     Serial
  property :login,  String

end

Nossa classe não possui nenhuma validação. Pelo menos é isso que aparenta o código acima. Na verdade, o MerbAuth já faz algumas validações como verificar a presença da senha e se a confirmação de senha está correta. Você provavelmente fará algumas validações adicionais para se certificar que o login é único e possui um tamanho mínimo.

class User
  include DataMapper::Resource

  property :id,     Serial
  property :login,  String

  validates_is_unique :login
  validates_length :login, :within => 3..30
  validates_length :password, :min => 4, :if => :password_required?
end

O MerbAuth também adiciona o método password_required?, que permite verificar se a senha é necessária ou não.

Vamos fazer alguns testes no console. Para abrir o console, execute o comando abaixo:

merb -i

Será preciso instalar o Webrat, que pode ser feito com o comando sudo gem install webrat.

Assim que você estiver no console, execute o comando DataMapper.auto_migrate! para executar a migração de seu banco de dados.

>> DataMapper.auto_migrate!
 ~ DROP TABLE IF EXISTS "sessions"
 ~ DROP TABLE IF EXISTS "users"
 ~ PRAGMA table_info('users')
 ~ SELECT sqlite_version(*)
 ~ CREATE TABLE "users" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "login" VARCHAR(50), "crypted_password" VARCHAR(50), "salt" VARCHAR(50))
 ~ PRAGMA table_info('sessions')
 ~ CREATE TABLE "sessions" ("session_id" VARCHAR(32) NOT NULL, "data" TEXT DEFAULT 'BAh7AA== ', "created_at" DATETIME, PRIMARY KEY("session_id"))
=> [User, Merb::DataMapperSessionStore]

Você também pode executar as migrações rodado a tarefa rake db:automigrate à partir da linha de comando.

Para criar um usuário, você pode chamar o método create, semelhante ao ActiveRecord.

>> User.create :login => 'fnando', :password => 'test', :password_confirmation => 'test'
 ~ SELECT "id" FROM "users" WHERE ("login" = 'fnando') ORDER BY "id" LIMIT 1
 ~ INSERT INTO "users" ("crypted_password", "login", "salt") VALUES ('769bd718b654e5da69a79d1dc34a7ab5f8dea58b', 'fnando', '050f1d293dc86e105ed1e0cdada6b5802e71dc85')
=> #<User id=1 login="fnando" crypted_password="769bd718b654e5da69a79d1dc34a7ab5f8dea58b" salt="050f1d293dc86e105ed1e0cdada6b5802e71dc85">

Ótimo! Já conseguimos criar um usuário através do console. Agora, podemos implementar uma interface para fazermos isso de maneira mais agradável. Vamos criar nosso controller users, que será responsável por esta tarefa. Execute o comando merb-gen controller users.

$ merb-gen controller users
Loading init file from /Users/fnando/Sites/contacts/config/init.rb
Loading /Users/fnando/Sites/contacts/config/environments/development.rb
Generating with controller generator:
Loading init file from /Users/fnando/Sites/contacts/config/init.rb
Loading /Users/fnando/Sites/contacts/config/environments/development.rb
     [ADDED]  app/controllers/users.rb
     [ADDED]  app/views/users/index.html.erb
     [ADDED]  spec/requests/users_spec.rb
     [ADDED]  app/helpers/users_helper.rb

Don't forget to add request/controller tests first.

Vamos alterar nossas rotas, adicionando o recurso (resource) de usuários. Abra o arquivo "config/router.rb" e adicione a linha resources :users.

Merb.logger.info("Compiling routes...")
Merb::Router.prepare do
  resources :users

  slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "")
  default_routes
end

Ao contrário do Rails, o Merb não nomeia os controllers como "_controller.rb". Abra o arquivo "app/controllers/users.rb". Ele é parecido com isto:

class Users < Application
  # ...and remember, everything returned from an action
  # goes to the client...
  def index
    render
  end
end

Como o comentário diz, tudo o que for retornado de uma ação será renderizado ao cliente. Sendo assim, se tivermos uma ação como o exemplo abaixo, a string "Yay! I'm alive!" será exibida. Particularmente, prefiro a maneira como isso é feito no Rails, já que o conteúdo que será renderizado fica explícito (a precedência é do render, depois é a view, etc).

class Users < Application
  def index
    "Yay! I'm alive!"
  end
end

Acesse o endereço "http://localhost:4000/users" e você verá uma página muito simples sendo renderizada.

Página padrão de usuários

Vamos melhorar as coisas. Primeiro, substituia o código do arquivo "app/views/layouts/application.html.erb" pelo abaixo.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <title>Fresh Merb App</title>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <link rel="stylesheet" href="/stylesheets/master.css" type="text/css" media="screen" />
</head>
 
<body>
<div id="page">
  <div id="header">
    <h1>Contacts</h1>
  </div>
 
  <div id="content">
    <div id="main">
      <%= all_messages %>
      <%= catch_content :for_layout %>
    </div>
  </div>
</div>
</body>
</html>

Iremos precisar do helper all_messages, que exibirá todas as mensagens do tipo flash messages. Para adicioná-lo, abra o arquivo "app/helpers/global_helpers.rb".

module Merb
  module GlobalHelpers
    def all_messages
      message.collect {|type, text| tag(:p, text, :class => "message #{type}") }.join
    end
  end
end

Agora, para estilizar a página, substitua o conteúdo do arquivo "public/stylesheets/master.css" por este:

* {
  margin: 0;
  padding: 0;
}

body {
  background: #727272 url(../images/bg.png) repeat-x;
  font-family: "Lucida Grande", Arial;
  font-size: 13px;
  padding: 20px;
}

#page {
  background: #fff;
  border: 1px solid #666;
  margin: 0 auto;
  padding: 20px;
  width: 410px;
  -webkit-box-shadow: 2px 2px #444;
}

h1, h2, h3 {
  font-family: "Arial Rounded MT Bold", Arial;
}

h1 {
  color: #03c;
}

h2 {
  color: #777;
  font-size: 20px;
}

p, ul {
  padding: 10px 0;
}

ul {
  padding-left: 30px;
}

label {
  display: block;
  font-size: 14px;
  padding-bottom: 3px;
}

label span {
  color: #999;
}

input[type=text],
input[type=password] {
  font-size: 18px;
  padding: 5px;
  width: 390px;
}

a {
  color: #03c;
}

a:hover {
  color: #900;
}

p.submit a {
  color: #f00;
  padding: 5px;
}

p.submit a:hover {
  background: #f00;
  color: #fff;
}

/* error message */
div.error {
  background: #fcc url(../images/error.png) no-repeat 15px 15px;
  margin: 15px 0 25px;
  min-height: 50px;
  padding: 15px 15px 15px 70px;
  -webkit-border-radius: 10px;
  -moz-border-radius: 10px;
}

div.error h2 {
  color: #900;
  font-size: 14px;
  padding-bottom: 5px;
}

div.error li {
  font-size: 12px;
  padding-bottom: 5px;
}

div.error ul {
  padding-left: 15px;
}

p.message {
  background-repeat: no-repeat;
  background-position: 20px center;
  font-size: 15px;
  margin: 20px 0;
  padding: 15px 15px 15px 60px;
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
}

p.notice {
  background-color: #E2F9E3;
  background-image: url(../images/ok.png);
  color: #0E6F0E;
}

p.error {
  background-color: #fcc;
  background-image: url(../images/error.png);
  color: #c00;
}

p.warning {
  background-color: #ffc;
  background-image: url(../images/warning.png);
  color: #333;
}

/* highlight */
p.highlight {
  background: #FFE640;
  margin-top: 15px;
  text-align: center;
}

p.highlight a {
  color: #900;
  font-weight: bold;
  font-size: 18px;
}

/* header */
#header {
  position: relative;
}

p.login-info {
  background: #ddd;
  font-size: 10px;
  padding: 5px;
  position: absolute;
  right: 0;
  top: 0;

  -webkit-border-radius: 6px;
  -moz-border-radius: 6px;
}

/* vcard */
#contacts {
  padding-top: 10px;
}

.vcard {
  background: #e9e9e9;
  border: 1px solid #ddd;
  font-size: 11px;
  margin-bottom: 15px;
  padding: 10px 10px 20px 60px;
  position: relative;
  -webkit-border-radius: 10px;
  -moz-border-radius: 10px;
}

img.photo {
  left: 10px;
  position: absolute;
}

.vcard span {
  font-weight: bold;
}

.vcard span.email,
.vcard span.street-address,
.vcard span.organization-name {
  font-weight: normal;
}

.vcard .type:after,
.vcard .label:after {
  content: ":";
}

.vcard .email-group .label {
  content: url(../images/mail.gif);
}

.vcard .adr .type {
  content: url(../images/home.gif);
}

.vcard .mobile .type {
  content: url(../images/mobile.gif);
}

.vcard .phone .type {
  content: url(../images/phone.gif);
}

.vcard .org .label {
  content: url(../images/globe.gif);
}

.vcard h3 {
  font-size: 16px;
}

.vcard div {
  padding-bottom: 3px;
}

.vcard .actions {
  display: none;
  padding: 0;
}

.vcard:hover .actions {
  display: block;
}

.vcard .actions form {
  bottom: 5px;
  cursor: pointer;
  display: inline;
  position: absolute;
  right: 5px;
}

.vcard .actions input {
  background: none;
  border: none;
  color: #333;
  cursor: pointer;
}

.vcard .actions a {
  bottom: 5px;
  color: #333;
  position: absolute;
  right: 50px;
  text-decoration: none;
}

.vcard .actions input,
.vcard .actions a {
  font-size: 11px;
}

.vcard .actions form:before {
  content: " | ";
}

Voltando ao nosso aplicativo… Nós não iremos listar os usuários que se cadastraram em nosso serviço. Renomeie esta ação para new. Nós precisamos instanciar um objeto User. Só que em vez de usar o método render utilizamos um outro, o método display.

class Users < Application
  def new
    @user = User.new
    display @user
  end
end

O Merb tenta seguir o protocolo HTTP até onde pode. Assim, toda URL representa um recurso e deve ser exibido de acordo com a requisição. O método display irá detectar o tipo de resposta esperado (XML, Javascript, JSON, etc.) e automaticamente irá chamar o método correspondente (to_json, to_xml, etc), caso ele tenha sido configurado para responder diferentes tipos de requisição.

Agora é a hora de criar nossa view. Crie o arquivo "app/views/users/new.html.erb" e adicione o conteúdo abaixo. Ele é bastante semelhante a um formulário do Rails.

<h2>Create your account</h2>

<p>
  Signup for a account! It's free. And it's quick.
</p>

<%= error_messages_for @user %>

<%= form_for @user, :action => url(:users) do %>
  <p>
    <%= text_field :login, :label => "Login: " %>
  </p>

  <p>
    <%= password_field :password, :label => "Password: " %>
  </p>

  <p>
    <%= password_field :password_confirmation, :label => "Password Confirmation: " %>
  </p>

  <p class="submit">
    <%= submit "Sign up" %>
  </p>
<% end =%>

Você precisa ficar atento a duas coisas: você deve utilizar abrir o bloco com <%= %> e fechá-lo com <% =%>. Se você não fizer isso, saberá, já que uma exceção referente a sintaxe inválida será lançada.

Temos que criar a ação que receberá os dados enviados pelo usuário. Ela irá se chamar create e também é bem simples e muito parecida com o que fazemos no Rails.

def create(user)
  @user = User.new(user)

  if @user.save
    message[:notice] = "Welcome to Contacts! Please login to access your account."
    redirect url(:session, :new), :message => message
  else
    render :new
  end
end

Uma coisa que você pode estar estranhando é o argumento passado ao método create. A gem merb-action-args é responsável por esta funcionalidade, que retorna para o método os parâmetros enviados na requisição e que estão disponíveis no método params. A única obrigatoriedade é que você precisa utilizar o mesmo nome do parâmetro como nome da variável.

Outra diferença em relação ao Rails, é a forma como uma flash message é definida no Merb; você especifica mensagem no próprio redirecionamento. Note que ao usar o MerbAuth, uma string horrível é enviada na hora do redirecionamento. Será algo como

http://localhost:4000/session/new?_message=BAh7BjoLbm90aWNlIj5XZWxjb21lIHRvI[...]

Ao enviar um formulário com erros, uma tela como esta será exibida:

Formulário com mensagem de erro

Agora, os usuários cadastrados precisam acessar nosso aplicativo. O MerbAuth já possui uma tela de login implementada. Você pode se autenticar no endereço "http://localhost:4000/login". Como você dificilmente vai utilizar a tela padrão, vamos criar nossa própria tela. Para isso, crie um novo arquivo em "app/views/exceptions/unauthorized.html.erb". Coloque o seguinte conteúdo:

<h2>Access your account</h2>

<p>
  If you don't have an account yet, <%= link_to "signup for free!", url(:new_user) %>
</p>

<%= error_messages_for session.authentication %>

<%= form :action => url(:login), :method => :put do %>
  <p>
    <%= text_field :login, :label => "Login: ", :id => 'login' %>
  </p>

  <p>
    <%= password_field :password, :label => "Password: ", :id => 'password' %>
  </p>

  <p class="submit">
    <%= submit "Log me in" %>
  </p>
<% end =%>

Perceba que o método error_messages_for recebe o objeto session.authentication. Ele possui diversos métodos que serão usados ao longo deste artigo.

Quando um usuário não consegue se autenticar, uma mensagem é exibida.

Erro de autenticação

Se você conseguir se autenticar, verá uma mensagem de erro, já que a página que você foi redirecionado ainda não foi criada. Vamos criar o controller home com o comando merb-gen controller home. Altere a view "app/views/home/index.html.erb", adicionando o conteúdo abaixo.

<h2>Welcome to Contacts!</h2>

<p>
  Contacts provides a flexible and convenient way to store contact
  information for family, friends, and colleagues online.
</p>

<ul>
  <li>Keep contact info centralized, sharable, and safe online.</li>
  <li>It's free. And it's simple.</li>
</ul>

<p class="highlight">
  <%= link_to 'Signup for free', url(:new_user) %> or
  <%= link_to 'Access your account', url(:login) %>
  <br/>
  Signup and be using Contacts in less than 30 seconds!
</p>

Precisamos diferenciar usuários logados de não-logados. Vamos exibir uma mensagem com um link para fazer o logout. Altere o layout "app/views/layouts/application.html.erb", adicionando o código abaixo.

<div id="header">
  <h1>Contacts</h1>

  <% if session.authenticated? %>
    <p class="login-info">
      Hi, <%= current_user.login %> |
      <%= link_to 'Logout', url(:logout) %>
    </p>
  <% end %>
</div>

O helper current_user deve ser adicionado ao arquivo "app/controllers/application.rb", tornando o método disponível tanto para o controller quanto para as views. Ao contrário do Rails, você não precisa definir um helper com o método helper_method.

class Application < Merb::Controller
  private
    def current_user
      session.authentication.user
    end
end

A página inicial pode ser modificada para exibir uma mensagem diferente para usuários logados. Altere o arquivo "app/views/home/index.html.erb", adicionando uma condição que verifica a autenticação.

<h2>Welcome to Contacts!</h2>

<p>
  Contacts provides a flexible and convenient way to store contact
  information for family, friends, and colleagues online.
</p>

<ul>
  <li>Keep contact info centralized, sharable, and safe online.</li>
  <li>It's free. And it's simple.</li>
</ul>

<p class="highlight">
  <% if session.authenticated? %>
    <%= link_to 'View your contacts', url(:contacts) %> or
    <%= link_to 'Create one', url(:new_contact) %>
    <br />
    It's so simple to manage your contacts. Check it out!
  <% else %>
    <%= link_to 'Signup for free', url(:new_user) %> or
    <%= link_to 'Access your account', url(:login) %>
    <br/>
    Signup and be using Contacts in less than 30 seconds!
  <% end %>
</p>

Se você tentar acessar a página inicial, verá que ela deixou de funcionar. O erro foi gerado porque não temos um recurso contacts criado e a rota para ele ainda não existe. Abra o arquivo "config/router.rb" e adicione-o.

Merb.logger.info("Compiling routes...")
Merb::Router.prepare do
  resources :users
  resources :session
  resources :contacts

  slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "")

  match('/signup').to(:controller => 'users', :action =>'new')
  match('/').to(:controller => 'home', :action =>'index')

  default_routes
end

Agora, já podemos criar o modelo Contact; execute o comando merb-gen model Contact. Abra o arquivo "app/models/contact.rb" e adicione a estrutura abaixo.

class Contact
  include DataMapper::Resource

  property :id, Serial
  property :name, String
  property :company, String
  property :email, String
  property :phone, String
  property :mobile, String
  property :address, String
end

À partir do terminal, execute o comando rake db:autoupgrade para criar a tabela.

ATENÇÃO: Se você executar o comando rake db:automigrate, as tabelas serão recriadas e os dados apagados! Utilize SEMPRE o comando rake db:autoupgrade quando não quiser recriar suas tabelas. Com certeza rake db:reset seria um nome muito melhor.

Agora, iremos aplicar algumas validações ao nosso modelo.

Em vez de usarmos os métodos validates_*, vamos usar um recurso muito interessante do DataMapper chamado validação implícita. Basta adicionar alguns parâmetros ao método property que a validação será feita automaticamente.

class Contact
  include DataMapper::Resource

  property :id, Serial
  property :name, String, :nullable => false
  property :company, String
  property :email, String, :format => :email_address
  property :phone, String
  property :mobile, String
  property :address, String
end

A opção :format aceita um único formato :email_address; segundo a documentação, mais formatos serão implementados.

Agora, precisamos adicionar os relacionamentos. Um usuário possui muitos contatos, um contato pertence a um usuário. No modelo User devemos adicionar o método has n:

class User
  include DataMapper::Resource

  property :id,     Serial
  property :login,  String

  validates_is_unique :login
  validates_length :login, :within => 3..30
  validates_length :password, :min => 4, :if => :password_required?

  has n, :contacts
end

No modelo Contact devemos adicionar o método belongs_to:

class Contact
  include DataMapper::Resource

  property :id, Serial
  property :name, String, :nullable => false
  property :company, String
  property :email, String, :format => :email_address
  property :phone, String
  property :mobile, String
  property :address, String

  belongs_to :user
end

Novamente, execute o comando rake db:autoupgrade.

Agora que nosso modelo está pronto, podemos partir para o controller. Crie-o com o comando merb-gen controller contacts.

Nossa primeira ação será new, que irá exibir o formulário de contatos.

class Contacts < Application
  def new
    @contact = Contact.new
    display @contact
  end
end

Vamos criar a view que permitirá adicionar novos contatos. Crie o arquivo "app/views/contacts/new.html.erb" e adicione o conteúdo abaixo.

<h2>Create a new contact</h2>

<%= error_messages_for @contact %>

<%= form_for @contact, :action => url(:contacts) do %>
  <%= partial :form %>

  <p class="submit">
    <%= submit "Add contact" %> or
    <%= link_to "Cancel", url(:contacts) %>
  </p>
<% end =%>

No Merb, partials podem ser usadas através do método partial. Ele pode receber um símbolo ou uma string. Se você fornecer um símbolo, o arquivo será procurado no diretório da view corrente. Se você fornecer uma string como "shared/sidebar", pode utilizar outros diretórios. No nosso caso, temos que criar um arquivo em "app/views/contacts/_form.html.erb".

<p>
  <%= text_field :name, :label => "Name: " %>
</p>

<p>
  <%= text_field :email, :label => "E-mail: <span>(optional)</span>" %>
</p>

<p>
  <%= text_field :company, :label => "Company: <span>(optional)</span>" %>
</p>

<p>
  <%= text_field :mobile, :label => "Mobile: <span>(optional)</span>" %>
</p>

<p>
  <%= text_field :phone, :label => "Phone: <span>(optional)</span>" %>
</p>

<p>
  <%= text_field :address, :label => "Address: <span>(optional)</span>" %>
</p>

Nosso formulário será submetido para a ação create.

def create(contact)
  @contact = current_user.contacts.build(contact)

  if @contact.save
    message[:notice] = "A new contact has been added!"
    redirect url(:contacts), :message => message
  else
    render :new
  end
end

Ao acessar o endereço "http://localhost:4000/contacts/new", você verá uma tela como esta:

Tela de novos contatos

Quando você criar um contato, será redirecionado para a listagem de contatos. Como ela ainda não foi criada, você verá a página de erro. Adicione a ação index ao controller.

def index
  @contacts = current_user.contacts.sorted
  display @contacts
end

Na ação index estamos listando todos os contatos do usuário logado, ordenados alfabeticamente. O método sorted é um named scope e deve ser adicionado ao seu modelo Contact; basta adicionar um método de classe com as suas definições.

class Contact
  include DataMapper::Resource

  property :id, Serial
  property :name, String, :nullable => false
  property :company, String
  property :email, String, :format => :email_address
  property :phone, String
  property :mobile, String
  property :address, String

  belongs_to :user

  def self.sorted
    all :order => [:name.asc]
  end
end

O DataMapper adiciona diversos métodos à classe Symbol, permitindo coisas como :name.asc, :title.like, dentre outros.

Agora precisamos criar a view index. Crie o arquivo "app/views/contacts/index.html.erb".

<h2>Your contacts</h2>

<p>
  <%= link_to "Add contact", url(:new_contact) %>
</p>

<div id="contacts">
  <% unless @contacts.empty? %>
    <%= partial :contact, :with => @contacts %>
  <% else %>
    <p>You have no contacts yet!</p>
  <% end %>
</div>

A linha que renderiza a coleção @contacts na partial contact é semelhante a render :partial => 'contact', :collection => @contacts, presente no Rails. Adicione o conteúdo abaixo ao um novo arquivo em "app/views/contacts/_contact.html.erb":

<div class="vcard">
  <%= photo_tag contact.email %>

  <h3 class="n">
    <%= contact.name %>
  </h3>

  <div class="email-group">
    <span class="label">Email</span>
    <span class="email"><%= link_to h(contact.email), "mailto:#{h contact.email}" %></span>
  </div>

  <% unless contact.company.blank? %>
    <div class="org">
      <span class="label">Company</span>
      <span class="organization-name"><%= h contact.company %></span>
    </div>
  <% end %>

  <% unless contact.address.blank? %>
    <div class="adr">
      <span class="type">Home</span>
      <span class="street-address"><%= h contact.address %></span>
    </div>
  <% end %>

  <% unless contact.mobile.blank? %>
    <div class="tel mobile">
      <span class="type">Mobile</span>
      <%= h contact.mobile %>
    </div>
  <% end %>

  <% unless contact.phone.blank? %>
    <div class="tel phone">
      <span class="type">Phone</span>
      <%= h contact.phone %>
    </div>
  <% end %>

  <div class="actions">
    <%= delete_button contact %>
    <%= link_to "Edit", url(:edit_contact, contact) %>
  </div>
</div>

Vamos analisar dois pontos importantes desta partial individualmente. Primeiro, temos um helper photo_tag, que irá montar a URL para o Gravatar à partir do e-mail do contato. Adicione este método ao arquivo "app/helpers/global_helpers.rb".

def photo_tag(email)
  info = [
    Digest::MD5.hexdigest(email), # => hash
    36, # => size
    'http%3A%2F%2Ff.simplesideias.com.br%2Fgravatar.gif' # => default gravatar
  ]

  src = "http://www.gravatar.com/avatar/%s?s=%s&amp;r=g&amp;d=%s" % info

  '<img src="%s" alt="" class="photo" />' % src
end

O outro ponto é o helper delete_button, que irá criar um formulário com método DESTROY para o recurso contact. Ele é semelhante ao button_to do Rails (button_to 'Delete', :method => :destroy).

Pronto! A listagem de contatos já deve estar funcionando. Ela se parece com isto:

Tela de contatos

Agora, temos que adicionar a funcionalidade de editar um contato. Crie a ação edit, que irá retornar o objeto para o formulário.

def edit(id)
  @contact = current_user.contacts.get(id)
  raise NotFound unless @contact

  display @contact
end

Se o contato não for encontrado, a exceção NotFound será lançada e a página de 404 será exibida. O DataMapper não possui o método find; em vez dele, você usar o método get.

Nossa view não será muito diferente da que usamos para criar um novo contato. A única diferença importante é que o formulário será enviado através do método PUT.

<h2>Edit contact</h2>

<%= error_messages_for @contact %>

<%= form_for @contact, :action => url(:contact, @contact), :method => :put do %>
  <%= partial :form %>

  <p class="submit">
    <%= submit "Edit contact" %> or
    <%= link_to "Cancel", url(:contacts) %>
  </p>
<% end =%>

A ação responsável por salvar as alterações no contato é update.

def update(id, contact)
  @contact = current_user.contacts.get(id)
  raise NotFound unless @contact

  if @contact.update_attributes(contact)
    message[:notice] = "The contact has been updated!"
    redirect url(:contacts), :message => message
  else
    render :edit
  end
end

Novamente, buscamos o contato e exibimos a página de 404 se ele não for encontrado. Depois, atualizamos o contato com os dados enviados pelo formulário. Se o formulário não tiver nenhum erro é redirecionado para a listagem; caso contrário, o formulário é novamente renderizado.

Para finalizar, precisamos permitir que contatos sejam removidos. A ação que irá fazer este trabalho é destroy, que como você pode notar, também é simples e muito parecida com as ações edit e update.

def destroy(id)
  @contact = current_user.contacts.get(id)
  raise NotFound unless @contact

  if @contact.destroy
    message[:notice] = "The contact has been removed"
    redirect url(:contacts), :message => message
  else
    raise InternalServerError
  end
end

Não sei se você percebeu, mas ainda não falamos que o recurso contacts só pode ser acessado por usuários autenticados. Se você não estiver logado e tentar listar os contatos, verá uma página de erro.

O MerbAuth permite que você defina áreas restritas de duas maneiras diferentes. Você pode usar o método authenticate diretamente no arquivo "router.rb":

authenticate do
  resources :contacts
end

Ou pode utilizar o método ensure_authenticated no controller:

class Contacts < Application
  before :ensure_authenticated

  # ...
end

A vantagem do primeiro método é que você para o processamento bem antes da execução do filtro ensure_authenticated do controller. Porém, para utilizá-lo, você precisa proteger o recurso inteiro, não podendo especificar ações que são públicas. Sempre que puder utilize o método authenticate.

E é isso! O Contacts está quase pronto. Muitas outras coisas poderiam ser feitas. Você pode, como exercício, adicionar os testes (talvez eu escreva sobre isso em um outro artigo) e criar a página de atualização de senha do usuário logado.

Para finalizar…

O Ruby on Rails é excelente, mas você não deve se prender a um único framework. Conheça o Merb. Experimente o Sinatra, um framework muito rápido se você pretende disponibilizar APIs. Veja outras opções para não ficar limitado. E lembre-se: conhecimento nunca é demais!