Exception in API Rails App using Grape with refactoring Services Object
Tóm tắt
Các bước thực hiện refactoring Services Object trong API Rails APP viết bằng Grape
- Tạo service
- Xử lý exception, bắt lỗi
- Sử dụng service vào API
Nội dung
Sau khi hoàn thiện API bằng gem Grape thì mình đã refactor lại code với Service Object, tuy nhiên mình đã gặp một số vấn đề xử lý exception. Dưới đây là đoạn code của mình trước khi refactor, mình có 2 API: sendactivationemail và activate.
# This module is responsible for API with version 1.
module V1
# This class is responsible for Account Activation API with version 1.
class AccountActivation < Grape::API
resources :users do
desc 'Send activation account email', {
success: [{ code: 200, message: 'Send activation account email successfully' }],
failure: [{ code: 400, message: 'Account has been activated' },
{ code: 404, message: 'Not found User with email address' }]
}
params do
requires :email, regexp: URI::MailTo::EMAIL_REGEXP
end
get '/send_activation_email' do
user = User.find_by(email: params[:email])
error!('Not found User with email address', 404) unless user
error!('Account has activated', 400) if user.activate?
user.update!({ activation_digest: User.generate_unique_secure_token, activated_at: Time.zone.now })
user.send_activation_email
end
desc 'Activated the current user', {
success: [{ code: 200, message: 'Activated account successfully' }],
failure: [{ code: 400, message: 'Account has been activated' },
{ code: 401, message: 'Failed to activate the current user' }, { code: 404, message: 'Not Found' }]
}
params do
requires :activation_digest, type: String
end
get '/activate' do
user = User.find_by(activation_digest: params[:activation_digest])
error!('Not found User! This link is not available now!', 400) unless user
if user.activate_account_expired?
error!('Activation Email has expired! Please try again in your email address', 410)
end
if user.activate?
error!('Account has activated', 400)
else
user.activate!
end
present user, with: Entities::V1::UserFormat
end
end
end
end
Đi refactor lại code
Tiếp theo mình tiến hành chia nhỏ các đoạn code trong API thành các Service để call lại. Mình tạo một thư mục services trong folder app , trong folder services tiếp tục tạo ra 3 file, cấu trúc thư mục như sau:
--- app
---------- services
+ application_service.rb -> lớp cha cho các service khác
+ activation_email_sender_service.rb -> service cho action send email trong API send_activation_email
+ activate_email_service.rb -> service cho action activate account trong API activate
Rồi, trong các file sẽ có gì? File application_service.rb
# This class is responsible for general service
class ApplicationService
def self.call(*args)
new(*args).call
end
end
*File activation_email_sender_service.rb *
# This class is responsible for sending activation email
class ActivationEmailSenderService < ApplicationService
def initialize(user)
super()
@user = user
end
def call
send_activation_email
end
private
def send_activation_email
** raise NotFoundError.new('Not found User with email address', 404) unless @user**
** raise ActivatedError.new('Account has activated', 400) if @user.activate?**
@user.update!({ activation_digest: User.generate_unique_secure_token, activated_at: Time.zone.now })
UserMailer.account_activation(@user).deliver_now
end
end
File activate_email_service.rb
# This class is responsible for activating account
class ActivateEmailService < ApplicationService
def initialize(params)
super()
@activation_digest = params[:activation_digest]
end
def call
activate_email
end
private
def activate_email
user = User.find_by(activation_digest: @activation_digest)
** raise NotFoundError.new('Not found User! This link is not available now', 404) unless user**
if user.activate_account_expired?
** raise GoneError.new('Activation Email has expired! Please try again in your email address', 410)**
end
** raise ActivatedError.new('Account has activated', 400) if user.activate?**
user.activate!
user
end
end
Vì sao mình lại thiết kế code như vậy?
Đầu tiên, dựa theo nguyên tắc DRY (Don't Repeat Yourself) thì mình định nghĩa một lớp cha ApplicationService có hàm class function call truyền vào args và trong thân hàm gọi đến new(\args).call sẽ tạo object và gọi hàm call trong trong class (cụ thể ở đây là 2 class con ActivateEmailService và ActivationEmailSenderService).
Thứ hai, chú ý cho việc refactor là một Service Object có 1 public method và có nhiều private method, public method ở đây của mình là function call. Ví dụ trong file activate_email_service.rb, mình có một hàm call gọi tới private function activate_email. Mọi thứ xử lý logic mình bỏ trong private function đó hết.
Thứ ba, ở đây xuất hiện một số class mới như NotFoundError, ActivatedError,... *(mình để trong cặp dấu ** trong code) * mà mình call để raise exception. Đây là cách xử lý khi mình chuyển từ function error! trong Grape. Mình để code dưới đây để cho các bạn dễ so sánh hì.
get '/activate' do
user = User.find_by(activation_digest: params[:activation_digest])
**error!('Not found User! This link is not available now!', 400) unless user**
if user.activate_account_expired?
** error!('Activation Email has expired! Please try again in your email address', 410)**
end
if user.activate?
**error!('Account has activated', 400)**
else
user.activate!
end
present user, with: Entities::V1::UserFormat
end
end
def activate_email
user = User.find_by(activation_digest: @activation_digest)
** raise NotFoundError.new('Not found User! This link is not available now', 404) unless user**
if user.activate_account_expired?
** raise GoneError.new('Activation Email has expired! Please try again in your email address', 410)**
end
** raise ActivatedError.new('Account has activated', 400) if user.activate?**
user.activate!
user
end
end
Tương quan code trong cặp dấu ** là cách mình xử lý exception. Lúc đầu mình tưởng rằng bê qua sẽ để i chang như vậy, ai mà có dè function error! là built-in method của class Grape::API, nên mình đâu dùng được. Vậy là mình phải đi build thêm mấy cái error đó nữa.
Mình đi tạo thêm folder errors trong folder app rồi tạo các file.rb trong trỏng. Cấu trúc thư mục như sau:
--- app
---------- services
---------- errors
+ application_error.rb
+ not_found_error.rb
+ activated_error.rb
+ inactivated_error.rb
+ gone_error.rb
Trong File application_error.rb, đây class cha cho mấy class error kia. Class này mình thừa kế từ StandardError class để từ nữa mình rescue lỗi (có 2 loại là StandardError vs Exception, mà Exception nó bắt hết nên không nên xài, tham khảo: StandardError vs Exception File application_error.rb
# This class is responsible for generating error
class ApplicationError < StandardError
attr_reader :message, :code
def initialize(message = 'Bad Request', code = 400)
@message = message
@code = code
super(message)
end
end
File not_found_error.rb
# This class is responsible 404 error
class NotFoundError < ApplicationError
end
Mấy class con kia tương tự he.
Nãy giờ là mình mới raise lỗi, giờ là lúc rescue lỗi trong API. Mình thêm dòng lệnh dưới đây vào các class API là được.
rescue_from :all do |e|
error!({ error: e.class, message: e.message }, e.code)
end
Cuối cùng, mình đi gọi các service của mình trong các API như sau:
ActivationEmailSenderService.call(user)
Kết quả như sau
Trong các class hiện thực API bằng Grape
# This module is responsible for API with version 1.
module V1
# This class is responsible for Account Activation API with version 1.
class AccountActivation < Grape::API
rescue_from :all do |e|
error!({ error: e.class, message: e.message }, e.code)
end
resources :users do
desc 'Send activation account email', {
success: [{ code: 200, message: 'Send activation account email successfully' }],
failure: [{ code: 400, message: 'Account has been activated' },
{ code: 404, message: 'Not found User with email address' }]
}
params do
requires :email, regexp: URI::MailTo::EMAIL_REGEXP
end
get '/send_activation_email' do
user = User.find_by(email: params[:email])
ActivationEmailSenderService.call(user)
end
desc 'Activated the current user', {
success: [{ code: 200, message: 'Activated account successfully' }],
failure: [{ code: 400, message: 'Account has been activated' },
{ code: 401, message: 'Failed to activate the current user' }, { code: 404, message: 'Not Found' }]
}
params do
requires :activation_digest, type: String
end
get '/activate' do
user = ActivateEmailService.call(params)
present user, with: Entities::V1::UserFormat
end
end
end
end
Tổng kết
Đó là một số vấn đề mình gặp phải khi refactor code. Hy vọng bài viết sẽ giúp ích được cho bạn.
All rights reserved