Wrapping Rails controller logic within if statements - ruby-on-rails

I am trying to wrap my controller logic within the same conditional. Not sure how to achieve this as I am a bit new to ruby on rails. The pseudo code below is what I am trying to achieve
class Api::BaseController
include Api::AuthConcern if SAME_CONDITION
before_action -> { some_function() } if SAME_CONDITION
if SAME_CONDITION
rescue_from Api::AuthConcern::Unauthenticated do |e|
render status: 401, json: { error: e.error }
end
rescue_from Api::AuthConcern::PermissionDenied do |e|
render status: 403, json: { error: e.error }
end
end
end

You ain't gonna need it. Just use methods instead:
module Api
# Avoid adding "Concern" to your module names
# it provides absolutely no information about what the object does
module AuthConcern
extend ActiveSupport::Concern
included do
rescue_from Unauthenticated { |e| respond_to_unauthenticated(e.error) }
rescue_from PermissionDenied { |e| respond_to_permission_denied(e.error) }
end
def respond_to_unauthenticated(message)
render status: 401, json: { error: message }
end
def respond_to_permission_denied(message)
render status: 403, json: { error: message }
end
end
end
This lets any class that includes the module customize the behavior by simply overriding the method:
module Api
class FooController < BaseController
# ...
private
def respond_to_unauthenticated(error)
render plain: 'Oh noes, you broke it!'
end
end
end
If you need to add more logic to to how a module augments the class with you can use the macro pattern. This is simply a class method that adds methods or behavior to the class:
module Api
module AuthConcern
extend ActiveSupport::Concern
# ....
module ClassMethods
def authenticate!(**kwargs)
before_action :authenticate!, **kwargs
end
end
end
end
module Api
class FooController < BaseController
authenticate! except: :index
end
end
This pattern is found all over Ruby in everything from attr_accessor to Rails callbacks and assocations.

Related

rescue_from handlers inside two different modules

I try to create facade module, for module that do some generic exceptions handling, with additional logic.
For examle, first module:
module GenericErrorHandler
extend ActiveSupport::Concern
included do
rescue_from Sequel::NoMatchingRow do |e|
render json: {code: 404, error: e}
end
rescue_from StandardError do |e|
render json: {code: 500, error: e}
end
end
end
And second module with logging:
module ErrorLogger
extend ActiveSupport::Concern
include GenericErrorHandler
included do
rescue_from StandardError do |e|
puts "logged error #{e.to_s}"
raise
end
end
end
When I include ErrorLogger in class that raises StandardException, only handler from ErrorLogger was called. But I expect, that handler from GenericErrorHandler must be called too because of raise in ErrorLogger handler. Is this possible to achieve such behavior? (looks like rescue_from from other module rewrites handlers after module inclusion)
Do you consider using methods instead of blocks?
module GenericErrorHandler
extend ActiveSupport::Concern
def handle_standard_error(e)
render json: {code: 500, error: e}
end
included do
rescue_from Sequel::NoMatchingRow do |e|
render json: {code: 404, error: e}
end
rescue_from StandardError, with: :handle_standard_error
end
end
module ErrorLogger
include GenericErrorHandler
def handle_standard_error(e)
puts "logged error #{e.to_s}"
super
end
end

Pundit gem error class overriding my custom error class in ruby

