1

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.

reesaspieces
  • 1,600
  • 4
  • 18
  • 47
  • 1. Are you sure `Command.run` isn't called anywhere else without the block? 2. Which Ruby version are you using? – eyevan May 17 '19 at 08:48
  • @eyevan 1. The controller file is the only place where `Command.run` is called. 2. I'm using `2.6.2`. – reesaspieces May 17 '19 at 08:52

2 Answers2

2

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
Marlin Pierce
  • 9,931
  • 4
  • 30
  • 52
  • Thank you for the idea, but I still got the same error - `LocalJumpError: no block given (yield)`. – reesaspieces May 17 '19 at 08:42
  • If I run the test with `byebug` inserted before `yield result`, and check the value of `result`, it returns `true`. So there's probably no issue in the model bit. Something must be wrong with the way I'm writing the block. – reesaspieces May 17 '19 at 08:46
  • Thank you so much! Passing the block explicitly worked. As a side note - it seems to have been unnecessary to use a variable for the result block. As this [comment](https://stackoverflow.com/a/28440111/11249670) explains, the block can be as simple as this and have no variable: `foo(1, 2) { "from the block" }`. So the controller code seems like it was fine as-was. – reesaspieces May 20 '19 at 01:08
  • Yes, but I thought you wanted to pass whether it was successful or failed. – Marlin Pierce May 20 '19 at 09:21
1

EDIT: ah, so you have a class method run and instance method run.

  1. Either do as Marlin has suggested and supply the block explicitly from class method to the instance method.

  2. 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
    
eyevan
  • 1,475
  • 8
  • 20
  • Thanks for the suggestion - this returned `NameError: undefined local variable or method 'block'`. But if you're unable to reproduce the behavior, maybe my simplified code was too simplified and not an accurate representation? I'm not actually calling the class method directly, instead I have the functions `run` and `self.run`, like I just updated in my question. Could this have something to do with it? – reesaspieces May 17 '19 at 09:36
  • For example, am I supposed to call `yield` in `self.run` instead of `run`? – reesaspieces May 17 '19 at 09:38
  • 1
    @Risa ah, I see. So you have a class method `run` and instance method `run`. Well, that's the reason of the issue. – eyevan May 17 '19 at 10:29
  • Thank you! You were right about the cause being my use of class methods. I needed to use a class method for a different reason here, so I went with approach #1. – reesaspieces May 20 '19 at 01:02