在PORO中实施验证

时间:2017-07-10 01:40:05

标签: ruby-on-rails ruby validation rubygems

我正在尝试在Ruby中实现我自己的验证。

这是一个有{2}验证的班级Item,我需要在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_ofvalidates_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

4 个答案:

答案 0 :(得分:1)

查看ActiveModel,您可以看到在调用validate_presence_of时它没有进行实际验证。参考:presence.rb

它实际上通过_validators创建了一个Validator实例到一个验证器列表(它是一个类变量validates_with);然后在记录的实例化过程中通过回调调用此验证器列表。参考: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 :(得分: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)。

答案 2 :(得分: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

答案 3 :(得分:0)

我不明白在Ruby中实现PORO验证的意义。我会在Rails中而不是在Ruby中做到这一点。

因此,假设您有一个Rails项目。为了模仿您的PORO的Active Record验证,您还需要具备以下三点:

  1. 您的PORO中有一种<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <div class="tab-wrapper"> <button class="add-tab">Add tab</button> <div class="tab-links"></div> <div class="tab-panels"></div> </div> <div class="tab-wrapper"> <p>Example of a second tab on the page, with a nested tab.</p> <button class="add-tab">Add tab</button> <div class="tab-links"> <h6 class='tab-header' tab-index='nested-tab'> Nested Tab </h6> </div> <div class="tab-panels"> <div class='tab' tab-index='nested-tab'> <div class="tab-wrapper"> <button class="add-tab">Add tab</button> <div class="tab-links"></div> <div class="tab-panels"></div> </div> </div> </div> </div>实例方法(用于调用验证)。

  2. 在PORO上处理CRUD的Rails控制器。

  3. 带有支架Flash消息区域的Rails视图。

通过提供所有这三个条件,我以这种方式实现了PORO验证(为简单起见,仅适用于save

name

在您的控制器中,您必须手动处理异常(因为您的PORO在ActiveRecord之外):

require_relative "base_class"

class Item < BaseClass
  attr_accessor :price, :name

  include ActiveModel::Validations

  class MyValidator
    def initialize(attrs, record)
      @attrs = attrs
      @record = record
    end

    def validate!
      if @attrs['name'].blank?
        @record.errors[:name] << 'can\'t be blank.'
      end

      raise ActiveRecord::RecordInvalid.new(@record) unless @record.errors[:name].blank?
    end
  end

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

  # your PORO save method
  def update_attributes(attrs)
    MyValidator.new(attrs, self).validate!
    #...actual update code here
    save
  end
end

在视图中-只是一个通用的脚手架生成的代码。这样的东西(或类似的东西):

class PorosController < ApplicationController
  rescue_from ActiveRecord::RecordInvalid do |exception|
    redirect_to :back, alert: exception.message
  end
...
end

就是这样。只需保持简单即可。