« HE:labs
HE:labs

Enums no rails com enumerize

Postado por Marcio Junior em 02/06/2014

É muito comum uma classe ter certos atributos que tem valores predeterminados. Como por exemplo uma fatura pode ter um campo status que pode ter os valores ["pending", "canceled", "paid"].

Como não queremos ter esses valores hardcoded no código, podemos criar constantes representando cada um deles. Assim sempre estaremos atribuindo ou comparando o status com os valores dessas constantes evitando por exemplo status inválidos como "padi".

Para resolver esse problema podemos implementar algo assim:

app/model/invoice.rb:

 1 class Invoice
 2   class Status            
 3     VALUES = [PENDING = "pending", CANCELED = "canceled", PAID = "paid"]
 4 
 5     def self.translate(status)
 6       I18n.t(status, scope: "enums.invoice.status") 
 7     end                   
 8 
 9     def self.form_options 
10       @options ||= VALUES.map do |status|
11         [translate(status), status]    
12       end         
13     end                   
14   end
15 
16   validates :status, inclusion: Status::VALUES
17 end

Como estamos usando o I18n.t precisamos adicionar a tradução de cada status, nesse exemplo ele fica assim:

config/locales/app.pt-BR.yml:

1 pt-BR:                    
2    # outras traduções
3    enums:                  
4      invoice:              
5        status:             
6          pending: pendente 
7          canceled: cancelada
8          paid: paga

Para verificarmos o status de uma instância invoice podemos usar as constantes declaradas em Invoice::Status

1 if invoice.status == Invoice::Status::PENDING
2   # faz algo com a fatura pendente
3 end

E para traduzir um determinado status usamos o Invoice::Status.translate

1 invoice.status = Invoice::Status::PAID
2   Invoice::Status.translate(invoice.status) # "paga"

No nosso formulário usamos o Invoice::Status.form_options, para exibir os labels dos selects traduzidos.

app/views/invoices/_form.html.slim:

1 = simple_form_for(@invoice) do |f|
2    = f.error_notification
3 
4    .form-inputs
5      / outros campos
6      = f.input :status, collection: Invoice::Status.form_options
7 
8    .form-action
9      = f.button :submit

Isso funciona bem, mas para cada campo que precisarmos que seja uma enumeração temos que ficar criando estruturas como essa. E nada disso é referente a regras de negócio, apenas estamos gastando tempo criando enumerações, que geram linhas de código que geram manutenção.

Toda vez que me vejo fazendo coisas como essa penso: "Com certeza deve existir algo que já resolva esse problema". Então comecei a pesquisar no github uma gem que pudesse ajudar com isso. Foi quando achei a enumerize

Usando ela podemos fazer uma limpa na nossa classe Invoice deixando ela assim:

app/model/invoice.rb:

1 class Invoice
2     extend Enumerize
3     enumerize :status, in: [:pending, :canceled, :paid]
4   end

Essa gem adiciona alguns métodos pra resolverem os problemas de tradução e opções em formulários. Podemos substituir o Invoice::Status.form_options criado anteriormente pelo Invoice.status.options gerado pela gem.

app/views/invoices/_form.html.slim:

1 = simple_form_for(@invoice) do |f|
2    = f.error_notification
3 
4    .form-inputs
5      / outros campos
6      = f.input :status, collection: Invoice.status.options
7 
8    .form-action
9      = f.button :submit

Para perguntar se a fatura está em um determinado status, podemos usar o nome do status com um ? no final, o qual é bem mais ruby like do que a feita anteriormente.

1 invoice.status = :canceled
2   invoice.status.canceled? # true
3   invoice.status.paid? # false

E para pegar a sua tradução simplesmente fazemos um status.text:

1 invoice.status = :canceled
2   invoice.status.text # "cancelada"

Os testes ficam bem mais legíveis por causa do matcher que a gem nos oferece:

app/specs/models/invoice_spec.rb:

1 describe Invoice do
2     it { should enumerize(:status).in(:pending, :canceled, :paid) }
3   end

Você pode conferir a lista completa de todas as funcionalidades visitando o repositório no github

Rails 4.1

Como enumerações vinham sendo muito requisitadas, o rails 4.1 adicionou o ActiveRecord::Enum, que vai disponibilizar pra você um método de classe chamado enum, pelo qual você poderá criar uma enumeração de forma parecida com o enumerize. Usando ele no nosso exemplo fica assim:

1 class Invoice
2     enum status: [:pending, :canceled, :paid]
3   end

Conclusão

Quando se vir brigando com coisas repetitivas, dê uma pesquisada no github se esse problema já não foi resolvido. A comunidade ruby é famosa por se unir pra resolver problemas comuns. Não reinvente a roda!

Compartilhe

Sabia que nosso blog agora está no Medium? Confira Aqui!