methlab 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/methlab.rb +247 -0
- metadata +53 -0
data/lib/methlab.rb
ADDED
@@ -0,0 +1,247 @@
|
|
1
|
+
#
|
2
|
+
# MethLab - a method toolkit for ruby.
|
3
|
+
#
|
4
|
+
# MethLab is next to useless without integrating it into your classes. You can
|
5
|
+
# do this several ways:
|
6
|
+
#
|
7
|
+
# * 'extend MethLab' in your class definitions before calling any of MethLab's helpers.
|
8
|
+
# * 'MethLab.integrate' anywhere. This will inject it into ::main and ::Module.
|
9
|
+
# * set $METHLAB_AUTOINTEGRATE to true before requiring methlab. This calls 'MethLab.integrate' automatically.
|
10
|
+
#
|
11
|
+
# Please see MethLab#build_ordered and MethLab#build_named for method creation
|
12
|
+
# syntax. Note that MethLab#def_named and MethLab#def_ordered will create named
|
13
|
+
# methods in your class for you, but they use the build methods underneath the
|
14
|
+
# hood.
|
15
|
+
#
|
16
|
+
# Here's an example:
|
17
|
+
#
|
18
|
+
# class Awesome
|
19
|
+
# def_ordered(:foo, String, [Integer, :optional]) do |params|
|
20
|
+
# str, int = params
|
21
|
+
# puts "I received #{str} as a String and #{int} as an Integer!"
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def_named(:bar, :foo => String, :bar => [Integer, :required]) do |params|
|
25
|
+
# puts "I received #{params[:foo]} as a String and #{params[:bar]} as an Integer!"
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Which yields these opportunities:
|
30
|
+
#
|
31
|
+
# a = Awesome.new
|
32
|
+
# a.foo(1, "str") # raises
|
33
|
+
# a.foo("str", 1) # prints the message
|
34
|
+
# a.foo("str") # prints the message with nil as the integer
|
35
|
+
#
|
36
|
+
# a.bar(:foo => 1, :bar => "str") # raises
|
37
|
+
# a.bar(:foo => "str") # raises (:bar is required)
|
38
|
+
# a.bar(:bar => 1) # prints message, with nil string
|
39
|
+
# a.bar(:foo => "str", :bar => 1) # prints message
|
40
|
+
#
|
41
|
+
# Using it is quite simple. Just remember a few things:
|
42
|
+
#
|
43
|
+
# * A class will always be compared with Object#kind_of? against the object.
|
44
|
+
# * An object implies certain semantics. Right now, we support direct checking against multiple objects:
|
45
|
+
# * Regexp's will convert the value to a string and compare them with String#=~
|
46
|
+
# * Ranges will use Range#include? to determine if the object occurs within the range.
|
47
|
+
# * A proc will allow you to do a custom check, taking one argument. Raises happen as such:
|
48
|
+
# * Returning false/nil will raise a generic error.
|
49
|
+
# * Returning a new exception object (e.g., ArgumentError.new) will raise your error as close to the call point as possible.
|
50
|
+
# * Raising yourself will raise in the validation routine, which will probably be confusing. Please use the above method.
|
51
|
+
# * If you need more than one constraint per parameter, enclose these constraints within an array.
|
52
|
+
# * Depending on the type of method you're constructing, there will be additional constraints both implied and explictly allowed:
|
53
|
+
# * named methods do not require any items by default, they must be specified as required.
|
54
|
+
# * ordered methods require everything by default, they must be specified as optional.
|
55
|
+
#
|
56
|
+
module MethLab
|
57
|
+
|
58
|
+
VERSION = "0.0.3"
|
59
|
+
|
60
|
+
# Integrates MethLab into all namespaces. It does this by patching itself
|
61
|
+
# into ::main and Module.
|
62
|
+
#
|
63
|
+
# You may also accomplish this automatically by setting
|
64
|
+
# $METHLAB_AUTOINTEGRATE before you require it.
|
65
|
+
def self.integrate
|
66
|
+
eval("self", TOPLEVEL_BINDING).send(:include, self)
|
67
|
+
::Module.send(:include, self)
|
68
|
+
end
|
69
|
+
|
70
|
+
# internal, please do not use directly.
|
71
|
+
#
|
72
|
+
# used to perform our standard checks.
|
73
|
+
def self.check_type(value_sig, value, key)
|
74
|
+
case value_sig
|
75
|
+
when Array
|
76
|
+
value_sig.flatten.each do |vs|
|
77
|
+
ret = check_type(vs, value, key)
|
78
|
+
return ret unless ret.nil?
|
79
|
+
end
|
80
|
+
when Class
|
81
|
+
unless value.kind_of?(value_sig)
|
82
|
+
return ArgumentError.new("value of argument '#{key}' is an invalid type. Requires '#{value_sig}'")
|
83
|
+
end
|
84
|
+
when Proc
|
85
|
+
ret = value_sig.call(value)
|
86
|
+
|
87
|
+
if ret.kind_of?(Exception)
|
88
|
+
return ret
|
89
|
+
elsif !ret
|
90
|
+
return ArgumentError.new("value of argument '#{key}' does not pass custom validation.")
|
91
|
+
else
|
92
|
+
return nil
|
93
|
+
end
|
94
|
+
when Regexp
|
95
|
+
unless value.to_s =~ value_sig
|
96
|
+
return ArgumentError.new("value of argument '#{key}' does not match this regexp: '#{value_sig.to_s}'")
|
97
|
+
end
|
98
|
+
when Range
|
99
|
+
unless value_sig.include?(value)
|
100
|
+
return ArgumentError.new("value of argument '#{key}' does not match range '#{value_sig.inspect}'")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
return nil
|
105
|
+
end
|
106
|
+
|
107
|
+
# internal, do not use directly.
|
108
|
+
#
|
109
|
+
# used to check the arity of array (ordered) method calls.
|
110
|
+
def self.validate_array_params(signature, args)
|
111
|
+
unless args.kind_of?(Array)
|
112
|
+
return ArgumentError.new("this method takes ordered arguments")
|
113
|
+
end
|
114
|
+
|
115
|
+
if args.length > signature.length
|
116
|
+
return ArgumentError.new("too many arguments (#{args.length} for #{signature.length})")
|
117
|
+
end
|
118
|
+
|
119
|
+
opt_index = signature.find_index { |x| [x].flatten.include?(:optional) } || 0
|
120
|
+
|
121
|
+
if args.length < opt_index
|
122
|
+
return ArgumentError.new("not enough arguments (#{args.length} for minimum #{opt_index})")
|
123
|
+
end
|
124
|
+
|
125
|
+
args.each_with_index do |value, key|
|
126
|
+
unless signature[key]
|
127
|
+
return ArgumentError.new("argument #{key} does not exist in prototype")
|
128
|
+
end
|
129
|
+
|
130
|
+
if !signature[key].nil?
|
131
|
+
ret = check_type(signature[key], value, key)
|
132
|
+
return ret unless ret.nil?
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
return args
|
137
|
+
end
|
138
|
+
|
139
|
+
# internal, do not use directly.
|
140
|
+
#
|
141
|
+
# Used to check the sanity of parameterized (named) method calls.
|
142
|
+
def self.validate_params(signature, *args)
|
143
|
+
args = args[0]
|
144
|
+
|
145
|
+
unless args.kind_of?(Hash)
|
146
|
+
return ArgumentError.new("this method takes a hash")
|
147
|
+
end
|
148
|
+
|
149
|
+
args.each do |key, value|
|
150
|
+
unless signature.has_key?(key)
|
151
|
+
return ArgumentError.new("argument '#{key}' does not exist in prototype")
|
152
|
+
end
|
153
|
+
|
154
|
+
if !signature[key].nil?
|
155
|
+
ret = check_type(signature[key], value, key)
|
156
|
+
return ret unless ret.nil?
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
keys = signature.each_key.select { |key| [signature[key]].flatten.include?(:required) and !args.has_key?(key) }
|
161
|
+
|
162
|
+
if keys.length > 0
|
163
|
+
return ArgumentError.new("argument(s) '#{keys.sort_by { |x| x.to_s }.join(", ")}' were not found but are required by the prototype")
|
164
|
+
end
|
165
|
+
|
166
|
+
return args
|
167
|
+
end
|
168
|
+
|
169
|
+
# Builds an unbound method as a proc with ordered parameters.
|
170
|
+
#
|
171
|
+
# Example:
|
172
|
+
#
|
173
|
+
# my_proc = build_ordered(String, [Integer, :optional]) do |params|
|
174
|
+
# str, int = params
|
175
|
+
# puts "I received #{str} as a String and #{int} as an Integer!"
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# my_proc.call("foo", 1)
|
179
|
+
#
|
180
|
+
# As explained above, an array to combine multiple parameters at a position
|
181
|
+
# may be used to flag it with additional data. At this time, these
|
182
|
+
# parameters are supported:
|
183
|
+
#
|
184
|
+
# * :optional - is not required as a part of the argument list.
|
185
|
+
#
|
186
|
+
def build_ordered(*args, &block)
|
187
|
+
signature = args
|
188
|
+
|
189
|
+
op_index = signature.index(:optional)
|
190
|
+
|
191
|
+
if op_index and signature.reject { |x| x == :optional }.length != op_index
|
192
|
+
raise ArgumentError, ":optional parameters must be at the end"
|
193
|
+
end
|
194
|
+
|
195
|
+
proc do |*args|
|
196
|
+
params = MethLab.validate_array_params(signature, args)
|
197
|
+
raise params if params.kind_of?(Exception)
|
198
|
+
block.call(params)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# similar to MethLab#build_ordered, but takes a method name as the first
|
203
|
+
# argument that binds to a method with the same name in the current class
|
204
|
+
# or module. Currently cannot be a class method.
|
205
|
+
def def_ordered(method_name, *args, &block)
|
206
|
+
self.send(:define_method, method_name, &build_ordered(*args, &block))
|
207
|
+
method_name
|
208
|
+
end
|
209
|
+
|
210
|
+
# Builds an unbound method as a proc with named (Hash) parameters.
|
211
|
+
#
|
212
|
+
# Example:
|
213
|
+
#
|
214
|
+
# my_proc = build_named(:foo => String, :bar => [Integer, :required]) do |params|
|
215
|
+
# puts "I received #{params[:foo]} as a String and #{params[:bar]} as an Integer!"
|
216
|
+
# end
|
217
|
+
#
|
218
|
+
# my_proc.call(:foo => "foo", :bar => 1)
|
219
|
+
#
|
220
|
+
# As explained above, an array to combine multiple parameters at a position
|
221
|
+
# may be used to flag it with additional data. At this time, these
|
222
|
+
# parameters are supported:
|
223
|
+
#
|
224
|
+
# * :required - this field is a required argument (parameters are default optional)
|
225
|
+
#
|
226
|
+
def build_named(*args, &block)
|
227
|
+
signature = args[0]
|
228
|
+
|
229
|
+
proc do |*args|
|
230
|
+
params = MethLab.validate_params(signature, *args)
|
231
|
+
raise params if params.kind_of?(Exception)
|
232
|
+
block.call(params)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# similar to MethLab#build_named, but takes a method name as the first
|
237
|
+
# argument that binds to a method with the same name in the current class
|
238
|
+
# or module. Currently cannot be a class method.
|
239
|
+
def def_named(method_name, *args, &block)
|
240
|
+
self.send(:define_method, method_name, &build_named(*args, &block))
|
241
|
+
method_name
|
242
|
+
end
|
243
|
+
|
244
|
+
if $METHLAB_AUTOINTEGRATE
|
245
|
+
integrate
|
246
|
+
end
|
247
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: methlab
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Erik Hollensbe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-04-27 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: erik@hollensbe.org
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib/methlab.rb
|
26
|
+
has_rdoc: true
|
27
|
+
homepage: http://github.com/erikh/methlab
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: "0"
|
38
|
+
version:
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
requirements: []
|
46
|
+
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 1.3.1
|
49
|
+
signing_key:
|
50
|
+
specification_version: 2
|
51
|
+
summary: A method construction and validation toolkit.
|
52
|
+
test_files: []
|
53
|
+
|