Sweeper never triggers - ruby

I have the following usecase: I have an API and a UI which both modify the same type of object Vehicle. However, when a vehicle is modified from the UI, it must have a Vehicle Identification Number provided, while if it is modified, that number may not yet be known as it has not yet been stamped on the Engine Block (this is clearly a manufactured example).
So my model is:
class Vehicle < ActiveRecord::Base
after_initialize :set_ivars
def set_ivars
#strict_vid_validation = true
end
validate :vid, length: {maximum: 100, minimum: 30}, presence: true, if: lambda { |o| o.instance_variable_get(:#strict_vid_validation) }
validate :custom_vid_validator, unless: lambda { |o| o.instance_variable_get(:#strict_vid_validation) }
end
Two parent controllers:
class ApplicationController < ActionController::Base
before_filter :set_api_filter
private
def set_api_filter
#api = false
end
end
and an ApiController that sets #api to true
and the following vehicle controller:
class VehicleController < ApplicationController
cache_sweeper VehicleSweeper, only: [:create, :update]
def create
#vehicle = Vehicle.new
#vehicle.update_attributes(params[:vehicle])
end
end
with the following sweeper:
class VehicleSweeper < ActionController::Caching::Sweeper
observe Vehicle
def before_validation(vehicle)
if self.instance_variable_get(:#api)
vehicle.instance_variable_set(:#strict_vid_validation, false)
else
vehicle.instance_variable_set(:#strict_vid_validation, true)
end
end
And a somewhat similiar ApiVehicleController
However, this does not work. Through debugging I have discovered that:
1) The before_validation (nor do any other callbacks configured) method in the sweeper never runs
2) The more stringent ui validation is always triggered, this is due to
3) inside the lambda on the if: on the validate, the instance_variable is always true as it is never set through a callback method
Why is this not working? How can I fix it? Is there a different approach I could take if not?

In the end, the following worked:
def my_controller_action
#vehicle = Vehicle.new
#vehicle.assign_attributes(params[:vehicle])
VehicleSweeper.send(:new).pick_a_validation(#vehicle)
#vehicle.save
end
and in the sweeper removing the line
observe Vehicle
and renaming the before_validation method to pick_a_validation.
Of course this only works because I wanted to do a before_validation. If I wanted to do say an after_validation I have no idea how I would have hacked it.
In either case, I would love to hear how this is done without bypassing the callback chain entirely like I did.

Related

Metrics/AbcSize Too High: How do I decrease the ABC in this method?

I have recently started using Rubocop to "standardise" my code, and it has helped me optimise a lot of my code, as well as help me learn a lot of Ruby "tricks". I understand that I should use my own judgement and disable Cops where necessary, but I have found myself quite stuck with the below code:
def index
if params[:filters].present?
if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
# if owned is true, then we don't need to filter by admin
params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
# if admin is true, then must not filter by owned if false
params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
companies_list =
case params[:filters][:admin]&.to_b
when true
current_user.admin_companies
when false
current_user.non_admin_companies
end
if params[:filters][:owned].present?
companies_list ||= current_user.companies
if params[:filters][:owned].to_b
companies_list = companies_list.where(owner: current_user)
else
companies_list = companies_list.where.not(owner: current_user)
end
end
else
# Filters for deleted companies
companies_list = {}
end
end
companies_list ||= current_user.companies
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
Among others, the error that I'm getting is the following:
C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]
I understand the maths behind it, but I don't know how to simplify this code to achieve the same result.
Could someone please give me some guidance on this?
Thanks in advance.
Well first and foremost, is this code fully tested, including all the myriad conditions? It's so complex that refactoring will surely be disastrous unless the test suite is rigorous. So, write a comprehensive test suite if you don't already have one. If there's already a test suite, make sure it tests all the conditions.
Second, apply the "fat model skinny controller" paradigm. So move all the complexity into a model, let's call it CompanyFilter
def index
companies_list = CompanyFilter.new(current_user, params).list
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
and move all those if/then/else statements into the CompanyFilter#list method
tests still pass? great, you'll still get the Rubocop warnings, but related to the CompanyFilter class.
Now you need to untangle all the conditions. It's a bit hard for me to understand what's going on, but it looks as if it should be reducible to a single case statement, with 5 possible outcomes. So the CompanyFilter class might look something like this:
class CompanyFilter
attr_accessors :current_user, :params
def initialize(current_user, params)
#current_user = current_user
#params = params
end
def list
case
when no_filter_specified
{}
when user_is_admin
#current_user.admin_companies
when user_is_owned
# etc
when # other condition
# etc
end
end
private
def no_filter_specified
#params[:filter].blank?
end
def user_is_admin
# returns boolean based on params hash
end
def user_is_owned
# returns boolean based on params hash
end
end
tests still passing? perfect! [Edit] Now you can move most of your controller tests into a model test for the CompanyFilter class.
Finally I would define all the different companies_list queries as scopes on the Company model, e.g.
class Company < ApplicationRecord
# some examples, I don't know what's appropriate in this app
scope :for_user, ->(user){ where("...") }
scope :administered_by, ->(user){ where("...") }
end
When composing database scopes ActiveRecord::SpawnMethods#merge is your friend.
Post.where(title: 'How to use .merge')
.merge(Post.where(published: true))
While it doesn't look like much it lets you programatically compose scopes without overelying on mutating assignment and if/else trees. You can for example compose an array of conditions and merge them together into a single ActiveRecord::Relation object with Array#reduce:
[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 AND "posts"."author" = $2 LIMIT $3
So lets combine that with a skinny controllers approach where you handle filtering in a seperate object:
class ApplicationFilter
include ActiveModel::Attributes
include ActiveModel::AttributeAssignment
attr_accessor :user
def initialize(**attributes)
super()
assign_attributes(attributes)
end
# A convenience method to both instanciate and apply the filters
def self.call(user, params, scope: model_class.all)
return scope unless params[:filters].present?
scope.merge(
new(
permit_params(params).merge(user: user)
).to_scope
)
end
def to_scope
filters.map { |filter| apply_filter(filter) }
.compact
.select {|f| f.respond_to?(:merge) }
.reduce(&:merge)
end
private
# calls a filter_by_foo method if present or
# defaults to where(key => value)
def apply_filter(attribute)
if respond_to? "filter_by_#{attribute}"
send("filter_by_#{attribute}")
else
self.class.model_class.where(
attribute => send(attribute)
)
end
end
# Convention over Configuration is sexy.
def self.model_class
name.chomp("Filter").constantize
end
# filters the incoming params hash based on the attributes of this filter class
def self.permit_params
params.permit(filters).reject{ |k,v| v.blank? }
end
# provided for modularity
def self.filters
attribute_names
end
end
This uses some of the goodness provided by Rails to setup objects with attributes that will dynamically handle filtering attributes. It looks at the list of attributes you have declared and then slices those off the params and applies a method for that filter if present.
We can then write a concrete implementation:
class CompanyFilter < ApplicationFilter
attribute :admin, :boolean, default: false
attribute :owned, :boolean
private
def filter_by_admin
if admin
user.admin_companies
else
user.non_admin_companies
end
end
# this should be refactored to use an assocation on User
def filter_by_owned
case owned
when nil
nil
when true
Company.where(owner: user)
when false
Company.where.not(owner: user)
end
end
end
And you can call it with:
# scope is optional
#companies = CompanyFilter.call(current_user, params), scope: current_user.companies)

Refactor with Strategy Pattern. In Ruby

Heads up! In the below example, using a pattern is probably overkill... however, if I were extending this to count genres, count the members in a given band, count the number of fans, count the number of venues played, count the number of records sold, count the number of downloads for a specific song etc... it seems like there could be a ton of stuff to count.
The Goal:
To create a new function that chooses the correct counting function based on the input.
The Example:
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count
self.songs.length
end
def artist_count
self.artists.length
end
end
P.S. If you are also a curious about this question, you may find this other question (unfortunately answered in C#) to be helpful as a supplemental context. Strategy or Command pattern? ...
In Ruby you can implement a strategy pattern quite easily using an (optional) block (assuming it's still unused).
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count(&strategy)
count_using_strategy(songs, &strategy)
end
def artist_count(&strategy)
count_using_strategy(artists, &strategy)
end
private
def count_using_strategy(collection, &strategy)
strategy ||= ->(collection) { collection.size }
strategy.call(collection)
end
end
The above code defaults to using the size strategy. If you ever want to use a specific strategy in a specific scenario you can simply provide the strategy alongside the call.
genre = Genre.last
genre.song_count # get the song_count using the default #size strategy
# or provide a custom stratigy
genre.song_count { |songs| songs.count } # get the song_count using #count
genre.song_count { |songs| songs.length } # get the song_count using #length
If you need to re-use some strategies more often you could save them in a constant or variable:
LENGTH_STRATEGY = ->(collection) { collection.length }
genre.artist_count(&LENGTH_STRATEGY)
Or create a specific class for them if they are more complicated (currently overkill):
class CollectionStrategy
def self.to_proc # called when providing the class as a block argument
->(collection) { new(collection).call }
end
attr_reader :collection
def initialize(collection)
#collection = collection
end
end
class LengthStrategy < CollectionStrategy
def call
collection.length
end
end
genre.artist_count(&LengthStrategy)

Custom ActiveModel Validator Ruby

I would like to write a custom validator for a given validates call:
class Worker
include ActiveModel::Validations
def initialize(graph_object)
#graph_object = graph_object
end
attr_accessor :graph_object
validates :graph_object, graph_object_type: {inclusion: [:ready, :active]}
end
class GraphObject
attr_accessor :state
end
I would like to validate Worker#graph_object based on a GraphObject#state. So the Worker is valid when the passed in GrapObject is in a :ready or :active state. I would like to reuse as much of the ActiveModel as possible.
Validations documentation describes the process of setting up the custom validator but I can't figure out how to do it.
I think I have to start with this:
class GraphObjectTypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
end
end
options[:inclusion] = [:ready, :active]
record is the instance of the Worker(i think...)
value I have no idea (is value = record.graph_object ?)
attribute same as for value - no idea
Maybe validates :graph_object, graph_object_type: {inclusion: [:ready, :active]} isn't defined right?
OK I think I figured it out - I love puts debugging! Who needs pry!
One way of doing it would be:
class GraphObjectTypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if options.key?(:inclusion) && not_included?(value.type)
record.errors.add(attribute, "wrong graph object type")
end
end
private
def not_included?(type)
!options[:inclusion].include?(type)
end
end
options[:inclusion]: [:ready, :active] array
record: instance of the Worker
value: instance of the GraphObject
attribute: :graph_object symbol

Testing before_create method in rspec and rails 3

I've looked into some tutes and all I saw were old posts on how to test before_create. Also it seems like they're all just testing that before_create was called i.e.:
#user = User.new
#user.should_receive(:method_name_called_by_before_create)
#user.send(:before_create) (sometimes they just do #user.save)
I want to actually test that my method worked and that it had assigned(and saved the variables) after creating the record.
Here are my models:
user.rb
class User < ActiveRecord::Base
has_one :character, :dependent => :destroy
after_create :generate_character
private
def generate_character
self.create_character(:name => "#{email}'s avatar")
end
end
and character.rb
class Character < ActiveRecord::Base
belongs_to :user
before_create :generate_character
private
def generate_character
response = api_call
#API CALL HERE
#set object attributes here
self.stat1 = calculate_stat1(response) + 5
self.stat2 = calculate_stat2(response) + 5
self.stat3 = calculate_stat3(response) + 5
end
def api_call
return api_call_response
end
end
I want to test that generate character indeed set the attributes without going online and calling the API call. Is this possible with rspec? I have a fixture of a json response so I was hoping I can stub out generate character and then use the fake response for testing.
Here's my character.spec:
describe Character do
before(:each) do
Character.any_instance.stub!(:api_call).and_return(fake_response.read)
#user = Factory(:user)
#character = #user.character
puts #character.inspect
end
def fake_response
File.open("spec/fixtures/api_response.json")
end
It prints out only 5 for each of the character's stats. Also I did a puts response in the generate_character method in character.rb and it still prints out the "real" api call.
I managed to do a puts in fake_response and it does goes through there but it also goes through the "real" api_call after, which makes the stub obsolete. How do I get through this?
A good approach here is extracting your api call into a self contained method. Something like this:
class Character < ActiveRecord::Base
belongs_to :user
before_create :generate_character
private
def generate_character
data = api_call
#set object attributes from data
end
def api_call
# returns a data structure
# resulting from the call
end
end
Then use RSpec's any_instance to stub the api_call method to return a fixed data structure
Character.any_instance.stub!(:api_call).and_return { {:id => 1, :attribute_one => "foo"} }
#user = User.create
#user.character.attribute_one.should == "foo"
for more info on any_instance check this commit

Rails3: Nested model - child validates_with method results in "NameError - uninitialized constant [parent]::[child]"

Consider the following parent/child relationship where Parent is 1..n with Kids (only the relevant stuff here)...
class Parent < ActiveRecord::Base
# !EDIT! - was missing this require originally -- was the root cause!
require "Kid"
has_many :kids, :dependent => :destroy, :validate => true
accepts_nested_attributes_for :kids
validates_associated :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
# for simplicity, assume a single field: #item
validates_presence_of :item, :message => "is expected"
end
The validates_presence_of methods on the Kid model works as expected on validation failure, generating a final string of Item is expected per the custom message attribute supplied.
But if try validates_with, instead...
class Kid < ActiveRecord::Base
belongs_to :parent
validates_with TrivialValidator
end
class TrivialValidator
def validate
if record.item != "good"
record.errors[:base] << "Bad item!"
end
end
end
...Rails returns a NameError - uninitialized constant Parent::Kid error following not only an attempt to create (initial persist) user data, but also when even attempting to build the initial form. Relevant bits from the controller:
def new
#parent = Parent.new
#parent.kids.new # NameError, validates_* methods called within
end
def create
#parent = Parent.new(params[:parent])
#parent.save # NameError, validates_* methods called within
end
The error suggests that somewhere during model name (and perhaps field name?) resolution for error message construction, something has run afoul. But why would it happen for some validates_* methods and not others?
Anybody else hit a wall with this? Is there some ceremony needed here that I've left out in order to make this work, particularly regarding model names?
After a few hours away, and returning fresh -- Was missing require "Kid" in Parent class. Will edit.

Resources