How to refactor so many conditionals statements? - ruby-on-rails

I've a class to compare the imported data with the database values. I ended up with so much conditions that rubocop was shouting loud for it. So I broke the method into smaller methods but there are still conditionals in those methods. Here is the code:
Before
class Utility
attr_reader :im_data, :db_data
def initialize(im_data, db_data)
#im_data = im_data
#db_data = db_data
#to_update = []
#to_delete = []
end
def compare_values
if !im_data[:name].present?
#to_delete << im_data[:name]
elsif im_data[:name].present?
if im_data[:lookup].present? && (im_data[:lookup] != db_data.full_name)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:lookup] }
elsif !im_data[:lookup].present? && (im_data[:name] != db_data.full_name)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:name] }
end
end
end
end
After
def compare_values(im_data, db_data)
deselection(im_data)
re_apply(im_data, db_data)
end
def presence?(value)
value.present?
end
def deselection(im_data)
#to_delete << im_data[:l_v_id] unless presence?(im_data[:name])
end
def re_apply(im_data, db_data)
fv_present = presence?(im_data[:name])
compare_lookup(im_data, db_data.full_name) if fv_present
compare_name(im_data, db_data.full_name) if fv_present
end
def compare_lookup(im_data, l_value)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:lookup] } if presence?(im_data[:lookup]) && (im_data[:lookup] != l_value)
end
def compare_name(im_data, full_name)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:name] } if !presence?(im_data[:lookup]) && (im_data[:name] != full_name)
end
I tried to follow this blog but no luck with it. I still feel there is a much much better way to refactor this code.

