2017-07-10 60 views
1

我試圖在Ruby中實現我自己的驗證。在PORO中實現驗證

這裏是一個類Item有2次驗證,這是我需要在BaseClass來實現:

require_relative "base_class" 

class Item < BaseClass 
    attr_accessor :price, :name 

    def initialize(attributes = {}) 
    @price = attributes[:price] 
    @name = attributes[:name] 
    end 

    validates_presence_of :name 
    validates_numericality_of :price 
end 

我的問題是:的驗證validates_presence_of,並validates_numericality_of將是類方法。我如何訪問實例對象來驗證這些類方法中的名稱和價格數據?

class BaseClass 
    attr_accessor :errors 

    def initialize 
    @errors = [] 
    end 

    def valid? 
    @errors.empty? 
    end 

    class << self 
    def validates_presence_of(attribute) 
     begin 
     # HERE IS THE PROBLEM, self HERE IS THE CLASS NOT THE INSTANCE! 
     data = self.send(attribute) 
     if data.empty? 
      @errors << ["#{attribute} can't be blank"] 
     end 
     rescue 
     end 
    end 

    def validates_numericality_of(attribute) 
     begin 
     data = self.send(attribute) 
     if data.empty? || !data.integer? 
      @valid = false 
      @errors << ["#{attribute} must be number"] 
     end 
     rescue 
     end 
    end 
    end 
end 

回答

1

望着加載ActiveModel,你可以看到,它不會做實際的驗證時validate_presence_of被調用。參考:presence.rb

它實際上通過validates_with創建一個Validator實例到驗證器列表(這是一個類變量_validators);然後在記錄的實例化過程中通過回調來調用驗證器列表。參考:with.rbvalidations.rb

我做了上述的簡化版本,但它與ActiveModel相信的相似。 (跳過回調和所有)

class PresenceValidator 
    attr_reader :attributes 

    def initialize(*attributes) 
    @attributes = attributes 
    end 

    def validate(record) 
    begin 
     @attributes.each do |attribute| 
     data = record.send(attribute) 
     if data.nil? || data.empty? 
      record.errors << ["#{attribute} can't be blank"] 
     end 
     end 
    rescue 
    end 
    end 
end 

class BaseClass 
    attr_accessor :errors 

    def initialize 
    @errors = [] 
    end 
end 

編輯:像什麼SimpleLime指出,驗證的名單將跨越共享,如果他們是在基類中,它會導致所有項目共享屬性(如果屬性集合有任何不同,將顯然失敗)。

他們可以被提取出來一個單獨的module Validations和包括,但我已經留在他們在這個答案。

require_relative "base_class" 

class Item < BaseClass 
    attr_accessor :price, :name 
    @@_validators = [] 

    def initialize(attributes = {}) 
    super() 
    @price = attributes[:price] 
    @name = attributes[:name] 
    end 

    def self.validates_presence_of(attribute) 
    @@_validators << PresenceValidator.new(attribute) 
    end 

    validates_presence_of :name 

    def valid? 
    @@_validators.each do |v| 
     v.validate(self) 
    end 

    @errors.empty? 
    end 
end 

p Item.new(name: 'asdf', price: 2).valid? 
p Item.new(price: 2).valid? 

參考文獻:

+1

請注意,'@@ _ validators'在這裏被'BaseClass'的所有子類共享......例如,在你的'attributes'中添加'puts'#{attribute}'。每個循環在驗證器中,然後創建一個新的'class Order

+0

是的,我想。在ActiveModel中,'_validators'是每個單獨類的一部分(無論如何都被提取到一個模塊中)。去更新答案。 –

0

首先,讓我們試着有驗證比克編入模型。一旦它工作,我們會解壓縮它。

我們的出發點是Item沒有任何形式的驗證:

class Item 
    attr_accessor :name, :price 

    def initialize(name: nil, price: nil) 
    @name = name 
    @price = price 
    end 
end 

我們將添加一個方法Item#validate這會返回一個代表錯誤的消息字符串數組。如果模型有效,則數組將爲空。

