+1

Exception in API Rails App using Grape with refactoring Services Object

Mayfest2023

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.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí