How to return a value or conditionally raise an error in Ruby? - ruby

I am calling an API from a Rails model and I would like to raise an error if the API returns on non-200 code. Otherwise I want to cache/lazy-load the data. This is my method:
def data
#data ||= SNL.get_list(name)
raise StandardError, #data.inspect unless #data.success?
#data
end
This works but I was wondering if I can accomplish this in one line. I tried using the and operator combined with an unless but couldn't get it to work.
Update: I have accepted tokland's answer because I asked for one line and he/she provided two very good solutions. In the end I am actually going to use
def data
#data ||= SNL.get_list(name)
#data.success? ? #data : (raise StandardError, #data.inspect)
end
for readability. I just hated having a third line just to return #data, since an exception will rarely be raised. I feel odiszapc's answer is a the best compromise of brevity and readability. Thanks everyone.

I wouldn't strain to write a one-liner, but you can use tap if you absolutely must:
def data
(#data ||= SNL.get_list(name)).tap { |d| d.success? or raise StandardError.new(d.inspect) }
end
Also with short-circuited logic:
def data
(#data ||= SNL.get_list(name)).success? && #data or
raise StandardError.new(#data.inspect) }
end

You can just use a terneray operator. However, I think its really important to keep your code as readable as possible. And generally in my experience, code that spreads a bit too much horizontally is generally a bit tough to follow.
There is one thing you need to be sure of. If SNL.get_list(name) returns nil and you're trying to use the and operator along with it, it won't work.
This issue has happened with me numerous times. A sample example:
nil and puts 'hello'
try this in your irb. It won't work. This issue has occurred with me numerous times.

Maybe
def data
#data ||= SNL.get_list(name)
#data.success? ? #data : (raise StandardError, #data.inspect)
end
Or I'm not sure, something like:
def data
(#data ||= SNL.get_list(name)).success? ? #data : (raise StandardError, #data.inspect)
end

try
def data
(#data ||= SNL.get_list(name)).success? ? #data : raise(StandardError, #data.inspect)
end
Again #Sohaib's point is valid this is not quite readable! and not a rubbish's way, lot of parentheses

Related

How to optimize Ruby method

I have a ruby method
def get_status(creds)
client = create_client(creds)
status = client.account_status
client.close_session
status
end
Usually, I optimize this kind of code by tap or yield_self, but here I can't find a nice way to optimize it.
The only solution I have come up:
def get_status(creds)
create_client(creds).yeild_self do |client|
[client, client.account_status]
end.yield_self do |client, status|
client.close_session
status
end
end
But it doesn't better than the original solution, is it?
One could write the following.
class Client
def account_status
"Overdrawn!"
end
def close_session
puts "It's closed"
end
end
def create_client(creds)
Client.new
end
def get_status(creds)
begin
client = create_client(creds)
client.account_status
ensure
client.close_session if client
end
end
get_status("Anything")
It's closed
#=> "Overdrawn!"
Do I prefer this to #1 in the question? No.
Do I prefer this to #2 in the question? Yes!
Do I prefer this to #max's answer? No.
I understand a finalizer could be created using the class method ObjectSpace::define_finalizer.
class Client
def initialize
ObjectSpace.define_finalizer(self, proc { puts "It's finalized!" })
end
def account_status
"Overdrawn!"
end
end
def create_client(creds)
Client.new
end
def get_status(creds)
create_client(creds).account_status
end
get_status("Anything")
#=> "Overdrawn!"
exit
It's finalized!
One must be careful when creating finalizers, as explained Here. I understand a technique sometimes used is to have finalizer's proc reference class-level objects. See, for example, this article, #Amadan's comments below and #Matt's comments on the question. I am not advocating the use of a finalizer. I merely thought readers unfamiliar with finalizers (as I was before writing this) would find this useful.
Let's list the goal of the function:
Open connection
Read value (and return it)
Close connection
I would consider this a "temporary connection", and that leads me to think it could be refactored to a separate method.
Reasoning: The get_status method is concerned with getting the status from a connection - it doesn't have to handle the details of actually closing/opening the connection itself.
def open_temporary_connection(creds, &block)
client = create_client(creds)
result = block.call(client)
client.close_session
result
end
def get_status(creds)
open_temporary_connection(creds, &:account_status)
end
Also, I should mention, I think yield_self is a bit of a trap. Unless you're dead set on making all of your code into a single expression, it makes the code look awkward without offering a lot of benefit.
I like your first version because it is short, easy to read, and easy to understand. I would not change it.
Nevertheless, an alternative version using tap might look like this:
def get_status(creds)
client = create_client(creds)
client.account_status.tap { client.close_session }
end

NilCheck fix on safe navigation operator (&.)

This simple method on a class just run the status method using the safe navigation operator.
def current_status
account&.status
end
But reek report this warning:
MyClass#current_status performs a nil-check [https://github.com/troessner/reek/blob/master/docs/Nil-Check.md]
How can I properly write methods like this to avoid Nil Check?
I've also verified this post from thoughtbot but it seem like "too much" for just a safe navigation operator.
Ruby 2.3.1
The advice from "Example 4" in the linked post is verbose but pretty good :
class MyClass
def initialize(with_account = nil)
#account = Account.new if with_account
end
def current_status
account.status
end
def account
#account || NilAccount.new
end
end
class Account
def status
"Up!"
end
end
class NilAccount
def status
"Down!"
end
end
puts MyClass.new(:with_account).current_status
#=> "Up!"
puts MyClass.new.current_status
#=> "Down!"
If it's "too much" for you, account&.status might be just fine.
Whatever you do : you'll need to test your code as much as possible!
well, tell-dont-ask looks pretty good, but Example 4 looks like an overkill to resolve this specific case.
#andredurao I think, we can use this workaround to pass checks, for some reason reek is fine with it:
def current_status
return unless account
account.status
end

Avoid reppetitive respond_to? calling

I need to check whether a object respond to an arbitrary number of methods.
And i'm tired of doing this:
if a.respond_to?(:foo) && a.respond_to?(:bar) && a.respond_to?(:blah)
What would be a more "correct" DRY way to do this?
You can always wrap it in a helper method:
def has_methods?(obj, *methods)
methods.all?{|method| obj.respond_to? method}
end
Try this if you have nothing against monkeypatching:
class Object
def respond_to_all? *meths
meths.all? { |m| self.respond_to?(m) }
end
def respond_to_any? *meths
meths.any? { |m| self.respond_to?(m) }
end
end
p 'a'.respond_to_all? :upcase, :downcase, :capitalize
#=> true
p 'a'.respond_to_all? :upcase, :downcase, :blah
#=> false
p 'a'.respond_to_any? :upcase, :downcase, :blah
#=> true
p 'a'.respond_to_any? :upcaze, :downcaze, :blah
#=> false
UPDATE: using meths.all? and meths.any?. #MarkThomas, thanks for refreshing my mind.
UPDATE: fixing responsd typo.
Check the Active Support extension from Rails.
It has method try. It's hard to say how you can use this method because of lack of context, maybe something like this:
if a.try(:foo) && a.try(:bar) && a.try(:blah)
In order to use this method you should
require 'active_support/core_ext/object/try.rb'
Also check my version of this method the tryit:
The 'correct' way (or one of many, anyway), is Tell Don't Ask, meaning that if you're sending the object a message, you expect it to respond without complaining. This is also known as Duck Typing (if it can quack, it's a duck).
I can't give you any specific advice, because you haven't asked a specific question. If you're testing for three different methods it seems like you don't know what kind of object a is, which can be an interesting case to deal with. Post more code!

How to test method that delegates to the initiation of another class with rspec?

How would you go about testing this with rspec?
class SomeClass
def map_url(size)
GoogleMap.new(point: model.location.point, size: size).map_url
end
end
The fact that your test seems "very coupled and brittle to mock" is a sign that the code itself is doing too many things at once.
To highlight the problem, look at this implementation of map_url, which is meaningless (returning "foo" for any size input) and yet passes your tests:
class SomeClass
def map_url(size)
GoogleMap.new.map_url
GoogleMap.new(point: model.location.point, size: size)
return "foo"
end
end
Notice that:
A new map is being initiated with the correct arguments, but is not contributing to the return value.
map_url is being called on a newly-initiated map, but not the one initiated with the correct arguments.
The result of map_url is not being returned.
I'd argue that the problem is that the way you have structured your code makes it look simpler than it actually is. As a result, your tests are too simple and thus fall short of fully covering the method's behaviour.
This comment from David Chelimsky seems relevant here:
There is an old guideline in TDD that suggests that you should listen to
your tests because when they hurt there is usually a design problem.
Tests are clients of the code under test, and if the test hurts, then so
do all of the other clients in the codebase. Shortcuts like this quickly
become an excuse for poor designs. I want it to stay painful because it
should hurt to do this.
Following this advice, I'd suggest first splitting the code into two separate methods, to isolate concerns:
class SomeClass
def new_map(size)
GoogleMap.new(point: model.location.point, size: size)
end
def map_url(size)
new_map(size).map_url
end
end
Then you can test them separately:
describe SomeClass do
let(:some_class) { SomeClass.new }
let(:mock_map) { double('map') }
describe "#new_map" do
it "returns a GoogleMap with the correct point and size" do
map = some_class.new_map('300x600')
map.point.should == [1,2]
map.size.should == '300x600'
end
end
describe "#map_url" do
before do
some_class.should_receive(:new_map).with('300x600').and_return(mock_map)
end
it "initiates a new map of the right size and call map_url on it" do
mock_map.should_receive(:map_url)
some_class.map_url('300x600')
end
it "returns the url" do
mock_map.stub(map_url: "http://www.example.com")
some_class.map_url('300x600').should == "http://www.example.com"
end
end
end
The resulting test code is a longer and there are 3 specs rather than two, but I think it more clearly and cleanly separates the steps involved in your code, and covers the method behaviour completely. Let me know if this makes sense.
So this is how I did it, it feels very coupled and brittle to mock it like this. Suggestions?
describe SomeClass do
let(:some_class) { SomeClass.new }
describe "#map_url" do
it "should instantiate a GoogleMap with the correct args" do
GoogleMap.should_receive(:new).with(point: [1,2], size: '300x600') { stub(map_url: nil) }
some_class.map_url('300x600')
end
it "should call map_url on GoogleMap instance" do
GoogleMap.any_instance.should_receive(:map_url)
some_class.map_url('300x600')
end
end
end

A ruby method to replace "="

I want to eliminate "=" sign for a particular reason. It might looks like this:
cat_that_has_name("Kelly").as(:kelly)
kelly.do_something
The "as" method here is used to generate a method "kelly" that reference my cat. Could anyone help me with this?
Any suggestions will be appreciated.
Update:
Jorg was right, I've add a simple test to demonstrate my intention:
require "test/unit"
class AsTest < Test::Unit::TestCase
def setup
#cats = ["Kelly", "Tommy"]
end
def teardown
end
def test_as
kelly1 = get_cat("Kelly")
get_cat("Kelly").as(:kelly2)
assert_equal(kelly1.object_id, kelly2.object_id)
end
private
def get_cat(name)
#cats.each do |cat|
if cat.to_s==name
return cat
end
end
return nil
end
end
It's kind of hard to figure out what you actually want. If you want some sensible answers, you will have to provide a complete code example of what you want to achieve (for example, the code you posted is missing definitions for the cat_that_has_name and so_something methods). You will also need to post a complete specification of what exactly you expect the as method to do, with usage examples and ideally also with a testsuite. After all, how do we know if our answer is correct if you haven't defined what "correct" means?
The best I could decipher from your cryptic question is something like this:
class Object
def as(name)
s = self
Object.send(:define_method, name) { s }
Object.send(:private, name)
end
end
But there is no way of knowing whether this works, because if I try to run your code example, I get a NoMethodError for cat_that_has_name and another NoMethodError for so_something.
Note also that your question is self-inconsistent: in your subject line you ask about a method to replace = (i.e. creating variables) but in your question you talk about creating methods, which would mean that you are looking for a replacement for def and not for =. Again, it would be much easier to answer correctly if there were a testsuite.

Resources