Em um aplicativo no qual estou trabalhando, as pessoas podem deixar comentários em um registro. Quando eles comentam, eles também podem escolher enviar esse comentário por e-mail para seus colegas de trabalho. O cliente queria que o aplicativo validasse esses endereços de e-mail e mostrasse um erro se o comentarista inserisse um endereço de e-mail incorreto.
Os endereços de e-mail não são mantidos. Não há razão para isso, pois eles são usados apenas para enviar o e-mail. Portanto, não existe um modelo ActiveRecord para CommentRecipent
ou algo parecido. Mas a sutileza da validação no estilo ActiveRecord era exatamente o que eu estava procurando. Tudo que eu queria que o controlador de comentários fizesse era algo assim:
def create
...
if comment.save?
# Send emails and notify the commenter of success
else
# notify commenter of problems with comment
end
end
Agora, provavelmente a maneira mais completa de abordar isso seria criar um ou dois objetos Ruby simples e antigos e fazer com que eles passassem nos testes de Lint do ActiveRecord e depois fazer com que usassem a validação do ActiveRecord. Funciona, e já fiz isso no passado. Mas desta vez eu mirei em algo um pouco mais leve. O que eu queria era que o modelo de comentário tivesse estas linhas:
validates_associated :recipients
def recipients
@recipients ||= CommentRecipients.new
end
E então um comentário poderia gerenciar seus destinatários com uma API como:
@comment.recipients.add "someemail@test.com"
@comment.recipients.valid?
@comment.recipients.each do {|recipient| send_email_to(recipient)}
Isso é tudo que eu precisava.
Olhando a documentação validates_associated , vejo que ele eventualmente chama este código:
if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
Então, para meu objeto de destinatários, ele precisa responder para valid? e . Válido? faz sentido, mas , o que é isso? Aqui está a descrição da documentação do Rails:marked_for_destruction?
marked_for_destruction?
Returns whether or not this record will be destroyed as part of the parents save transaction.
Visto que os recipientes nunca são perisistidos, eles nunca são marcados para destruição. Então, posso simplesmente ter esse retorno falso. Então, agora eu sei a maioria dos métodos que meu objeto de destinatários terá que lidar:
- válido?
- marcado para destruição?
- cada
- adicionar
E aqui está uma primeira passagem:
class CommentRecipients
include Enumerable
def add(address)
recipients << address
end
def recipients
@recipients ||= []
end
def each(&block)
@recipients.each do |r|
block.call(r)
end
end
#This method is here just so Comment can use validates_associated
def marked_for_destruction?
false
end
def valid?
end
private
end
Incluir Enumerable me permite fazer isso funcionar como uma coleção. A coleção de destinatários vive em um Array (um Set pode ser melhor, na verdade), que aumenta com o uso add
. Eu defini #each do apenas iterar através da coleção de destinatários e codifiquei para sempre retornar false. A única coisa que falta aqui é válida?marked_for_destruction?
Você poderia fazer isso de duas maneiras. Defina válido para iterar por meio de @recipients e retornar, a false
menos que todos tenham endereços de e-mail válidos. Ou você pode extrair um CommentRecipent para sua própria classe. Eu fiz o último. Todo o CommentRecipient tem de responder a é new
, valid
e fornecer uma maneira de definir o seu endereço:
class CommentRecipient
attr_accessor :address
def initialize(address="")
self.address = address
end
def address=(x)
@address = x.strip
end
def valid?
/^.+@.+$/.match(address)
end
end
Não há muito para explicar lá. A única parte estranha pode ser minha válida? regex, pois é muito menos complexo do que a maioria dos padrões de validação de e-mail. Fui assim depois de ler esta postagem do blog sobre o incômodo da validação de e-mail baseada em regex. Este padrão super-burro é bom o suficiente por enquanto.
A introdução do novo CommentRecipient introduz algumas alterações na minha coleção de destinatários:
class CommentRecipients
include Enumerable
def add(address)
recipients << CommentRecipient.new(address)
end
...
def each(&block)
@recipients.each do |r|
block.call(r.address)
end
end
...
def valid?
invalid_recipients.empty?
end
private
def invalid_recipients
recipients.select {|r| !r.valid?}
end
end
Normalmente, cada definição retornaria cada destinatário, mas eu a faço retornando cada endereço com o propósito de integração com algum outro código que provavelmente deveria ser reescrito.
E isso funciona. Um comentário agora valida seus destinatários e não será salvo se um deles for inválido. E o aplicativo retorna esta mensagem de erro incrível:
Recipient is invalid
Bem, isso é super útil! Vamos transformar isso em algo que um humano possa entender. Eu poderia definir a coleção de erros e configurar as mensagens lá. Ou eu poderia usar a biblioteca de internacionalização do Rails, que é muito mais fácil para este caso simples. Se eu tivesse uma situação em que um Destinatário pudesse ser inválido por vários motivos, eu usaria a abordagem de erros mais robusta. Mas, nesta situação, eu poderia apenas adicionar o seguinte aconfig/locals/en.yml
activerecord:
attributes:
comment:
recipients: "Other recipients"
errors:
models:
comment:
attributes:
recipients:
invalid: "can only contain valid email addresses. Separate multiple addresses with a comma."
E agora temos uma mensagem de validação muito melhor.