Create Class Specific RuboCop Cop - ruby-on-rails

We have a library that we use internally that requires you to return a specific type from the method you define. I'm trying to write a cop that enables us to detect that case. Here is the situation I want to detect and flag:
You are inside the Slayer::Command class
You've returned something other than a call to pass or flunk!
It doesn't seem like there's an easy way to check both the call and the parent class, but I know it's doable, because the Rails/HasAndBelongsToMany cop only triggers inside Rails models, not Rails controllers. More specifically, there definitely isn't an easy way to iterate all returns, because the number 1 cause of this bug is people forgetting that the last branch of a method is the return balue.
What I have so far is this:
require 'rubocop'
module Slayer
class CommandReturn < RuboCop::Cop::Base
def_node_search :explicit_returns, 'return'
def_node_matcher :slayer_command?, "(class (const (const nil :Slayer) :Command) _)"
def_node_matcher :is_call_to_pass?, "(send nil :pass ?)"
def_node_matcher :is_call_to_flunk?, "(send nil :flunk! ?)"
def on_def(node)
return unless node.method?(:call)
return unless in_slayer_command?(node)
explicit_returns(node) do |node|
validate_return! node.child_nodes.first, node
end
return # Temporarily does not look at implicit returns
implicit_returns(node) do |node|
validate_return! node
end
end
private
# Continue traversing `node` until you get to the last expression.
# If that expression is a call to `.can_see?`, then add an offense.
def implicit_returns(node)
raise "Not Implemented Yet"
end
def in_slayer_command?(node)
node.ancestors.any? &:slayer_command?
end
def validate_return!(node, return_node = nil)
return if is_call_to_pass? node
return if is_call_to_flunk? node
add_offense(return_node || node, message: "call in Slayer::Command must return the result of `pass` or call `flunk!`")
end
end
end
I need help with two things:
Is this really the best way to check the class' parent? What about inheriting from something else that inherits from Slayer::Command? Something feels off here.
What's the best way to implement the implicit_returns method? That feels like it should be baked in and I'm just missing it.

Related

How to mock a chain of methods in Minitest

It looks like the only source of information for stubbing a chain of methods properly are 10+ years ago:
https://www.viget.com/articles/stubbing-method-chains-with-mocha/
RoR: Chained Stub using Mocha
I feel pretty frustrated that I can't find information of how to do this properly. I want to basically mock Rails.logger.error.
UPDATE: I basically want to do something as simple as
def my_action
Rails.logger.error "My Error"
render json: { success: true }
end
And want to write a test like this:
it 'should call Rails.logger.error' do
post my_action_url
???
end
I think maybe you misunderstood the term chain of methods in this case, they imply the chain of ActiveRecord::Relation those be able to append another. In your case Rails.logger is a ActiveSupport::Logger and that's it. You can mock the logger and test your case like this:
mock = Minitest::Mock.new
mock.expect :error, nil, ["My Error"] # expect method error be called
Rails.stub :logger, mock do
post my_action_url
end
mock.verify
Beside that, I personally don't like the way they test by stub chain of method, it's so detail, for example: i have a test case for query top ten users and i write a stub for chain of methods such as User.where().order()..., it's ok at first, then suppose i need to refactor the query or create a database view top users for optimize performance purpose, as you see, i need to stub the chain of methods again. Why do we just treat the test case as black box, in my case, the test case should not know (and don't care) how i implement the query, it only check that the result should be the top ten users.
update
Since each request Rails internal call Logger.info then if you want ignore it, you could stub that function:
def mock.info; end
In case you want to test the number of method called or validate the error messages, you can use a spy (i think you already know those unit test concepts)
mock = Minitest::Mock.new
def mock.info; end
spy = Hash.new { |hash, key| hash[key] = [] }
mock.expect(:error, nil) do |error|
spy[:error] << error
end
Rails.stub :logger, mock do
post my_action_url
end
assert spy[:error].size == 1 # call time
assert spy[:error] == ["My Error"] # error messages
It's better to create a test helper method to reuse above code. You can improve that helper method behave like the mockImplementation in Jest if you want.

Mocking Rspec any_instance_of method return value relative to instance's actual args

