How to make this Sinatra code DRYer? - ruby

These are my Sinatra routes. They pass the storename as an ActiveRecord class and the params with which my helpermethods do what they have to.
def self.get_or_post(url,&block)
get(url,&block)
post(url,&block)
end
get_or_post "/read" do
jsonp(read(params[:store].constantize, params))
end
get_or_post "/create" do
jsonp(create(params[:store].constantize, params))
end
get_or_post "/update" do
jsonp(update(params[:store].constantize, params))
end
get_or_post "/destroy" do
jsonp(destroy(params[:store].constantize, params))
end
It seems to me this can be made DRYer doing something like:
case route
when read, create, update, destroy
jsonp(method(route).call(params[:store].constantize, params))
else
# neglect or give error
end
How can I get this route variable, and is my use of method(route).call correct?

How about:
%w(read create update destroy).each do |action|
[:get, :post].each do |method|
send(method, "/#{action}") do
jsonp(send(action, params[:store].constantize, params))
end
end
end

Related

Best practice of error handling on controller and interactor

# users_show_controller.rb
class Controllers::Users::Show
include Hanami::Action
params do
required(:id).filled(:str?)
end
def call(params)
result = users_show_interactor(id: params[:id])
halt 404 if result.failure?
#user = result.user
end
end
# users_show_interactor.rb
class Users::Show::Interactor
include Hanami::Interactor
expose :user
def call(:id)
#user = UserRepository.find_by(:id)
end
end
I have a controller and a interactor like above.
And I'm considering the better way to distinguish ClientError from ServerError, on the controller.
I think It is nice if I could handle an error like below.
handle_exeption StandardError => :some_handler
But, hanami-interactor wraps errors raised inside themselves and so, controller receive errors through result object from interactor.
I don't think that re-raising an error on the controller is good way.
result = some_interactor.call(params)
raise result.error if result.failure
How about implementing the error handler like this?
I know the if statement will increase easily and so this way is not smart.
def call(params)
result = some_interactor.call(params)
handle_error(result.error) if result.faulure?
end
private
def handle_error(error)
return handle_client_error(error) if error.is_a?(ClientError)
return server_error(error) if error.is_a?(ServerError)
end
Not actually hanami-oriented way, but please have a look at dry-monads with do notation. The basic idea is that you can write the interactor-like processing code in the following way
def some_action
value_1 = yield step_1
value_2 = yield step_2(value_1)
return yield(step_3(value_2))
end
def step_1
if condition
Success(some_value)
else
Failure(:some_error_code)
end
end
def step_2
if condition
Success(some_value)
else
Failure(:some_error_code_2)
end
end
Then in the controller you can match the failures using dry-matcher:
matcher.(result) do |m|
m.success do |v|
# ok
end
m.failure :some_error_code do |v|
halt 400
end
m.failure :some_error_2 do |v|
halt 422
end
end
The matcher may be defined in the prepend code for all controllers, so it's easy to remove the code duplication.
Hanami way is validating input parameters before each request handler. So, ClientError must be identified always before actions logic.
halt 400 unless params.valid? #halt ClientError
#your code
result = users_show_interactor(id: params[:id])
halt 422 if result.failure? #ServerError
halt 404 unless result.user
#user = result.user
I normally go about by raising scoped errors in the interactor, then the controller only has to rescue the errors raised by the interactor and return the appropriate status response.
Interactor:
module Users
class Delete
include Tnt::Interactor
class UserNotFoundError < ApplicationError; end
def call(report_id)
deleted = UserRepository.new.delete(report_id)
fail_with!(UserNotFoundError) unless deleted
end
end
end
Controller:
module Api::Controllers::Users
class Destroy
include Api::Action
include Api::Halt
params do
required(:id).filled(:str?, :uuid?)
end
def call(params)
halt 422 unless params.valid?
Users::Delete.new.call(params[:id])
rescue Users::Delete::UserNotFoundError => e
halt_with_status_and_error(404, e)
end
end
end
fail_with! and halt_with_status_and_error are helper methods common to my interactors and controllers, respectively.
# module Api::Halt
def halt_with_status_and_error(status, error = ApplicationError)
halt status, JSON.generate(
errors: [{ key: error.key, message: error.message }],
)
end
# module Tnt::Interactor
def fail_with!(exception)
#__result.fail!
raise exception
end

