Why is yield not passing the result to block (Rails)? - ruby-on-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

Related

Rails RABL: how to respond with specified http status code?

Basically I have the following controller method:
def create
begin
#check_in = create_check_in()
rescue => exception
render json: { message: exception }, status: 500
end
end
and the following json.rabl file:
object #check_in => :event_check_in
attributes :id
What I try to achieve is to set manually the HTTP status code of the response. It currently responds with 200, and I need it to be 201 instead.
I saw very few similar question and the answer was generally to render / respond_with from the controller action, so I tried something like this:
def create
begin
#check_in = create_check_in()
render #check_in, status: 201
rescue => exception
render json: { message: exception }, status: 500
end
end
but all my attempts failed, throwing various errors.
Is there a way I could set the status code?
The issue is you're passing in #check_in as the first argument of the render method when it expects the first argument to be a hash of options, including the status option.
Your status: 201 option is being passed in as a hash to the methods second argument and being ignored.
Typicallya render call will look something like:
render json: #check_in.as_json, status: 201
# or more commonly something like
render action: :create, status: 201 # the #check_in variable is already accessible to the view and doesn't need to be passed in
# or just
render status: 201
# since by default it will render the view with the same name as the action - which is `create`.
There are lots of ways to call render, see the docs for more.
-- EDIT --
Max has a great comment - I'd strongly advise against rescuing from all exceptions and also against doing it in a specific controller action. In addition to his suggestion, Rails 5+ supports :api formatting for exceptions out of the box or, if you need more, I'd look at a guide like this one.

Rails - Pass object as argument without serializing

I have a controller from which I have to pass an object instance as an argument to a Worker class of Sidekiq.
My controller with the call WegWorker.synchronize(#service)
class Api::V2::Events::WegSessionsController < Api::V2::Events::ApplicationController
before_action :load_service
def synchronize
#service.keep_cancelled = params[:keep_cancelled].to_b
if #service.valid_connection?
WegWorker.perform_async(#service)
render json: {
status: :ok,
message: #service.message
}, status: :ok
else
render json: {
status: :unprocessable_entity,
errors: #event.errors.full_messages
}, status: :ok
end
end
.. some code with service object instantiation
end
My worker class
class WegWorker < ::CarrierWave::Workers::ProcessAsset
include Sidekiq::Worker
sidekiq_options retry: false
def perform(service)
service.synchronize
end
end
But I am currently getting the error as
WARN: NoMethodError: undefined method `synchronize' for "#<WegService:0x0000000013f9abb0>":String
How can I pass the object without serializing it to the worker?
Sidekiq jobs can only take simple parameters, like string, number, etc. To work around this, the recommended approach is to have the following methods defined on your WegService:
class WegService
def self.from_params(params)
new(...) #rehydrate your service using params
end
def to_params
[43, 'foo', 'bar'] # pickle your service
end
# ...
end
Then, you would enqueue like this:
WegWorker.perform_async(#service.to_params)
And the worker would have:
def perform(params)
WebService.from_params(params).synchronize
end
You are queuing a complex Ruby object with this WegWorker.perform_async(#service). This won't be serialized properly and you will end up with a string which looks something like this "#<WegService:0x0000000013f9abb0>". While deserializing Sidekiq won't convert it into the original class like you are expecting it to.
You are supposed to queue simple objects only. So you can push identifiers to the async job and reinstantiate the service yourself inside the perform method.
Also, my advice would be to go through Sidekiq docs. They are quite small and you would save yourself from such kind of errors. Relevant link for this section.

How to test with rspec a returned exception?

def show
begin
#cart = Cart.find(params[:id])
authorize(#cart)
#cart_entries = CartEntry.where(:cart_id => #cart.id)
#products = {}
#pr_references = {}
#cart_entries.each do |cart_entry|
#pr_references[cart_entry.id] = Reference.find(cart_entry.reference_id)
#products[cart_entry.id] = Product.find(#pr_references[cart_entry.id].product_id)
end
rescue ActiveRecord::RecordNotFound => e
respond_to do |format|
format.json {render json: {'error': e}, status: :not_found}
end
end
I want to test when Cart.find() doesn't find the cart and I want to test the method return a 404 HTTP code with the test below.
it 'don\'t find cart, should return 404 error status' do
delete :destroy, params: {id: 123, format: 'json'}
expect(response).to have_http_status(404)
end
Have you got some indications or solution to do that ?
I'm a nooby with ruby on rails, if you have some tips with the code I posted I'll take it.
Thank you :)
It seems some other code is raising an exception before your Cart.find statement is executed. For this reason, the ActiveRecord::RecordNotFound exception is never risen, and it is never captured by the rescue block.
Based on the exception that is raising, it seems you are using Pundit gem for dealing with authorization. The authorization rules offered by this gem are surely running before your show method starts. Probably this is happening as a consequence of a before_filter statement, either in this controller or in a parent controller.
You will need to handle this kind of errors in your application. It may be handy to use a rescue_form statement in a base controller that is inherited by all other controllers, so that you don't have to deal with this kind of errors in every controller.

Ruby/Rails break out of nested method

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

Wrong number of arguments (1 for 2) for update method

I'm having an issue tracking down why my update method isn't getting the needed arguments. I have a similar test for show and the payloads are working. In this scenario the route in question is invoice/invoice_id/trip/id. If you can help me spot the error and give any suggestions on how to troubleshoot this type of problem in the future that'd be great.
This is the update method.
def update
if #trip.update(#trip.trip_id, trip_params)
head :no_content
else
render json: [#invoice, #trip].errors, status: :unprocessable_entity
end
end
With the following private methods.
private
def set_trip
#trip = Trip.where(:invoice_id => params[:invoice_id], :trip_id => params[:id] )
end
def trip_params
params.require(:trip).permit(:trip_id, :depart_airport, :arrive_airport, :passenger_first_name, :passenger_last_name, :passenger_count, :departure_date, :merchant_id)
end
def load_invoice
#invoice = Invoice.find(params[:invoice_id])
end
end
My failing test looks like this.
test "should update trip" do
put :update, invoice_id: #invoice.invoice_id, id: #trip,
trip: {arrive_airport: #trip.arrive_airport,
depart_airport: #trip.depart_airport,
departure_date: #trip.departure_date,
passenger_count: #trip.passenger_count,
passenger_first_name: #trip.passenger_first_name,
passenger_last_name: #trip.passenger_last_name}
assert_response 204
end
if you are calling set_trip in before_action then update() method should look like this
def update
if #trip.update(trip_params)
head :no_content
else
render json: [#invoice, #trip].errors, status: :unprocessable_entity
end
end
update() is an instance method that can be called using object, you only need to pass trip_params into it, Hope that helps!
You can get this error message when the method is calling another which is being passed the wrong number of arguments.
update takes a hash as its one and only argument, but you are are passing two arguments (#trip.trip_id, trip_params) in the update method. This is why you are getting the "Wrong number of arguments (1 for 2) for update method" error message. as #RSB said, just pass in the trip_params and the Trip instance will be updated.
RSB was right on the money. It turned out in this case that my issue was at the database level. The table didn't have a primary key so I was using
#trip = Trip.where in the private method and this was causing it to come back with an array of possible rows rather than the specific one. I changed things at the database level to have a primary key and updated the private method. VoilĂ  RSB's code worked!

Resources