How to refactor method to lower RuboCop's ABCsize - ruby-on-rails

On my journey to learn ruby & rails I went on and installed Rubocop. So far it's been a great help in refactoring my code the ruby-way, but now I think I've hit the wall with this helpless case. Given the following method for creating a new entity, I'm looking for a way to refactor it to make Rubocop stop yelling at me about:
Line length
Assignment Branch Condition size (currently 26.02/15)
the only thing that I can think of for the moment, except of disabling those cops ofc, is actually splitting up the model into two smaller ones (say basic info and financial) and set them up accordingly, but I get the impression that this would move the complexity out of the creation method and put it elsewhere, as I would need to remember to create both related entities etc. Any hints are more than welcome.
def create_store_information(store, meta)
user = #datasource.user
user.store_informations.create!(
name: store['name'],
description: store['description'],
status: 1,
url: store['URL'].downcase,
store_version: store['version'],
api_version: store['wc_version'],
timezone: meta['timezone'],
currency: meta['currency'],
currency_format: meta['currency_format'],
currency_position: meta['currency_position'],
thousand_separator: meta['thousand_separator'],
decimal_separator: meta['decimal_separator'],
price_num_decimals: meta['price_num_decimals'],
tax_included: cast_to_bool(meta['tax_included']),
weight_unit: meta['weight_unit'],
dimension_unit: meta['dimension_unit'],
ssl_enabled: cast_to_bool(meta['ssl_enabled']),
permalinks_enabled: cast_to_bool(meta['permalinks_enabled']),
generate_password: cast_to_bool(meta['generate_password']),
user: user
)
end
Edit:
As per request, I'm attaching a second sample of creating store_information from a different class.
def create_store_information(store, meta)
user = #datasource.user
user.store_informations.create!(
name: store['id'],
description: store['name'],
status: 1,
url: store['domain'].downcase,
store_version: '1.0',
api_version: '1.0',
timezone: meta['timezone'],
currency: meta['currency'],
currency_format: meta['money_format'],
currency_position: '', # not applicable
thousand_separator: '', # not applicable, take from user's locale
decimal_separator: '', # not applicable, take from user's locale
price_num_decimals: '', # not applicable, take from user's locale
tax_included: cast_to_bool(meta['taxes_included']),
weight_unit: nil, # not applicable
dimension_unit: nil, # not applicable
ssl_enabled: cast_to_bool(meta['force_ssl']),
permalinks_enabled: true,
generate_password: false,
user: user
)
end

This is just 1 suggestion out of many possibilities.
You can use Ruby's meta programming abilities to dynamically send methods.
The meta object's fields are easy to assign the user.store_informations because the fields match 1 for 1.
It is also possible for the store object but it wouldn't be as straightforward.
You can move the fields to an array inside your class definition:
CAST_TO_BOOL = %w(
tax_included
ssl_enabled
permalinks_enabled
generate_password
).freeze
META_FIELDS = %w(
timezone
currency
currency_format
currency_position
thousand_separator
decimal_separator
price_num_decimals
tax_included
weight_unit
dimension_unit
ssl_enabled
permalinks_enabled
generate_password
).freeze
then you could define a private method which dynamically sets the meta fields of the user.store_informations
private
def set_meta_fields_to_store_information(user)
META_FIELDS.each do |field|
if CAST_TO_BOOL.include? field
user.store_informations.__send__ "#{f}=" { cast_to_bool( meta[field] ) }
next
end
user.store_informations.__send__ "#{f}=" { meta[field] }
end
end
then you could call that method instead:
def create_store_information(store, meta)
user = #datasource.user
user.store_informations.new(
name: store['name'],
description: store['description'],
status: 1,
url: store['URL'].downcase,
store_version: store['version'],
api_version: store['wc_version'],
user: user
)
set_meta_fields_to_store_information(user)
user.save!
end
Edit#2
Regarding populating the fields with objects of different classes;
One way to go about it would be to define a method which assign's the fields for you depending on the class of the store.
But then again, if you have thousands of different stores, this probably wouldn't be optimal.
class StoreA; end
class StoreB; end
class StoreC; end
then:
# you could also use dynamic method dispatching here instead:
def set_store_information_to_user(store, user)
case store
when StoreA
assign_store_a_method(store, user)
when StoreB
assign_store_b_method(store, user)
when StoreC
assign_store_c_method(store, user)
end
end
private
def assign_store_a_method(store, user); end
def assign_store_b_method(store, user); end
def assign_store_c_method(store, user); end