I personally find "after" much harder to follow. How about the following?
def compare_values
if #im_data[:name].present?
to_upd(#im_data[:lookup].present? ? :lookup : :name)
else
#to_delete << #im_data[:name]
end
end
def to_upd(key)
#to_update << { id: #im_data[:l_v_id], full_name: #im_data[key] } unless
#im_data[key] == #db_data.full_name
end

Related

DRYing up similar code in controller methods

I've got two methods in a controller with very similar code. Wondering how I could DRY them up! They both utilize csv-importer gem to parse a csv file.
sales_controller.rb
def import_csv_test
user_id = params[:user_id]
import = ImportSaleCSV.new(file: params[:file]) do
after_build do |sale|
sale.user_id = user_id
skip! if sale.email == nil
skip! if sale.order_date == nil
skip! if sale.amount == nil
end
end
import.run!
redirect_to lifecycle_grid_sales_path, notice: import.report.message
end
def import_ftp
user_id = params[:user_id]
import = ImportSaleCSV.new(path: './public/uploads/gotcha.csv') do
after_build do |sale|
sale.user_id = user_id
skip! if sale.email == nil
skip! if sale.order_date == nil
skip! if sale.amount == nil
end
end
import.run!
redirect_to lifecycle_grid_sales_path, notice: import.report.message
end
Thanks!
I think you can extract a class to do the heavy lifting.
class ImportSaleCSVCreator
def initialize(csv_options = {}, csv_attributes = {})
#csv_options = csv_options
#csv_attributes = csv_attributes
end
def build
ImportSaleCSV.new(csv_options) do
after_build do |sale|
csv_attributes.each { |k, v| sale.public_send("#{k}=", v) }
skip! if sale.email.nil? || sale.order_date.nil? || sale.amount.nil?
end
end
end
private
attr_reader :csv_options, :csv_attributes
end
class Controller
def import_csv
import = ImportSaleCSVCreator.new({ file: params[:file] }, { user_id: params[:user_id] })
import.run!
end
def import_ftp
import = ImportSaleCSVCreator.new({ path: './gotcha.csv' }, { user_id: params[:user_id] })
import.run!
end
end
Make sure you check attributes passed. Especially when dealing with files, paths, etc. You might want to filter the parameters in ImportSaleCSVCreator.
You may refactor both your methods into single one:
def import(hash)
user_id = params[:user_id]
import = ImportSaleCSV.new(hash) do
after_build do |sale|
sale.user_id = user_id
skip! if sale.email == nil
skip! if sale.order_date == nil
skip! if sale.amount == nil
end
end
import.run!
redirect_to lifecycle_grid_sales_path, notice: import.report.message
end
And then call it:
import({file: params[:file]})
import({path: './public/uploads/gotcha.csv'})
It doesn't seem that method belongs to your controller so you may want to extract it somewhere. I encourage you to check this great article and extract your method into brand new Service object.

wrapping code that runs in Rails environment into a module

I have this code and it works perfectly
require "date"
#past = []
#future = []
#artist = Artist.find(2)
def sort_by_date(artist)
artist.events.each do |event|
if event.date < DateTime.now
#past << event.id
else
#future << event.id
end
end
end
def event_title(arr)
arr.each do |event_id|
e = Event.find(event_id)
artist_names = []
e.artists.each do |artist|
unless artist.name == #artist.name
artist_names << artist.name
end
end
puts "#{e.name} with #{artist_names.join(", ")} at #{(Venue.find(e.venue_id)).name}"
end
end
sort_by_date(#artist)
puts "Upcoming Events: "
event_title(#future)
puts "Past Events: "
event_title(#past)
I want to run wrap this operation into a module, but I'm having trouble understanding how to pass artist_id to it properly. With this command rails runner app/modules/artist_event_sort.rb, I'm getting this error: ``': undefined method sort_by_date' for SortedArtistEvents:Module (NoMethodError). The two methods sort_by_date and event_title worked as they should before I tried wrapping this whole operation up into a module, so that's where I know I've missed something.
module SortedArtistEvents
require "date"
attr_accessor :artist_id
def initialize(artist_id)
#past = []
#future = []
#artist = Artist.find(artist_id)
end
def sort_by_date(artist)
artist.events.each do |event|
if event.date < DateTime.now
#past << event.id
else
#future << event.id
end
end
end
def event_title(arr)
arr.each do |event_id|
e = Event.find(event_id)
artist_names = []
e.artists.each do |artist|
unless artist.name == #artist.name
artist_names << artist.name
end
end
puts "#{e.name} with #{artist_names.join(", ")} at #{(Venue.find(e.venue_id)).name}"
end
end
sort_by_date(#artist)
puts "Upcoming Events: "
self.event_title(#future)
puts "Past Events: "
event_title(#past)
end
class LetsSort
include SortedArtistEvents
end
test_artist_sort = LetsSort.new(2)
It looks like there are a couple things wrong here. You are trying to initialize a module, you can only initialize a class, e.g. class SortedArtistEvents.
If you have this:
module Foo
def bar; end
end
bar is only accessible by including or extending a module or class with Foo. With your error undefined method sort_by_date' for SortedArtistEvents:Module you would have to do
module SortedArtistsEvents
def self.sort_by_date; end
end
to get behavior like SortedArtistsEvents.sort_by_date

More ruby-like way of writing simple ActiveRecord code

Here is some fairly standard Ruby on Rails 4 ActiveRecord code:
def hide(user)
self.hidden = true
self.hidden_on = DateTime.now
self.hidden_by = user.id
end
def unhide
self.hidden = false
self.hidden_on = nil
self.hidden_by = nil
end
def lock(user)
self.locked = true
self.locked_on = DateTime.now
self.locked_by = user.id
end
def unlock
self.locked = false
self.locked_on = nil
self.locked_by = nil
end
# In effect this is a soft delete
def take_offline(user)
hide(user)
lock(user)
end
The code is easy to understand and doesn't try to be clever. However it feels verbose. What would be a more succinct or canonical way of specifying this code/behaviour?
Well, it's a trade-off, but if you want to be more clever, you can do something like:
def self.def_toggle(type, field)
define_method(type) do |user|
send("#{field}=", true)
send("#{field}_on=", DateTime.now)
send("#{field}_by=", user.id)
end
define_method("un#{type}") do
send("#{field}=", false)
send("#{field}_on=", nil)
send("#{field}_by=", nil)
end
end
def_toggle(:hide, :hidden)
def_toggle(:lock, :locked)
It's a bit extreme unless you have a lot of these or you want to encapsulate a bit more logic. But you can do something like the following using composed_of
class Model < ActiveRecord::Base
composed_of :hidden, class_name: 'State', mapping: %w(hidden, hidden_on, hidden_by)
composed_of :locked, class_name: 'State', mapping: %w(locked, locked_on, locked_by)
def hide(user)
hidden.on
end
def unhide
hidden.off
end
def lock(user)
locked.on
end
def unlock
locked.off
end
end
class State < Struct.new(:state, :on, :by)
def on(user)
set(true, user)
end
def off
set(false, nil, nil)
end
def on?
state
end
def off?
!on
end
private
def set(state, by, on = Time.current)
self.state = state
self.by = by
self.on = on
end
end

Setup fake data just once

I'm working on a rails application with no models. I have a class in lib, FakeMaker, that builds up a bunch of fake entities for display purposed.
I want to test out deletion functionality but the problem is that my fake data set re-initializes every time I hit the controller.
I'd like to run the data test creator only once so that I only have one set of fake data.
I have tried using ||=, before_filter, class methods in FakeMaker, sessions storage but they all seem to have the issue of reinitializing the data set everytime the controller is hit.
Controller code:
class HomeController < ApplicationController
include FakeMaker
before_filter :set_up_fake_data
def index
#workstations = #data[:workstations]
#data_sources = #data[:data_sources]
end
private
def set_fake_data
#data ||= session[:fake_data]
end
def initialize_data
session[:fake_data] = set_up_fake_data
end
end
FakeMaker in lib:
module FakeMaker
include ActionView::Helpers::NumberHelper
SOURCE_CIDNE = "CIDNE"
SOURCE_DCGS = "DCGS"
TYPES_CIDNE = Faker::Lorem.words(num = 10)
TYPES_DCGS = Faker::Lorem.words(num = 4)
def set_up_fake_data
#data ||= { workstations: fake_maker("workstation", 8), data_sources: fake_maker("data_source", 2) }
end
def fake_maker(type, count)
fake = []
case type
when "report"
count.times { fake << fake_report }
when "workstation"
count.times { fake << fake_workstation }
when "data_source"
fake = fake_data_source
end
fake
end
def fake_report
report = { source: [SOURCE_CIDNE, SOURCE_DCGS].sample,
count: number_with_delimiter(Faker::Number.number(5), :delimiter => ',') }
report[:type] = report[:source] == SOURCE_CIDNE ? TYPES_CIDNE.sample : TYPES_DCGS.sample.capitalize
report
end
def fake_workstation
{ name: Faker::Lorem.word,
downloaded: number_with_delimiter(Faker::Number.number(3), :delimiter => ','),
available: number_with_delimiter(Faker::Number.number(5), :delimiter => ','),
last_connect: fake_time,
queueJSON: fake_queueJSON,
historyJSON: fake_historyJSON }
end
def fake_data_source
data_sources = []
["CIDNE", "DCGS"].each do |source|
data_sources << { type: source,
name: Faker::Internet.url,
status: ["UP", "DOWN"].sample }
end
data_sources
end
def fake_historyJSON
history = []
12.times { history << fake_history }
history
end
def fake_queueJSON
queue = []
35.times { queue << fake_queue }
queue
end
def fake_history
{ sentTimestamp: fake_time,
reportID: Faker::Number.number(5)}
end
def fake_queue
{ priority: Faker::Number.number(3),
queuedTimestamp: fake_time,
reportID: Faker::Number.number(5)}
end
def fake_time
Random.rand(10.weeks).ago.strftime("%Y-%m-%d %H:%M:%S")
end
end

How can I refactor these common controller methods?

I have a few controller methods that are extremely similar and I was wondering what the best way to refactor them would be. First thing that comes to mind would be somehow passing in two blocks to a helper method, but I'm not sure how to do that either.
def action_a
if #last_updated.nil?
#variable_a = #stuff_a
else
#variable_a = (#stuff_a.select{ |item| item.updated_at > #last_updated }
end
end
def action_b
if #last_updated.nil?
#variable_b = #stuff_b.some_method
else
#variable_b = #stuff_b.some_method.select{ |stuff| item.updated_at > #last_updated }
end
end
It just seems like I'm constantly checking if #last_updated is nil (I set the #last_updated instance variable in a before_filter. If I could somehow pass the stuff inside the if as a block and the stuff in the else as another block, then I could remove the if #last_updated.nil? duplication?
What is the best way of accomplishing this for many methods?
Update
Where I specify #stuff_a and #stuff_b, they are always returning an array (since I use .select).
Take a look at this. It's DRYer and should yield identical results.
def action_a
do_the_processing :"#variable_a", #stuff_a
end
def action_b
do_the_processing :"#variable_b", #stuff_b.some_method
end
private
def do_the_processing var_name, collection
if #last_updated.nil?
instance_variable_set var_name, collection
else
instance_variable_set var_name, collection.select{ |item| item.updated_at > #last_updated }
end
end
Update
And here's the two blocks approach (just for fun) (uses 1.9's stabby lambda syntax)
def action_a
check_last_updated is_nil: -> { #variable_a = #stuff_a },
is_not_nil: -> { #variable_a = (#stuff_a.select{ |item| item.updated_at > #last_updated } }
end
def action_b
check_last_updated is_nil: -> { #variable_b = #stuff_b.some_method },
is_not_nil: -> { #variable_b = #stuff_b.some_method.select{ |stuff| item.updated_at > #last_updated } }
end
private
def check_last_updated blocks = {}
if #last_updated.nil?
blocks[:is_nil].try(:call)
else
blocks[:is_not_nil].try(:call)
end
end
You need to extract your condition in a separate def block and use it later on:
def select_updates a
#last_updated.nil? ? a : a.select{ |item| item.updated_at > #last_updated }
end
def action_a; #variable_a = select_updates(#stuff_a) end
def action_b; #variable_b = select_updates(#stuff_b.some_method) end
AS I can see, you could do the followings
have two scope for each
Ex:
class Stuff < ActiveRecord::Base
scope :updated_at, lambda {|updated_date|
{:conditions => "updated_at > #{updated_date}"}
}
end
class Item < ActiveRecord::Base
scope :updated_at, lambda {|updated_date|
{:conditions => "updated_at > #{updated_date}"}
}
end
in your controller do this
def action_a
#variable_a = update_method(#stuff_a)
end
def action_b
#variable_b = update_method(#stuff_b)
end
private
def update_method(obj)
result = nil
if #last_updated.nil?
result = obj.some_method
else
result = obj.some_method.updated_at(#last_updated)
end
result
end
HTH

Resources