thorero 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
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