Chia sẻ một số validator hữu dụng trong Rails
Bài đăng này đã không được cập nhật trong 7 năm
Validate là một bước hết sức quan trọng để kiểm tra tính hợp lệ của dữ liệu trước khi được lưu xuống database. Có rất nhiều cách để validate dữ liệu trước khi nó được lưu vào database của bạn, bao gồm cả việc ràng buộc dữ liệu ở model, validate ở phía client và validate ở tầng controller. Đối với Rails, Rails tổ chức việc validate dữ liệu ở tầng model để đảm bảo tính hiệu quả trong hầu hết các trường hợp. Rails cung cấp cho người dùng một loạt các phương thức (validation helpers), bạn có thể sử dụng trực tiếp các phương thức này ngay trong model. Dù phong phú là vậy nhưng trong một số trường hợp nhất định hoặc khi làm việc với một số khách hàng "kỳ quái" với những yêu cầu oái ăm, sẽ có những trường hợp validate mà Rails chưa hỗ trợ. Những lúc đó hoặc là tìm đến các gem hoặc xui hơn là bạn phải tự định nghĩa một validator chuyên biệt cho trường hợp của mình.
Bài viết sẽ không đề cập đến phần cơ bản về validations trong Ruby on Rails mà sẽ xem như bạn đọc đã nắm vững được cách thức hoạt động cơ bản của model validator trong Rails. Mình sẽ giới thiệu qua một số validator mà mình đã viết và hướng dẫn cách sử dụng chúng như thế nào.
1. DateValidator - Validate ngày giờ
Giả sử mình có một model Event
, trong đó có 2 trường thời gian bắt đầu (start_time
) và thời gian kết thúc (end_time
), cả 2 đều có kiểu dữ liệu là datetime
.
Quá dễ hiểu là end_time
phải nằm sau (hoặc bằng :LOL) start_time
. Trong trường hợp này cần 1 validator về thời gian, something like this:
validates :end_time, date: {after: :start_time}
# or
validates :start_time, date: {before_or_equal_to: :end_time}
Trên ý tưởng đó, mình sẽ xây dựng một validator để xác thực end_time
phải nằm sau start_time
như sau:
# app/validators/date_validator.rb
class DateValidator < ActiveModel::EachValidator
def validate_each record, attribute, value
return if value.blank?
criteria = options.keys.first
comparation_field = parse_date record, options
return unless comparation_field[:value]
case criteria
when :after
unless value > record.try(comparation_field[:value])
message = options[:message] || :after
record.errors.add attribute, message, date: comparation_field[:field_name]
end
when :after_or_equal_to
unless value >= record.try(comparation_field[:value])
message = options[:message] || :after_or_equal_to
record.errors.add attribute, message, date: comparation_field[:field_name]
end
end
protected
def parse_date record, options
comparation_field = options.values.first
comparation_field = begin
{
value: comparation_field.to_datetime,
field_name: comparation_field
}
rescue NoMethodError, ArgumentError
{
value: record.try comparation_field,
field_name: record.class.human_attribute_name comparation_field
}
end
end
end
end
Tương tự cho validate before
và before_or_equal_to
các bạn có thể tự viết thêm 2 case đó vào.
Sử dụng validator như sau:
class Event < ApplicationRecord
validates :end_time, date: {after: :start_time}
# Hoặc:
validates :end_time, date: {after: "30/04/2017"}
# Hoặc nếu muốn override error message, chỉ cần truyền thêm option[:message]
validates :end_time, date: {after: "30/04/2017", message: " phải nằm sau 30/04/2017"}
end
message = options[:message] || :after
message = options[:message] || :after_or_equal_to
Nếu có ai thắc mắc 2 dòng này là gì thì mình sẽ giải thích như sau: after
và after_or_equal_to
là 2 key được định nghĩa trong file locale chứa message lỗi mặc định nếu người dùng không override message option (Cái này tự định nghĩa trước chứ i18n
không có sẵn đâu nhá).
en:
errors:
messages:
after: " must be after %{date}."
after_or_equal_to: " must be after or equal to %{date}."
Như vậy trong 3 trường hợp ví dụ nêu trên, 3 message lỗi tương ứng được bắn ra khi dữ liệu không hợp lệ sẽ là:
- Start time must be after End time.
- Start time must be after or equal to 30/04/2017.
- Start time phải nằm sau 30/04/2017.
2. PlainTextLengthValidator - Validate độ dài string trong đoạn mã HTML
Đây là một tình huống phổ biến nếu như trong project của bạn có sử dụng một text editor, chuỗi mà bạn sẽ lưu vào DB là một chuỗi lẫn lộn với các thẻ HTML. Do đó nếu dùng LengthValidator
sẽ không hoạt động được.
Trong trường hợp này, ý tưởng là bạn sẽ phải đọc text này và chỉ lấy phần text bên trong các thẻ HTML. May mắn cho chúng ta là gem Nokogiri làm việc này. Để cài đặt gem bạn vui lòng ghé github của gem và làm theo hướng dẫn.
Phần code sẽ như bên dưới:
# app/validators/plain_text_validator.rb
class PlainTextLengthValidator < ActiveModel::EachValidator
def validate_each record, attribute, value
return if value.blank?
message = options[:message]
html_text = record.try(attribute)&.gsub /[\n\t\r]/, ""
txt_length = Nokogiri::HTML(html_text).text.size
message = options[:message]
case true
when options[:maximum].present? && txt_length > options[:maximum]
message ||= :too_long
record.errors.add attribute, message, count: options[:maximum]
when options[:minimum].present? && txt_length < options[:minimum]
message ||= :too_short
record.errors.add attribute, message, count: options[:minimum]
when options[:within].present? && !txt_length.between? options[:within].first, options[:within].last
message ||= :invalid_length
record.errors.add attribute, message, min: options[:within].first, max: options[:within].last
end
end
end
# Nhớ định nghĩa :invalid_length trong file ngôn ngữ của i18n, đường dẫn tương tự phần 1.
Ví dụ sử dụng với model Article
, có trường content chứa nội dung của bài viết
class Article < ActiveRecord
validates :content, plain_text_length: {maximum: 1000}
# Hoặc
validates :content, plain_text_length: {within: 300..1000, message: :wrong_length}
end
Messageslỗi tương ứng:
- Content must be 1000 charactorsor less.
- Content phải nằm trong khoảng 300 - 1000 ký tự.
Trong đó file ngôn ngữ của i18n như sau:
en:
errors:
messages:
too_long: " must be %{count} charactorsor less."
too_short: " must be %{count} or more."
invalid_length: " must be within %{min} to %{max} charactors."
wrong_length: " phải nằm trong khoảng %{min} - %{max} ký tự."
3. ImageValidator - Validate kích thước file ảnh
Thật ra đây là cái validator thừa thải nhất mình từng viết ra. Thực tế sử dụng gem carrierwave
và mini_magick
hoàn toàn có thể xử lý resize ảnh trước khi lưu, thì người dùng thoải mái upload ảnh mọi kích thước đều có thể đưa về một size tối đa.
Tuy nhiên khách hàng là thượng đế, thích thì chiều thôi
# app/validators/image_validator.rb
class ImageValidator < ActiveModel::EachValidator
def validate_each record, attribute, value
return unless record.send("#{attribute}_changed?") || record.send("#{attribute}?")
max_width = options[:max_width]
max_height = options[:max_height]
message = options[:message]
begin
image = MiniMagick::Image.open record.image.path
if image[:width] > max_width || image[:height] > max_height
message ||= :invalid_image_size
record.errors.add attribute, message, size: "#{max_width}x#{max_height}"
end
rescue
end
end
end
Để sử dụng được validator này cần phải cài gem MiniMagick
, hướng dẫn cài đã có sẵn tại github của gem, mình không hướng dẫn lại.
Sử dụng:
class User < ActiveRecord
validates :avatar, image: {max_width: 1024, max_height: 768}
end
Kết luận
Trên đây là một số validator được mình viết ra trong suốt quá trình làm dự án. Có thể còn nhiều thiếu sót hoặc nhiều chỗ cần được cải thiện thêm, mong nhận được góp ý từ các bạn đọc bằng cách để lại comment phía dưới bài viết.
All rights reserved