2016-10-25 53 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將返回多個錯誤代碼。