thorero 0.9.4

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