I was reading this SO question: https://stackoverflow.com/a/4402761/2379703 and the last post showing the rails render impl was interesting. It's signature is:
def render(options = {}, locals = {}, &block)
If the first argument is a plain string, it assigns that to options and the rest is interpreted as a hash and assigned to locals. For example:
render('partial_name', key: 'value', key2:, 'value2')
Which results in:
options = "partial_name"
locals = {key: 'value', key2:, 'value2'}
If you just pass key/value pairs, it assumes you passed a single argument of a hash and assigns it all to options and leaves locals empty:
render(partial: 'partial_name', key: 'value', key2", 'value2')
Which results in:
options = {partial: 'partial_name', key: 'value', key2:, 'value2'}
locals = {}
So my question really comes down to is: What is the logic that ruby uses to figure out which parameter gets assigned what when there are multiple optional parameters? Furthermore, it seems that hashes make the answer to this question more interesting since hashes clearly don't need to be delimited with outer {} when passed in as arguments.
A secondary observation, in a test where I used the same signature for a test method like render, and I passed the following in:
render(key: 'value', key2: 'value2', 'string')
And that results in a syntax error:
test_hash_param.rb:15: syntax error, unexpected ')', expecting =>
Why is this? Why doesn't it assign the two key/value pairs to options and sets locals to 'string'?
However this works as I assumed it would:
render({key: 'value', key2: 'value2'}, 'string')
Firstly, you can only pass hash without {} brackets as a last argument to the method. Otherwise it would be much harder for interpreter to find out where does one param ends and another starts.
Having that said, when ruby sees a list of hash-like arguments at the end of argument list, it will always treat it as a single hash. Hence in your example only options have assigned value, as locals has not been passed and default value has been used. There are a lot of issue you can find here here on stackoverflow being result of that. If you need to pass two separate hashes, you need to wrap at least first of them in brackets (and naturally the second one as well if it is not the last argument)
because you're passing hashes without boundaries, ruby has to make decisions about how to interpret them. instead of picking an arbitrary place to divide your key:value pairs into two hashes, it will just group them all in one hash. that means:
render( foo: "bar", hello: "world", "bananas")
gets read as one hash, because it can't tell where you want to end the hash. 'bananas' gets included as a key(because strings can be keys) and pops a syntax error because you didn't assign it a value.
options and locals don't have to be hashes though, because ruby variable types are dynamic. their default value is an empty hash, but if you pass two strings, they'll both be assigned strings. when you pass one string, that gets assigned to options because it is a complete variable. when you pass a symbol/string and a hash rocket though(or a symbol with the colon flipped), you're telling ruby "this is the start of a hash" so it starts looking for key:value pairs. in order to end that hash before the end of your arguments so that you can pass another argument, you have to explicitly tell ruby to stop looking for key:value pairs.
Related
I've just been bit by the warning
... Non-attribute arguments will be disallowed in Rails 6.0. This
method should not be called with user-provided values, such as request
parameters or model attributes. Known-safe values can be passed by
wrapping them in Arel.sql()...
Cool. I'd already checked the column choices against the column names, so I'm in good shape. But they are a list of values, not just a single string. So I've added
['some', 'column', 'names'].map{ |string| Arel.sql(string) }
three times in the same method (for different groups of things). So I'm wondering if there is a shorthand like:
Arel.sql_elements(['some', 'list', 'of', 'strings'])
Does it exist and I'm not finding it - or am I going to monkey patch it in?
In the below, I want the first ? to be used literally, only the second ? should be used as the bind variable marker:
Foo.find_by_sql ["select IFNULL(col,'?') from foos where id = ?",1]
This errors:
wrong number of bind variables (1 for 2)
How would I escape the first ? so it is treated literally?
ActiveRecord isn't smart enough to ignore placeholders in string literals so it thinks that the ? in '?' is a placeholder rather than part of a string. The easiest way around this is to use named placeholders:
Foo.find_by_sql ["select IFNULL(col, '?') from foos where id = :id", :id => 1]
When ActiveRecord sees a Hash in the array's second element it will look for named placeholders (which use Ruby symbol notation) rather than positional ? placeholders. I tend to lean towards named placeholders period as they're more readable and more robust.
Simply pass '?' as a param:
Foo.find_by_sql ["select IFNULL(col,?) from foos where id = ?",'?',1]
I'm newbie in ruby on rails, trying to rewrite rails project in PHP, can someone explain me please what next lines of code do?
def payment_types_billing
I18n.t("customer.payment_types_billing").inject({}){|memo,(k,v)|
memo[k.to_s.to_i] = v; memo}.
merge({
PaymentType::ACCOUNTCREDIT => I18n.t("customer.payment_types_billing.#{PaymentType::ACCOUNTCREDIT}",
:amount => number_to_currency(get_account_credit))
})
end
I don't understand part after .inject, if someone can just explain that part in human language, I will be very thankful. :)
These methods work in conjunction. By calling payment_types, the following will occur:
First it grabs a section of the localization yaml (likely in config/locales/en.yml). For more on internationalization/localization, see this!
I18n.t("customer.payment_types_billing")
Then it runs an inject block on the resulting enumerable (in this case a hash), with the intent of returning a newly-formed result (see about .inject here)
.inject({}){|memo,(k,v)| memo[k.to_s.to_i] = v; memo}
The result of this block appears to be a hash whose keys were the keys of the retrieved hash, converted to integers (without knowing the data being accessed, I can't know how this is meant to function).
Addendum:
I suspect the purpose of the above block is to assign integer keys to a new hash (something that is impossible otherwise). Seeing the later steps with the invert, this would mean that the final printed hash will have integer values, not strings.
It then adds two new key value pairs to the hash:
.merge({PaymentType::ACCOUNTCREDIT => I18n.t("customer.payment_types_billing.#{PaymentType::ACCOUNTCREDIT}", :amount => number_to_currency(get_account_credit))})
The first pair has a key equal to ACCOUNTCREDIT, with another value retrieved from the YAML. The second is the key :amount, with the value of "get_account_credit" (presumably a method with a decimal output) converted to currency for the current region.
As we reach the actual content of the payment_types method, the results from above (the newly-formed hash) is a block with a delete condition. If get_account_credit is returning a non-positive number, the ACCOUNTCREDIT keyed pair is deleted
.delete_if {|key, value| (key == PaymentType::ACCOUNTCREDIT && get_account_credit <= 0) }
Finally, the hash is inverted (the keys become the values, and the values become the keys):
.invert
Does Hash.slice support strings ?
so for example
a = {"b" => 1, "c" => 2}
a.slice("b")
If not then how does it react to strings ?
I am trying to track down a bug where data is being lost and I think it is because the keys in hashes switch intermittently between strings and symbols. Filtering is done exclusively using Hash.slice(*keys)
In Rails, Hash#slice with a single key value (such as a string) from the hash returns a hash containing the matching key-value pair, or nil if it isn't matched to a key. Hash#slice takes any number of arguments, each representing a key. See the documentation here, and note *keys argument.
I have a hash that will render my html differently based on a particular variable. The variable is within the hash. So I am wondering how I can pass a hash value to a group by. to sort the rest heres what I am trying, maybe this will explain it better than me wording it.
<% grouped = svcs.group_by { |svc| svc[val[:sorttype]] } %>
val is a multidimensional hash. the first 2 key value pairs sorttype and one other are simple key and value, the 3rd piece (svcs) contains the equivilent of a 2D hash. Which if I manually type the type of sort I want to apply to it for the group by it works ie:
<% grouped = svcs.group_by { |svc| svc[:service_name] } %>
in PHP i know in a similar instance I can pass a variable of some sort to something like this and have it work. I assume such is the case here. However Im not sure how to put the variable in. Cause all the ways Ive tried don't work
It depends a little.
Rails' has a HashWithIndifferentAccess that will not distinguish between string and symbol keys; if you're using one of those, it should work as-is.
If it's not, it depends what the val entries are--if they're strings, convert to a symbol using to_sym, e.g., svc[val[:sorttype].to_sym].