Rubocop warning SymbolProc in blueprinter serializer custom field in Rails - ruby-on-rails

I have a Blueprinter serializer with a custom field (see screenshot)
field :score_details do |user|
user.score_details
end
and you can see a Rubocop warning for this block and I can't make it disappear. I read the Rubocop doc : SymbolProc but without success..
To explain in details: I have a User model in which I include a concern in order to calculate a score. In this concern I have 1 method (with no parameter) which returns a simple integer.
Finally, I use this method in my UserSerializer in order to render my score to my frontend.
Here is my include in my User model:
class User < ApplicationRecord
include UserScoresConcern
end
Here is my concern:
module UserScoresConcern
extend ActiveSupport::Concern
included do
def score_details
# this method return 45 for example
calculate_user_score_details
end
end
end
What can I do to fix this warning? Has anyone ever encountered the same issue?
Thanks 🙏

The field method takes a block that will be called with 2 arguments the object and the local_options. See BlockExtractor#extract
Your issue is that your block ignores the fact that 2 arguments are provided and thus Rubocop believes you can use Symbol#to_proc (because it thinks there is only a single argument sent to the block); however if you acknowledge the second argument this warning will go away.
field :score_details do |user,_|
user.score_details
end
Here we acknowledge the second argument using the underscore character _. This is a standard convention to show that we do not intend to use this argument.
Another way to determine the method is calling your block with 2 arguments is to use a lambda instead
field :score_details, &->(user) {user.score_details}
This will result in ArgumentError (wrong number of arguments (given 2, expected 1)) because the lambda is only expecting 1 argument but there are 2 arguments being passed in. This is one of the distinct differences between a standard Proc and a lambda (which is a "special" type of Proc).
The error you experienced Commented Here "wrong number of arguments (given 1, expected 0)" can be easily recreated to show what is actually occurring
def example
yield "10",2
end
example(&:to_s)
ArgumentError (wrong number of arguments (given 1, expected 0))
This because when using Symbol#to_proc in this instance it will evaluate as "10".to_s(2) but String#to_s does not take any arguments just like User#score_details thus causing user.score_details(local_options) to fail in the same fashion.

You just have to write it as
field :score_details, &:score_details
The reason for it is that the & prefix operator takes its argument (in this case, a symbol), calls its to_proc method, ensure it is actually a proc, and then use it as block argument.
For symbols, the to_proc method returns a one-argument proc that is equivalent to the __send__ method on the argument. That is:
# these are functionally equivalent:
:my_symbol.to_proc
->(arg) { arg.__send__(:my_symbol) }
->(arg) { arg.my_symbol }
The difference is that Symbol#to_proc is arguably more optimized.

I manage to make it work but without a custom field.
I add my method directly into my fields list like that and it works:
fields :email,
:score_details

Related

Style/OptionalBooleanParameter: Use keyword arguments when defining method with boolean argument

