Ruby/Rails break out of nested method - ruby-on-rails

I have a method1 which calls other methods depending on params and then returns a json. One of these methods checks if a specific user exists. If the user doesn't exist the method should render a JavaScript alert. At first I got an error that render was called multiple times (which was correct). So I tried adding break but received an invalid break error. So I tried return but then I still get the Render and/or redirect were called multiple times in this action. How can I break out of method1 when I'm in method2, so that only the render in method2 gets called?
def method1
data = case params["foobar"]
when "case1"
methodxy
...
else
method2
end
render json: data
end
def method2
if user.exists?
return {...}
else
render(
html: "<script>alert('Unknown user!')</script>".html_safe,
layout: 'application'
)
return
end
end

Technically you could achieve this with a throw ... catch, which is essentially acting like a GOTO statement. But I would not advise this; the code is already too messy, and you'd be making the problem worse.
Instead, you should aim to clean up the flow in method1. Make the logical flow and responses clearer. For example, maybe something like:
def method1
if !user.exists?
render(
html: "<script>alert('Unknown user!')</script>".html_safe,
layout: 'application'
)
else
data = case params["foobar"]
when "case1"
methodxy
...
else
method2
end
render json: data
end
end
def method2
# ...
end
You could then refactor this further, e.g. by moving the user.exists? check into a before_filter and moving that case statement into its own method, to simplify things.
The end result could look something along the lines of:
before_filter :ensure_user_exists, only: :method1
def method1
render json: foobar_data
end

Related

Why is yield not passing the result to block (Rails)?

I know there are several SO questions as well as online articles on using yield in Rails. But I'm still having trouble understanding what's wrong with my code below, and would appreciate any advice.
In my app, I have:
A controller that passes data to the command class's run method, and returns the request status based on the result of the Command.run (true/false)
A command class that deals with the actual meat of the process, then yields true if it succeeded, or false if it failed
However, the command class seems to be failing to yield the results to my controller. According to the error messages when I run my tests, it seems like my block in the controller isn't being recognized as a block:
# If I use "yield result":
LocalJumpError: no block given (yield)
# If I use "yield result if block_given?":
# (This is because I have "assert_response :success" in my tests)
Expected response to be a <2XX: success>, but was a <400: Bad Request>
How should I rewrite the block (do ... end part in the controller below) so that yield works correctly? Or if the issue lies elsewhere, what am I doing wrong?
I've provided a simplified version of my code below. Thank you in advance!
# controller
def create
Command.run(params) do
render json: { message: 'Successfully processed request' }
return
end
render json: { message: 'Encountered an error' }, status: :bad_request
end
# command class
def run(params)
# Do some stuff, then send HTTP request
# "result" below returns true or false
result = send_http_request.parsed_response == 'ok'
yield result
end
def self.run(params)
new.run(params)
end
Note: This code works if I use if true... else... in the controller instead of a block, and just return the boolean result instead of yielding it. But here I'd like to know how to make yield work.
In your controller you need to have a variable for the result.
def create
Command.run(params) do |result|
if result
render json: { message: 'Successfully processed request' }, status: :success
else
render json: { message: 'Encountered an error' }, status: :bad_request
end
return
end
render json: { message: 'Encountered an error' }, status: :bad_request
end
(EDIT)
Also, you are calling the class method which call the instance method. You have to pass the block from the calling code to the instance method you are calling.
def self.run(params, &block)
new.run(params, &block)
end
EDIT: ah, so you have a class method run and instance method run.
Either do as Marlin has suggested and supply the block explicitly from class method to the instance method.
Or use only the class method as I've initially suggested (it doesn't
seem like there's any reason to instantiate Command in your case):
def self.run(params, &block)
result = send_http_request.parsed_response == 'ok'
block.yield(result)
end

and return at the end of a method

Sometimes I spot the code like this:
def bar
#......
if response && response.body
#......
render(:text => html) and return
end
end
I wonder, is there any point of using and return at the very end of a method?
This is to help avoiding double render errors. Layouts and Rendering
I saw such code only not at the bottom of a method (it makes sense because render is not the end of the method). But here I don't see any point.
See Avoiding Double Render Errors
This ensures that you return after the render because if you call render multiple times, you get an exception: Render and/or redirect were called multiple times in this action
One of the reasons is if someone edits your code to become as below, you'd be in trouble:
def bar
...
x = true
...
if x
render(:text => html)
end
...
render(:text => html)
end

return from method used by before_filter

