33

I'm interested in getting the nested 'name' parameter of a params hash. Calling something like

params[:subject][:name]

throws an error when params[:subject] is empty. To avoid this error I usually write something like this:

if params[:subject] && params[:subject][:name]

Is there a cleaner way to implement this?

tokland
  • 66,169
  • 13
  • 144
  • 170
Jack Kinsella
  • 4,491
  • 3
  • 38
  • 56
  • 2
    few days ago was the same question :) http://stackoverflow.com/questions/5393974/ruby-nils-in-an-if-statement/5394240#5394240 – fl00r Mar 25 '11 at 10:56

12 Answers12

20

Check Ick's maybe. You don't need to significantly refactor your code, just intersperse maybe proxies when necessary:

params[:subject].maybe[:name]

The same author (raganwald) also wrote andand, with the same idea.

tokland
  • 66,169
  • 13
  • 144
  • 170
  • Interesting, I hadn't heard of Ick before. – bowsersenior Mar 25 '11 at 16:24
  • Now that rubyforge is gone, the link above to Ick's maybe plugin is broken. The new gems site, rubygems.org, has a gem called maybe here: https://rubygems.org/gems/maybe. Is this the same one as Ick's from ruby forge? – Ryan Grow Jun 20 '14 at 17:30
  • @steel has the best answer below: use the built-in functionality called `dig`, introduced in Ruby 2.3.0. – user513951 Aug 14 '17 at 21:57
17
  1. You can use #try, but I don't think it's much better:

    params[:subject].try(:[], :name)
    
  2. Or use #fetch with default parameter:

    params.fetch(:subject, {}).fetch(:name, nil)
    
  3. Or you can set #default= to new empty hash, but then don't try to modify values returned from this:

    params.default = {}
    params[:subject][:name]
    

    It also breaks all simple tests for existence, so you can't write:

    if params[:subject]
    

    because it will return empty hash, now you have to add #present? call to every test.

    Also this always returns hash when there is no value for key, even when you expect string.

But from what I see, you try to extract nested parameter, instead of assigning it to model and there placing your logic. If you have Subject model, then simply assigning:

@subject = Subject.new(params[:subject])

shuld extract all your parameters user filled in form. Then you try to save them, to see if user passed valid values.

If you're worrying about accessing fields which user should not set, then add attr_accessible whitelist for fields whoich should be allowed to set with mass assignment (as in my example, of with @subject.attributes = params[:subject] for update)

MBO
  • 30,379
  • 5
  • 50
  • 52
9

Ruby 2.3.0 makes this very easy to do with #dig

h = {foo: {bar: {baz: 1}}}

h.dig(:foo, :bar, :baz)           #=> 1
h.dig(:foo, :zot, :baz)           #=> nil
steel
  • 11,883
  • 7
  • 72
  • 109
  • 1
    `h.dig` will fail if `h` is `nil`. Consider using the safe navigation operator instead or combined with `.dig` as: `h&.dig(:foo, :bar, :baz)` or `h&.foo&.bar&.baz`. – thisismydesign Sep 11 '17 at 15:32
  • The syntax of the safe navigator operator on hashes in my previous comment is incorrect. The correct syntax is: `h&.[](:foo)&.[](:bar)&.[](:baz)`. – thisismydesign Sep 18 '17 at 15:22
5

params[:subject].try(:[], :name) is the cleanest way

