ActiveModel自定义属性类型能否实现类似ActiveRecord的自动类型验证?
可以实现自定义属性类型的自动验证
完全可以在ActiveModel中为自定义query_param类型实现类似ActiveRecord内置类型的自动验证逻辑,核心思路是将验证规则与属性类型绑定,通过基类自动注入验证逻辑,避免重复代码。以下是具体实现方案:
1. 扩展自定义属性类型,内置验证逻辑
在QueryParamType中添加验证方法,根据accepts参数定义的规则判断值是否合法:
class QueryParamType < ActiveModel::Type::Value attr_reader :accepts def initialize(accepts: nil) @accepts = accepts super() end def type = :query_param # 根据accepts规则验证值的合法性 def valid?(value) return true if value.nil? # 可根据需求调整是否允许空值 case @accepts when :time !parse_time(value).nil? when :string value.is_a?(String) when Class # 支持单个对象或对象数组 value.is_a?(@accepts) || (value.is_a?(Array) && value.all? { |v| v.is_a?(@accepts) }) when Array if @accepts.first.is_a?(Hash) # 处理枚举值规则,比如[{ string: Comment::ALL_TYPE_TAGS }] enum_list = @accepts.first.values.first value.is_a?(Array) && value.all? { |v| enum_list.include?(v) } else # 处理对象数组规则,比如[Comment] klass = @accepts.first value.is_a?(Array) && value.all? { |v| v.is_a?(klass) } end when Hash # 处理嵌套规则,比如{ type: :string, id: AbstractModel } value.is_a?(Hash) && valid_subvalue(value[:type], @accepts[:type]) && valid_subvalue(value[:id], @accepts[:id]) else true # 默认通过,或抛出异常提示未处理的规则类型 end end private def parse_time(value) Time.zone.parse(value) rescue nil end # 递归验证嵌套属性值 def valid_subvalue(value, accepts_rule) self.class.new(accepts: accepts_rule).valid?(value) end end
2. 编写通用验证器,关联属性类型
创建一个通用验证器,调用自定义类型的valid?方法完成验证:
class QueryParamValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) attr_type = record.class.attribute_types[attribute.to_s] return unless attr_type.is_a?(QueryParamType) unless attr_type.valid?(value) record.errors.add(attribute, :invalid_query_param, message: "不符合#{attr_type.accepts}规则") end end end
3. 在基类中自动注入验证逻辑
在你的查询模型基类(Query::Base)中添加逻辑,自动为所有query_param类型的属性绑定验证器:
class Query::Base include ActiveModel::API include ActiveModel::Attributes include ActiveModel::Validations::Callbacks # 子类继承时自动处理验证绑定 def self.inherited(subclass) super subclass.class_eval do attribute_types.each do |attr_name, type| validates attr_name.to_sym, query_param: true if type.is_a?(QueryParamType) end end end end
4. 分离预处理与验证逻辑
将原来的clean_and_validate_params拆分为预处理和自动验证两部分,职责更清晰:
class Query::Comments < Query::Base include Query::Modules::Validation # 属性定义保持不变 attribute :created_at, :query_param, accepts: [:time] attribute :updated_at, :query_param, accepts: [:time] attribute :id_in_set, :query_param, accepts: [Comment] attribute :by_users, :query_param, accepts: [User] # ... 其他属性 ... before_validation :clean_params private def clean_params # 处理时间格式为标准ISO8601 self.created_at = Time.zone.parse(created_at)&.iso8601 if created_at.present? self.updated_at = Time.zone.parse(updated_at)&.iso8601 if updated_at.present? # 将User对象转换为ID数组 self.by_users = by_users.map { |u| u.is_a?(User) ? u.id : u }.compact if by_users.present? # 其他属性的预处理逻辑... end end
这种实现方式将验证逻辑与属性类型深度绑定,无需在每个模型中手动编写验证规则,完全达到了类似ActiveRecord内置类型自动验证的效果,同时保留了自定义accepts规则的灵活性。
内容的提问来源于stack exchange,提问作者nimmolo
相关产品推荐
相关产品推荐

