hash_keyword_args

Defines Hash#keyword_args, which provides convenient features when using a Hash to provide keyword args to a method:

  • Argument checking

  • Accessor methods for values

  • Default values

  • Required vs. optional arguments

  • Argument value validation

Typical simplest usage is as follows:

def my_method(args={})
  args = args.keyword_args(:name, :rank)

  puts "name is #{args.name}" if args.name
  puts "rank is #{name.rank}" if args.rank
end

my_method()                                    # prints nothing
my_method(:name => "Kilroy")                   # prints: name is Kilroy
my_method(:name => "Kilroy", :rank => "Pawn")  # prints: name is Kilroy / rank is Pawn
my_method(:name => "Kilroy", :serial => 666)   # raises ArgumentError

Notice that you declare the keyword arguments you’re willing to accept, and keyword_args returns a new object that has accessors for each argument. If an non-matching keyword is detected, keyword_args raises ArgumentError with a descriptive message.

For fancier features (such as required arguments, default values, and whatnot) you specify properties for keywords, as discussed below.

Note another common idiom is to define the keyword args after positional args (“rails style”), e.g.:

def find(id, opts={})
  opts = opts.keyword_args(:conditions, :order)
  ...
end
find(123, :order => :date)

Details

Default Values

Normally if a keyword isn’t included in the args, the corresponding accessor will return nil. But you can provide default values that will be filled in if the keyword isn’t provided. E.g.

def my_method(args={})
  args = args.keyword_args(:name, :rank => "Knight")

  puts "name is #{args.name}" if args.name
  puts "rank is #{name.rank}"
end

my_method()                                    # prints: rank is Knight
my_method(:name => "Kilroy")                   # prints: name is Kilroy / rank is Knight
my_method(:name => "Kilroy", :rank => "Pawn")  # prints: name is Kilroy / rank is Pawn

Notice that because unadorned hashes must be at the end of ruby calls, any keywords with default values or other properties need to come after the ordinary optional keywords. If you care about the order (or like symmetry in your code), you can specify the magic symbol :optional instead of a default value. So this is equivalent to the above:

args = args.keyword_args(:rank => "Pawn",
                         :name => :optional)

The above actually show the shortcut form for specifying a default. There’s also a long form, which can be used in combination with other properties or if the default value conflicts with a shortcut:

args = args.keyword_args(:rank => { :default => "Pawn" },
                         :name => { },                       
                         )

Required Keyword Args

By default, keyword arguments are optional; and if not provided the value is nil or the specified default. But you can require that an argument be specified:

def my_method(args={})
  args = args.keyword_args(:name => :required, :rank => "Pawn")

  puts "name is #{args.name}"
  puts "rank is #{name.rank}"
end

my_method()                         # raises ArgumentError with a descriptive message
my_method(:name => "Kilroy")        # prints: name is Kilroy / rank is Pawn

Again, the above is the shortcut form. The equivalent long form would be:

args = args.keyword_args(:name => { :required => true },
                         :rank => { :default => "Pawn"},
                         )

Value Validation

keyword_args can check that the provided values have a given type or are chosen from among a specified array of values.

def my_roll(args={})
  args = args.keyword_args(:lucky => Integer,
                           :dice => [:d6, :d10, :d20])

  ...
end

my_roll(:lucky => 17, :dice => :d20)    # OK
my_roll(:lucky => "yes", :dice => :d20) # raises ArgumentError with a descriptive message for :lucky
my_roll(:lucky => 17, :dice => :d4)     # raises ArgumentError with a descriptive message for :dice
my_roll(:dice => :d4)                   # raises ArgumentError; :dice is OK but :lucky (nil) isn't an Integer

Note that since the default value, nil isn’t a valid Integer and wasn’t listed in the collection, the above declaration has implicitly caused those keywords to be required. But it’s possible to specify default values and/or allow nil using the long form:

args = args.keyword_args(:lucky => {:valid => Integer, :allow_nil => true})
args = args.keyword_args(:lucky => {:valid => Integer, :default => 7})
args = args.keyword_args(:lucky => {:valid => Integer, :default => 7, :allow_nil => true})

In the third form above, the default value is 7, but if args explicitly included :lucky => nil it would override the default.

Enumerable Values

You can specify that you expect a keyword to take an array of values (or other Enumerable), via

def my_report(args={})
  args = args.keyword_args(:winners => :enumerable)

  args.winners.each do |winner|
    puts "#{winner} is a winner"
  end
end

my_report(:winners => ["Bonnie", "Clyde"])  # prints: Bonnie is a winner / Clyde is a winner
my_report(:winners => "Nero")               # prints: Nero is a winner
my_report()                                 # prints nothing

Notice that a non-enumerable value gets automatically wrapped in an array for you, and the default value is an empty array. If you want to do type checking on the values, you can use the long form:

args = args.keyword_args(:winners => { :enumerable => true, :valid => String})

which will perform validity checking on element of the array. You can combine :enumerable with a default as well:

args = args.keyword_args(:winners => { :enumerable => true, :default => ["Huey", "Dewey"] }

But I like having my options in a Hash!

Not to worry. The returned object is actually a Hash with the accessors defined in its singleton class, so you can use hash operations on it if needed. In particular you can pass it in turn to another method. For example:

def my_wrapper(args={})
  args = args.keyword_args(:name, :rank, :serial_number)
  @serial_number = args.delete(:serial_number)
  my_method(args)
end

Suppress Argument Checking

If you want to suppress argument checking you can specify the magic keyword :OTHERS (with the intended meaning “and other keyword args that aren’t listed here”):

def execute(operator, opts={})
  opts = opts.keyword_args(:immediately, :OTHERS)
  immediately = opts.delete(:immediately)  # take :immediately out of the list...
  opts.each do |opt, value|                # ...and loop over all the others
    ...
  end
end

execute(operator, :yabba => 1, :dabba => 2, :doo => 3)  # not an argument error

No accessor methods are defined for undeclared keywords, but the values will be available in the hash. The properties to :OTHERS are ignored, but you can use :OTHERS => :optional to make it look nice at the end of a list.

args = args.keyword_args(:name => String,
                         :OTHERS => :optional)

Summary

Complete list of long form properties that can be specified, individually or in combination:

:key => { :required => boolean }
:key => { :default => "your default value" }
:key => { :valid => Class-or-enumeration, :allow_nil => boolean }
:key => { :enumerable => boolean }  # implies :default => []

Complete list of shortcuts

:key => :optional     # short for :key => {}
:key => :required     # short for :key => {:required => true}
:key => :enumerable   # short for :key => {:enumerable => true} which in turn implies :default => []
:key => [1, 2, 3]     # short for :key => {:valid => [1, 2, 3]} validates inclusion in the list
:key => Class         # short for :key => {:valid => Class}     validates is_a Class
:key => "whatever"    # anything else is short for :key => {:default => "whatever"}

Installation

Install via:

% gem install hash_keyword_args

or in your Gemfile:

gem "hash_keyword_args

Versions

Has been tested on MRI 1.8.7 and MRI 1.9.3

History

Past: I’ve been using this for years and carrying it around from project to project. Finally got around to bundling it into a gem and putting it on rubygems, since that’s now so darn easy to do! Maybe somebody other than me will find this gem useful too.

Future: I hope that this gem will be obviated in future versions of ruby.

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. Make sure that the coverage report (generated automatically when you run rspec with ruby >= 1.9) is at 100%

  • Send me a pull request.

Released under the MIT License. See LICENSE for details.