How can I make this method more concise? - ruby-on-rails

I get a warning when running reek on a Rails project:
[36]:ArborReloaded::UserStoryService#destroy_stories has approx 8 statements (TooManyStatements)
Here's the method:
def destroy_stories(project_id, user_stories)
errors = []
#project = Project.find(project_id)
user_stories.each do |current_user_story_id|
unless #project.user_stories.find(current_user_story_id).destroy
errors.push("Error destroying user_story: #{current_user_story_id}")
end
end
if errors.compact.length == 0
#common_response.success = true
else
#common_response.success = false
#common_response.errors = errors
end
#common_response
end
How can this method be minimized?

First, I find that class and method size are useful for finding code that might need refactoring, but sometimes you really do need a long class or method. And there is always a way to make your code shorter to get around such limits, but that might make it less readable. So I disable that type of inspection when using static analysis tools.
Also, it's unclear to me why you'd expect to have an error when deleting a story, or who benefits from an error message that just includes the ID and nothing about what error occurred.
That said, I'd write that method like this, to reduce the explicit local state and to better separate concerns:
def destroy_stories(project_id, story_ids)
project = Project.find(project_id) # I don't see a need for an instance variable
errors = story_ids.
select { |story_id| !project.user_stories.find(story_id).destroy }.
map { |story_id| "Error destroying user_story: #{story_id}" }
respond errors
end
# Lots of services probably need to do this, so it can go in a superclass.
# Even better, move it to #common_response's class.
def respond(errors)
# It would be best to move this behavior to #common_response.
#common_response.success = errors.any?
# Hopefully this works even when errors == []. If not, fix your framework.
#common_response.errors = errors
#common_response
end
You can see how taking some care in your framework can save a lot of noise in your components.

Related

cyclomatic complexity is too high rubocop for method