Related

Rails how to use where method for search or return all?

I am trying to do a search with multiple attributes for Address at my Rails API.
I want to search by state, city and/or street. But user doesn't need to send all attributes, he can search only by city if he wants.
So I need something like this: if the condition exists search by condition or return all results of this condition.
Example:
search request: street = 'some street', city = '', state = ''
How can I use rails where method to return all if some condition is nil?
I was trying something like this, but I know that ||:all doesn't work, it's just to illustrate what I have in mind.:
def get_address
address = Adress.where(
state: params[:state] || :all,
city: params[:city] || :all,
street: params[:street] || :all)
end
It's possible to do something like that? Or maybe there is a better way to do it?
This is a more elegant solution using some simple hash manipulation:
def filter_addesses(scope = Adress.all)
# slice takes only the keys we want
# compact removes nil values
filters = params.permit(:state, :city, :street).to_h.compact
scope = scope.where(filters) if filters.any?
scope
end
Once you're passing a column to where, there isn't an option that means "on second thought don't filter by this". Instead, you can construct the relation progressively:
def get_address
addresses = Address.all
addresses = addresses.where(state: params[:state]) if params[:state]
addresses = addresses.where(city: params[:city]) if params[:city]
addresses = addresses.where(street: params[:street]) if params[:street]
addresses
end
I highly recommend using the Searchlight gem. It solves precisely the problem you're describing. Instead of cluttering up your controllers, pass your search params to a Searchlight class. This will DRY up your code and keep your controllers skinny too. You'll not only solve your problem, but you'll have more maintainable code too. Win-win!
So in your case, you'd make an AddressSearch class:
class AddressSearch < Searchlight::Search
# This is the starting point for any chaining we do, and it's what
# will be returned if no search options are passed.
# In this case, it's an ActiveRecord model.
def base_query
Address.all # or `.scoped` for ActiveRecord 3
end
# A search method.
def search_state
query.where(state: options[:state])
end
# Another search method.
def search_city
query.where(city: options[:city])
end
# Another search method.
def search_street
query.where(street: options[:street])
end
end
Then in your controller you just need to search by passing in your search params into the class above:
AddressSearch.new(params).results
One nice thing about this gem is that any extraneous parameters will be scrubbed automatically by Searchlight. Only the State, City, and Street params will be used.

Updating Lots of Records at Once in Rails

I've got a background job that I run about 5,000 of them every 10 minutes. Each job makes a request to an external API and then either adds new or updates existing records in my database. Each API request returns around 100 items, so every 10 minutes I am making 50,000 CREATE or UPDATE sql queries.
The way I handle this now is, each API item returned has a unique ID. I search my database for a post that has this id, and if it exists, it updates the model. If it doesn't exist, it creates a new one.
Imagine the api response looks like this:
[
{
external_id: '123',
text: 'blah blah',
count: 450
},
{
external_id: 'abc',
text: 'something else',
count: 393
}
]
which is set to the variable collection
Then I run this code in my parent model:
class ParentModel < ApplicationRecord
def update
collection.each do |attrs|
child = ChildModel.find_or_initialize_by(external_id: attrs[:external_id], parent_model_id: self.id)
child.assign_attributes attrs
child.save if child.changed?
end
end
end
Each of these individual calls is extremely quick, but when I am doing 50,000 in a short period of time it really adds up and can slow things down.
I'm wondering if there's a more efficient way I can handle this, I was thinking of doing something instead like:
class ParentModel < ApplicationRecord
def update
eager_loaded_children = ChildModel.where(parent_model_id: self.id).limit(100)
collection.each do |attrs|
cached_child = eager_loaded_children.select {|child| child.external_id == attrs[:external_id] }.first
if cached_child
cached_child.update_attributes attrs
else
ChildModel.create attrs
end
end
end
end
Essentially I would be saving the lookups and instead doing a bigger query up front (this is also quite fast) but making a tradeoff in memory. But this doesn't seem like it would be that much time, maybe slightly speeding up the lookup part, but I'd still have to do 100 updates and creates.
Is there some kind of way I can do batch updates that I'm not thinking of? Anything else obvious that could make this go faster, or reduce the amount of queries I am doing?
You can do something like this:
collection2 = collection.map { |c| [c[:external_id], c.except(:external_id)]}.to_h
def update
ChildModel.where(external_id: collection2.keys).each |cm| do
ext_id = cm.external_id
cm.assign_attributes collection2[ext_id]
cm.save if cm.changed?
collection2.delete(ext_id)
end
if collection2.present?
new_ids = collection2.keys
new = collection.select { |c| new_ids.include? c[:external_id] }
ChildModel.create(new)
end
end
Better because
fetches all required records all at once
creates all new records at once
You can use update_columns if you don't need callbacks/validations
Only drawback, more ruby code manipulation which I think is a good tradeoff for db queries..

