Module: MethLab

Defined in:
lib/methlab.rb

Overview

MethLab - a method toolkit for ruby.

MethLab is next to useless without integrating it into your classes. You can do this several ways:

  • ‘extend MethLab’ in your class definitions before calling any of MethLab’s helpers.

  • ‘MethLab.integrate’ anywhere. This will inject it into ::main and ::Module.

  • set $METHLAB_AUTOINTEGRATE to true before requiring methlab. This calls ‘MethLab.integrate’ automatically.

Please see MethLab#build_ordered and MethLab#build_named for method creation syntax. Note that MethLab#def_named and MethLab#def_ordered will create named methods in your class for you, but they use the build methods underneath the hood.

Here’s an example:

class Awesome
  def_ordered(:foo, String, [Integer, :optional]) do |params|
    str, int = params
    puts "I received #{str} as a String and #{int} as an Integer!"
  end

  def_named(:bar, :foo => String, :bar => [Integer, :required]) do |params|
    puts "I received #{params[:foo]} as a String and #{params[:bar]} as an Integer!"
  end

  def some_method(*args) # a hash
    params = MethLab.validate_params(:foo => String, :bar => [Integer, :required])
    raise params if params.kind_of? Exception

    puts "I received #{params[:foo]} as a String and #{params[:bar]} as an Integer!"
  end
end

Which yields these opportunities:

a = Awesome.new
a.foo(1, "str") # raises
a.foo("str", 1) # prints the message
a.foo("str")    # prints the message with nil as the integer

a.bar(:foo => 1, :bar => "str") # raises
a.bar(:foo => "str")            # raises (:bar is required)
a.bar(:bar => 1)                # prints message, with nil string
a.bar(:foo => "str", :bar => 1) # prints message

a.some_method(:foo => "str", :bar => 1) # prints message

Using it is quite simple. Just remember a few things:

  • A class will always be compared with Object#kind_of? against the object.

  • An object implies certain semantics. Right now, we support direct checking against multiple objects:

    • Regexp’s will convert the value to a string and compare them with String#=~

    • Ranges will use Range#include? to determine if the object occurs within the range.

    • A proc will allow you to do a custom check, taking one argument. Raises happen as such:

      • Returning false/nil will raise a generic error.

      • Returning a new exception object (e.g., ArgumentError.new) will raise your error as close to the call point as possible.

      • Raising yourself will raise in the validation routine, which will probably be confusing. Please use the above method.

    • A symbol is a pragma that implies a constraint – see below.

    • A hash is a way of specifying a pragma (or check) with a parameter:

      • :respond_to calls Object#respond_to? on the method named as the value (a symbol)

      • :default specifies a default argument. This is still checked, so get it right!

  • If you need more than one constraint per parameter, enclose these constraints within an array.

  • Depending on the type of method you’re constructing, there will be additional constraints both implied and explictly allowed:

    • named methods do not require any items by default, they must be specified as required.

    • ordered methods require everything by default, they must be specified as optional.

Constant Summary collapse

VERSION =
"0.1.0"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.check_hash_types(value_key, value_value, value, key) ⇒ Object

internal, please do not use directly.

used to perform our standard checks that are supplied via hash.



118
119
120
121
122
123
124
125
126
# File 'lib/methlab.rb', line 118

def self.check_hash_types(value_key, value_value, value, key)
    case value_key
    when :respond_to
        unless value.respond_to?(value_value)
            return ArgumentError.new("value of argument '#{key}' does not respond to '#{value_value}'")
        end
    end
    return nil
end

.check_type(value_sig, value, key) ⇒ Object

internal, please do not use directly.

used to perform our standard checks.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/methlab.rb', line 131

def self.check_type(value_sig, value, key)
    case value_sig
    when Array
        value_sig.flatten.each do |vs|
            ret = check_type(vs, value, key)
            return ret if ret
        end
    when Hash
        value_sig.each do |value_key, value_value| # GUH
            ret = check_hash_types(value_key, value_value, value, key)
            return ret if ret
        end
    when Class 
        unless value.kind_of?(value_sig)
            return ArgumentError.new("value of argument '#{key}' is an invalid type. Requires '#{value_sig}'")
        end
    when Proc
        ret = value_sig.call(value)

        if ret.kind_of?(Exception)
            return ret
        elsif !ret
            return ArgumentError.new("value of argument '#{key}' does not pass custom validation.")
        else
            return nil
        end
    when Regexp
        unless value.to_s =~ value_sig
            return ArgumentError.new("value of argument '#{key}' does not match this regexp: '#{value_sig.to_s}'")
        end
    when Range
        unless value_sig.include?(value)
            return ArgumentError.new("value of argument '#{key}' does not match range '#{value_sig.inspect}'")
        end
    end

    return nil
