2016-10-25 31 views
4

我在Rails 4中编写了一个非常标准的CRUD RESTful API。虽然我在错误处理方面做得很少。将ActiveRecord验证错误转换为API消耗性错误

想象我有以下型号:

class Book < ActiveRecord::Base 
    validates :title, presence: true 
end 

如果我尝试没有一个头衔,我会得到下面的错误,以创建一个book对象:

{ 
    "title": [ 
    "can't be blank" 
    ] 
} 

ActiveRecord的验证被设计成与表单一起使用。理想情况下,我希望将每个可读验证错误与API消费者可以使用的常量进行匹配。因此,像:

{ 
    "title": [ 
    "can't be blank" 
    ], 
    "error_code": "TITLE_ERROR" 
} 

这既可以用来显示用户面对错误(“称号不能为空”),并可以在其他代码中使用(if response.error_code === TITLE_ERROR ...)。 Rails中有没有这样的工具?

编辑:这是very similar question from Rails 2 days

+0

你可以有一些控制器代码。它是API和你的应用程序请求相同的代码吗?因为您向我们展示了您对模型的验证,但我认为您应该在控制器上处理您的问题。 –

回答

6

error_codes.yml定义标准的API错误,包括status_codetitledetails和内部code那么你可以用它来提供有关您的API文档的错误进一步信息。

这里有一个基本的例子:

api: 
    invalid_resource: 
    code: '1' 
    status: '400' 
    title: 'Bad Request' 

not_found: 
    code: '2' 
    status: '404' 
    title: 'Not Found' 
    details: 'Resource not found.' 

配置/初始化/ api_errors.rb载荷YAML文件转换成一个常数。

API_ERRORS = YAML.load_file(Rails.root.join('doc','error-codes.yml'))['api'] 

app/controllers/concerns/error_handling。RB定义一个可重用的方法来呈现的JSON格式的API错误:

module ErrorHandling 
    def respond_with_error(error, invalid_resource = nil) 
    error = API_ERRORS[error] 
    error['details'] = invalid_resource.errors.full_messages if invalid_resource 
    render json: error, status: error['status'] 
    end 
end 

在您的API基本控制器包括关注所以它适用于所有从它继承控制器:

include ErrorHandling 

你然后将能够在任何这些控制器上使用您的方法:

respond_with_error('not_found') # For standard API errors 
respond_with_error('invalid_resource', @user) # For invalid resources 

例如在您的用户控制器上,您可能有以下内容:

def create 
    if @user.save(your_api_params) 
    # Do whatever your API needs to do 
    else 
    respond_with_error('invalid_resource', @user) 
    end 
end 

的错误,你的API将输出看起来就像这样:

# For invalid resources 
{ 
    "code": "1", 
    "status": "400", 
    "title": "Bad Request", 
    "details": [ 
    "Email format is incorrect" 
    ] 
} 

# For standard API errors 
{ 
    "code": "2", 
    "status": "404", 
    "title": "Not Found", 
    "details": "Route not found." 
} 

当你的API的增长,你就可以轻松地在你的YAML文件,此添加新的错误代码,并利用它们方法避免重复,并使您的错误代码在整个API中保持一致。

+0

这是一个可爱的方法谢谢。 – EightyEight

+0

很高兴你喜欢它!你介意接受答案吗? :) – rebagliatte

+0

我仍在思考如何将其与验证代码结婚。 – EightyEight

1

你创建方法看起来应该仅仅是这样的:

def create 
    book = Book.new(book_params) 
    if user.save 
    render json: book, status: 201 
    else 
    render json: { errors: book.errors, error_code: "TITLE_ERROR" }, status: 422 
    end 
end 

这将返回JSON看起来像你问什么,只是“标题”和“ERROR_CODE”将被嵌套在“误差范围内。 “我希望处理的不是一个大问题。

+0

我想概括一下,以便验证错误以某种方式对应于错误代码。想象一下,我添加了一堆其他字段,“作者”,“isbn”,“价格”。 – EightyEight

+0

您可以根据需要构造散列,甚至可以嵌套它。 'render json:{author:book.author,somemore_stuff:{isbn:12345,price:“$ 55”},error_code:“坏东西”,a_thing:a_thing_variable}它会按照您的期望转换为json。 – baron816