Rails -- Export CSV failing if there is a blank field

I have code in my Rails app that allows me to export a CSV file. It works fine unless there is a record that has a field with no value in it. In that case it fails. As an example, the specific failure I'm getting is saying something liek "No Method Error" and it specifically references "address_line_1" because there are some users with no address_line_1. That is just one example though. Really all fields should be protected against potential blanks. Here is the code:
def download_kids_csv
#csv_headers = ['First',
'Last',
'Child First',
'Child Last',
'Parent Email',
'School',
'Class',
'Address',
'City',
'State',
'Zip',
'Parent Phone']
#kid_data = []
#school = School.find(params[:school_id])
#school.classrooms.each do |classroom|
classroom.kids.includes(:users).each do |kid|
kid.users.each do |parent|
#kid_data << {
first: parent.first_name,
last: parent.last_name,
child_first: kid.first_name,
child_last: kid.last_name,
parent_email: parent.email,
school: #school.name,
class: classroom.classroom_name,
address: parent.addresses.first.address_line_1,
city: parent.addresses.first.city,
state: parent.addresses.first.state,
zip: parent.addresses.first.zip_code,
parent_phone: parent.phones.first.phone_number
}
end
end
end
respond_to do |format|
format.csv do
headers['Content-Disposition'] = "attachment; filename=\"#{#school.name.downcase.gsub(' ', '-')}-data.csv\""
headers['Content-Type'] ||= 'text/csv'
end
end
end
Ok so the problem you are get is because you are calling method on a nil value.
So for example when you do:
kid.first_name
and kid is nil you are doing this
nil.first_name
nil does not implement the first_name method so it throws an error. WHat you could do to circumvent this (its kinda ugly) is this
kid.try(:first_name)
This will prevent you form getting those method missing errors
For those long chains you can do the following
parent.try(:addresses).try(:first).try(:zip_code)
This should save you a lot of headache, but the root cause of your issue is data integrity you would not have to do all of this if you ensured that your data was not blank. I do however understand in the real world it easier said than done. I could give you a lecture about The Law of Demeter and how you should not be running across object to access their attributes, and how thats a code smell of bad organization of data, but its a spread sheet and sometimes you just need the data. Good luck!
To build off of the earlier answer, you can also utilize the so-called lonely operator &. if you're on Ruby 2.3.
An example would look something like this: kid&.first_name.
If you're not on that version of ruby yet, there's a good gem that can help you out in this situation that's a little bit more robust than .try.
Using that gem your code would look like kid.andand.first_name. It might be overkill in this case but the difference here is that it will only perform the first_name method call if kid is not nil. For your longer chains, parent.address.first.zip_code, this would mean that the function chain would exit immediately if parent was nil instead of calling all of the different attributes with try.
Is it possible to use unless or another conditional?
unless parent.addresses.first.address_line_1.blank?
address: parent.addresses.first.address_line_1,
end
or
if parent.addresses.first.address_line_1 != nil
address: parent.addresses.first.address_line_1,
else
address: nil || "address is empty"
end

Rspec testing inside a loop

