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)
Related
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
I have these code that executes a dynamic method. I'm using eval here to execute it but what I wanted to do is changed it to public_send because I was told so and it's much safer.
Current code:
# update workstep logic here.
incoming_status = params[params[:name]]
# grab workflow, this is current data, use this to compare status to in comming status
workflow = get_workorder_product_workstep(params[:workflow_id])
# check current status if its pending allow to update
# security concern EVAL!
if eval("workflow.can_#{incoming_status}?")
# update status
eval("workflow.#{incoming_status}")
# updated attribute handled_by
workflow.update_attributes(handled_by_id: #curr_user.id)
workflow.save
else
flash[:notice] = 'Action not allowed'
end
The eval here is the concern. How can I changed this to public_send?
Here's what I did.
public_send("workflow.can_#{incoming_status}?")
public_send("#{workflow}.can_#{incoming_status}?")
both of them doesn't work. gives me an error of no method. The first public error returns this undefined method workflow.can_queue? for #<Spree::Admin::WorkordersController:0x00007ff71c8e6f00>
But it should work because I have a method workflow.can_queue?
the second error on public is this
undefined method #<Spree::WorkorderProductWorkstep:0x00007ff765663550>.can_queue? for #<Spree::Admin::WorkordersController:0x00007ff76597f798>
I think for the second workflow is being evaluated separately? I'm not sure.
Working with public_send you can change the relevant lines to:
if workflow.public_send("can_#{incoming_status}?")
# update status
workflow.public_send(incoming_status.to_s)
# ...
A note about security and risks
workflow.public_send("can_#{xyz}?") can only call methods on workflow that are public and which start with the prefix can_ and end with ?. That is probably only a small number of methods and you can easily decide if you want to allow all those methods.
workflow.public_send("#{incoming_status'}) is different because it allows all public methods on workflow – even destroy. That means using this without the "can_#{incoming_status}?" is probably a bad idea. Or you should at least first check if incoming_status is in a whitelist of allowed methods.
eval is the worst because it will evaluate the whole string without any context (e.q. an object like workflow). Imaging you have eval("workflow.#{incoming_status}") without to check first if incoming_status is actually allowed. If someone then sends an incoming_status like this "to_s; system('xyz')"then xyz could be everything – like commands to send a hidden file via email, to install a backdoor or to delete some files.
I found RailsCasts episode, and used this logic and code samples for my needs.
But one thing bothers me.
constraint looks like:
constraints(Subdomain.new) do ... end
which uses this code:
class Subdomain
def matches?(request)
....
end
end
end
And it works.
But I don't get two things. First, I do not invoke matches? anywhere, why this method is just executed on initializing Subdomain.new?
Second concern. I don't pass any parameter, but it somehow assigns request argument to actual rack request and it just works.
For example, I didn't like this syntax:
constraints(Subdomain.new) do ... end
so I decided to make it module with method subdomain(request), but as made it module, it started raising wrong number or arguments error (0 for 1).
I found out that method matches? is defined in mapper.rb, may be it is called somewhere backwards in rails, but this way it should be overwritten by my subdomain file? If not, as my matches is class method, how it works without any Subdomain instance to which it is applied?
As I said, everything works fine, but I would like to understand what exactly happens, because I don't like using something that appears david blane magic code to me.
Reading some source code of Rails mapper module didn't give me understanding.
Well, little more of reading source code gave me a clue. I found one more matches?, defined for #constraints
def matches?(req)
#constraints.all? do |constraint|
(constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
(constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
end
end
So for every constraint it checks if it responds to matches? and then invokes it with rack request argument.
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
In a plugin I am writing I am using alias to override one of the default Rails validators like so:
# Alias the original validator so it's still available under a different name
alias original_validates_uniqueness_of :validates_uniqueness_of unless method_defined?(:original_validates_uniqueness_of)
# Then alias the custom validator under the original name
alias validates_uniqueness_of :custom_validates_uniqueness_of
This all works pretty well. When "validates_uniqueness_of" is defined on an attribute in a AR model, it will use my "custom_validates_uniqueness_of" method instead. Validations are running as expected.
However, when I call:
SomeARclass.respond_to?(:validates_uniqueness_of)
..it will return false. This behavior will mess with several popular plugins.
My question:
Why is respond_to returning "false"? Is this behavior the result of alliasing? How can adjust my custom validator to make it return true?
Thank you for your help.
Erwin
Your sample, on the surface, looks like it should work. I expect you've found that, inside of the class body for class SomeARclass, you actually can call validates_uniqueness_of.
class SomeARclass
validates_uniqueness_of :first_field # works
end
However, I expect that scoped_validates_uniqueness_of is a private method. That's why you have been unable to send that message to SomeARclass from outside of its class body.
SomeARclass.validates_scoped_uniqueness_of :first_field # fails
#=> NoMethodError: private method "validates_scoped_uniqueness_of" called
for SomeARclass:Class
When you alias a private method with a new name, such as aliasing the private method validates_scoped_uniqueness_of with the new name validates_uniqueness_of, then that new method is also private. I expect that's what happened here.
The documentation for #respond_to? reads:
- (Boolean) respond_to?(symbol, include_private = false)
Returns true if obj responds to the given method. Private methods are included in the search only if the optional second parameter evaluates to true.