end

.integrateObject

Integrates MethLab into all namespaces. It does this by patching itself into ::main and Module.

You may also accomplish this automatically by setting $METHLAB_AUTOINTEGRATE before you require it.



78
79
80
81
# File 'lib/methlab.rb', line 78

def self.integrate
    eval("self", TOPLEVEL_BINDING).send(:include, self)
    ::Module.send(:include, self)
end

.set_defaults(signature, params, kind = :array) ⇒ Object

internal, please do not use directly.

used to set defaults on parameters that require one



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
112
113
# File 'lib/methlab.rb', line 86

def self.set_defaults(signature, params, kind=:array)
    params = params[0] if kind == :hash

    signature.each_with_index do |value, index|
        case kind
        when :array
            if value.kind_of?(Array)
                if hashes = value.find_all { |x| x.kind_of?(Hash) } and !hashes.empty?
                    hashes.each do |hash|
                        if hash.has_key?(:default) and (params.length - 1) < index
                            params[index] = hash[:default]
                        end
                    end
                end
            end
        when :hash
            if value[1].kind_of?(Array)
                if hashes = value[1].find_all { |x| x.kind_of?(Hash) } and !hashes.empty?
                    hashes.each do |hash|
                        if hash.has_key?(:default) and !params.has_key?(value[0]) 
                            params[value[0]] = hash[:default]
                        end
                    end
                end
            end
        end
    end
end

.validate_array_params(signature, args) ⇒ Object

This method takes the same signature as Methlab#build_ordered, and the arguments you wish to validate. It will process everything just like you built a method to handle this, but just with the arguments you prefer.

This method will return either an Exception or an Array; if you receive an exception, this means that parsing errors occured, you may raise this exception from the point of your method if you wish.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/methlab.rb', line 177

def self.validate_array_params(signature, args)
    args = [] unless args

    MethLab.set_defaults(signature, args, :array)

    unless args.kind_of?(Array)
        return ArgumentError.new("this method takes ordered arguments")
    end

    if args.length > signature.length
        return ArgumentError.new("too many arguments (#{args.length} for #{signature.length})")
    end

    opt_index = signature.find_index { |x| [x].flatten.include?(:optional) } || 0
    
    if args.length < opt_index
        return ArgumentError.new("not enough arguments (#{args.length} for minimum #{opt_index})")
    end

    args.each_with_index do |value, key|
        unless signature[key]
            return ArgumentError.new("argument #{key} does not exist in prototype")
        end

        if signature[key]
            ret = check_type(signature[key], value, key)
            return ret if ret
        end
    end

    return args
end

.validate_params(signature, *args) ⇒ Object

This method takes the same signature as Methlab#build_named, and the arguments you wish to validate. It will process everything just like you built a method to handle this, but just with the arguments you prefer.

This method will return either an Exception or an Array; if you receive an exception, this means that parsing errors occured, you may raise this exception from the point of your method if you wish.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/methlab.rb', line 217

def self.validate_params(signature, *args)
    args = [{}] if args.empty?
    MethLab.set_defaults(signature, args, :hash)
    args = args[0]
    
    unless args.kind_of?(Hash)
        return ArgumentError.new("this method takes a hash")
    end

    args.each do |key, value|
        unless signature.has_key?(key)
            return ArgumentError.new("argument '#{key}' does not exist in prototype")
        end

        if signature[key]
            ret = check_type(signature[key], value, key)
            return ret if ret
        end
    end

    keys = signature.each_key.select { |key| [signature[key]].flatten.include?(:required) and !args.has_key?(key) }

    if keys.length > 0
        return ArgumentError.new("argument(s) '#{keys.sort_by { |x| x.to_s }.join(", ")}' were not found but are required by the prototype")
    end

    return args
end

Instance Method Details

#attr_threaded_accessor(*method_names) ⇒ Object

attr_threaded_accessor creates thread-local accessors via Thread.current. As a result, while these are accessed in your class, they live in a flat namespace, and must be used with caution.

Usage:

class Foo
  attr_threaded_accessor(:one, :two)

  def bar
    self.one = 1
    self.two = 2
  end
end