I am trying to test the code inside a loop, how would I go about this:
class MyClass
def initialize(topics, env, config, limit)
#client = Twitter::Streaming::Client.new(config)
#topics = topics
#env = env
#limit = limit
end
def start
#client.filter(track: #topics.join(",")) do |object|
# how would I test the code inside here, basically logical stuff
next if !object.is_a?(Twitter::Tweet)
txt = get_txt(object.text)
end
end
Is there a way to do this?
If think that you can use a double of your Twitter::Streaming::Client that has a method filter and when this method is invoked it returns the desired output:
let(:client) { double 'Twitter Client', filter: twitters }
You will need to built manually the twitters object (sorry by my lack of context but I never used the Twitter client) and then you can make the assertions for the result of the start method.
As you can see, testing that code is quite tricky. This is because of the dependency on the Twitter client gem.
You can go down couple of paths:
Don't test it - the Twitter client gem should provide you with Twitter::Tweet objects. You only test your logic, i.e. get_txt method
Do what #Marcus Gomes said - create a collection double that has the filter method implemented.
What I would prefer to do is to stub the #client.filter call in the spec.
For example, in your spec:
some_collection_of_tweets = [
double(Twitter::Tweet, text: "I'll be back!"),
double(Twitter::Tweet, text: "I dare ya, I double dare ya!")
]
#my_class = MyClass.new(topics, env, config, limit)
allow(#my_class.client).to receive(:filter).and_return(some_collection_of_tweets)
This means that the some_collection_of_tweets collection will be returned every time the class calls #client.filter, and by having the data built by you, you what expectations to set.
One thing that you will have to change is to set an attr_reader :client on the class. The only side effect of this type of testing is that you are tying your code to the interfaces of the Twitter client.
But like everything else... tradeoffs :)
Hope that helps!
Perhaps you could do something like this if you really wanted to test your infinite loop logic?
RSpec.describe MyClass do
subject { MyClass.new(['foo','bar'], 'test', 'config', 1) }
let(:streaming_client) { Twitter::Streaming::Client.new }
describe '#start' do
let(:valid_tweet) { Twitter::Tweet.new(id: 1) }
before do
allow(Twitter::Streaming::Client).to receive(:new)
.with('config').and_return(streaming_client)
end
after { subject.start }
it '#get_txt receives valid tweets only' do
allow(valid_tweet).to receive(:text)
.and_return('Valid Tweet')
allow(streaming_client).to receive(:filter)
.with(track: 'foo,bar')
.and_yield(valid_tweet)
expect(subject).to receive(:get_txt)
.with('Valid Tweet')
end
it '#get_txt does not receive invalid tweets' do
allow(streaming_client).to receive(:filter)
.with(track: 'foo,bar')
.and_yield('Invalid Tweet')
expect(subject).not_to receive(:get_txt)
end
end
end

Moching rails association methods

Here is my helper method which I want to test.
def posts_correlation(name)
if name.present?
author = User.find_by_name(name)
author.posts.count * 100 / Post.count if author
end
end
A factory for user.
factory :user do
email 'user#example.com'
password 'secret'
password_confirmation { password }
name 'Brian'
end
And finally a test which permanently fails.
test "should calculate posts count correlation" do
#author = FactoryGirl.create(:user, name: 'Jason')
#author.posts.expects(:count).returns(40)
Post.expects(:count).returns(100)
assert_equal 40, posts_correlation('Jason')
end
Like this.
UsersHelperTest:
FAIL should calculate posts count correlation (0.42s)
<40> expected but was <0>.
test/unit/helpers/users_helper_test.rb:11:in `block in <class:UsersHelperTest>'
And the whole problem is that mocha doesn't really mock the count value of author's posts, and it returns 0 instead of 40.
Are there any better ways of doing this: #author.posts.expects(:count).returns(40) ?
When your helper method runs, it's retrieving its own object reference to your author, not the #author defined in the test. If you were to puts #author.object_id and puts author.object_id in the helper method, you would see this problem.
A better way is to pass the setup data for the author in to your mocked record as opposed to setting up expectations on the test object.
It's been a while since I used FactoryGirl, but I think something like this should work:
#author = FactoryGirl.create(:user, name: 'Jason')
(1..40).each { |i| FactoryGirl.create(:post, user_id: #author.id ) }
Not terribly efficient, but should at least get the desired result in that the data will actually be attached to the record.

Resources