Rails api how to handle service object exceptions in controller - ruby-on-rails

I wanted use Service Object in my Rails API. Inside my Service Object I want to save Model and return true if saving was successful, but if saving was unsuccessful then I want to return false and send error messages. My Service Object looks like that:
class ModelSaver
include ActiveModel::Validations
def initialize(params)
#params = params
end
def save_model
model ||= Model.new(params)
return false unless model.valid?
model.save!
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
model.errors.add(:base, 'here I want default error message')
false
end
private
attr_reader :params
end
The problem is that I don't know how to send errors messages in response. When I try to send service_object.errors.messages it displays an empty array to me. It looks like that in Controller:
class ModelController < ApplicationController
...
def create
service_object = ModelSaver.new(params)
if service_object.save_model
render json: service_object
else
render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
end
end
...
end
So how can I get Model errors from Service Object inside Controller?

You can solve this by providing methods to your service object that allow returning the model or the errors even after save_model returned with false.
I would change the service object to
class ModelSaver
include ActiveModel::Validations
attr_reader :model
def initialize(params)
#params = params
end
def errors
#model.errors
end
def save_model
#model ||= Model.new(params)
return false unless model.valid?
#model.save!
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
#model.errors.add(:base, 'here I want default error message')
false
end
private
attr_reader :params
end
and the controller method to
def create
service_object = ModelSaver.new(params)
if service_object.save_model
render json: service_object.model
else
render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
end
end

I would refactor the service object so that it just delegates to the underlying model:
class ModelSaver
attr_reader :model
delegate :errors, to: :model
def initialize(**params)
#model = Model.new(**params)
end
def save_model
if model.save
true
else
model.errors.add(:base, 'you forgot to frobnobize the whatchamacallit')
false
end
end
end
class ModelController < ApplicationController
def create
service_object = ModelSaver.new(**params)
if service_object.save_model
render json: service_object
else
render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
end
end
end

Related

How to access #current_user variable defined in ApplicationController inside of Serializer

