- vừa được xem lúc

Exception in API Rails App using Grape with refactoring Services Object

0 0 20

Người đăng: Phong Nguyễn Trung

Theo Viblo Asia

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: sendactivationemailactivate.

# 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 ActivateEmailServiceActivationEmailSenderService).

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.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tổng hợp gem thông dụng trong Rails - Phần 2: Faker và Friendly Id

Ở phần này mình sẽ giới thiệu 2 gems rất quen thuộc với Ruby developers và hơn nữa, mình nghĩ đây cũng là những gem sẽ giúp ích cho các bạn newbies trong quá trình học Ruby on Rails. I. Gem Faker. 1.

0 0 96

- vừa được xem lúc

Gem paper_trail

1. Introduce. . Gem paper_trail được dùng để tracking sự thay đổi của model object và chia thành các version khác nhau.

0 0 40

- vừa được xem lúc

How to drop a table in Ruby

Issue. Với những dự án mới hoặc những dự án nhỏ tự làm, hầu hết bạn sẽ phải thường xuyên thay đổi cấu trúc DB dẫn tới việc phải thực hiện các câu lệnh như rails db:drop db:create db:migrate khá nhiều lần.

0 0 29

- vừa được xem lúc

Một số tips giúp ứng dụng Ruby on Rails bảo mật hơn

Tổng quan. Các Web framework ra đời giúp cho các lập trình viên phát triển các ứng dụng web một cách nhanh chóng và tiện lợi.

0 0 94

- vừa được xem lúc

6 vòng lặp Ruby nâng cao

. Ruby là ngôn ngữ với nguyên tắc "tối ưu hóa cho nhà phát triển". Đó là một ngôn ngữ dễ đọc, dễ viết, tích hợp, mẫu và nguyên mẫu cùng với các lợi ích khác.

0 0 94

- vừa được xem lúc

Rails bundler

1. Introduce. . Một trong những command được sử dụng nhiều nhất khi làm việc với Rails có lẽ là bundle install =)).

0 0 39