Syntax for handling parameters dynamically - ruby-on-rails

A series of parameters are being submitted to a Rails controller:
"25"=>"true", "30"=>"false", "35"=>"true", "40"=>"true", "45"=>"true", "50"=>"true", "55"=>"true", "60"=>"true", "65"=>"false", "70"=>"true", "75"=>"true", "80"=>"false", "100"=>"true"
The goal is to loop through:
#age_limits = [25,30,35,40,45,50,55,60,65,70,75,80,100]
and process based on each param's value.
The problem is that the syntax for referring to the param is evading me:
#age_limits.each do |limit|
puts params[:"#{limit}"]
if params[:"#{limit}"] == "true"
session[:demographic_agegroups] << params[:"#{limit}"]
puts session[:demographic_agegroups]
end
end
Both params[:"#{limit}"] and params[":#{limit}"] do not allow to access the parameter. The former will return the value by putting into console (editor BBEdit does not consider it properly formed Ruby), but will not allow to process in its basis.
How can the controller properly access the param and process based on its value?

You'r missing to convert int to string.
Since your params keys are string, you can use params[limit.to_s].
Also, if its key based "keyX".to_sym would be :keyX

Related

Can't modify frozen String on Hash

I am a bit confused with the frozen string and utilizing them with test cases.
I just added the following line at the top of my test cases :
# frozen_string_literal: true
And i have the following two test cases:
test "Create upload invoice invalid invoice id" do
post :upload, params = {invoices_data: [{invoice_id: 987654, unit_id: 1321}]}
assert_response :not_found
end
test "Create upload invoice request to fortnox with non array request parameter" do
request = {invoices_data: {invoice_id: "invoice.id", unit_id: 321}}
post :upload_invoices, params = request
assert_response :bad_request
end
All of a sudden my second test failed with
RuntimeError: can't modify frozen String
at this line
post :upload_invoices, params = request
however, if I change the string to some symbol for instance :invoice_id then it works just fine.
Can someone guide why about the following two things:
Why does sending a string value fails in this case reporting that I
am trying to modify a String and which string value I am trying to
modify?
Why does it fail on post request, if it has to fail then it
should fail when creating the request i.e request = {invoices_data: {invoice_id: "invoice.id", unit_id: 321}}
What i can do to send string value instead of Symbol in the hash?
1a) Sending a string value fails in this case because your upload_invoices controller action attempts to modify the invoice_id parameter itself. (Or you're running an old version of Rails where the #post method itself attempts to modify the invoice_id parameter by converting it to UTF-8 encoding.)
1b) The string value you're trying to modify is "invoice.id".
2 ) It fails on the post request and not the assignment to the request variable because the assignment to the request variable is not where the attempted modification happens. The frozen string literal is attempted to be modified by the call to #post. See answer 1a above.
3 ) You can send a non-frozen string value in the hash a few different ways. You could remove the # frozen_string_literal: true magic comment, but I feel you don't want to do that. Otherwise, the simplest thing to do is to send along a duplicate of the string with either +'invoice.id' or the less esoteric 'invoice.id'.dup. Or you can create a non-literal string with something silly like ['invoice', 'id'].join('.') or :invoice.to_s. No doubt there are other ways.
However, it seems EXTREMELY unlikely you want to pass a string here at all. The invoice_id parameter is almost assuredly an integer, and passing a string to it makes little sense unless I guess you're trying to test that the controller action can handle that kind of erroneous input. If so, one of the string duplication techniques +'string_literal'/'string_literal'.dup would be your best option.
I would wager by the name of the test that you're actually trying to send along a real invoice_id which means you don't want to pass along a string, but instead an integer. Maybe the ID of an Invoice fixture you have setup?
And on another slightly unrelated note, you're not passing params to the #post method properly. It should be params: ... not params = ....

Can I append attributes to Ruby OpenStruct on the go?