I'm running into an issue where I'm trying to mock out an async method in a service class, but I want the result set to be based on the inputs to the call.
eg:
allow_any_instance_of(Emails::EmailService)
.to receive(:send_many_async)
.and_wrap_original { |m, *args| args[1].map { |recipient| Emails::EmailResult.new(recipient.email) } }
The signature for this method is send_many_async(email, users)
I don't think that and_wrap_original is behaving as I expect though, as when I print out the values for m and args I see:
#<Method: Emails::EmailService#__send_many_async_without_any_instance__(send_many_async)>
#<Emails::EmailService:0x007fa5c4e1b448>
#<Emails::MyEmail:0x007fa5c4e1b4e8>
As in, where I would expect arg[0] to be the email object, and arg[1] to be the list of users -- instead arg[0] is the service I'm mocking and there is no users list at all (eg: an arg[2])
I effectively just want to confirm the method is called, and that the users are the count I expect them to be.
Alternatively, if I could substitute the method for another on the same class that would work too (the non-async call) but I haven't been able to find a way to replace :send_many_async with the regular :send_many which is successfully mocked in another way (that I cannot apply here)
Anyone succeed doing something like this? I was expecting and_wrap_original to provide what was passed into the instance's method, but clearly I am confused in this regard.
Edit
If anyone is wondering, here's a working test sample. It's unfortunately not working for me quite yet in my application but others may find it helpful.
class TestEmailSvc
def send_many_async(email, to_email_users, from_email_user: nil, send_at: nil)
puts 'Real one called!'
end
end
class TestEmail
end
class TestEmailUser
attr_reader :email
def initialize(email)
#email = email
end
end
class TestEmailResult
attr_reader :user
def initialize(user)
#user = user
end
def status
:pending
end
end
describe 'Test Spec' do
it 'tests my mock' do
allow_any_instance_of(TestEmailSvc)
.to receive(:send_many_async)
.and_wrap_original { |m, _, args| args.map { |recipient| TestEmailResult.new(recipient) } }
svc = TestEmailSvc.new
users = [
TestEmailUser.new('email 1'),
TestEmailUser.new('email 2')
]
results = svc.send_many_async(TestEmail.new, users)
expect(results.count).to eql 2
end
end
When you use allow_any_instance_of(...).to receive(...).and_wrap_original, the block receives the following:
The original method
The instance that received the message (in case you want to use any of the state of the instance in your block)
The arguments passed by the caller, in order
In your case, it sounds like you don't need to use the EmailService instance in your block, so you can just ignore that argument:
allow_any_instance_of(Emails::EmailService)
.to receive(:send_many_async)
.and_wrap_original { |m, _, *args| args[1].map { |recipient| Emails::EmailResult.new(recipient.email) } }
Alternately, I'd encourage you to consider if there's a way to improve the interface of Emails::EmailService so that you don't need to use any_instance (which suffers from these sorts of confusing usability problems and also tends to calcify existing designs instead of putting pressure on your design). We talk about this in my book Effective Testing with RSpec 3: Build Ruby Apps with Confidence.

How it works find_by_* in rails

May be its weird for some people about the question. By looking at the syntax its identifiable as class method.
Model.find_by_*
So if its class method it should be defined either in model we created or in
ActiveRecord::Base
So my question is how rails manages to add these methods and makes us available.
Examples like
Model.find_by_id
Model.find_by_name
Model.find_by_status
and etc.
You need to look at ActiveRecord::FinderMethods. Here you can find more details.
Internally, it fires a WHERE query based on attributes present in find_by_attributes. It returns the first matching object.
def find_by_attributes(match, attributes, *args)
conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
result = where(conditions).send(match.finder)
if match.bang? && result.nil?
raise RecordNotFound, "Couldn't find #{#klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
else
yield(result) if block_given?
result
end
end
There is also find_all_by_attributes that returns all matching records.
Rails are using ruby metaprogramming method_missing for that. The method find_by_name is not in a model, instead of this rails are taking name as first argument and it calls it like find_by(name: ?) which is calling where(name: ?).take

How to return both results and errors from a Repository in an implementation-independent way?

