Module: HashKeywordArgs::Hash

Defined in:
lib/hash_keyword_args/hash.rb

Instance Method Summary collapse

Instance Method Details

#keyword_args(*args) ⇒ Object

given an argument hash and a description of acceptable keyword args and their default values, checks validity of the arguments and raises any errors, otherwise returns a new hash containing all arg values with defaults filled in as needed.

args = hash.keyword_args(:a, :b, :c, # these are all optional args

:d => :optional,   # same as listing it the arg up front
:e => :required,   # raises an error if arg not provided
:f => "hello"      # default value
:g => [:x,:y,:z]   # valid values
:h => Integer      # valid type
:i => :enumerable  # expect/coerce an enumerable, check all items, default is []
:j => { :valid => Integer, :allow_nil => true}   # Integer or nil
:j => { :valid => [:x,:y,:z], :default => :x, :required => true }   # combo
)

by default this will raise an error if the hash contains any keys not listed. however, if :OTHERS is specified as a keyword arg, that test will be disabled and any other key/value pairs will be passed through.

Returns a Struct whose values are the



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/hash_keyword_args/hash.rb', line 27

def keyword_args(*args)
  argshash = args[-1].is_a?(Hash) ? args.pop : {}
  argshash = args.hashify(:optional).merge(argshash)
  others_OK = argshash.delete(:OTHERS)
  ret = {}

  # defaults, required, and checked
  required = []
  check = {}
  argshash.each do |key, val|
    # construct fleshed-out attribute hash for all args
    attrs = case val
            when Hash
              val[:default] ||= [] if val[:enumerable]
              val
            when :required
              { :required => true }
            when :optional
              {}
            when :enumerable
              { :enumerable => true, :default => [] }
            when Array
              { :valid => val }
            when Class, Module
              { :valid => val }
            else
              { :default => val }
            end

    # extract required flag, validity checks, and default vlues from attribute hash
    required << key if attrs[:required]
    check[key] = case valid = attrs[:valid]
                 when Enumerable
                   [:one_of, valid]
                 when Class, Module
                   [:is_a, valid]
                 else
                   [:ok]
                 end
    check[key][2] = [:allow_nil, :enumerable].select{|mod| attrs[mod]}
    ret[key] = attrs[:default] if attrs.include?(:default)
  end

  # check validity of keys
  unless others_OK or (others = self.keys - argshash.keys).empty?
    raise ArgumentError, "Invalid keyword arg#{others.length>1 && "s" or ""} #{others.collect{|a| a.inspect}.join(', ')}", caller
  end

  # process values, checking validity
  self.each do |key, val|
    code, valid, mods = check[key]
    mods ||= []
    val = [ val ] unless mods.include?(:enumerable) and val.is_a?(Enumerable)
    ok = val.all? { |v|
      if mods.include?(:allow_nil) and v.nil?
        true
      else
        case code
        when nil     then true      # for OTHERS args, there's no code
        when :ok     then true
        when :one_of then valid.include?(v)
        when :is_a   then v.is_a?(valid)
        end
      end
    }
    val = val.first unless mods.include?(:enumerable)
    raise ArgumentError, "Invalid value for keyword arg #{key.inspect} => #{val.inspect}", caller unless ok
    ret[key] = val
    required.delete(key)
  end

  unless required.empty?
    raise ArgumentError, "Missing required keyword arg#{required.length>1 && "s" or ""} #{required.collect{|a| a.inspect}.join(', ')}", caller
  end

  (class << ret ; self ; end).class_eval do
    argshash.keys.each do |key|
      define_method(key) do
        self[key]
      end
    end
  end

  ret
end