I am new to Ruby and this is a really basic question, when I searched for adding/appending values to OpenStruct, I couldn't find any resource.
I'm trying to wrap the response body with extra params and the code in place uses OpenStruct. Now I need to append some key/value later in the code before sending the final reponse.
OpenStruct.new(
body : api_response.body
check1? : true
)
I want to add check2? : false.
The whole point of OpenStruct is that you can add new fields on the fly.
response = OpenStruct.new(
body: 'foo',
check1: true
)
response.check2 = false
p response
# => #<OpenStruct body="foo", check1=true, check2=false>
This is the only advantage that it has over Struct. Using OpenStruct incurs a considerable performance penalty, so if you don't need to add new fields later, it should never be used (unless of course you absolutely don't care about performance); use Struct instead.
However, specifically in your case, Ruby's parser does not allow methods of form check1?=, as both the question mark and the equality sign are only permitted at the end of the identifier; i.e. check1= is a valid method name, check1? is a valid method name, but check1?= is not.
tl;dr: Drop the question mark.
There are two ways to do it depending on what works best for the use case.
One can either do a quick fix with something like this
openstruct_object.check2? = false
OR an elegant way of doing it is to wrap the creation of your OpenStruct instance in a method that accepts the check2? param. (This is what I did and it works great with named args!)
def wrap_reponse(body, check1 = "your_default", check2: "named_args")
OpenStruct.new(
body : body,
check1? : true,
check2? : false
)
end
There is a good blog for reference, which I got after considerable google search.

Rails 4 parameters; how to whitelist a param to a set of values

I have already read some posts, such as Value whitelist using strong parameters in Rails 4, but it's not quite what I need.
I have a controller which takes an ID and loads a model. It also optionally takes a query string param (style) which can be one of 3 values, small, medium or large. This is passed to a method on the model which uses it to fetch an attached image (using paperclip). I noticed that if I pass an invalid param (eg style=wibble), then I get a 400 error and a notice that the internal file path doesn't exist. Brakeman also notes this as a security issue...
def show
style = params[:style] || :medium
thing = Model.find(params[:id])
path = "#{Rails.root}/public#{thing.image_url(style)}"
content_type = thing.image.content_type || 'image/png'
send_file path, type: content_type, disposition: 'inline'
end
We use ActionController Parameters elsewhere to great effect; but I cannot see how that could "whitelist" a parameters options? Everywhere I have seen says to use a model validator, but that's assuming I'm submitting a parameter to update a model, which I am not.
I'm aware I could do something like:
return head :not_found unless %w(small medium large).include? style
Is this the best way?
I wrote a gem that extends ActionController::Parameters for exactly this purpose: whitelisting parameter values. It's simple and lightweight. You would use it like this:
def model_params
params.require(:model).permit(:style, :other_attribute).allow(style: %w[small medium large])
end
Hope you find it useful
https://github.com/msimonborg/allowable
First, you need to define a Constant white-listing all the valid style values (I would argue that size_type is a more explicit name). (Watch out for the Symbol / String comparisons).
Then, you either return a 404 if the params[:style] is not included in the white-listed values OR you fallback to 'medium':
style = Model::AVAILABLE_SIZE_TYPES.include?(params[:style]) ? params[:style] || Model::AVAILABLE_SIZE_TYPES.first
Personally, I don't like fallbacks for user's input. They did something wrong (manipulated the URL, changed a value in a form, etc), they deserve to get an error page.
I implied the constant is defined in the Model, but it should not be there as it is not related to the business logic but related to the display of the Model. If you have a ModelDecorator, define it there.

Rails: Convert string to variable (to store a value)