class Item 
    attr_accessor :name, :price 

    def initialize(name: nil, price: nil) 
    @name = name 
    @price = price 
    end 

    def validate 
    validators.flat_map do |validator| 
     validator.run(self) 
    end 
    end 

    private 

    def validators 
    [] 
    end 
end 

驗證模型意味着遍歷所有關聯的驗證器,在模型上運行它們並收集結果。注意我們提供了一個Item#validators的虛擬實現,它返回一個空數組。

驗證程序是一個響應#run並返回錯誤數組(如果有的話)的對象。我們定義NumberValidator,驗證給定的屬性是否爲Numeric的實例。這個類的每個實例都負責驗證單個參數。我們需要通過屬性名稱的驗證器的構造,使之意識到其屬性來驗證:

class NumberValidator 
    def initialize(attribute) 
    @attribute = attribute 
    end 

    def run(model) 
    unless model.public_send(@attribute).is_a?(Numeric) 
     ["#{@attribute} should be an instance of Numeric"] 
    end 
    end 
end 

如果我們從Item#validators返回此驗證,並設置price"foo"預期它會工作。

讓我們將驗證相關方法提取到模塊。

module Validation 
    def validate 
    validators.flat_map do |validator| 
     validator.run(self) 
    end 
    end 

    private 

    def validators 
    [NumberValidator.new(:price)] 
    end 
end 

class Item 
    include Validation 

    # ... 
end 

驗證器應該在每個模型基礎來定義。爲了跟蹤它們,我們將在模型類上定義一個類實例變量@validators。它只需通過爲給定模型指定的驗證程序數組即可。我們需要一些元編程來實現這一點。

當我們將任何模型包含到類中時,included在模型上被調用,並接收模型作爲參數包含的類。我們可以使用這種方法在包含時間定製類。我們將使用#class_eval這樣做:

module Validation 
    def self.included(klass) 
    klass.class_eval do 
     # Define a class instance variable on the model class. 
     @validators = [NumberValidator.new(:price)] 

     def self.validators 
     @validators 
     end 
    end 
    end 

    def validate 
    validators.flat_map do |validator| 
     validator.run(self) 
    end 
    end 

    def validators 
    # The validators are defined on the class so we need to delegate. 
    self.class.validators 
    end 
end 

我們需要一種方法來驗證添加到模型。讓我們Validation在模型類中定義add_validator:現在

module Validation 
    def self.included(klass) 
    klass.class_eval do 
     @validators = [] 

     # ... 

     def self.add_validator(validator) 
     @validators << validator 
     end 
    end 
    end 

    # ... 
end 

,我們可以做到以下幾點:

class Item 
    include Validation 

    attr_accessor :name, :price 

    add_validator NumberValidator.new(:price) 

    def initialize(name: nil, price: nil) 
    @name = name 
    @price = price 
    end 
end 

這應該是一個很好的起點。有很多你可以進一步增強:

  • 更多驗證器。
  • 可配置的驗證器。
  • 有條件的驗證器。
  • 驗證器的DSL(例如validate_presence_of)。
  • 自動驗證器發現(例如,如果您定義了FooValidator,您將自動能夠呼叫validate_foo)。
+0

如果在模型中調用'validate:name,presence:true'並在未調用時跳過驗證,那麼如何實現'save'方法來執行驗證? – jedi

0

如果你的目標是模仿ActiveRecord,其他答案你有覆蓋。但是,如果你真的想專注於一個簡單的PORO,那麼你可能會重新考慮的類方法:

class Item < BaseClass 
    attr_accessor :price, :name 

    def initialize(attributes = {}) 
    @price = attributes[:price] 
    @name = attributes[:name] 
    end 

    # validators are defined in BaseClass and are expected to return 
    # an error message if the attribute is invalid 
    def valid? 
    errors = [ 
     validates_presence_of(name), 
     validates_numericality_of(price) 
    ] 
    errors.compact.none? 
    end 
end 

如果您需要訪問的錯誤之後,你需要儲存它們:

class Item < BaseClass 
    attr_reader :errors 

    # ... 

    def valid? 
    @errors = { 
     name: [validates_presence_of(name)].compact, 
     price: [validates_numericality_of(price)].compact 
    } 
    @errors.values.flatten.compact.any? 
    end 
end