I am working on my rails RESTful API and I have set up a versioning feature on some of the endpoints. I have a class ApiVersion which Is responsible for determining which controllers to render base on the arguments passed to it on initialization.
The class definition looks as follows:
class ApiVersion
attr_reader :version, :default
def initialize(version, default = false)
#version = version
#default = default
end
# check whether version is specified or is default
def matches?(request)
check_headers(request.headers) || default
end
private
def check_headers(headers)
# check version from Accept headers; expect custom media type 'suits'
accept = headers[:accept]
accept&.include?("application/vnd.suits.#{version}+json")
end
end
The requests work perfectly fine but when I run rubocop -A I get an error that says:
Style/OptionalBooleanParameter: Use keyword arguments when defining method with boolean argument.
def initialize(version, default = false)
I searched on the internet how to fix this type of error & got some interesting ideas which could not work in my case. For example I found one post that said I should alternate the def initialize(version, default = false) with def initialize(version, default: false) which passes the rubocop tests but then I get an internal server error with an exception: ArgumentError: wrong number of arguments (given 2, expected 1).
Does anyone have an idea on how I can fix this, or how I can alternate the class definition, to get around this issue? Thank you
First off: if you disagree with a particular rule in a linter, then turn it off. In particular, this rule is in the "Style" category, so it is not a correctness or security issue, it is a matter of style.
Secondly, boolean parameters are a code smell, since they are often Flag Parameters. A method with a flag parameter will generally do two different things depending on the value of the boolean argument, because … why else would it have the flag parameter?
However, a method that does two different things should probably be two methods.
Or, in this particular case, since it is an object initializer method specifically, that hints at the fact that there should be two classes.
Okay, with that out of the way, the nice thing about Rubocop is that it generally tells you how to fix whatever it is complaining about. In this case, it suggests using a keyword parameter. That doesn't fix the problem that the method is likely still doing two different things, but at least, it gives a name to that difference, so you can see it at the call site.
So, what Rubocop is suggest is to change the positional parameter into a keyword parameter, something like this:
def initialize(version, default: false)
Now, obviously, when you change the parameter list at the definition site, you also need to change every argument list at every call site. So, if you have a call like this (remember that #initialize gets called by ::new):
ApiVersion.new('1.2.3', true)
You need to replace it with
ApiVersion.new('1.2.3', default: true)

Why isn't the args parameter used in ActionController::Instrumentation::render?

I am new to Ruby and to Rails, and am trying to understand fully what I'm reading.
I am looking at some of the Rails source code, in this case action_controller/metal/instrumentation.rb.
def render(*args)
render_output = nil
self.view_runtime = cleanup_view_runtime do
Benchmark.ms { render_output = super }
end
render_output
end
I understand that *args is using the splat operator to collect the arguments together into an array. But after that, it stops making much sense to me.
I can't fathom why render_output is set to nil before being reassigned to equal super and then called with no arguments. I gather that some speedtest is being done, but coming from other languages I'd expect this to just be something more like Benchmark.ms(render_output) or perhaps Benchmark.start followed by render_output followed by Benchmark.end. I'm having a hard time following the way it works here.
But more importantly, I don't really follow why args isn't used again. Why bother defining a param that isn't used? And I mean, clearly it is getting used-- I just don't see how. There's some hidden mechanism here that I haven't learned about yet.
In this context, it is important to note how super works, because in some cases it passes implicitly arguments and you might not expect that.
When you have method like
def method(argument)
super
end
then super is calling the overridden implementation of method implicitly with the exact same arguments as the current method was called. That means in this example super will actually call super(argument).
Of course, you can still define a method call that explicitly sends other arguments to the original implementation, like in this example:
def method(argument)
super(argument + 1)
end
Another important edge-case is when you want to explicitly call super without any arguments although the current method was called with arguments then you need to be very explicit like this
def method(argument)
super() # note the empty parentheses
end
Let me try to describe you what I think this code does.
*args*
is using the splat operator to collect the arguments together into an array
that is totally correct, however they don't use it, and if you will go to master branch, they just changed it to *. Asking why it is defined and not used, I think that's question about bad design. They should have called it _args or at least like it is now just single splat *.
render_output is set to nil because of scopes, it has to be explicitly defined out block, lambda, proc in order to store value in it, otherwise its visibility will be locked only to those lambda, proc, block execution. Refer to this article
Benchmark.start. Blocks are great ruby construction. You are totally correct that speedtest is done, we can see it is just decorator for benchmark library.
source.
You are wondering why we cannot just pass it as Benchmark.ms(render_output), that's because what will be given to benchmark ms function? It will be given result, like <div> my html </div. And how we can measure this string result - no how. That's why we calling super right in this block, we want to access parent class function and wrap it inside block, so we are not calling it, we just construct it, and it will be called inside benchmark lib, and measured execution like
class Benchmark
...
def realtime # :yield:
r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
Process.clock_gettime(Process::CLOCK_MONOTONIC) - r0
end
...
end
So here we can count realtime of function execution, this is the code from original library

Getting "wrong number of arguments" when trying to overload method

I’m using Rails 4.2.7 on Ubuntu 14.04. According to this — Why doesn't ruby support method overloading? , I should be able to overload methods in my service class if each method has a different number of arguments. So I have created …
def create_my_object_time_obj(data_hash)
create_my_object_time_obj(data_hash, nil)
end
def create_my_object_time_obj(data_hash, my_object_id)
…
Yet, when I try and invoke the call that only takes a single argument
my_object_time = service.create_my_object_time_obj(data_hash)
I get the error
Error during processing: wrong number of arguments (given 1, expected 2)
/Users/login/Documents/workspace/myproject/app/services/text_table_to_my_object_time_converter_service.rb:82:in `create_my_object_time_obj'
What’s the right way to overload my methods from my service class?
Ruby allows one and only one method for a given unique name, regardless of the method signature. The Ruby interpreter only looks at the names of methods, as opposed to the Java interpreter, which considers parameters as well as method names.
You can only override methods, with the latest version of a method of a given name taking precedence over all previous methods which share that name.
You can, however, get around the inability to overload by taking advantage of the splat (*) operator to pass a variable number of arguments to your method in an array, and then branching based on the number of passed parameters.
def foo(*args)
if args.length == 1
# logic for a single argument
elsif args.length == 2
# logic for two arguments
else
# logic for other conditions
end
end
Just simple define your method as per below:
def create_my_object_time_obj(data_hash, my_object_id = nil)
create_my_object_time_obj(data_hash, my_object_id)
end
now you can call this method by single argument:
my_object_time = service.create_my_object_time_obj(data_hash)
this will work file. try this :)

The meaning of `*` when using as a param(not like *arg, just *) [duplicate]

This question already has an answer here:
naked asterisk as parameter in method definition: def f(*)
(1 answer)
Closed 10 years ago.
When I was reading Rails code, I found this
def save(*)
create_or_update || raise(RecordNotSaved)
end
What does the * do? :O
I know what happens when we use it like *args, but in this case, it's just plain *.
ref https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb#L119
It means the same thing as it does when used with a parameter name: gobble up all the remaining arguments. Except, since there is no name to bind them to, the arguments are inaccessible. In other words: it takes any number of arguments but ignores them all.
Note that there actually is one way to use the arguments: when you call super without an argument list, the arguments get forwarded as-is to the superclass method.
In this specific case, save doesn't take any arguments. That's what happens with a naked splat. But, as you may be aware, calling save on an ActiveRecord model accepts options because this method gets overridden by ActiveRecord::Validations here:
https://github.com/rails/rails/blob/v3.1.3/activerecord/lib/active_record/validations.rb#L47
# The validation process on save can be skipped by passing <tt>:validate => false</tt>. The regular Base#save method is
# replaced with this when the validations module is mixed in, which it is by default.
def save(options={})
perform_validations(options) ? super : false
end

Why does is_a? fail to match type, and results in split throwing an exception?

While running RSpec and FactoryGirl, it automatically converts any :id param into a fixnum, whereas, in real life, it gets passed as a string. Code I have that processes that :id as a composite primary key tries to call split on params[:id]. That results in an exception, like so:
NoMethodError:
undefined method 'split' for 234:Fixnum
When trying to call something like this:
#myinstance = MyClass.find(params[:id].split("-").map{|x| x.to_i})
In an attempt to get around this issue, I have added a simple type check:
if params[:id].is_a? Fixnum
#myinstance = MyClass.find(params[:id])
else
#myinstance = MyClass.find(params[:id].split("-").map{|x| x.to_i})
end
Yet, this does not work as I expect it to. The if statement evaluates to false, and the split is still attempted, resulting in the same error. How is it possible that Ruby recognizes params[:id] as a Fixnum in the "else" logic, yet fails to evaluate to true in the if statement?
It's sheer logic: params[:id] is a String or nil.
Oh... I must edit my answer:
When you do controller specs, the params you send are sent as-is and not stringified...
That's a pain but it should be fixed sooner or later.
So in your specs, stringify your params to be closer to reality.
That's a really important point: don't adapt your code to your specs, do the opposite.

Resources