我正在尝试在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_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
答案 0 :(得分:1)
查看ActiveModel,您可以看到在调用validate_presence_of
时它没有进行实际验证。参考:presence.rb。
它实际上通过_validators
创建了一个Validator实例到一个验证器列表(它是一个类变量validates_with
);然后在记录的实例化过程中通过回调调用此验证器列表。参考:with.rb和validations.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
这应该是一个很好的起点。您可以进行许多进一步的改进:
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验证,您还需要具备以下三点:
您的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>
实例方法(用于调用验证)。
在PORO上处理CRUD的Rails控制器。
带有支架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
就是这样。只需保持简单即可。