1

您只有两种实现方法:您为验证器编写代码(在验证过程中将测试错误的组件)或者编写渲染器。

我假设你知道如何编写渲染器,因为@ baron816的回答是暗示,并且做一些DRY到以某种方式推广它。

让我带你通过验证的技术:

让我们为您的错误代码的存储,我打电话给他们custom_error_codes,我想你可以一次,所以我会用设置有多个错误代码一个Array(你改变,否则)。

创建模式关注

module ErrorCodesConcern 
    extend ActiveSupport::Concern 

    included do 
    # storage for the error codes 
    attr_reader :custom_error_codes 
    # reset error codes storage when validation process starts 
    before_validation :clear_error_codes 
    end 

    # default value so the variable is not empty when accessed improperly 
    def custom_error_codes 
    @custom_error_codes ||= [] 
    end 

    private 
    def clear_error_codes 
    @custom_error_codes = [] 
    end 
end 

然后关注添加到您的模型

class MyModel < ActiveRecord::Base 
    include ErrorCodesConcern 
    ... 
end 

2-让我们破解验证加入的错误代码标记。首先,我们需要查看验证器源代码,它们位于(activemodel-gem-path)/ lib/active_model/validations/中。

应用目录下创建一个验证目录,然后创建下面的验证

class CustomPresenceValidator < ActiveModel::Validations::PresenceValidator 
    # this method is copied from the original validator 
    def validate_each(record, attr_name, value) 
    if value.blank? 
     record.errors.add(attr_name, :blank, options) 
     # Those lines are our customization where we add the error code to the model 
     error_code = "#{attr_name.upcase}_ERROR" 
     record.custom_error_codes << error_code unless record.custom_error_codes.include? error_code 
    end 
    end 
end 

然后使用我们的自定义验证我们的模型

class Book < ActiveRecord::Base 
    validates :title, custom_presence: true 
end 

3-所以你必须修改您的代码使用的所有rails验证器并创建渲染器(请参阅@ baron816的答案)和respon与模型的custom_error_codes值。

+0

谢谢,这更接近我试图实现的目标。有没有办法重新使用现有的验证器,而无需手动扩展它们?手动扩展它将会花费太多精力。 – EightyEight

+1

也许你可以尝试破解(基于同样的原理)'ActiveModel :: Errors#add',这似乎减少了短期的努力,但我个人觉得从长远来看会破解验证器。而他们只是其中的一小部分,应该花几分钟时间来实现。 – Benj

1

这似乎是你不考虑多个验证错误。

在您的示例中,Book模型只有一个验证,但其他模型可能有更多的验证。

我的回答包含占多重验证,并只使用该模型发现的第一个验证错误的另一个解决方案第一溶液

解决方案1 ​​ - 处理多个验证

在你的ApplicationController添加此

# Handle validation errors 
rescue_from ActiveRecord::RecordInvalid do |exception| 
    messages = exception.record.errors.messages 
    messages[:error_codes] = messages.map {|k,v| k.to_s.upcase << "_ERROR" } 
    render json: messages, status: 422 
end 

请注意,error_codes在这种情况下是一个允许多个错误代码的数组。例如:

{ 
    "title": [ 
    "can't be blank" 
    ], 
    "author": [ 
    "can't be blank" 
    ], 
    "error_codes": ["TITLE_ERROR", "AUTHOR_ERROR"] 
} 

解决方案2 - 处理只有第一个验证错误

如果你真的想保持只有一个验证错误,用这个来代替

# Handle validation errors 
rescue_from ActiveRecord::RecordInvalid do |exception| 
    key = exception.record.errors.messages.keys[0] 
    msg = exception.record.errors.messages[key] 
    render json: { key => msg, :error_code => key.to_s.upcase << "_ERROR" }, status: 422 
end 

,这将给你一个回应像

{ 
    "title": [ 
    "can't be blank" 
    ], 
    "error_code": "TITLE_ERROR" 
} 

即使当你有多个错误

1

试试这个:

book = Book.new(book_params) 
if user.save 
    render json: book, status: 201 
else 
    render json: { 
      errors: book.errors, 
      error_codes: book.errors.keys.map { |f| f.upcase + "_ERROR" } 
     }, 
     status: 422 
end 

error_codes将返回多个错误代码。