在 Rails 中对非 CRUD 路由实施验证的简洁方法?
Clean way to implement validation on non-crud routes in Rails?
您如何在非 CRUD 路由上实施验证?比如有一个浏览的路由recipes
。它接受参数 term
(用于搜索)、sort
、page
等。
在 Rails 中执行此操作的最巧妙方法是什么?
最直接的解决方案是使用控制器中的一堆 if-else
表达式来验证所有这些东西,但我正在寻找更干净、更容易扩展和维护的东西。
我得到了以下结果
class RequestValidation
attr_reader :validators
attr_reader :errors
def initialize
@validators = []
@errors = []
end
def add_validator(validator)
@validators << validator
end
def validate
@validators.each do |validator|
validator.validate
@errors += validator.errors
end
end
def valid?
validate
errors.empty?
end
end
验证器示例class
class SortValidator < Validator
VALID_SORT_FIELD = [:created_at, :rate, :total_time]
VALID_SORT_DIRECTIONS = [:asc, :desc]
def validate
return unless param
valid = true
begin
sort = param.to_unsafe_h
if sort.size == 1
k = sort.keys[0].to_sym
v = sort.values[0].to_sym
valid = VALID_SORT_FIELD.include?(k) && VALID_SORT_DIRECTIONS.include?(v)
else
valid = false
end
rescue => ex
valid = false
end
@errors << "Sort parameter invalid" unless valid
end
def is_valid?
!errors.empty?
end
end
因此,RequestValidation
就像所有验证器的构建器/容器 class。然后,在控制器中:
validation = RequestValidation.new
validation.add_validator(RecipesValidators::SortValidator.new(params[:sort]))
validation.validate
if validation.errors.empty?
这样,当需要新的验证时,我们只需要创建一个新的验证器class并将其添加到请求验证中。
鉴于我从Rails停了好几年,怀疑这段代码是否干净利落,是否符合Ruby原则。
或者有更好的方法吗?
提前致谢
现在我会在控制器和数据库之间放置一些契约和强类型结构。
类似于:
# obtain an instance of RecipeSearchParamsContract
def contract
@contract ||= SomeValidationContract.new
end
# obtain an instance of a coercing struct
def input
MyStruct.new(params)
end
def query
MyQuery.new
end
def your_action
if contract.call(params).success?
render query.call(input)
end
end
所以你完全分离了所有的关注点。 dry-validator
和 dry-struct
是您编写代码的良好库,您可以使用它们以下列方式生成合约:
class SearchParamsContract < Dry::Validation::Contract
params do
optional(:sort).hash(max_size?: 1) do
optional(:rate).value(included_in?: %w[asc desc])
optional(:created_at).value(included_in?: %w[asc desc])
optional(:total_time).value(included_in?: %w[asc desc])
end
optional(:page).value(:integer)
optional(:term).filled(:string)
end
end
使用这个,强制和验证的结构在结果本身中是可用的,所以你可以将所有链接在一起并使用结构模式匹配来获得很棒的非常 ruby 代码(没有 rails 额外东西)
def search
case contract.call(params)
in Success(input)
@recipes = query.call(input)
else
render :error
end
end
这里的解决方案从来都不是真正的自定义验证器 - 相反,您应该创建一个 class 例如模型或表单对象来封装验证和数据。仅仅因为您的操作不会持久化任何东西并不意味着您不能使用模型来表示数据和业务逻辑。
class RecipeSearch
include ActiveModel::Model
include ActiveModel::Attributes
attribute :query, :string
attribute :sort_by, :string
# ...
validates :query, length: { minimum: 5 }
validates :sort_by, in: ['newest', 'quickest', 'cheapest']
# ...
# @todo apply filters to Recipe and return collection
def perform
# ...
end
end
这让您可以添加验证和逻辑。并将其传递给表单:
<%= form_with(model: local_assigns(:recipe_seach) || RecipeSearch.new) do |f| %>
# ...
<% end %>
搜索不能成为 CRUD 的一部分也是一种误解。毕竟它只是阅读并应用了过滤器。如果您真的需要一个与普通索引不同的视图,那么一定要创建一个单独的搜索操作,但这并不是必需的。
class RecipesController
def index
@recipe_search = RecipeSearch.new(search_params)
@recipes = @recipe_search.perform
end
private
def search_params
params.fetch(:recipe_search, {})
.permit(:query, :sort_by)
end
end
您如何在非 CRUD 路由上实施验证?比如有一个浏览的路由recipes
。它接受参数 term
(用于搜索)、sort
、page
等。
在 Rails 中执行此操作的最巧妙方法是什么?
最直接的解决方案是使用控制器中的一堆 if-else
表达式来验证所有这些东西,但我正在寻找更干净、更容易扩展和维护的东西。
我得到了以下结果
class RequestValidation
attr_reader :validators
attr_reader :errors
def initialize
@validators = []
@errors = []
end
def add_validator(validator)
@validators << validator
end
def validate
@validators.each do |validator|
validator.validate
@errors += validator.errors
end
end
def valid?
validate
errors.empty?
end
end
验证器示例class
class SortValidator < Validator
VALID_SORT_FIELD = [:created_at, :rate, :total_time]
VALID_SORT_DIRECTIONS = [:asc, :desc]
def validate
return unless param
valid = true
begin
sort = param.to_unsafe_h
if sort.size == 1
k = sort.keys[0].to_sym
v = sort.values[0].to_sym
valid = VALID_SORT_FIELD.include?(k) && VALID_SORT_DIRECTIONS.include?(v)
else
valid = false
end
rescue => ex
valid = false
end
@errors << "Sort parameter invalid" unless valid
end
def is_valid?
!errors.empty?
end
end
因此,RequestValidation
就像所有验证器的构建器/容器 class。然后,在控制器中:
validation = RequestValidation.new
validation.add_validator(RecipesValidators::SortValidator.new(params[:sort]))
validation.validate
if validation.errors.empty?
这样,当需要新的验证时,我们只需要创建一个新的验证器class并将其添加到请求验证中。
鉴于我从Rails停了好几年,怀疑这段代码是否干净利落,是否符合Ruby原则。
或者有更好的方法吗?
提前致谢
现在我会在控制器和数据库之间放置一些契约和强类型结构。
类似于:
# obtain an instance of RecipeSearchParamsContract
def contract
@contract ||= SomeValidationContract.new
end
# obtain an instance of a coercing struct
def input
MyStruct.new(params)
end
def query
MyQuery.new
end
def your_action
if contract.call(params).success?
render query.call(input)
end
end
所以你完全分离了所有的关注点。 dry-validator
和 dry-struct
是您编写代码的良好库,您可以使用它们以下列方式生成合约:
class SearchParamsContract < Dry::Validation::Contract
params do
optional(:sort).hash(max_size?: 1) do
optional(:rate).value(included_in?: %w[asc desc])
optional(:created_at).value(included_in?: %w[asc desc])
optional(:total_time).value(included_in?: %w[asc desc])
end
optional(:page).value(:integer)
optional(:term).filled(:string)
end
end
使用这个,强制和验证的结构在结果本身中是可用的,所以你可以将所有链接在一起并使用结构模式匹配来获得很棒的非常 ruby 代码(没有 rails 额外东西)
def search
case contract.call(params)
in Success(input)
@recipes = query.call(input)
else
render :error
end
end
这里的解决方案从来都不是真正的自定义验证器 - 相反,您应该创建一个 class 例如模型或表单对象来封装验证和数据。仅仅因为您的操作不会持久化任何东西并不意味着您不能使用模型来表示数据和业务逻辑。
class RecipeSearch
include ActiveModel::Model
include ActiveModel::Attributes
attribute :query, :string
attribute :sort_by, :string
# ...
validates :query, length: { minimum: 5 }
validates :sort_by, in: ['newest', 'quickest', 'cheapest']
# ...
# @todo apply filters to Recipe and return collection
def perform
# ...
end
end
这让您可以添加验证和逻辑。并将其传递给表单:
<%= form_with(model: local_assigns(:recipe_seach) || RecipeSearch.new) do |f| %>
# ...
<% end %>
搜索不能成为 CRUD 的一部分也是一种误解。毕竟它只是阅读并应用了过滤器。如果您真的需要一个与普通索引不同的视图,那么一定要创建一个单独的搜索操作,但这并不是必需的。
class RecipesController
def index
@recipe_search = RecipeSearch.new(search_params)
@recipes = @recipe_search.perform
end
private
def search_params
params.fetch(:recipe_search, {})
.permit(:query, :sort_by)
end
end