Viblo Code
+4

Custom Rails Validator

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ứ hailà để 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:

  1. http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations

  2. http://womanonrails.com/custom-rails-validators

  3. http://blog.honeybadger.io/ruby_unicode_normalization/

                                                      END
    

All Rights Reserved