lebreeze
  • 5,094
  • 26
  • 34
  • But then it throws exception if you don't have `:name` field in subject hash. You have to add default value to `#fetch` – MBO Mar 25 '11 at 08:05
  • Ahh sorry I meant `params[:subject].try(:[], :name)` which I agree with you isn't necessarily any better than the basic verbose way. – lebreeze Mar 25 '11 at 08:13
  • In a railscasts, I think Ryan uses try without the :[] (and I'm using this since then). This would make this solution more compact ;) – Pierre Mar 25 '11 at 08:17
  • :[] is pure ruby. It's how you represent [] type methods as symbols. – Jack Kinsella Mar 25 '11 at 14:37
4

When I have same problem in coding, I sometimes use `rescue'.

name = params[:subject][:name] rescue ""
# => ""

This is not good manners, but I think it is simple way.

EDIT: I don't use this way often anymore. I recommend try or fetch.

kyanny
  • 1,231
  • 13
  • 20
2

Not really. You can try fetch or try (from ActiveSupport) but it's not much cleaner than what you already have.

More info here:

UPDATE: Forgot about andand:

andand lets you do:

params[:user].andand[:name] # nil guard is built-in

Similarly, you can use maybe from the Ick library per the answer above.

Community
  • 1
  • 1
bowsersenior
  • 12,524
  • 2
  • 46
  • 52
2

Or, add [] to it.

class NilClass; def [](*); nil end end
params[:subject][:name]
sawa
  • 165,429
  • 45
  • 277
  • 381
punund
  • 4,321
  • 3
  • 34
  • 45
  • 1
    This is great. I deleted mine because this one is much better than mine. And, thanks for showing the use of the splat operator `*` without a name. I did not know that and learned it from you. – sawa Nov 03 '11 at 07:30
  • 1
    Can more people comment on this? This seems brilliant to me! – Ben Wheeler Mar 13 '15 at 06:17
1
class Hash
  def fetch2(*keys)
    keys.inject(self) do |hash, key|
      hash.fetch(key, Hash.new)
    end
  end
end

e.g.

require 'minitest/autorun'

describe Hash do
  it "#fetch2" do
    { yo: :lo }.fetch2(:yo).must_equal :lo
    { yo: { lo: :mo } }.fetch2(:yo, :lo).must_equal :mo
  end
end
Moriarty
  • 3,957
  • 1
  • 31
  • 27
1

I cross posted this from my answer over here:

How to check if params[:some][:field] is nil?

I have been looking for a better solution too.

So I figured let's use try a different way to test for a nested key being set:

params[:some].try(:has_key?, :field)

It's not bad. You get nil vs. false if it's not set. You also get true if the param is set to nil.

Community
  • 1
  • 1
Dan Tappin
  • 2,692
  • 3
  • 37
  • 77
0

I wrote Dottie for just this use case — reaching deep into a hash without first knowing whether the entire expected tree exists. The syntax is more succinct than using try (Rails) or maybe (Ick). For example:

# in a Rails request, assuming `params` contains:
{ 'person' => { 'email' => 'jon@example.com' } } # there is no 'subject'

# standard hash access (symbols will work here
# because params is a HashWithIndifferentAccess)
params[:person][:email] # => 'jon@example.com'
params[:subject][:name] # undefined method `[]' for nil:NilClass

# with Dottie
Dottie(params)['person.email'] # => 'jon@example.com'
Dottie(params)['subject.name'] # => nil

# with Dottie's optional class extensions loaded, this is even easier
dp = params.dottie
dp['person.email'] # => 'jon@example.com'
dp['subject.name'] # => nil
dp['some.other.deeply.nested.key'] # => nil

Check out the docs if you want to see more: https://github.com/nickpearson/dottie

Nick
  • 993
  • 9
  • 10
0

I used:

params = {:subject => {:name => "Jack", :actions => {:peaceful => "use internet"}}}

def extract_params(params, param_chain)
  param_chain.inject(params){|r,e| r=((r.class.ancestors.include?(Hash)) ? r[e] : nil)}
end

extract_params(params, [:subject,:name])
extract_params(params, [:subject,:actions,:peaceful])
extract_params(params, [:subject,:actions,:foo,:bar,:baz,:qux])

gives:

=> "Jack"
=> "use internet"
=> nil
xxjjnn
  • 14,591
  • 19
  • 61
  • 94
0

You can avoid the double hash access with an inline assignment:

my_param = subj_params = params[:subject] && subj_params[:name]
David Moles
  • 48,006
  • 27
  • 136
  • 235