I've inherited some rails code that checks to see if a user is defined in a method that is called by a before filter:
before_filter :get_user
def get_user
#user = User.find(params[:id])
if !#user
return false
end
end
Now, the problem is that this doesn't work :) If the user isn't found, we don't return from the controller, we just return from the get_user() method and then continue execution in show() or update() method with #user set to nil.
My simple solution is to add a redirect to get_user() if #user is nil:
def get_user
#user = User.find(params[:id])
if !#user
redirect_back
return false
end
end
Now, my test are passing and everything seems right with the world. But ... I don't understand what's going on. Can someone please explain why the return in get_user() didn't stop execution in the controller completely but only breaks us out of get_user() and causes us to fall into the controller method that was originally called?
Thanks!
http://guides.rubyonrails.org/action_controller_overview.html#filters
"The method simply stores an error message in the flash and redirects to the login form if the user is not logged in. If a "before" filter renders or redirects, the action will not run. If there are additional filters scheduled to run after that filter, they are also cancelled."
Pretty self explanatory, but the nature is that you can't return to halt execution in a filter.
Return inside an method just returns and break that method and nothing else. Se code below.
def foo
return "foo"
return "bar"
end
puts my_method # this will puts "foo" and the second return will never be called.
However, in ruby you still can execute code after an return with ensure.
def bar
return "bar"
ensure
#bar = 'Hello world'
end
puts bar # returns and prints "bar"
puts #bar # prints "Hello world" because the ensure part was still executed
And keep in mind that the last executed code in your method will be returned, so you don't always need to write return before your value. And if you have an ensure part in your method the last executed code before that will be returned if you haven't returned something already.
And theres no need to return false in your before filters. If i remember right rails before version 3.1 did stop the controller when an before filter was returning an falsy value. Nil is still falsy in ruby and to strip some rows you cud write your filter like below because if no user is found #user will be nil in this example.
def get_user
#user = User.find_by id: params[:id] # I use find_by to prevent exception, else we may return an 500 error which you may or may not want.
redirect_back unless #user
end
I would rewrite this code this way
def get_user
redirect_back if User.where(id: params[:id]).empty?
end
for two reasons. First, why if and all that if you can check it in a more simple way. Second, find raises an exception if object is not found so this check makes no sense at all!

Rails : render and exit immediately

Using the omniauth gem, I am forced to define a single route callback for succesful logins, regardless of the provider :
def auth_callback
auth_data = request.env['omniauth.auth']
if auth_data.has_key('something')
process_one(auth_data)
else
process_two(auth_data)
end
# No view is available here
end
def process_one
# do something then render view for process_one
return
end
def process_two
# do something then render view for process_two
return
end
How can I prevent the controller from returning to the auth_callback method and try to display the corresponding view (which does not exist) ? Treatment should be considered as complete once the process_one or process_two methods have returned.
Why not specifically call render in those methods?
def process_one
# do something then render view for process_one
render :process_one and return
end
Rails should detect that you've already run it and not try to render again.
If you want to return from the chain of methods, e.g.
def a
...
b
...
render "smth"
end
...
def b
...
# render from some conditional from here
...
end
will cause AbstractController::DoubleRenderError, which means that you call render twice.
You can read this article to find out 4 ways to manage such situation.

How can I determine whether a controller action has rendered (rather than redirected?)

The performed? method returns true in a controller if the application has redirected or rendered.
How can I determine whether the application rendered (rather than redirected)?
Judging from the performed? docs, I can look at #performed_render, but this seems hacky.
Edit: Why do I want to do this?
I'm trying to cache the output of a controller action only in the case that it results in a render (and not a redirect). Here's the code I'm using now: (based on render-caching)
def render_with_cache(options = {})
key = ([request.request_uri, request.format] | (options.delete(:key_additions) || [])).join("::")
body = Rails.cache.read(key)
if body
render :text => body
else
yield if block_given?
render unless performed?
if #performed_render
Rails.cache.write(key, response.body, options) # :expires_in
end
end
end
Rails 3.2+ now uses a method called performed? for determining whether or not a render or redirect has already been performed.
Contrived example:
def index
redirect_to not_found_path unless authenticated?
render action: "show_all" unless performed?
end
Look at #performed_render. :) You don't explain why you need to detect this, so I am unable to suggest alternative solutions.
In an after filter, check the codes for the response status .
MyController < ActionController
after_filter :check_response
# define your actions that render or redirect
protected
def check_response
# put real app logic here
puts :rendered! if response.status == "200 OK"
puts :redirected! if response.status == "302 Found"
end
end

Resources