ruby-on-rails 在PORO中实施验证

bt1cpqcv  于 2023-02-10  发布在  Ruby
关注(0)|答案(4)|浏览(176)

我尝试在Ruby中实现我自己的验证以供实践。
下面是一个类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_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
ltqd579y

ltqd579y1#

查看ActiveModel,您可以看到在调用validate_presence_of时它并不执行实际的验证。presence.rb.
它实际上通过validates_with为验证器列表(类变量_validators)创建了一个验证器示例;然后在记录示例化期间通过回调调用此验证器列表。参考:带有. rb和验证. 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?

参考文献:

  • presence.rb
  • with.rb
  • validators.rb
  • 类变量验证器
xiozqbni

xiozqbni2#

首先,让我们尝试将验证融入模型中,一旦它开始工作,我们将提取它。
我们的起点是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)。
3bygqnnd

3bygqnnd3#

如果你的目标是模仿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
72qzrwbm

72qzrwbm4#

我不明白在Ruby中实现PORO验证的意义,我宁愿在Rails中而不是Ruby中实现。
假设您有一个Rails项目,为了模拟PORO的活动记录验证,您还需要具备以下3项:

  1. PORO中的某种save示例方法(从中调用验证)。
    1.处理PORO上的CRUD的Rails控制器。
    1.一个带有脚手架即时消息区域的Rails视图。
    提供所有这3个条件,我以如下方式实施PORO确认(为简单起见,仅针对name):
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

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

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

在视图中--只是一个普通的scaffold生成的代码,类似于(或类似的):

<%= form_with(model: poro, local: true) do |form| %>
  <% if poro.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(poro.errors.count, "error") %> prohibited this poro from being saved:</h2>

      <ul>
      <% poro.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name, id: :poro_name %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

就这样。简单点。

相关问题