Custom Rails Validator
Bài đăng này đã không được cập nhật trong 3 năm
Vừa rồi mình có gặp một yêu cầu là khi tạo name cho group thi không được có các từ bậy bạ, các từ thô tục trong đó. Nên mình đã tìm hiểu và viết một cái validation cho riêng trường hợp này.
1. Các class kế thừa khi tạo 1 class validator
ActiveModel::Validator || ActiveModel::EachValidator
ActiveModel::Validator thì sẽ phải hiện thực function validate(record) nhận param là 1 record, và mình thực hiện validate trên record này. Khi hiện thực xong thì mình gọi validate bằng hàm validates_with
.
Ví dụ:
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
_
ActiveModel::EachValidator thì phổ biến hơn, mình sẽ phải hiện thực hàm validate_each với 3 param (record, attribute, and value), đối với cách này thì mình có thể chỉ định attribute cần validate và biết được value của attribute đó. Và khi hiện thực xong mình có thể validate bằng method validates
.
Ví dụ:
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
class Person < ApplicationRecord
validates :email, presence: true, email: true
end
2. Custom Validators
Sau khi mình tìm hiểu 1 chút về các class Validate thì quay trờ lại với yêu cầu đầu bài: khi tạo name cho group thi không được có các từ bậy bạ, các từ thô tục trong đó. Yêu cầu ở đây có attribute là field name và value của nó không được chứa các từ bậy bạ, chắc mình sẽ dùng class ActiveModel::EachValidator
Mình sẽ tạo class ở trong folder app/validators, nếu chưa có thì mình tạo folder validators
# app/validators/blacklist_validator.rb
# Validate list of words that can not be use in name field
class BlacklistValidator < ActiveModel::EachValidator
def validate_each record, attribute, value
converted_value = value.unicode_normalize(:nfkc).downcase
plain_text = Nokogiri::HTML(converted_value).text
BlackWord.pluck(:word).each do |word|
if plain_text.include? word
message = options[:message] || :words_in_blacklist
record.errors.add attribute, message, word: plain_text
break
end
end
end
end
Ở trên các "black words" mình được lưu trong bảng BlackWord của database, Nếu bạn muốn lưu các "black words" này vào file và đọc lên để validate thì bạn có thể tham khảo cách làm của trang sau đây: http://womanonrails.com/custom-rails-validators
Mình sẽ nói 1 chút về 2 hàng này
1. converted_value = value.unicode_normalize(:nfkc).downcase
2. plain_text = Nokogiri::HTML(converted_value).text
Hàng thứ nhất
để mình tránh các dùng các kí tự unicode để qua mặt compare thì mình dùng hàm unicode_normalize với Unicode normalization là NFKC để convert các kí tự tiếng Nhật 1 byte (Hankaku - 半角) thành 2 bytes (Zenkaku - 全角). Hình như nó còn tác dụng khác mà mình không biết
Hàng thứ hai
là để lấy ra cái plain text cho chính xác, ở trường title hay name chắc không cần, nhưng nếu nội dung của meno hay bài viết gì đấy có editor lưu theo dạng HTML thi cần.
3. Ứng dụng
Chỉ cần khai báo như thế này là xong.
class Group < ApplicationRecord
validates :name, blacklist: true
end
Nếu muốn có message đi kèm thì thêm
class Group < ApplicationRecord
validates :name, blacklist: {message: I18n.t ("custom_message")}
end
_ Demo cái cho sinh động: _
Bonus: Chỉ validate khi có value input.
Trường hợp mình thường thấy là khi check validate thi mọi người hay validate nhiều điều kiện chung 1 scope validates. Kiểu như thế này
validates :name, presence: {message: :company_name_blank},
uniqueness: {message: :company_name_exists},
format: {with: Settings.alphabetnumber_with_whitespace, message: :company_name_invalid}
Nên mỗi khi không có nhập gì hết thì mình sẽ bị 3 hay 4 messages cùng lúc. Theo mình như vậy cũng tốt, người dùng nhìn vào là biết tất cả điều kiện ràng buộc ngay. Nhưng đôi lúc mình chỉ muốn không nhập thì chỉ hiện thị message là "Xin hãy nhập vào ô abc" gì đó thôi, thì mình làm sao.
Mình sẽ tách validates ở trên thành 2 nhóm validates và thêm điều kiện ràng buộc cho nhóm validates thứ 2 là "Nếu ở ô input có giá trị thì check validates thứ 2 này"
Ta chỉnh lại như sau:
validates :name, presence: {message: :company_name_blank}, nickname: true
validates :name, uniqueness: {message: :company_name_exists},
format: {with: Settings.alphabetnumber_with_whitespace,
message: :company_name_invalid}, if: -> {name.present?}
"Nếu ở ô input có giá trị thì check validates thứ 2 này" => if: -> {name.present?} Tức là nếu field Name có nhập thì hãy check các validates ở scope thứ 2 này. _
Tham khảo:
All rights reserved