methlab 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/methlab.rb +247 -0
  2. metadata +53 -0
@@ -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
+