robsharp-extlib 0.9.15

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.
Files changed (73) hide show
  1. data/.autotest +21 -0
  2. data/.document +5 -0
  3. data/.gitignore +22 -0
  4. data/LICENSE +47 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +28 -0
  7. data/VERSION +1 -0
  8. data/extlib.gemspec +147 -0
  9. data/lib/extlib.rb +50 -0
  10. data/lib/extlib/array.rb +38 -0
  11. data/lib/extlib/assertions.rb +8 -0
  12. data/lib/extlib/blank.rb +89 -0
  13. data/lib/extlib/boolean.rb +11 -0
  14. data/lib/extlib/byte_array.rb +6 -0
  15. data/lib/extlib/class.rb +179 -0
  16. data/lib/extlib/datetime.rb +29 -0
  17. data/lib/extlib/dictionary.rb +433 -0
  18. data/lib/extlib/hash.rb +450 -0
  19. data/lib/extlib/hook.rb +407 -0
  20. data/lib/extlib/inflection.rb +442 -0
  21. data/lib/extlib/lazy_array.rb +453 -0
  22. data/lib/extlib/lazy_module.rb +18 -0
  23. data/lib/extlib/logger.rb +198 -0
  24. data/lib/extlib/mash.rb +157 -0
  25. data/lib/extlib/module.rb +51 -0
  26. data/lib/extlib/nil.rb +5 -0
  27. data/lib/extlib/numeric.rb +5 -0
  28. data/lib/extlib/object.rb +178 -0
  29. data/lib/extlib/object_space.rb +13 -0
  30. data/lib/extlib/pathname.rb +20 -0
  31. data/lib/extlib/pooling.rb +235 -0
  32. data/lib/extlib/rubygems.rb +38 -0
  33. data/lib/extlib/simple_set.rb +66 -0
  34. data/lib/extlib/string.rb +176 -0
  35. data/lib/extlib/struct.rb +17 -0
  36. data/lib/extlib/symbol.rb +21 -0
  37. data/lib/extlib/time.rb +44 -0
  38. data/lib/extlib/try_dup.rb +44 -0
  39. data/lib/extlib/virtual_file.rb +10 -0
  40. data/spec/array_spec.rb +40 -0
  41. data/spec/blank_spec.rb +86 -0
  42. data/spec/byte_array_spec.rb +8 -0
  43. data/spec/class_spec.rb +158 -0
  44. data/spec/datetime_spec.rb +22 -0
  45. data/spec/hash_spec.rb +536 -0
  46. data/spec/hook_spec.rb +1235 -0
  47. data/spec/inflection/plural_spec.rb +565 -0
  48. data/spec/inflection/singular_spec.rb +498 -0
  49. data/spec/inflection_extras_spec.rb +111 -0
  50. data/spec/lazy_array_spec.rb +1961 -0
  51. data/spec/lazy_module_spec.rb +38 -0
  52. data/spec/mash_spec.rb +312 -0
  53. data/spec/module_spec.rb +71 -0
  54. data/spec/object_space_spec.rb +10 -0
  55. data/spec/object_spec.rb +114 -0
  56. data/spec/pooling_spec.rb +511 -0
  57. data/spec/rcov.opts +6 -0
  58. data/spec/simple_set_spec.rb +58 -0
  59. data/spec/spec.opts +4 -0
  60. data/spec/spec_helper.rb +7 -0
  61. data/spec/string_spec.rb +222 -0
  62. data/spec/struct_spec.rb +13 -0
  63. data/spec/symbol_spec.rb +9 -0
  64. data/spec/time_spec.rb +31 -0
  65. data/spec/try_call_spec.rb +74 -0
  66. data/spec/try_dup_spec.rb +46 -0
  67. data/spec/virtual_file_spec.rb +22 -0
  68. data/tasks/ci.rake +1 -0
  69. data/tasks/metrics.rake +36 -0
  70. data/tasks/spec.rake +25 -0
  71. data/tasks/yard.rake +9 -0
  72. data/tasks/yardstick.rake +19 -0
  73. metadata +198 -0