This is code I have using in my project.
Please suggest some optimizations (I have refactored this code a lot but I can't think of any progress further to optimize it )
def convert_uuid_to_emails(user_payload)
return unless (user_payload[:target] == 'ticket' or user_payload[:target] == 'change')
action_data = user_payload[:actions]
action_data.each do |data|
is_add_project = data[:name] == 'add_fr_project'
is_task = data[:name] == 'add_fr_task'
next unless (is_add_project or is_task)
has_reporter_uuid = is_task && Va::Action::USER_TYPES.exclude?(data[:reporter_uuid])
user_uuids = data[:user_uuids] || []
user_uuids << data[:owner_uuid] if Va::Action::USER_TYPES.exclude?(data[:owner_uuid])
user_uuids << data[:reporter_uuid] if has_reporter_uuid
users_data = current_account.authorizations.includes(:user).where(uid: user_uuids).each_with_object({}) { |a, o| o[a.uid] = {uuid: a.uid, user_id: a.user.id, user_name: a.user.name} }
if Va::Action::USER_TYPES.include? data[:owner_uuid]
data['owner_details'] = {}
else
data['owner_details'] = users_data[data[:owner_uuid]]
users_data.delete(data[:owner_uuid])
end
data['reporter_details'] = has_reporter_uuid ? users_data[data[:reporter_uuid]] : {}
data['user_details'] = users_data.values
end
end
Note that Rubocop is complaining that your code is too hard to understand, not that it won't work correctly. The method is called convert_uuid_to_emails, but it doesn't just do that:
validates payload is one of two types
filters the items in the payload by two other types
determines the presence of various user roles in the input
shove all the found user UUIDs into an array
convert the UUIDs into users by looking them up
find them again in the array to enrich the various types of user details in the payload
This comes down to a big violation of the SRP (single responsibility principle), not to mention that it is a method that might surprise the caller with its unexpected list of side effects.
Obviously, all of these steps still need to be done, just not all in the same method.
Consider breaking these steps out into separate methods that you can compose into an enrich_payload_data method that works at a higher level of abstraction, keeping the details of how each part works local to each method. I would probably create a method that takes a UUID and converts it to a user, which can be called each time you need to look up a UUID to get the user details, as this doesn't appear to be role-specific.
The booleans is_task, is_add_project, and has_reporter_uuid are just intermediate variables that clutter up the code, and you probably won't need them if you break it down into smaller methods.

Do I need to stub EVERYTHING in this method in order to pass?

I am generally new to mocha and I hate using this gem but I need to use it in order to pass a test that I am constructing. What is giving me problems is what I'm supposed to mock and how I am supposed to mock it. To illustrate my point, Here is an example of a method that I am testing:
def statistics_of_last_24_hrs
stats = ses.statistics.find_all { |s| s[:sent].between?(Time.now.utc - 24.hours, Time.now.utc) }
sent_last_24_hrs = ses.quotas[:sent_last_24_hours].to_f
no_of_bounces = stats.inject(0.0) { |a, e| a + e[:bounces] }
no_of_complaints = stats.inject(0.0) { |a, e| a + e[:complaints] }
bounce_rate = sent_last_24_hrs.zero? ? 0.0 : (no_of_bounces / sent_last_24_hrs) * 100
complaint_rate = sent_last_24_hrs.zero? ? 0.0 : (no_of_complaints / sent_last_24_hrs) * 100
fail(Reverification::SimpleEmailServiceLimitError, 'Bounce Rate exceeded 5%') if bounce_rate >= 5.0
fail(Reverification::SimpleEmailServiceLimitError, 'Complant Rate exceeded .1%')if complaint_rate >= 0.1
end
Basically what this code is doing is getting some statistics from an Amazon api call and then calculating them to determine if my bounce/complaint rate has exceeded the limit. The limit is 5% and 0.1% respectively.
Basically for my test all I really need to do is to stub the variables bounce_rate and complaint_rate in order to test whether the right exception is thrown.
This is where I am getting stuck. Here is a barebones test that I would ideally write:
it 'should raise SimpleEmailServieLimitError if bounce rate is above 5%' do
assert_raise Reverification::SimpleEmailServiceLimitError do
Reverification::Process.statistics_of_last_24_hrs
end
end
How can I stub the bounce_rate and then the complaint_rate. I've done some searching around and came to the conclusion that there isn't a way to stub variables. I also looked at this link List of Mocha MethodsWhich confirms my findings.
Is there a way I can just write a test like this:
it 'should raise SimpleEmailServieLimitError if bounce rate is above 5%' do
stubs(:bounce_rate).returns(true)
assert_raise Reverification::SimpleEmailServiceLimitError do
Reverification::Process.statistics_of_last_24_hrs
end
end
Or do I have to stub every method call in this method so that the test will look something like this:
it 'should raise SimpleEmailServieLimitError if bounce rate is above 5%' do
sent_last_24_hrs = 20
over_bounce_limit = MOCK::AWS::SimpleEmailService.over_bounce_limit
AWS::SimpleEmailService.any_instance.stubs(:statistics).returns(stub(find_all: over_bounce_limit))
AWS::SimpleEmailService.any_instance.stubs(:quotas).returns(stub(sent_last_24_hours: sent_last_24_hrs))
etc. etc. etc...........
assert_raise Reverification::SimpleEmailServiceLimitError do
Reverification::Process.statistics_of_last_24_hrs
end
end
Is there an easier way to do this?
Even if there was a way to stub local variables, that feature would produce tests that are very hard to maintain because you'd not be able to refactor your code without changing tests.
Nested stubs are design smell too - your tests will know about too much implementation details and will become unmaintainable.
The same can be said for stubbing third-party code as any changes to the third-party library will allow your tests to pass while the code does not work.
It is a lot better to create your own wrapper around AWS SimpleEmailService - gateway. You'd implement it to have a very narrow stable interface like
class BounceStatistics
def no_of_bounces
def no_of_complaints
def sent_last_24_hrs
end
Since this interface is your own and it is stable, you can safely stub it and provide alternative implementation for your tests:
assert_raise Reverification::SimpleEmailServiceLimitError do
Reverification::Process.statistics_of_last_24_hrs(
stub(no_of_bounces: 2, no_of_complaints: 3, sent_last_24_hrs: 5))
end
alternatively you may implement it as
BounceStatistics.any_instance.stubs(:no_of_bounces).returns(2)
BounceStatistics.any_instance.stubs(:no_of_complaints).returns(3)
BounceStatistics.any_instance.stubs(:sent_last_24_hrs).returns(5)
assert_raise Reverification::SimpleEmailServiceLimitError do
Reverification::Process.statistics_of_last_24_hrs
end
However passing dependencies explicitly allows you to have more maintainable code and simpler tests.

How to read the Rails API

I'm having a difficult time understanding the Rails API. I am trying to figure out a way to understand what I can call from certain points inside Rails, such as when I'm in a controller, so I wrote something to tell me all the methods that are available sorted by what Module/Class they fall under:
last_sig = ""
self.methods.each do |method|
#i_am = self.method(method).owner
#puts i_am.class
#places.push(self.method(method).owner)
m = self.method(method)
sig = "#{m.owner.class}: #{m.owner}"
if sig != last_sig
last_sig = sig
puts sig
end
puts " #{method}"
end
As an example, I find out (just using this as an easy example) that I can use the render() method and it is located at ActionController::Instrumentation, so then I look at the render() function there and it says:
render(*args)
# File actionpack/lib/action_controller/metal/instrumentation.rb, line 38
def render(*args)
render_output = nil
self.view_runtime = cleanup_view_runtime do
Benchmark.ms { render_output = super }
end
render_output
end
That is all is says, I don't understand how from this I could understand how it works, then I do some more searching and by "luck" I discover that it is documented in ActionView, and I wonder how I was able to know this? Anyway, any tips on how to read the API would be appreciated- It seems like many of the things in the API are not documented for a User, and I don't know if they are for the User or for the developers of Rails- I'm used to using a documentation like jQuery which seems much easier to Discover functionality by using-

MongoDB― need to display status of db (running or not)

I am currently using MongoDB for tracking of various things in a Rails 2 app. I am using the following code to see if MongoDB is up and running and, depending upon the status, displaying a link or an "Offline" message.
This is only for admins, so it's not mission-critical, as the app will continue to run without MongoDB, but I do want to keep disabling the link in the menu when it's not running. However, I don't like the overhead of the below code (doesn't take long to run, but hope that there is a cleaner, faster way):
def verify_mongodb_status
begin
track = Track.first
#mongodb_running = true
rescue
#mongodb_running = false
logger.debug("***MongoDB not running.***")
notify_admin_about_errors("***MongoDB is not running***)
end
end
EDIT: I forgot to mention that I'm already doing a before_filter for this; the method sits in application_controller.rb.
I decided to go with action_caching as there doesn't seem to be a great way to do this. The result was quite a large speed increase from ~120ms to ~16-25ms:
def verify_mongodb_status
begin
track = Track.first
#mongodb_running = true
rescue => e
#mongodb_running = false
logger.debug("***MONGODB OFFLINE***: #{e}")
notify_admin_about_errors("MongoDB", "MongoDB error:\n#{e}", nil)
expire_action :action => :verify_mongodb_status
return
end
end
I'm adding logic now to keep from getting bombarded by emails when MongoDB goes offline (1 is enough).

How do I temporarily monkey with a global module constant?

Greetings,
I want to tinker with the global memcache object, and I found the following problems.
Cache is a constant
Cache is a module
I only want to modify the behavior of Cache globally for a small section of code for a possible major performance gain.
Since Cache is a module, I can't re-assign it, or encapsulate it.
I Would Like To Do This:
Deep in a controller method...
code code code...
old_cache = Cache
Cache = MyCache.new
code code code...
Cache = old_cache
code code code...
However, since Cache is a constant I'm forbidden to change it. Threading is not an issue at the moment. :)
Would it be "good manners" for me to just alias_method the special code I need
just for a small section of code and then later unalias it again? That doesn't
pass the smell test IMHO.
Does anyone have any ideas?
TIA,
-daniel
But you can overwrite constants in Ruby (regardless of whether it's a module or class or simple other object):
MyConst = 1
# do stuff...
old_my_const = MyConst
MyConst = 5
puts "MyConst is temporarily #{MyConst}"
MyConst = old_my_const
puts "MyConst is back to #{MyConst}"
Output:
a.rb:6: warning: already initialized constant MyConst
MyConst is temporarily 5
a.rb:8: warning: already initialized constant MyConst
MyConst is back to 1
The warnings are simply that: warnings. Your code will continue to run the same.
Okay, maybe the warnings are unacceptable in your situation for some reason. Use this suppress_all_warnings method I've written. Example includes reassigning a module.
def suppress_all_warnings
old_verbose = $VERBOSE
begin
$VERBOSE = nil
yield if block_given?
ensure
# always re-set to old value, even if block raises an exception
$VERBOSE = old_verbose
end
end
module OriginalModule
MyConst = 1
end
module OtherModule
MyConst = 5
end
def print_const
puts OriginalModule::MyConst
end
print_const
suppress_all_warnings do
old_module = OriginalModule
OriginalModule = OtherModule
print_const
OriginalModule = old_module
end
print_const
Now you get the correct output, but without the warnings:
1
5
1

Resources