f = Foo.new
f.one # 1
f.two # 2

Thread.current[:one] # => 1
Thread.current[:two] # => 2


396
397
398
399
400
401
402
# File 'lib/methlab.rb', line 396

def attr_threaded_accessor(*method_names)
    method_names.each do |meth|
        self.send(:define_method, meth, proc { Thread.current[meth] })
        meth2 = meth.to_s.gsub(/$/, '=').to_sym
        self.send(:define_method, meth2, proc { |x| Thread.current[meth] = x })
    end
end

#build_named(*args, &block) ⇒ Object

Builds an unbound method as a proc with named (Hash) parameters.

Example:

my_proc = build_named(:foo => String, :bar => [Integer, :required]) do |params|
  puts "I received #{params[:foo]} as a String and #{params[:bar]} as an Integer!"
end

my_proc.call(:foo => "foo", :bar => 1)

As explained above, an array to combine multiple parameters at a position may be used to flag it with additional data. At this time, these parameters are supported:

  • :required - this field is a required argument (parameters are default optional)



303
304
305
306
307
308
309
310
311
# File 'lib/methlab.rb', line 303

def build_named(*args, &block)
    signature = args[0]

    proc do |*args|
        params = MethLab.validate_params(signature, *args)
        raise params if params.kind_of?(Exception)
        block.call(params)
    end
end

#build_ordered(*args, &block) ⇒ Object

Builds an unbound method as a proc with ordered parameters.

Example:

my_proc = build_ordered(String, [Integer, :optional]) do |params|
  str, int = params
  puts "I received #{str} as a String and #{int} as an Integer!"
end

my_proc.call("foo", 1)

As explained above, an array to combine multiple parameters at a position may be used to flag it with additional data. At this time, these parameters are supported:

  • :optional - is not required as a part of the argument list.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/methlab.rb', line 263

def build_ordered(*args, &block)
    signature = args

    op_index = signature.index(:optional)

    if op_index and signature.reject { |x| x == :optional }.length != op_index
        raise ArgumentError, ":optional parameters must be at the end"
    end
    
    proc do |*args| 
        params = MethLab.validate_array_params(signature, args)
        raise params if params.kind_of?(Exception)
        block.call(params)
    end
end

#def_attr(method_name, arg) ⇒ Object

Similar to MethLab#build_ordered, but builds attributes similar to attr_accessor. Takes a single parameter which is the constraint specification.

Example:

def_attr :set_me, String

# later on..
myobj.set_me = 0 # raises
myobj.set_me = "String" # valid


333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/methlab.rb', line 333

def def_attr(method_name, arg)

    self.send(:define_method, (method_name.to_s + "=").to_sym) do |value| 
        signature = [arg]
        params = MethLab.validate_array_params(signature, [value])
        raise params if params.kind_of?(Exception)
        send(:instance_variable_set, "@" + method_name.to_s, params[0])
    end

    self.send(:define_method, method_name) do
        unless self.instance_variables.select { |x| x == "@#{method_name}" || x == "@#{method_name}".to_sym }[0]
            args = []
            MethLab.set_defaults([arg], args, :array)
            send(:instance_variable_set, "@#{method_name}", args[0])
        end

        instance_variable_get("@#{method_name}")
    end
end

#def_named(method_name, *args, &block) ⇒ Object

similar to MethLab#build_named, but takes a method name as the first argument that binds to a method with the same name in the current class or module. Currently cannot be a class method.



316
317
318
319
# File 'lib/methlab.rb', line 316

def def_named(method_name, *args, &block)
    self.send(:define_method, method_name, &build_named(*args, &block))
    return method_name
end

#def_ordered(method_name, *args, &block) ⇒ Object

similar to MethLab#build_ordered, but takes a method name as the first argument that binds to a method with the same name in the current class or module. Currently cannot be a class method.



282
283
284
285
# File 'lib/methlab.rb', line 282

def def_ordered(method_name, *args, &block)
    self.send(:define_method, method_name, &build_ordered(*args, &block)) 
    return method_name
end

#inline(*method_names, &block) ⇒ Object

inline is useful to spec out several methods at once to yield similar values.

Usage:

class Foo
  inline(:one, :two, :three) { 1 }
end

f = Foo.new
f.one   # => 1
f.two   # => 1
f.three # => 1


367
368
369
370
371
# File 'lib/methlab.rb', line 367

def inline(*method_names, &block)
    method_names.each do |meth|
        self.send(:define_method, meth, block)
    end
end