@@ -0,0 +1,450 @@
1
+ require "time"
2
+ require 'bigdecimal'
3
+
4
+ require 'extlib/object'
5
+ require 'extlib/mash'
6
+ require 'extlib/class'
7
+ require 'extlib/string'
8
+
9
+ class Hash
10
+ class << self
11
+ # Converts valid XML into a Ruby Hash structure.
12
+ #
13
+ # Mixed content is treated as text and any tags in it are left unparsed
14
+ #
15
+ # Any attributes other than type on a node containing a text node will be
16
+ # discarded
17
+ #
18
+ # [Typecasting is performed on elements that have a +type+ attribute:]
19
+ # integer:: Returns an Integer
20
+ # boolean:: Anything other than "true" evaluates to false.
21
+ # datetime::
22
+ # Returns a Time object. See Time documentation for valid Time strings.
23
+ # date::
24
+ # Returns a Date object. See Date documentation for valid Date strings.
25
+ #
26
+ # Keys are automatically converted to +snake_case+
27
+ #
28
+ # [Simple]
29
+ #
30
+ # <user gender='m'>
31
+ # <age type='integer'>35</age>
32
+ # <name>Home Simpson</name>
33
+ # <dob type='date'>1988-01-01</dob>
34
+ # <joined-at type='datetime'>2000-04-28 23:01</joined-at>
35
+ # <is-cool type='boolean'>true</is-cool>
36
+ # </user>
37
+ #
38
+ # Becomes:
39
+ #
40
+ # { "user" => {
41
+ # "gender" => "m",
42
+ # "age" => 35,
43
+ # "name" => "Home Simpson",
44
+ # "dob" => DateObject( 1998-01-01 ),
45
+ # "joined_at" => TimeObject( 2000-04-28 23:01),
46
+ # "is_cool" => true
47
+ # }
48
+ # }
49
+ #
50
+ # [Mixed Content]
51
+ #
52
+ # <story>
53
+ # A Quick <em>brown</em> Fox
54
+ # </story>
55
+ #
56
+ # Evaluates to:
57
+ #
58
+ # { "story" => "A Quick <em>brown</em> Fox" }
59
+ #
60
+ # [Attributes other than type on a node containing text]
61
+ #
62
+ # <story is-good='false'>
63
+ # A Quick <em>brown</em> Fox
64
+ # </story>
65
+ #
66
+ # Are ignored:
67
+ #
68
+ # { "story" => "A Quick <em>brown</em> Fox" }
69
+ #
70
+ # [Other attributes in addition to +type+]
71
+ #
72
+ # <bicep unit='inches' type='integer'>60</bicep>
73
+ #
74
+ # Evaluates with a typecast to an integer. But unit attribute is ignored:
75
+ #
76
+ # { "bicep" => 60 }
77
+ #
78
+ # @param [String] xml A string representation of valid XML.
79
+ #
80
+ # @return [Hash] A hash created by parsing +xml+
81
+ def from_xml( xml )
82
+ ToHashParser.from_xml(xml)
83
+ end
84
+ end
85
+
86
+ # Convert to Mash. This class has semantics of ActiveSupport's
87
+ # HashWithIndifferentAccess and we only have it so that people can write
88
+ # params[:key] instead of params['key'].
89
+ #
90
+ # @return [Mash] This hash as a Mash for string or symbol key access.
91
+ def to_mash
92
+ hash = Mash.new(self)
93
+ hash.default = default
94
+ hash
95
+ end
96
+
97
+ ##
98
+ # Convert to URL query param string
99
+ #
100
+ # { :name => "Bob",
101
+ # :address => {
102
+ # :street => '111 Ruby Ave.',
103
+ # :city => 'Ruby Central',
104
+ # :phones => ['111-111-1111', '222-222-2222']
105
+ # }
106
+ # }.to_params
107
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
108
+ #
109
+ # @return [String] This hash as a query string
110
+ #
111
+ # @api public
112
+ def to_params
113
+ params = self.map { |k,v| normalize_param(k,v) }.join
114
+ params.chop! # trailing &
115
+ params
116
+ end
117
+
118
+ ##
119
+ # Convert a key, value pair into a URL query param string
120
+ #
121
+ # normalize_param(:name, "Bob") #=> "name=Bob&"
122
+ #
123
+ # @param [Object] key The key for the param.
124
+ # @param [Object] value The value for the param.
125
+ #
126
+ # @return [String] This key value pair as a param
127
+ #
128
+ # @api public
129
+ def normalize_param(key, value)
130
+ param = ''
131
+ stack = []
132
+
133
+ if value.is_a?(Array)
134
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
135
+ elsif value.is_a?(Hash)
136
+ stack << [key,value]
137
+ else
138
+ param << "#{key}=#{value}&"
139
+ end
140
+
141
+ stack.each do |parent, hash|
142
+ hash.each do |k, v|
143
+ if v.is_a?(Hash)
144
+ stack << ["#{parent}[#{k}]", v]
145
+ else
146
+ param << normalize_param("#{parent}[#{k}]", v)
147
+ end
148
+ end
149
+ end
150
+
151
+ param
152
+ end
153
+
154
+ ##
155
+ # Create a hash with *only* key/value pairs in receiver and +allowed+
156
+ #
157
+ # { :one => 1, :two => 2, :three => 3 }.only(:one) #=> { :one => 1 }
158
+ #
159
+ # @param [Array[String, Symbol]] *allowed The hash keys to include.
160
+ #
161
+ # @return [Hash] A new hash with only the selected keys.
162
+ #
163
+ # @api public
164
+ def only(*allowed)
165
+ hash = {}
166
+ allowed.each {|k| hash[k] = self[k] if self.has_key?(k) }
167
+ hash
168
+ end
169
+
170
+ ##
171
+ # Create a hash with all key/value pairs in receiver *except* +rejected+
172
+ #
173
+ # { :one => 1, :two => 2, :three => 3 }.except(:one)
174
+ # #=> { :two => 2, :three => 3 }
175
+ #
176
+ # @param [Array[String, Symbol]] *rejected The hash keys to exclude.
177
+ #
178
+ # @return [Hash] A new hash without the selected keys.
179
+ #
180
+ # @api public
181
+ def except(*rejected)
182
+ hash = self.dup
183
+ rejected.each {|k| hash.delete(k) }
184
+ hash
185
+ end
186
+
187
+ # @return [String] The hash as attributes for an XML tag.
188
+ #
189
+ # @example
190
+ # { :one => 1, "two"=>"TWO" }.to_xml_attributes
191
+ # #=> 'one="1" two="TWO"'
192
+ def to_xml_attributes
193
+ map do |k,v|
194
+ %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
195
+ end.join(' ')
196
+ end
197
+
198
+ alias_method :to_html_attributes, :to_xml_attributes
199
+
200
+ # @param html_class<#to_s>
201
+ # The HTML class to add to the :class key. The html_class will be
202
+ # concatenated to any existing classes.
203
+ #
204
+ # @example hash[:class] #=> nil
205
+ # @example hash.add_html_class!(:selected)
206
+ # @example hash[:class] #=> "selected"
207
+ # @example hash.add_html_class!("class1 class2")
208
+ # @example hash[:class] #=> "selected class1 class2"
209
+ def add_html_class!(html_class)
210
+ if self[:class]
211
+ self[:class] = "#{self[:class]} #{html_class}"
212
+ else
213
+ self[:class] = html_class.to_s
214
+ end
215
+ end
216
+
217
+ # Converts all keys into string values. This is used during reloading to
218
+ # prevent problems when classes are no longer declared.
219
+ #
220
+ # @return [Array] An array of they hash's keys
221
+ #
222
+ # @example
223
+ # hash = { One => 1, Two => 2 }.proctect_keys!
224
+ # hash # => { "One" => 1, "Two" => 2 }
225
+ def protect_keys!
226
+ keys.each {|key| self[key.to_s] = delete(key) }
227
+ end
228
+
229
+ # Attempts to convert all string keys into Class keys. We run this after
230
+ # reloading to convert protected hashes back into usable hashes.
231
+ #
232
+ # @example
233
+ # # Provided that classes One and Two are declared in this scope:
234
+ # hash = { "One" => 1, "Two" => 2 }.unproctect_keys!
235
+ # hash # => { One => 1, Two => 2 }
236
+ def unprotect_keys!
237
+ keys.each do |key|
238
+ (self[Object.full_const_get(key)] = delete(key)) rescue nil
239
+ end
240
+ end
241
+
242
+ # Destructively and non-recursively convert each key to an uppercase string,
243
+ # deleting nil values along the way.
244
+ #
245
+ # @return [Hash] The newly environmentized hash.
246
+ #
247
+ # @example
248
+ # { :name => "Bob", :contact => { :email => "bob@bob.com" } }.environmentize_keys!
249
+ # #=> { "NAME" => "Bob", "CONTACT" => { :email => "bob@bob.com" } }
250
+ def environmentize_keys!
251
+ keys.each do |key|
252
+ val = delete(key)
253
+ next if val.nil?
254
+ self[key.to_s.upcase] = val
255
+ end
256
+ self
257
+ end
258
+ end
259
+
260
+ require 'rexml/parsers/streamparser'
261
+ require 'rexml/parsers/baseparser'
262
+ require 'rexml/light/node'
263
+
264
+ # This is a slighly modified version of the XMLUtilityNode from
265
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
266
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
267
+ # This represents the hard part of the work, all I did was change the
268
+ # underlying parser.
269
+ class REXMLUtilityNode
270
+ attr_accessor :name, :attributes, :children, :type
271
+ cattr_accessor :typecasts, :available_typecasts
272
+
273
+ self.typecasts = {}
274
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
275
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
276
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
277
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
278
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
279
+ self.typecasts["decimal"] = lambda{|v| BigDecimal(v)}
280
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
281
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
282
+ self.typecasts["symbol"] = lambda{|v| v.to_sym}
283
+ self.typecasts["string"] = lambda{|v| v.to_s}
284
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
285
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
286
+
287
+ self.available_typecasts = self.typecasts.keys
288
+
289
+ def initialize(name, attributes = {})
290
+ @name = name.tr("-", "_")
291
+ # leave the type alone if we don't know what it is
292
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
293
+
294
+ @nil_element = attributes.delete("nil") == "true"
295
+ @attributes = undasherize_keys(attributes)
296
+ @children = []
297
+ @text = false
298
+ end
299
+
300
+ def add_node(node)
301
+ @text = true if node.is_a? String
302
+ @children << node
303
+ end
304
+
305
+ def to_hash
306
+ if @type == "file"
307
+ f = StringIO.new((@children.first || '').unpack('m').first)
308
+ class << f
309
+ attr_accessor :original_filename, :content_type
310
+ end
311
+ f.original_filename = attributes['name'] || 'untitled'
312
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
313
+ return {name => f}
314
+ end
315
+
316
+ if @text
317
+ return { name => typecast_value( translate_xml_entities( inner_html ) ) }
318
+ else
319
+ #change repeating groups into an array
320
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
321
+
322
+ out = nil
323
+ if @type == "array"
324
+ out = []
325
+ groups.each do |k, v|
326
+ if v.size == 1
327
+ out << v.first.to_hash.entries.first.last
328
+ else
329
+ out << v.map{|e| e.to_hash[k]}
330
+ end
331
+ end
332
+ out = out.flatten
333
+
334
+ else # If Hash
335
+ out = {}
336
+ groups.each do |k,v|
337
+ if v.size == 1
338
+ out.merge!(v.first)
339
+ else
340
+ out.merge!( k => v.map{|e| e.to_hash[k]})
341
+ end
342
+ end
343
+ out.merge! attributes unless attributes.empty?
344
+ out = out.empty? ? nil : out
345
+ end
346
+
347
+ if @type && out.nil?
348
+ { name => typecast_value(out) }
349
+ else
350
+ { name => out }
351
+ end
352
+ end
353
+ end
354
+
355
+ # Typecasts a value based upon its type. For instance, if
356
+ # +node+ has #type == "integer",
357
+ # {{[node.typecast_value("12") #=> 12]}}
358
+ #
359
+ # @param value<String> The value that is being typecast.
360
+ #
361
+ # @details [:type options]
362
+ # "integer"::
363
+ # converts +value+ to an integer with #to_i
364
+ # "boolean"::
365
+ # checks whether +value+, after removing spaces, is the literal
366
+ # "true"
367
+ # "datetime"::
368
+ # Parses +value+ using Time.parse, and returns a UTC Time
369
+ # "date"::
370
+ # Parses +value+ using Date.parse
371
+ #
372
+ # @return [Integer, Boolean, Time, Date, Object]
373
+ # The result of typecasting +value+.
374
+ #
375
+ # @note
376
+ # If +self+ does not have a "type" key, or if it's not one of the
377
+ # options specified above, the raw +value+ will be returned.
378
+ def typecast_value(value)
379
+ return value unless @type
380
+ proc = self.class.typecasts[@type]
381
+ proc.nil? ? value : proc.call(value)
382
+ end
383
+
384
+ # Convert basic XML entities into their literal values.
385
+ #
386
+ # @param value<#gsub> An XML fragment.
387
+ #
388
+ # @return [#gsub] The XML fragment after converting entities.
389
+ def translate_xml_entities(value)
390
+ value.gsub(/&lt;/, "<").
391
+ gsub(/&gt;/, ">").
392
+ gsub(/&quot;/, '"').
393
+ gsub(/&apos;/, "'").
394
+ gsub(/&amp;/, "&")
395
+ end
396
+
397
+ # Take keys of the form foo-bar and convert them to foo_bar
398
+ def undasherize_keys(params)
399
+ params.keys.each do |key, value|
400
+ params[key.tr("-", "_")] = params.delete(key)
401
+ end
402
+ params
403
+ end
404
+
405
+ # Get the inner_html of the REXML node.
406
+ def inner_html
407
+ @children.join
408
+ end
409
+
410
+ # Converts the node into a readable HTML node.
411
+ #
412
+ # @return [String] The HTML node in text form.
413
+ def to_html
414
+ attributes.merge!(:type => @type ) if @type
415
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
416
+ end
417
+
418
+ # @alias #to_html #to_s
419
+ def to_s
420
+ to_html
421
+ end
422
+ end
423
+
424
+ class ToHashParser
425
+
426
+ def self.from_xml(xml)
427
+ stack = []
428
+ parser = REXML::Parsers::BaseParser.new(xml)
429
+
430
+ while true
431
+ event = parser.pull
432
+ case event[0]
433
+ when :end_document
434
+ break
435
+ when :end_doctype, :start_doctype
436
+ # do nothing
437
+ when :start_element
438
+ stack.push REXMLUtilityNode.new(event[1], event[2])
439
+ when :end_element
440
+ if stack.size > 1
441
+ temp = stack.pop
442
+ stack.last.add_node(temp)
443
+ end
444
+ when :text, :cdata
445
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0
446
+ end
447
+ end
448
+ stack.pop.to_hash
449
+ end
450
+ end
@@ -0,0 +1,407 @@
1
+ require 'extlib/assertions'
2
+ require 'extlib/class'
3
+ require 'extlib/object'
4
+
5
+ module Extlib
6
+ #
7
+ # TODO: Write more documentation!
8
+ #
9
+ # Overview
10
+ # ========
11
+ #
12
+ # The Hook module is a very simple set of AOP helpers. Basically, it
13
+ # allows the developer to specify a method or block that should run
14
+ # before or after another method.
15
+ #
16
+ # Usage
17
+ # =====
18
+ #
19
+ # Halting The Hook Stack
20
+ #
21
+ # Inheritance
22
+ #
23
+ # Other Goodies
24
+ #
25
+ # Please bring up any issues regarding Hooks with carllerche on IRC
26
+ #
27
+ module Hook
28
+
29
+ def self.included(base)
30
+ base.extend(ClassMethods)
31
+ base.const_set("CLASS_HOOKS", {}) unless base.const_defined?("CLASS_HOOKS")
32
+ base.const_set("INSTANCE_HOOKS", {}) unless base.const_defined?("INSTANCE_HOOKS")
33
+ base.class_eval do
34
+ class << self
35
+ def method_added(name)
36
+ process_method_added(name, :instance)
37
+ super
38
+ end
39
+
40
+ def singleton_method_added(name)
41
+ process_method_added(name, :class)
42
+ super
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ include Extlib::Assertions
50
+ # Inject code that executes before the target class method.
51
+ #
52
+ # @param target_method<Symbol> the name of the class method to inject before
53
+ # @param method_sym<Symbol> the name of the method to run before the
54
+ # target_method
55
+ # @param block<Block> the code to run before the target_method
56
+ #
57
+ # @note
58
+ # Either method_sym or block is required.
59
+ # -
60
+ # @api public
61
+ def before_class_method(target_method, method_sym = nil, &block)
62
+ install_hook :before, target_method, method_sym, :class, &block
63
+ end
64
+
65
+ #
66
+ # Inject code that executes after the target class method.
67
+ #
68
+ # @param target_method<Symbol> the name of the class method to inject after
69
+ # @param method_sym<Symbol> the name of the method to run after the target_method
70
+ # @param block<Block> the code to run after the target_method
71
+ #
72
+ # @note
73
+ # Either method_sym or block is required.
74
+ # -
75
+ # @api public
76
+ def after_class_method(target_method, method_sym = nil, &block)
77
+ install_hook :after, target_method, method_sym, :class, &block
78
+ end
79
+
80
+ #
81
+ # Inject code that executes before the target instance method.
82
+ #
83
+ # @param target_method<Symbol> the name of the instance method to inject before
84
+ # @param method_sym<Symbol> the name of the method to run before the
85
+ # target_method
86
+ # @param block<Block> the code to run before the target_method
87
+ #
88
+ # @note
89
+ # Either method_sym or block is required.
90
+ # -
91
+ # @api public
92
+ def before(target_method, method_sym = nil, &block)
93
+ install_hook :before, target_method, method_sym, :instance, &block
94
+ end
95
+
96
+ #
97
+ # Inject code that executes after the target instance method.
98
+ #
99
+ # @param target_method<Symbol> the name of the instance method to inject after
100
+ # @param method_sym<Symbol> the name of the method to run after the
101
+ # target_method
102
+ # @param block<Block> the code to run after the target_method
103
+ #
104
+ # @note
105
+ # Either method_sym or block is required.
106
+ # -
107
+ # @api public
108
+ def after(target_method, method_sym = nil, &block)
109
+ install_hook :after, target_method, method_sym, :instance, &block
110
+ end
111
+
112
+ # Register a class method as hookable. Registering a method means that
113
+ # before hooks will be run immediately before the method is invoked and
114
+ # after hooks will be called immediately after the method is invoked.
115
+ #
116
+ # @param hookable_method<Symbol> The name of the class method that should
117
+ # be hookable
118
+ # -
119
+ # @api public
120
+ def register_class_hooks(*hooks)
121
+ hooks.each { |hook| register_hook(hook, :class) }
122
+ end
123
+
124
+ # Register aninstance method as hookable. Registering a method means that
125
+ # before hooks will be run immediately before the method is invoked and
126
+ # after hooks will be called immediately after the method is invoked.
127
+ #
128
+ # @param hookable_method<Symbol> The name of the instance method that should
129
+ # be hookable
130
+ # -
131
+ # @api public
132
+ def register_instance_hooks(*hooks)
133
+ hooks.each { |hook| register_hook(hook, :instance) }
134
+ end
135
+
136
+ # Not yet implemented
137
+ def reset_hook!(target_method, scope)
138
+ raise NotImplementedError
139
+ end
140
+
141
+ # --- Alright kids... the rest is internal stuff ---
142
+
143
+ # Returns the correct HOOKS Hash depending on whether we are
144
+ # working with class methods or instance methods
145
+ def hooks_with_scope(scope)
146
+ case scope
147
+ when :class then class_hooks
148
+ when :instance then instance_hooks
149
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
150
+ end
151
+ end
152
+
153
+ def class_hooks
154
+ self.const_get("CLASS_HOOKS")
155
+ end
156
+
157
+ def instance_hooks
158
+ self.const_get("INSTANCE_HOOKS")
159
+ end
160
+
161
+ # Registers a method as hookable. Registering hooks involves the following
162
+ # process
163
+ #
164
+ # * Create a blank entry in the HOOK Hash for the method.
165
+ # * Define the methods that execute the before and after hook stack.
166
+ # These methods will be no-ops at first, but everytime a new hook is
167
+ # defined, the methods will be redefined to incorporate the new hook.
168
+ # * Redefine the method that is to be hookable so that the hook stacks
169
+ # are invoked approprietly.
170
+ def register_hook(target_method, scope)
171
+ if scope == :instance && !method_defined?(target_method)
172
+ raise ArgumentError, "#{target_method} instance method does not exist"
173
+ elsif scope == :class && !respond_to?(target_method)
174
+ raise ArgumentError, "#{target_method} class method does not exist"
175
+ end
176
+
177
+ hooks = hooks_with_scope(scope)
178
+
179
+ if hooks[target_method].nil?
180
+ hooks[target_method] = {
181
+ # We need to keep track of which class in the Inheritance chain the
182
+ # method was declared hookable in. Every time a child declares a new
183
+ # hook for the method, the hook stack invocations need to be redefined
184
+ # in the original Class. See #define_hook_stack_execution_methods
185
+ :before => [], :after => [], :in => self
186
+ }
187
+
188
+ define_hook_stack_execution_methods(target_method, scope)
189
+ define_advised_method(target_method, scope)
190
+ end
191
+ end
192
+
193
+ # Is the method registered as a hookable in the given scope.
194
+ def registered_as_hook?(target_method, scope)
195
+ ! hooks_with_scope(scope)[target_method].nil?
196
+ end
197
+
198
+ # Generates names for the various utility methods. We need to do this because
199
+ # the various utility methods should not end in = so, while we're at it, we
200
+ # might as well get rid of all punctuation.
201
+ def hook_method_name(target_method, prefix, suffix)
202
+ target_method = target_method.to_s
203
+
204
+ case target_method[-1,1]
205
+ when '?' then "#{prefix}_#{target_method[0..-2]}_ques_#{suffix}"
206
+ when '!' then "#{prefix}_#{target_method[0..-2]}_bang_#{suffix}"
207
+ when '=' then "#{prefix}_#{target_method[0..-2]}_eq_#{suffix}"
208
+ # I add a _nan_ suffix here so that we don't ever encounter
209
+ # any naming conflicts.
210
+ else "#{prefix}_#{target_method[0..-1]}_nan_#{suffix}"
211
+ end
212
+ end
213
+
214
+ # This will need to be refactored
215
+ def process_method_added(method_name, scope)
216
+ hooks_with_scope(scope).each do |target_method, hooks|
217
+ if hooks[:before].any? { |hook| hook[:name] == method_name }
218
+ define_hook_stack_execution_methods(target_method, scope)
219
+ end
220
+
221
+ if hooks[:after].any? { |hook| hook[:name] == method_name }
222
+ define_hook_stack_execution_methods(target_method, scope)
223
+ end
224
+ end
225
+ end
226
+
227
+ # Defines two methods. One method executes the before hook stack. The other executes
228
+ # the after hook stack. This method will be called many times during the Class definition
229
+ # process. It should be called for each hook that is defined. It will also be called
230
+ # when a hook is redefined (to make sure that the arity hasn't changed).
231
+ def define_hook_stack_execution_methods(target_method, scope)
232
+ unless registered_as_hook?(target_method, scope)
233
+ raise ArgumentError, "#{target_method} has not be registered as a hookable #{scope} method"
234
+ end
235
+
236
+ hooks = hooks_with_scope(scope)
237
+
238
+ before_hooks = hooks[target_method][:before]
239
+ before_hooks = before_hooks.map{ |info| inline_call(info, scope) }.join("\n")
240
+
241
+ after_hooks = hooks[target_method][:after]
242
+ after_hooks = after_hooks.map{ |info| inline_call(info, scope) }.join("\n")
243
+
244
+ before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
245
+ after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
246
+
247
+ hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
248
+ #{scope == :class ? 'class << self' : ''}
249
+
250
+ private
251
+
252
+ remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
253
+ def #{before_hook_name}(*args)
254
+ #{before_hooks}
255
+ end
256
+
257
+ remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{after_hook_name} }
258
+ def #{after_hook_name}(*args)
259
+ #{after_hooks}
260
+ end
261
+
262
+ #{scope == :class ? 'end' : ''}
263
+ RUBY
264
+ end
265
+
266
+ # Returns ruby code that will invoke the hook. It checks the arity of the hook method
267
+ # and passes arguments accordingly.
268
+ def inline_call(method_info, scope)
269
+ name = method_info[:name]
270
+
271
+ if scope == :instance
272
+ args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : ''
273
+ %(#{name}(#{args}) if self.class <= ObjectSpace._id2ref(#{method_info[:from].object_id}))
274
+ else
275
+ args = respond_to?(name) && method(name).arity != 0 ? '*args' : ''
276
+ %(#{name}(#{args}) if self <= ObjectSpace._id2ref(#{method_info[:from].object_id}))
277
+ end
278
+ end
279
+
280
+ def define_advised_method(target_method, scope)
281
+ args = args_for(method_with_scope(target_method, scope))
282
+
283
+ renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised')
284
+
285
+ source = <<-EOD
286
+ def #{target_method}(#{args})
287
+ retval = nil
288
+ catch(:halt) do
289
+ #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(#{args})
290
+ retval = #{renamed_target}(#{args})
291
+ #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(retval, #{args})
292
+ retval
293
+ end
294
+ end
295
+ EOD
296
+
297
+ if scope == :instance && !instance_methods(false).any? { |m| m.to_sym == target_method }
298
+ send(:alias_method, renamed_target, target_method)
299
+
300
+ proxy_module = Module.new
301
+ proxy_module.class_eval(source, __FILE__, __LINE__)
302
+ self.send(:include, proxy_module)
303
+ else
304
+ source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}}
305
+ source = %{class << self\n#{source}\nend} if scope == :class
306
+ class_eval(source, __FILE__, __LINE__)
307
+ end
308
+ end
309
+
310
+ # --- Add a hook ---
311
+
312
+ def install_hook(type, target_method, method_sym, scope, &block)
313
+ assert_kind_of 'target_method', target_method, Symbol
314
+ assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil?
315
+ assert_kind_of 'scope', scope, Symbol
316
+
317
+ if !block_given? and method_sym.nil?
318
+ raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"."
319
+ end
320
+
321
+ if method_sym.to_s[-1,1] == '='
322
+ raise ArgumentError, "Methods ending in = cannot be hooks"
323
+ end
324
+
325
+ unless [ :class, :instance ].include?(scope)
326
+ raise ArgumentError, 'You need to pass :class or :instance as scope'
327
+ end
328
+
329
+ if registered_as_hook?(target_method, scope)
330
+ hooks = hooks_with_scope(scope)
331
+
332
+ #if this hook is previously declared in a sibling or cousin we must move the :in class
333
+ #to the common ancestor to get both hooks to run.
334
+ if !(hooks[target_method][:in] <=> self)
335
+ before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
336
+ after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
337
+
338
+ hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
339
+ remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
340
+ def #{before_hook_name}(*args)
341
+ super
342
+ end
343
+
344
+ remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
345
+ def #{after_hook_name}(*args)
346
+ super
347
+ end
348
+ RUBY
349
+
350
+ while !(hooks[target_method][:in] <=> self) do
351
+ hooks[target_method][:in] = hooks[target_method][:in].superclass
352
+ end
353
+
354
+ define_hook_stack_execution_methods(target_method, scope)
355
+ hooks[target_method][:in].class_eval{define_advised_method(target_method, scope)}
356
+ end
357
+ else
358
+ register_hook(target_method, scope)
359
+ hooks = hooks_with_scope(scope)
360
+ end
361
+
362
+ #if we were passed a block, create a method out of it.
363
+ if block
364
+ method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym
365
+ if scope == :class
366
+ meta_class.instance_eval do
367
+ define_method(method_sym, &block)
368
+ end
369
+ else
370
+ define_method(method_sym, &block)
371
+ end
372
+ end
373
+
374
+ # Adds method to the stack an redefines the hook invocation method
375
+ hooks[target_method][type] << { :name => method_sym, :from => self }
376
+ define_hook_stack_execution_methods(target_method, scope)
377
+ end
378
+
379
+ # --- Helpers ---
380
+
381
+ def args_for(method)
382
+ if method.arity == 0
383
+ "&block"
384
+ elsif method.arity > 0
385
+ "_" << (1 .. method.arity).to_a.join(", _") << ", &block"
386
+ elsif (method.arity + 1) < 0
387
+ "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block"
388
+ else
389
+ "*args, &block"
390
+ end
391
+ end
392
+
393
+ def method_with_scope(name, scope)
394
+ case scope
395
+ when :class then method(name)
396
+ when :instance then instance_method(name)
397
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
398
+ end
399
+ end
400
+
401
+ def quote_method(name)
402
+ name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_')
403
+ end
404
+ end
405
+
406
+ end
407
+ end