my application has custom error classes defined lib/error/*
example
#lib/error/custom_error.rb
module Error
class CustomError < StandardError
attr_accessor :error, :status, :message
def initialize error, status, message
#error = error || :unprocessable_entity
#status = status || 422
#message = message || 'Something went wrong'
end
# this is just an example
def raise_unauth_error
raise "un auth"
end
end
end
this has been used in a ton of controller just by including it like
class Api::UsersController < Api::ApiBaseController
include Error::CustomError
def set_user
.
.
.
raise Error::CustomError.new('sample message', 404, ""?) unless #user
end
def user_check
raise_unauth_error unless #user.admin?
end
end
I recently added pundit gem to my application
class Api::ApiBaseController < ActionController::Api
include Pundit
end
now I am getting errors saying Pundit::Error::CustomError (NameError), from everywhere. it goes away if I included the error class like this ::Error::CustomError but if I have to do this there are a ton of places where I have to make this edit
is there a way to include pundit gem without overriding the custom error class?
It's a known issue in the Pundit repository, and as noted in the discussion there, the only way around it (currently) is through using ::Error::CustomError for now.

Pundit: how to handle multiple error codes for one unauthorized action?

I use pundit to handle my API policies, I have an item show that can be forbidden to user in some cases, and in other cases just restricted. By restricted I mean it's forbidden now, but if he pays he could access it then. So I need my API to respond with a specific code (402 Payment Required) so the client can invite the user to pay in order to unlock the show.
This is my current code, it only respond with 403 when pundit returns false.
Where would it be best to implement a condition to return 403 OR 402 in order to be DRY and clean?
class Api::V1::ItemController < Api::V1::BaseController
def show
#item = Item.find(params[:id])
authorize #item
end
end
class ItemPolicy < ApplicationPolicy
def show?
return true if record.public?
# 403 will be generated, that's ok.
return false if !record.band.members.include?(user)
# If that condition is false I want to generate a 402 error at the end, not a 403.
user.premium?
end
end
class Api::V1::BaseController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized(_exception)
# Here I've got the exception with :policy, :record and :query,
# also I can access :current_user so I could go for a condition,
# but that would include duplicated code from ItemPolicy#show?.
render json: { error: { message: "Access denied" } }, status: :forbidden
end
end
Unfortunately Pundit cannot handle different error types out of the box. And it is built to always expect the policy's methods to return true or false false. Therefore raising another custom error and rescuing from that in the controller will not work, because it would break view methods too.
I suggest a workaround to introduce different error types. Something like this might work:
# in the policy
class ItemPolicy < ApplicationPolicy
def show?
return true if record.public?
return false unless record.band.members.include?(user)
if user.premium?
true
else
Current.specific_response_error_code = :payment_required
false
end
end
end
# in the controller
class Api::V1::BaseController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized(_exception)
case Current.specific_response_error_code
when :payment_required
render json: { error: { message: "Premium required" } }, status: :payment_required
else
render json: { error: { message: "Access denied" } }, status: :forbidden
end
end
end
I would not consider using the global CurrentAttributes a good practice but they are part of Rails and in this case, using this global data store avoids overriding pundit internals.
You might want to read the API docs about CurrentAttributes.
create Response module in app/controllers/concerns/response.rb
module Response
def json_response(object, status = :ok)
render json: object, status: status
end
end
create ExceptionHandler in app/controllers/concerns/exception_handler.rb
module ExceptionHandler
extend ActiveSupport::Concern
included do
rescue_from Pundit::NotAuthorizedError, with: :unauthorized_request
end
private
# JSON response with message; Status code 401 - Unauthorized
def unauthorized_request(e)
json_response({ message: e.message }, :unauthorized)
end
end
in app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
end
THAT'S IT

What is the Rails 4 way to deal with filters?

Background: I have a few filters which have to be available through every CRUD page on the system: search, livesearch, column sorting and pagination;
This is what I currently have:
.lib/record_filters.rb
module RecordFilters
##valid_directions = %w[asc desc]
def search_for(record)
record.present? ? where('name LIKE ?', record+"%") : all
end
def records_matching(search_term)
where('name LIKE ?', search_term+"%").map(&:name)
end
def order_records_by(attribute, direction)
order(sort_table_by(attribute) +" "+ sort_records_order_by(direction))
end
private
def sort_table_by(attribute)
column_names.include?(attribute) ? attribute : "name"
end
def sort_records_order_by(direction)
##valid_directions.include?(direction) ? direction : "asc"
end
end
./app/models/ticket_type.rb
class TicketType < ActiveRecord::Base
include RecordFilters
validates_presence_of :name
validates_uniqueness_of :name
end
./app/controllers/ticket_types_controller.rb
class TicketTypesController < ApplicationController
before_action :set_ticket_type, only: [:show, :edit, :update, :destroy]
def index
#ticket_types = TicketType.search_for(params[:search]).order_records_by(params[:sort], params[:direction]).paginate(per_page: 12, page: params[:page])
respond_to do |format|
format.html
format.js
format.json { render json: TicketType.records_matching(params[:term]) }
end
end
...
end
./config/application.rb
...
config.autoload_paths << "#{Rails.root}/lib"
The problem: Upon accessing the index on the browser, Rails returns NoMethodError for search_for
Question: What is the Rails Way to implement such filters? What am I doing wrong?
Thanks!
This is because Ruby's include will add the module's methods as instance methods:
module A
def value
5
end
end
class B
include A
end
puts B.new.a # prints 5
puts B.a # fails
If you want them as class methods, like the class object itself was extended, use extend:
method A
def value
5
end
end
class C
extend A
end
puts C.a # prints 5
puts C.new.a # fails
You can also, if you really want include, define some new methods in the module's included callback:
module A
def self.included(mod)
# mod is whatever (module or class) included A.
# in this case, it's B.
mod.class_eval do
def self.value
"class"
end
# there's no reason to do this here, instead of
# in the module itself, but just for demonstration purposes:
def inst
"inst"
end
end
end
end
class B
include A
end
puts B.value # prints "class"
puts B.new.inst # prints "inst"
Your methods are getting added as instance methods, but you're calling them like class methods, I'd recommend you look into http://api.rubyonrails.org/classes/ActiveSupport/Concern.html to implement the concern pattern for your models.

Rails shared controller actions

I am having trouble building a controller concern. I would like the concern to extend the classes available actions.
Given I have the controller 'SamplesController'
class SamplesController < ApplicationController
include Searchable
perform_search_on(Sample, handle: [ClothingType, Company, Collection, Color])
end
I include the module 'Searchable'
module Searchable
extend ActiveSupport::Concern
module ClassMethods
def perform_search_on(klass, associations = {})
.............
end
def filter
respond_to do |format|
format.json { render 'api/search/filters.json' }
end
end
end
end
and, despite setting up a route i get the error 'The action 'filter' could not be found for SamplesController'.
I thought it might be to do with wether I include, or extend the module. I tried using extend but that also gave the same error.
I still need to be able to feed the module some configuration options on a per controller basis. Is it possible to achieve what I am trying to do here?
Any help appreciated, thanks
You should pass actions to the included block and perform_search_on to the class_methods block.
module Searchable
extend ActiveSupport::Concern
class_methods do
def perform_search_on(klass, associations = {})
.............
end
end
included do
def filter
respond_to do |format|
format.json { render 'api/search/filters.json' }
end
end
end
end
When your Searchable module include a method perform_search_on and the filter action.
Try removing the methods from the module ClassMethods. That is making them instance methods.

Resources