I'm trying to implement a subset of the Repository pattern using Rails and am having some trouble understanding how to pass errors from a class or repository, back to the controller and then in to the view.
For instance, I have a class DomainClass which simply allows users to register a subdomain. There are some rules there - it must be unique, must only contain letters and numbers - what have you.
When one or more model validations fails, I want to pass this value to the view somehow. While passing those errors, I must also return "false" to the controller so it knows that whatever I've tried to do has failed.
This seems very simple. What am I missing here?
Class - if this fails, I should pass the validation error to my controller.
# Create a domain ID and return the newly injected ID.do
# If a new Domain ID couldn't be created for any reason,
# return FALSE.
def create_domain(domain_name)
#domain.domain_name = domain_name
if #domain.save
return #domain.id
else
return false
end
end
Controller - From here, I should return the model's validation error to the view.
# Try to save our user to the database
if new_user = #domain.create_domain(domain_name)
# Do things that are good.
else
# Return model's validation error here.
end
I see two options for designing create_domain in a way that will still make sense when you reimplement it on top of some non-ActiveRecord store. Which one you would use depends on the situations in which you expect to use it.
Define a class to hold all of create_domain's possible return values. This would be a start:
class SaveResult < Struct.new :id, :errors
def succeeded?
errors.empty?
end
end
In create_domain,
return SaveResult.new *(
if #domain.save
#domain.id, []
else
nil, #domain.errors # this is an ActiveModel::Errors, but tell your callers it's a Hash
end
)
Then a caller can do something like
result = #domain.create_domain name
if result.succeeded?
# celebrate
else
# propagate errors to model
end
This design has the disadvantage that a caller has to remember to check whether there are errors. It would work well if most callers have to do something explicitly with the errors if there are any (as is the case above).
Define an exception to be raised when there are errors:
class SaveError < Exception
attr_accessor :errors # again, pretend it's just a hash
def initialize(errors)
self.errors = errors
end
end
In create_domain,
if #domain.save
return #domain.id
else
raise SaveResult, #domain.errors
emd
Then a caller can do something like
begin
new_user_id = #domain.create_domain name
# celebrate
rescue SaveError => e
# propagate errors to model
end
This design has the disadvantage that exception handling is a bit uglier to write than an if/else. It has the advantage that if you can just allow all such exceptions to propagate out of the caller and handle them in one place in ActionController#rescue_from or something like that, callers wouldn't need to write any error handling at all.

How to transform a string into a variable/field?

I'm new to Ruby and I would like to find out what the best way of doing things is.
Assume the following scenario:
I have a text field where the user can input strings. Based on what the user inputs (after validation) I would like to access different fields of an instance variable.
Example: #zoo is an instance variable. The user inputs "monkey" and I would like to access #zoo.monkey. How can I do that in Ruby?
One idea that crossed my mind is to have a hash:
zoo_hash = { "monkey" => #zoo.monkey, ... }
but I was wondering if there is a better way to do this?
Thanks!
#zoo.attributes gives you a hash of the object attributes. So you can access them like
#zoo.attributes['monkey']
This will give nil if the attribute is not present. Calling a method which doesn't exist will throw NoMethodError
In your controller you could use the public_send (or even send) method like this:
def your_action
#zoo.public_send(params[:your_field])
end
Obviously this is no good, since someone can post somehing like delete_all as the method name, so you must sanitize the value you get from the form. As a simple example:
ALLOWED_METHODS = [:monkey, :tiger]
def your_action
raise unless ALLOWED_METHODS.include?(params[:your_field])
#zoo.public_send(params[:your_field])
end
There is much better way to do this - you should use Object#send or (even better, because it raises error if you try to call private or protected method) Object#public_send, like this:
message = 'monkey'
#zoo.public_send( message )
You could implement method_missing in your class and have it interrogate #zoo for a matching method. Documentation: http://ruby-doc.org/core-1.9.3/BasicObject.html#method-i-method_missing
require 'ostruct' # only necessary for my example
class ZooKeeper
def initialize
#zoo = OpenStruct.new(monkey: 'chimp')
end
def method_missing(method, *args)
if #zoo.respond_to?(method)
return #zoo.send(method)
else
super
end
end
end
keeper = ZooKeeper.new
keeper.monkey #=> "chimp"
keeper.lion #=> NoMethodError: undefined method `lion'

Resources