I have a parameter hash that contains different variable and name pairs such as:
param_hash = {"system_used"=>"metric", "person_height_feet"=>"5"}
I also have an object CalculationValidator that is not an ActiveRecord but a ActiveModel::Validations. The Object validates different types of input from forms. Thus it does not have a specific set of variables.
I want to create an Object to validate it like this:
validator = CalculationValidator.new()
validator.system_used = "metric"
validator.person_height_feet = 5
validator.valid?
my problem right now is that I really would not prefer to code each CalculationValidator manually but rather use the information in the Hash. The information is all there so what I would like to do is something like this, where MAKE_INTO_VARIABLE() is the functionality I am looking for.
validator = CalculationValidator.new()
param_hash.each do |param_pair|
["validator.", param_pair[0]].join.MAKE_INTO_VARIABLE() = param_pair[1]
# thus creating
# "validator.system_used".MAKE_INTO_VARIABLE() = "metric"
# while wanting: validator.system_used = "metric"
# ...and in the next loop
# "validator.person_height_feet".MAKE_INTO_VARIABLE() = 5
# while wanting: validator.person_height_feet = 5
end
validator.valid?
Problem:
Basically my problem is, how do I make the string "validator.person_height" into the variable validator.person_height that I can use to store the number 5?
Additionally, it is very important that the values of param_pair[1] are stored as their real formats (integer, string etc) since they will be validated.
I have tried .send() and instance_variable_set but I am not sure if they will do the trick.
Something like this might work for you:
param_hash.each do |param, val|
validator.instance_eval("def #{param}; ##{param} end")
validator.instance_variable_set("##{param}", val)
end
However, you might notice there's no casting or anything here. You'd need to communicate what type of value each is somehow, as it can't be assumed that "5" is supposed to be an integer, for example.
And of course I probably don't have to mention, eval'ing input that comes in from a form isn't exactly the safest thing in the world, so you'd have to think about how you want to handle this.
Have you looked at eval. As long as you can trust the inputs it should be ok to use.

Scope and Field as Parameter in Rails

I've added a scope to a Rails model that allows for searching based on a specified parameter field using a range. Here is what it looks like:
scope :upcoming, lambda { |field|
where("to_char(#{field}, 'DDD') BETWEEN :alpha AND :omega",
alpha: Time.now.advance(days: 4).strftime('%j'),
omega: Time.now.advance(days: 8).strftime('%j'),
)
}
Event.upcoming(:registration) # Query all events with registration shortly.
Event.upcoming(:completion) # Query all events with completion shortly.
The above works fine, however in creating I read the Ruby on Rails Guides and found the following:
Putting the variable directly into the conditions string will pass the variable to the database as-is. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string.
Although the scope is currently never called with a user parameter, I am curious if a way exists of setting the field without using the interpolation in order to better conform with the above recommendation. I've tried using another named parameter, however this will escape the field with quotes (and thus cause it to fail). Any ideas?
I would recommend validating the field parameter against the model's attributes, essentially using the model as a whitelist for values that are allowed to be passed. Something like this:
scope :upcoming, lambda { |field|
if column_names.include? field.to_s
where("to_char(#{field}, 'DDD') BETWEEN :alpha AND :omega",
alpha: Time.now.advance(days: 4).strftime('%j'),
omega: Time.now.advance(days: 8).strftime('%j'),
)
else
# throw some error or return nil
end
}
Okay, reading all the way to the end might help(thanks rubyprince). It looks like you are doing a between query on a field that is storing a date in Oracle. The problem is that to_char is looking for a variable, not a string. And the act of escaping a variable in rails turns it into a string. So, in this particular case you might convert :alpha and :omega into the format of the value stored in field. That way you can escape field in a straightforward manner. Of course there is the issue with Oracle treating dates as Time. I'm guessing that is why you converted to day-of-year for the compare. If you are using the Oracle Enhanced Adaptor you can set
self.emulate_dates_by_column_name = true
to make sure that the field is treated like a date. Then use the to_date function(which takes a string) with :alpha and :omega
scope :upcoming, lambda { |field|
where(":field BETWEEN to_date(:alpha,'yyyy/mm/dd') AND to_date(:omega,'yyyy/mm/dd')",
field: field,
alpha: Time.now.advance(days: 4).strftime('%Y/%m/%d'),
omega: Time.now.advance(days: 8).strftime('%Y/%m/%d'),
)
}
I have no way of testing this so I might be off in the weeds here.
Validating user input as per Jordan is always a good idea.

Resources