How I create a simples questionnaire with Lita.io?

I try implementing a little questionnaire in Lita as the sample:
For which system do you want to open a call?
SYSInitials
What's your problem?
I forgot my password
Thanks! Your call was opened!
Any help how I can do this?
So, I'm try this:
module Lita
module Handlers
class Helpdesk < Handler
on :shut_down_complete, :clear_context
route(/^abrir chamado$/i, :abrir_chamado)
route(/^.*$/i, :motivo)
http.get '/info', :web
def motivo(response)
return unless context == 'abrir_chamado'
response.reply('Thanks! Your call was opened!')
clear_context
end
def abrir_chamado(response)
redis.set(:context, :abrir_chamado)
user = response.user
response.reply(
%(Hello #{user.name}, What is your problem?)
)
end
def context
#contetx ||= redis.get(:context)
end
def clear_context
redis.del(:context)
end
Lita.register_handler(Helpdesk)
end
end
end
But when I register, :informar_motivo route, after passing of the :abrir_chamado route, is matched :informar_motivo route too.
but I need:
me: abrir chamado
Lita: Hello Shell User, What is your problem?
me: I forgot my password
Lita: Thanks! Your call was opened!
I found a ugly solution, but works :P
module Lita
module Handlers
class Helpdesk < Handler
on :shut_down_complete, :clear_context
on :unhandled_message, :motivo
route(/^abrir chamado$/i, :abrir_chamado)
http.get '/info', :web
def motivo(payload)
response = payload[:message]
return unless context == 'abrir_chamado'
response.reply('Thanks! Your call was opened!')
clear_context
end
def abrir_chamado(response)
redis.set(:context, :abrir_chamado)
user = response.user
response.reply(
%(Hello #{user.name}, What is your problem?)
)
end
def context
#contetx ||= redis.get(:context)
end
def clear_context
redis.del(:context)
end
Lita.register_handler(Helpdesk)
end
end
end

Before filter on condition

I have a Sinatra app where all routes require a user login by default. Something like this:
before do
env['warden'].authenticate!
end
get :index do
render :index
end
Now I would like to use a custom Sinatra condition to make exceptions, but I cannot find a way to read if the condition is true/false/nil
def self.public(enable)
condition {
if enable
puts 'yes'
else
puts 'no'
end
}
end
before do
# unless public?
env['warden'].authenticate!
end
get :index do
render :index
end
get :foo, :public => true do
render :index
end
Since the authentication check must be done even if the condition is not defined, I guess I still must use a before filter, but I am not sure how to access my custom condition.
I was able to solve this using Sinatra's helpers and some digging into Sinatra's internals. I think this should work for you:
helpers do
def skip_authentication?
possible_routes = self.class.routes[request.request_method]
possible_routes.any? do |pattern, _, conditions, _|
pattern.match(request.path_info) &&
conditions.any? {|c| c.name == :authentication }
end
end
end
before do
skip_authentication? || env['warden'].authenticate!
end
set(:authentication) do |enabled|
condition(:authentication) { true } unless enabled
end
get :index do
render :index
end
get :foo, authentication: false do
render :index
end

Test helpers with RSpec in Padrino (Sinatra)

I'm trying to test a helper in a Padrino (Sinatra) app. My helper method is itself calling Padrino core helper methods but they are undefined. The error appears only in RSpec, while the app works fine. So the way I'm including my helper in RSpec makes it loose "Padrino scope" but I don't know how to bring Padrino helper's scope properly in my RSpec environment.
My helper:
module AdminHelper
Sort = Struct.new(:column, :order)
def sort_link(model, column)
order = sorted_by_this?(column) ? 'desc' : 'asc'
link_to mat(model, column), url(:pages, :index, sort: column, order: order)
end
def sorted_by_this?(column)
column.to_s == #sort.column && #sort.order == 'asc'
end
end
Lenstroy::Admin.helpers AdminHelper
My spec:
describe AdminHelper do
before(:all) do
class AdminHelperClass
include AdminHelper
end
end
subject(:helper) { AdminHelperClass.new }
describe '#sort_link' do
context "with :pages and :title parameters" do
before do
sort = AdminHelperClass::Sort.new('title', 'asc')
helper.instance_variable_set('#sort', sort)
end
subject { helper.sort_link(:pages, :title) }
it { should match(/<a href=([^ ]+)pages/) }
end
end
end
Results in error:
1) AdminHelper#sort_link with :pages and :title parameters
Failure/Error: subject { helper.sort_link(:pages, :title) }
NoMethodError:
undefined method `mat' for #<AdminHelperClass:0x007f1d951dc4a0>
Including a helper where mat is defined doesn't work, as one method is dependent on another helper and it goes on and on...
Update
In my spec helper I have:
def app(app = nil, &blk)
#app ||= block_given? ? app.instance_eval(&blk) : app
#app ||= Lenstroy::Admin
#app.register Padrino::Helpers
#app.register Padrino::Rendering
#app
end
in my spec I have:
it "returns link to resource with sort parameters" do
app do
get '/' do
sort_link(:pages, :title)
end
end
get "/"
last_response.body.should =~ /<a href=([^ >]+)pages/
end
And now tests fail, last_response.body is ''.
Method #mat is defined in Padrino::Admin::Helpers::ViewHelpers. You can do
class AdminHelperClass
include Padrino::Admin::Helpers::ViewHelpers
include AdminHelper
end
Update:
If your methods are really dependent on all these routes and helpers you should consider doing full mockup of your app like this:
def mock_app(base=Padrino::Application, &block)
#app = Sinatra.new(base, &block)
#app.register Padrino::Helpers
#app.register Padrino::Rendering
# register other things
end
def app
Rack::Lint.new(#app)
end
mock_app do
get '/' do
sort_link(my_model, my_column)
end
end
get "/"
assert_equal "some test text", body
Here's how it's done in padrino-admin: https://github.com/padrino/padrino-framework/blob/master/padrino-admin/test/test_admin_application.rb
I was having the same problem (and getting very frustrated tracking down the modules and including them). So far, I've got my specs working by:
1) Explicitly defining my module (as explained in how to use padrino helper methods in rspec)
module MyHelper
...
end
MyApp::App.helpers MyHelper
2) Automatically including helpers at the top of my spec. (Right now I only have one helper spec, but in the future I might try to move this into spec_helper.rb.)
describe MyHelper do
let(:helpers) { Class.new }
before { MyApp::App.included_modules.each { |m| helpers.extend m } }
subject { helpers }
it 'blah' do
expect(subject.helper_method).to eq 'foo'
end
end

Testing #current_user method using RSpec

I've been trying to do this for a couple of days now, but I can't figure it out. I have the following code in my controller:
#some_object = #current_user.some_method
In my spec, I want to attach a should_receive hook on that method, but I can't make it work. I've tried all of these, but none of them work:
assigns[:current_user].should_receive(:some_method).at_least(:once) # expected 1, got 0
User.should_receive(:some_method).at_least(:once) # expected 1, got 0
How is the correct way of testing this? I'm running this in my spec, and login is working:
setup :activate_authlogic
...
UserSession.create(users(:rune))
Thanks!
One example comes from the Ruby on Rails Tutorial. Rather than setting and reading #current_user directly, it defines two helper methods:
def current_user=(user)
#current_user = user
end
def current_user
#current_user
end
Later, they access this method in the tests using the controller method:
def test_sign_in(user)
controller.current_user = user
end
Using this methodology, you should be able to use
controller.current_user.should_receive(:some_method).at_least(:once)
You can’t call something like in the controllers:
expect(current_user).to be_present
expect(user_signed_in?).to be_true
So to do so, you can do this :
module ControllerMacros
def current_user
user_session_info = response.request.env['rack.session']['warden.user.user.key']
if user_session_info
user_id = user_session_info[0][0]
User.find(user_id)
else
nil
end
end
def user_signed_in?
!!current_user
end
end
You can either include the ControllerMacros in the top of the controller spec or include it in the spec_helper.rb like so :
RSpec.configure do |config|
config.include ControllerMacros, type: :controller
end

Resources