I'm using Active Model Serializers.
I am trying to access #current_user which is defined inside ApplicationController like this:
class ApplicationController < ActionController::API
before_action :authenticate_request
private
def authenticate_request
auth_header = request.headers['Authorization']
regex = /^Bearer /
auth_header = auth_header.gsub(regex, '') if auth_header
begin
#current_user = AccessToken.get_user_from_token(auth_header)
rescue JWT::ExpiredSignature
return render json: {error: "Token expired"}, status: 401
end
render json: { error: 'Not Authorized' }, status: 401 unless #current_user
end
end
I can use #current_user anywhere I want except inside my ProjectSerializer, which looks like this:
class V1::ProjectSerializer < ActiveModel::Serializer
attributes(:id, :name, :key, :type, :category, :created_at)
attribute :is_favorited
belongs_to :user, key: :lead
def is_favorited
if object.favorited_by.where(user_id: #current_user.id).present?
return true
else
return false
end
end
end
ProjectSerializer is located in my app/ project tree structure:
app/
serializers/
v1/
project_serializer.rb
I get an error trying to access #current_user:
NoMethodError in V1::UsersController#get_current_user
undefined method `id' for nil:NilClass
This happens when I'm calling the function from UserController, which then goes to UserSerializer and then that serializer has has_many :projects field which calls ProjectSerializer.
You can access the variables using instance_options. I believe you can access #current_user in projects controller. For example :
def projects
#projects = Project.all
render_json: #projects, serializer: ProjectSerializer, current_user: #current_user
end
Inside the serializer you can access the current_user like wise:
def is_favorited
if object.favorited_by.where(user_id: #instance_options[:current_user].id).present?
return true
else
return false
end
end

Private Params Method not showing form data

So I am working on creating a playercard, which is basically a profile page for a user. The issue I am having on the backend is my private method playercard_params is only returning user_id, and not all the information inputted into the form...although regular params shows all the data needed to create the playercard. I thought the issue might be on the frontend, but working my way backwards came to the conclusion the issue is here on the backend.
Here is my controller:
class Api::V1::PlayercardController < ApplicationController
before_action :set_user
def index
if params[:user_id]
#playercard = #user.playercard
else
#playercard = Playercard.all
end
render json: #playercard
end
def show
#playercard = Playercard.find(params[:id])
render json: #playercard
end
def create
#playercard = Playercard.new(playercard_params)
binding.pry
if #playercard.save
render json: #user
else
render json: {
error: #playercard.errors.full_messages.to_sentence
}
end
end
def update
#playercard = Playercard.find(params[:id])
if #playercard.update(playercard_params)
render json: #playercard
else
render json: {
error: #playercard.errors.full_messages.to_sentence
}
end
end
private
def playercard_params
params.require(:playercard).permit(:player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player, :user_id)
end
def set_user
#user = User.find(params[:user_id])
end
end
My playercard model:
class Playercard < ApplicationRecord
belongs_to :user
validates :player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player, presence: true
end
and the serializer if that helps:
class PlayercardSerializer < ActiveModel::Serializer
attributes :id, :player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player
belongs_to :user
end
Here are my params:
<ActionController::Parameters {"playerNickname"=>"white mamba", "playerHeightFeet"=>"6", "playerHeightInches"=>"3", "playerAge"=>"30", "playerWeight"=>"170", "playerFavPlayer"=>"Kobe", "user_id"=>"1", "controller"=>"api/v1/playercard", "action"=>"create", "playercard"=><ActionController::Parameters {"user_id"=>1} permitted: false>} permitted: false>
When I submit the form on the front end, I get errors saying each field is empty...in the pry, if I type playercard_params, only user_id shows up (with the correct id)
I solved the issue by lining up the naming convention for the attributes with the front-end and back-end. And it worked!
Thank you #jvillian for the insight!!

How to handle and dry validations in Rails controllers

I am trying to find an elegant way to handle some shared validations between two controllers.
Example:
I have two Accounts controllers. One to process accounts associations to a user synchronously (using, for instance, a PORO that contains the logic for this case), and another for treating the association asynchronously with a worker. Please assume that the logic differs in each scenario and the fact that being sync/async isn't the only difference.
Then I have this two controllers:
module Accounts
class AssociationsController < ApplicationController
def create
return already_associated_account_error if user_has_some_account_associated?
# action = call some account association PORO
render json: action.response, status: action.status_code
end
private
def user_has_some_account_associated?
params[:accounts].any? { |account_number| user_account_associated?(account_number) }
end
def user_account_associated?(account_number)
current_user.accounts.exists?(number: account_number)
end
def already_associated_account_error
render json: 'You already have associated one or more accounts', status: :unprocessable_entity
end
end
end
Now I have another controller in which I'd want to apply the same validation:
module Accounts
class AsyncAssociationsController < ApplicationController
def create
return already_associated_account_error if user_has_some_account_associated?
# Perform asynchronously some account association WORKER
render json: 'Your request is being processed', status: :ok
end
private
def user_has_some_account_associated?
params[:accounts].any? { |account_number| user_account_associated?(account_number) }
end
def user_account_associated?(account_number)
current_user.accounts.exists?(number: account_number)
end
def already_associated_account_error
render json: 'You already have associated one or more accounts', status: :unprocessable_entity
end
end
end
...
HOW and WHERE could I place the validation logic in ONLY ONE SPOT and use it in both controllers? I think in extracting to a concern at first, but I'm not sure if they are intended for this cases of validation logic only.
For this you should use concerns. It's what's they are designed for.
Under the controllers directory make a concerns directory (if it isn't already there) and inside that make the file association_concern.rb with this content:
module AssociationConcern
extend ActiveSupport::Concern
private
def user_has_some_account_associated?
params[:accounts].any? { |account_number| user_account_associated?(account_number) }
end
def user_account_associated?(account_number)
current_user.accounts.exists?(number: account_number)
end
def already_associated_account_error
render json: 'You already have associated one or more accounts', status: :unprocessable_entity
end
end
Anything that is common to the controllers can go in the concern
Then in your controllers simply include AssociationConcern
class AssociationsController < ApplicationController
include AssociationConcern
def create
return already_associated_account_error if user_has_some_account_associated?
# action = call some account association PORO
render json: action.response, status: action.status_code
end
end
Make them inherit from some new controller and add a before_action, like this:
module Accounts
class AbstractAssociationsController < ApplicationController
before_action :check_for_associated_account, only: [:create]
def check_for_associated_account
if user_has_associated_account?
render json: 'You already have associated one or more accounts', status: :unprocessable_entity
end
end
end
end
module Accounts
class AssociationsController < AbstractAssociationsController
def create
# action = call some account association PORO
render json: action.response, status: action.status_code
end
end
end
Then, depending on whether the logic is really different, you can define the user_has_associated_account? in either this abstract controller, separate controllers or delegate it to some PORO class.

Rails 4; Raise an error and catch within controller

I'd love to define a rescue_from handler in my controller to respond to the error.
modudle Api
module V1
class TreesController < Api::V1::ApiController
rescue_from TreeNotFound, with: :missing_tree
def show
#tree = find_tree
end
private
def missing_tree(error)
redirect_to(action: :index, flash: error.message)
end
def find_tree
find_forest.trees.find(params[:id])
rescue ActiveRecord::RecordNotFound
raise TreeNotFound, "Couldn't find a tree to hug"
end
end
end
end
However I got some error Api::V1::TreesController::TreeNotFound.
Any idea?
Update
# api_controller.rb
module Api
module V1
class ApiController < JSONAPI::ResourceController
skip_before_action :verify_authenticity_token # Disable CSRF to enable to function as API
respond_to :json
# NOTE: This block is used when you put unrelated values
rescue_from(ArgumentError) do |e|
render json: { error: e.message }, states: 400 # :bad_request
end
rescue_from(ActionController::ParameterMissing) do |e|
error = {}
error[e.param] = ['parameter is required']
response = { errors: [error] }
render json: response, status: 422 # :unprocessable_entity
end
end
end
end
you need to declare the error class first before you can use it. Do this by inheriting from StandardError.
class TreeNotFound < StandardError
end

Rails custom validate - uniqueness of a property across models

I'm stuck at defining a custom validation method that's purpose is to verify uniqueness of a property across two models
I realize this is bad code, but i wanted to get the test passing before refactor
here is a model with the custom validation to check another model property, error undefined local variable or method `params' (be gentle I'm still trying to figure out RoR)
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models
def uniqueness_of_a_slug_across_models
#sprocket = Sprocket.where(slug: params[:widget_slug]).first
if #sprocket.present?
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
end
end
You don't have access to params in a model. It belongs to controller and view. What you could do is to call custom method in widgets controller (instead of regular save) in order to pass params to a model:
class WidgetsController < ActionController::Base
def create
#widget = Widget.new(widget_params)
if #widget.save_with_slug_validation(params)
redirect_to widgets_path
else
render :new
end
end
end
and define it:
class Widget < ActiveRecord::Base
# ...
def save_with_slug_validation(params)
sprocket = Sprocket.find_by(slug: params[:widget_slug])
if sprocket
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
save
end
end
I didn't test it but it should work.
P.S. Rails 4 style is used.
UPD
I should have tested it, sorry. Please use another approach.
Widgets controller:
# POST /widgets
# POST /widgets.json
def create
#widget = widget.new(widget_params)
#widget.has_sprocket! if Sprocket.find_by(slug: params[:widget_slug])
respond_to do |format|
if #widget.save
format.html { redirect_to [:admin, #widget], notice: 'widget was successfully created.' }
format.json { render action: 'show', status: :created, location: #widget }
else
format.html { render action: 'new' }
format.json { render json: #widget.errors, status: :unprocessable_entity }
end
end
end
Widget model:
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models, if: 'has_sprocket?'
def uniqueness_of_a_slug_across_models
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
def has_sprocket!
#has_sprocket = true
end
def has_sprocket?
!!#has_sprocket
end
end
It would be better to move has_sprocket! and has_sprocket? methods and maybe validation itself to Slugable concern.

Resources