innate 2009.04

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 (128) hide show
  1. data/CHANGELOG +2981 -0
  2. data/COPYING +18 -0
  3. data/MANIFEST +127 -0
  4. data/README.md +563 -0
  5. data/Rakefile +35 -0
  6. data/example/app/retro_games.rb +60 -0
  7. data/example/app/todo/layout/default.xhtml +11 -0
  8. data/example/app/todo/spec/todo.rb +63 -0
  9. data/example/app/todo/start.rb +51 -0
  10. data/example/app/todo/view/index.xhtml +39 -0
  11. data/example/app/whywiki_erb/layout/wiki.html.erb +15 -0
  12. data/example/app/whywiki_erb/spec/wiki.rb +19 -0
  13. data/example/app/whywiki_erb/start.rb +42 -0
  14. data/example/app/whywiki_erb/view/edit.erb +6 -0
  15. data/example/app/whywiki_erb/view/index.erb +12 -0
  16. data/example/custom_middleware.rb +35 -0
  17. data/example/hello.rb +11 -0
  18. data/example/howto_spec.rb +35 -0
  19. data/example/link.rb +27 -0
  20. data/example/provides.rb +31 -0
  21. data/example/session.rb +38 -0
  22. data/innate.gemspec +29 -0
  23. data/lib/innate.rb +269 -0
  24. data/lib/innate/action.rb +150 -0
  25. data/lib/innate/adapter.rb +76 -0
  26. data/lib/innate/cache.rb +134 -0
  27. data/lib/innate/cache/api.rb +128 -0
  28. data/lib/innate/cache/drb.rb +58 -0
  29. data/lib/innate/cache/file_based.rb +41 -0
  30. data/lib/innate/cache/marshal.rb +17 -0
  31. data/lib/innate/cache/memory.rb +22 -0
  32. data/lib/innate/cache/yaml.rb +17 -0
  33. data/lib/innate/current.rb +37 -0
  34. data/lib/innate/dynamap.rb +96 -0
  35. data/lib/innate/helper.rb +183 -0
  36. data/lib/innate/helper/aspect.rb +124 -0
  37. data/lib/innate/helper/cgi.rb +54 -0
  38. data/lib/innate/helper/flash.rb +36 -0
  39. data/lib/innate/helper/link.rb +94 -0
  40. data/lib/innate/helper/redirect.rb +85 -0
  41. data/lib/innate/helper/render.rb +87 -0
  42. data/lib/innate/helper/send_file.rb +26 -0
  43. data/lib/innate/log.rb +20 -0
  44. data/lib/innate/log/color_formatter.rb +43 -0
  45. data/lib/innate/log/hub.rb +73 -0
  46. data/lib/innate/middleware_compiler.rb +65 -0
  47. data/lib/innate/mock.rb +49 -0
  48. data/lib/innate/node.rb +1025 -0
  49. data/lib/innate/options.rb +37 -0
  50. data/lib/innate/options/dsl.rb +202 -0
  51. data/lib/innate/options/stub.rb +7 -0
  52. data/lib/innate/request.rb +141 -0
  53. data/lib/innate/response.rb +23 -0
  54. data/lib/innate/route.rb +110 -0
  55. data/lib/innate/session.rb +121 -0
  56. data/lib/innate/session/flash.rb +94 -0
  57. data/lib/innate/spec.rb +23 -0
  58. data/lib/innate/state.rb +27 -0
  59. data/lib/innate/state/accessor.rb +130 -0
  60. data/lib/innate/state/fiber.rb +74 -0
  61. data/lib/innate/state/thread.rb +47 -0
  62. data/lib/innate/traited.rb +85 -0
  63. data/lib/innate/trinity.rb +18 -0
  64. data/lib/innate/version.rb +3 -0
  65. data/lib/innate/view.rb +60 -0
  66. data/lib/innate/view/erb.rb +15 -0
  67. data/lib/innate/view/etanni.rb +36 -0
  68. data/lib/innate/view/none.rb +9 -0
  69. data/spec/example/app/retro_games.rb +30 -0
  70. data/spec/example/hello.rb +13 -0
  71. data/spec/example/link.rb +25 -0
  72. data/spec/example/provides.rb +16 -0
  73. data/spec/example/session.rb +22 -0
  74. data/spec/helper.rb +10 -0
  75. data/spec/innate/action/layout.rb +107 -0
  76. data/spec/innate/action/layout/file_layout.xhtml +1 -0
  77. data/spec/innate/cache/common.rb +47 -0
  78. data/spec/innate/cache/marshal.rb +5 -0
  79. data/spec/innate/cache/memory.rb +5 -0
  80. data/spec/innate/cache/yaml.rb +5 -0
  81. data/spec/innate/dynamap.rb +22 -0
  82. data/spec/innate/helper.rb +86 -0
  83. data/spec/innate/helper/aspect.rb +75 -0
  84. data/spec/innate/helper/cgi.rb +37 -0
  85. data/spec/innate/helper/flash.rb +118 -0
  86. data/spec/innate/helper/link.rb +139 -0
  87. data/spec/innate/helper/redirect.rb +160 -0
  88. data/spec/innate/helper/render.rb +133 -0
  89. data/spec/innate/helper/send_file.rb +21 -0
  90. data/spec/innate/helper/view/aspect_hello.xhtml +1 -0
  91. data/spec/innate/helper/view/locals.xhtml +1 -0
  92. data/spec/innate/helper/view/loop.xhtml +4 -0
  93. data/spec/innate/helper/view/num.xhtml +1 -0
  94. data/spec/innate/helper/view/partial.xhtml +1 -0
  95. data/spec/innate/helper/view/recursive.xhtml +7 -0
  96. data/spec/innate/mock.rb +84 -0
  97. data/spec/innate/node/mapping.rb +37 -0
  98. data/spec/innate/node/node.rb +134 -0
  99. data/spec/innate/node/resolve.rb +82 -0
  100. data/spec/innate/node/view/another_layout/another_layout.xhtml +3 -0
  101. data/spec/innate/node/view/bar.xhtml +1 -0
  102. data/spec/innate/node/view/foo.html.xhtml +1 -0
  103. data/spec/innate/node/view/only_view.xhtml +1 -0
  104. data/spec/innate/node/view/with_layout.xhtml +1 -0
  105. data/spec/innate/node/wrap_action_call.rb +83 -0
  106. data/spec/innate/options.rb +115 -0
  107. data/spec/innate/parameter.rb +154 -0
  108. data/spec/innate/provides.rb +99 -0
  109. data/spec/innate/provides/list.html.xhtml +1 -0
  110. data/spec/innate/provides/list.txt.xhtml +1 -0
  111. data/spec/innate/request.rb +77 -0
  112. data/spec/innate/route.rb +135 -0
  113. data/spec/innate/session.rb +54 -0
  114. data/spec/innate/state/fiber.rb +58 -0
  115. data/spec/innate/state/thread.rb +40 -0
  116. data/spec/innate/traited.rb +55 -0
  117. data/tasks/bacon.rake +66 -0
  118. data/tasks/changelog.rake +18 -0
  119. data/tasks/gem.rake +22 -0
  120. data/tasks/gem_installer.rake +76 -0
  121. data/tasks/grancher.rake +12 -0
  122. data/tasks/install_dependencies.rake +4 -0
  123. data/tasks/manifest.rake +4 -0
  124. data/tasks/rcov.rake +19 -0
  125. data/tasks/release.rake +51 -0
  126. data/tasks/reversion.rake +8 -0
  127. data/tasks/setup.rake +28 -0
  128. metadata +181 -0
@@ -0,0 +1,73 @@
1
+ module Innate
2
+ # Innate only provides logging via stdlib Logger to avoid bloat and
3
+ # dependencies, you may specify multiple loggers in the Log instance of LogHub
4
+ # to accomendate your needs, by default we log to $stderr to be compatible with
5
+ # CGI.
6
+ #
7
+ # Please read the documentation of logger.rb (or even better, its source) to
8
+ # get a feeling of how to use it correctly within Innate
9
+ #
10
+ # A few shortcuts:
11
+ #
12
+ # 1. Create logger for stderr/stdout
13
+ # logger = Logger.new($stdout)
14
+ # logger = Logger.new($stderr)
15
+ #
16
+ # 2. Create logger for a file
17
+ #
18
+ # logger = Logger.new('test.log')
19
+ #
20
+ # 3. Create logger for file object
21
+ #
22
+ # file = File.open('test.log', 'a+')
23
+ # logger = Logger.new(file)
24
+ #
25
+ # 4. Create logger with rotation on specified file size
26
+ #
27
+ # # 10 files history, 5 MB each
28
+ # logger = Logger.new('test.log', 10, (5 << 20))
29
+ #
30
+ # # 100 files history, 1 MB each
31
+ # logger = Logger.new('test.log', 100, (1 << 20))
32
+ #
33
+ # 5. Create a logger which ages logfiles daily/weekly/monthly
34
+ #
35
+ # logger = Logger.new('test.log', 'daily')
36
+ # logger = Logger.new('test.log', 'weekly')
37
+ # logger = Logger.new('test.log', 'monthly')
38
+
39
+ class LogHub
40
+ include Logger::Severity
41
+ include Optioned
42
+
43
+ attr_accessor :loggers, :program, :active
44
+
45
+ # +loggers+ should be a list of Logger instances
46
+ def initialize(*loggers)
47
+ @loggers = loggers.flatten
48
+ @program = nil
49
+ @active = true
50
+ self.level = DEBUG
51
+ end
52
+
53
+ # set level for all loggers
54
+ def level=(lvl)
55
+ @loggers.each{|l| l.level = lvl }
56
+ @level = lvl
57
+ end
58
+
59
+ def start; @active = true; end
60
+ def stop; @active = false; end
61
+
62
+ def method_missing(meth, *args, &block)
63
+ eval %~
64
+ def #{meth}(*args, &block)
65
+ return unless @active
66
+ @loggers.each{|l| l.#{meth}(*args, &block) }
67
+ end
68
+ ~
69
+
70
+ send(meth, *args, &block)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,65 @@
1
+ module Innate
2
+ class MiddlewareCompiler
3
+ COMPILED = {}
4
+
5
+ def self.build(name, &block)
6
+ COMPILED[name] ||= new(name, &block)
7
+ end
8
+
9
+ def self.build!(name, &block)
10
+ COMPILED[name] = new(name, &block)
11
+ end
12
+
13
+ attr_reader :middlewares, :name
14
+
15
+ def initialize(name)
16
+ @name = name.to_sym
17
+ @middlewares = []
18
+ @compiled = nil
19
+ yield(self) if block_given?
20
+ end
21
+
22
+ def use(app, *args, &block)
23
+ @middlewares << [app, args, block]
24
+ end
25
+
26
+ def apps(*middlewares)
27
+ @middlewares.concat(middlewares.map{|mw| [mw, [], nil]})
28
+ end
29
+
30
+ def run(app)
31
+ @app = app
32
+ end
33
+
34
+ def cascade(*apps)
35
+ @app = Rack::Cascade.new(apps)
36
+ end
37
+
38
+ # Default application for Innate
39
+ def innate(app = Innate::DynaMap, options = Innate.options)
40
+ roots, publics = options[:roots], options[:publics]
41
+
42
+ joined = roots.map{|root| publics.map{|public| ::File.join(root, public)}}
43
+
44
+ apps = joined.flatten.map{|pr| Rack::File.new(pr) }
45
+ apps << Current.new(Route.new(app), Rewrite.new(app))
46
+
47
+ cascade(*apps)
48
+ end
49
+
50
+ def call(env)
51
+ compile
52
+ @compiled.call(env)
53
+ end
54
+
55
+ def compile
56
+ @compiled ? self : compile!
57
+ end
58
+
59
+ def compile!
60
+ @compiled = @middlewares.inject(@app){|s, (app, args, block)|
61
+ app.new(s, *args, &block) }
62
+ self
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,49 @@
1
+ module Innate
2
+ module Mock
3
+ HTTP_METHODS = %w[ CONNECT DELETE GET HEAD OPTIONS POST PUT TRACE ]
4
+ OPTIONS = {:app => Innate}
5
+
6
+ HTTP_METHODS.each do |method|
7
+ (class << self; self; end).
8
+ send(:define_method, method.downcase){|*args|
9
+ mock(method, *args)
10
+ }
11
+ end
12
+
13
+ def self.mock(method, *args)
14
+ mock_request.request(method, *args)
15
+ end
16
+
17
+ def self.mock_request(app = OPTIONS[:app])
18
+ Rack::MockRequest.new(app)
19
+ end
20
+
21
+ def self.session
22
+ yield Session.new
23
+ end
24
+
25
+ class Session
26
+ attr_accessor :cookie
27
+
28
+ def initialize
29
+ @cookie = nil
30
+ end
31
+
32
+ HTTP_METHODS.each do |method|
33
+ define_method(method.downcase){|*args|
34
+ extract_cookie(method, *args)
35
+ }
36
+ end
37
+
38
+ def extract_cookie(method, path, hash = {})
39
+ hash['HTTP_COOKIE'] ||= @cookie if @cookie
40
+ response = Mock::mock(method, path, hash)
41
+
42
+ cookie = response['Set-Cookie']
43
+ @cookie = cookie if cookie
44
+
45
+ response
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,1025 @@
1
+ module Innate
2
+
3
+ # The nervous system of {Innate}, so you can relax.
4
+ #
5
+ # Node may be included into any class to make it a valid responder to
6
+ # requests.
7
+ #
8
+ # The major difference between this and the old Ramaze controller is that
9
+ # every Node acts as a standalone application with its own dispatcher.
10
+ #
11
+ # What's also an important difference is the fact that {Node} is a module, so
12
+ # we don't have to spend a lot of time designing the perfect subclassing
13
+ # scheme.
14
+ #
15
+ # This makes dispatching more fun, avoids a lot of processing that is done by
16
+ # {Rack} anyway and lets you tailor your application down to the last action
17
+ # exactly the way you want without worrying about side-effects to other
18
+ # {Node}s.
19
+ #
20
+ # Upon inclusion, it will also include {Innate::Trinity} and {Innate::Helper}
21
+ # to provide you with {Innate::Request}, {Innate::Response},
22
+ # {Innate::Session} instances, and all the standard helper methods as well as
23
+ # the ability to simply add other helpers.
24
+ #
25
+ # Please note that method_missing will _not_ be considered when building an
26
+ # {Action}. There might be future demand for this, but for now you can simply
27
+ # use `def index(*args); end` to make a catch-all action.
28
+ module Node
29
+ include Traited
30
+
31
+ attr_reader :method_arities, :layout_templates, :view_templates
32
+
33
+ NODE_LIST = Set.new
34
+
35
+ # These traits are inherited into ancestors, changing a trait in an
36
+ # ancestor doesn't affect the higher ones.
37
+ #
38
+ # class Foo; include Innate::Node; end
39
+ # class Bar < Foo; end
40
+ #
41
+ # Foo.trait[:wrap] == Bar.trait[:wrap] # => true
42
+ # Bar.trait(:wrap => [:cache_wrap])
43
+ # Foo.trait[:wrap] == Bar.trait[:wrap] # => false
44
+
45
+ trait :views => []
46
+ trait :layouts => []
47
+ trait :layout => nil
48
+ trait :alias_view => {}
49
+ trait :provide => {}
50
+
51
+ # @see wrap_action_call
52
+ trait :wrap => SortedSet.new
53
+ trait :provide_set => false
54
+ trait :needs_method => false
55
+ trait :skip_node_map => false
56
+
57
+ # Upon inclusion we make ourselves comfortable.
58
+ def self.included(into)
59
+ into.__send__(:include, Helper)
60
+ into.extend(Trinity, self)
61
+
62
+ NODE_LIST << into
63
+
64
+ return if into.provide_set?
65
+ into.provide(:html, :engine => :Etanni)
66
+ into.trait(:provide_set => false)
67
+ end
68
+
69
+ # node mapping procedure
70
+ #
71
+ # when Node is included into an object, it's added to NODE_LIST
72
+ # when object::map(location) is sent, it maps the object into DynaMap
73
+ # when Innate.start is issued, it calls Node::setup
74
+ # Node::setup iterates NODE_LIST and maps all objects not in DynaMap by
75
+ # using Node::generate_mapping(object.name) as location
76
+ #
77
+ # when object::map(nil) is sent, the object will be skipped in Node::setup
78
+
79
+ def self.setup
80
+ NODE_LIST.each{|node|
81
+ node.map(generate_mapping(node.name)) unless node.trait[:skip_node_map]
82
+ }
83
+ # Log.debug("Mapped Nodes: %p" % DynaMap.to_hash) unless NODE_LIST.empty?
84
+ end
85
+
86
+ def self.generate_mapping(object_name = self.name)
87
+ return '/' if NODE_LIST.size == 1
88
+ parts = object_name.split('::').map{|part|
89
+ part.gsub(/^[A-Z]+/){|sub| sub.downcase }.gsub(/[A-Z]+[^A-Z]/, '_\&')
90
+ }
91
+ '/' << parts.join('/').downcase
92
+ end
93
+
94
+ # Tries to find the relative url that this {Node} is mapped to.
95
+ # If it cannot find one it will instead generate one based on the
96
+ # snake_cased name of itself.
97
+ #
98
+ # @example Usage:
99
+ #
100
+ # class FooBar
101
+ # include Innate::Node
102
+ # end
103
+ # FooBar.mapping # => '/foo_bar'
104
+ #
105
+ # @return [String] the relative path to the node
106
+ #
107
+ # @api external
108
+ # @see Innate::SingletonMethods#to
109
+ # @author manveru
110
+ def mapping
111
+ Innate.to(self)
112
+ end
113
+
114
+ # Shortcut to map or remap this Node.
115
+ #
116
+ # @example Usage for explicit mapping:
117
+ #
118
+ # class FooBar
119
+ # include Innate::Node
120
+ # map '/foo_bar'
121
+ # end
122
+ #
123
+ # Innate.to(FooBar) # => '/foo_bar'
124
+ #
125
+ # @example Usage for automatic mapping:
126
+ #
127
+ # class FooBar
128
+ # include Innate::Node
129
+ # map mapping
130
+ # end
131
+ #
132
+ # Innate.to(FooBar) # => '/foo_bar'
133
+ #
134
+ # @param [#to_s] location
135
+ #
136
+ # @api external
137
+ # @see Innate::SingletonMethods::map
138
+ # @author manveru
139
+ def map(location)
140
+ trait :skip_node_map => true
141
+ Innate.map(location, self) if location
142
+ end
143
+
144
+ # Specify which way contents are provided and processed.
145
+ #
146
+ # Use this to set a templating engine, custom Content-Type, or pass a block
147
+ # to take over the processing of the {Action} and template yourself.
148
+ #
149
+ # Provides set via this method will be inherited into subclasses.
150
+ #
151
+ # The +format+ is extracted from the PATH_INFO, it simply represents the
152
+ # last extension name in the path.
153
+ #
154
+ # The provide also has influence on the chosen templates for the {Action}.
155
+ #
156
+ # @example providing RSS with ERB templating
157
+ #
158
+ # provide :rss, :engine => :ERB
159
+ #
160
+ # Given a request to `/list.rss` the template lookup first tries to find
161
+ # `list.rss.erb`, if that fails it falls back to `list.erb`.
162
+ # If neither of these are available it will try to use the return value of
163
+ # the method in the {Action} as template.
164
+ #
165
+ # A request to `/list.yaml` would match the format 'yaml'
166
+ #
167
+ # @example providing a yaml version of actions
168
+ #
169
+ # class Articles
170
+ # include Innate::Node
171
+ # map '/article'
172
+ #
173
+ # provide(:yaml, :type => 'text/yaml'){|action, value| value.to_yaml }
174
+ #
175
+ # def list
176
+ # @articles = Article.list
177
+ # end
178
+ # end
179
+ #
180
+ # @example providing plain text inspect version
181
+ #
182
+ # class Articles
183
+ # include Innate::Node
184
+ # map '/article'
185
+ #
186
+ # provide(:txt, :type => 'text/plain'){|action, value| value.inspect }
187
+ #
188
+ # def list
189
+ # @articles = Article.list
190
+ # end
191
+ # end
192
+ #
193
+ # @param [Proc] block
194
+ # upon calling the action, [action, value] will be passed to it and its
195
+ # return value becomes the response body.
196
+ #
197
+ # @option param :engine [Symbol String]
198
+ # Name of an engine for View::get
199
+ # @option param :type [String]
200
+ # default Content-Type if none was set in Response
201
+ #
202
+ # @raise [ArgumentError] if neither a block nor an engine was given
203
+ #
204
+ # @api external
205
+ # @see View::get Node#provides
206
+ # @author manveru
207
+ #
208
+ # @todo
209
+ # The comment of this method may be too short for the effects it has on
210
+ # the rest of Innate, if you feel something is missing please let me
211
+ # know.
212
+
213
+ def provide(format, param = {}, &block)
214
+ if param.respond_to?(:to_hash)
215
+ param = param.to_hash
216
+ handler = block || View.get(param[:engine])
217
+ content_type = param[:type]
218
+ else
219
+ handler = View.get(param)
220
+ end
221
+
222
+ raise(ArgumentError, "Need an engine or block") unless handler
223
+
224
+ trait("#{format}_handler" => handler, :provide_set => true)
225
+ trait("#{format}_content_type" => content_type) if content_type
226
+ end
227
+
228
+ def provides
229
+ ancestral_trait.reject{|k,v| k !~ /_handler$/ }
230
+ end
231
+
232
+ # This makes the Node a valid application for Rack.
233
+ # +env+ is the environment hash passed from the Rack::Handler
234
+ #
235
+ # We rely on correct PATH_INFO.
236
+ #
237
+ # As defined by the Rack spec, PATH_INFO may be empty if it wants the root
238
+ # of the application, so we insert '/' to make our dispatcher simple.
239
+ #
240
+ # Innate will not rescue any errors for you or do any error handling, this
241
+ # should be done by an underlying middleware.
242
+ #
243
+ # We do however log errors at some vital points in order to provide you
244
+ # with feedback in your logs.
245
+ #
246
+ # A lot of functionality in here relies on the fact that call is executed
247
+ # within Innate::STATE.wrap which populates the variables used by Trinity.
248
+ # So if you use the Node directly as a middleware make sure that you #use
249
+ # Innate::Current as a middleware before it.
250
+ #
251
+ # @param [Hash] env
252
+ #
253
+ # @return [Array]
254
+ #
255
+ # @api external
256
+ # @see Response#reset Node#try_resolve Session#flush
257
+ # @author manveru
258
+
259
+ def call(env)
260
+ path = env['PATH_INFO']
261
+ path << '/' if path.empty?
262
+
263
+ response.reset
264
+ response = try_resolve(path)
265
+
266
+ Current.session.flush(response)
267
+
268
+ response.finish
269
+ end
270
+
271
+ # Let's try to find some valid action for given +path+.
272
+ # Otherwise we dispatch to {action_missing}.
273
+ #
274
+ # @param [String] path from env['PATH_INFO']
275
+ #
276
+ # @return [Response]
277
+ #
278
+ # @api external
279
+ # @see Node#resolve Node#action_found Node#action_missing
280
+ # @author manveru
281
+ def try_resolve(path)
282
+ action = resolve(path)
283
+ action ? action_found(action) : action_missing(path)
284
+ end
285
+
286
+ # Executed once an {Action} has been found.
287
+ #
288
+ # Reset the {Innate::Response} instance, catch :respond and :redirect.
289
+ # {Action#call} has to return a String.
290
+ #
291
+ # @param [Action] action
292
+ #
293
+ # @return [Innate::Response]
294
+ #
295
+ # @api external
296
+ # @see Action#call Innate::Response
297
+ # @author manveru
298
+ def action_found(action)
299
+ response = catch(:respond){ catch(:redirect){ action.call }}
300
+
301
+ unless response.respond_to?(:finish)
302
+ self.response.write(response)
303
+ response = self.response
304
+ end
305
+
306
+ response['Content-Type'] ||= action.options[:content_type]
307
+ response
308
+ end
309
+
310
+ # The default handler in case no action was found, kind of method_missing.
311
+ # Must modify the response in order to have any lasting effect.
312
+ #
313
+ # Reasoning:
314
+ # * We are doing this is in order to avoid tons of special error handling
315
+ # code that would impact runtime and make the overall API more
316
+ # complicated.
317
+ # * This cannot be a normal action is that methods defined in
318
+ # {Innate::Node} will never be considered for actions.
319
+ #
320
+ # To use a normal action with template do following:
321
+ #
322
+ # @example
323
+ #
324
+ # class Hi
325
+ # include Innate::Node
326
+ # map '/'
327
+ #
328
+ # def self.action_missing(path)
329
+ # return if path == '/not_found'
330
+ # # No normal action, runs on bare metal
331
+ # try_resolve('/not_found')
332
+ # end
333
+ #
334
+ # def not_found
335
+ # # Normal action
336
+ # "Sorry, I do not exist"
337
+ # end
338
+ # end
339
+ #
340
+ # @param [String] path
341
+ #
342
+ # @api external
343
+ # @see Innate::Response Node#try_resolve
344
+ # @author manveru
345
+ def action_missing(path)
346
+ response.status = 404
347
+ response['Content-Type'] = 'text/plain'
348
+ response.write("No action found at: %p" % path)
349
+
350
+ response
351
+ end
352
+
353
+ # Let's get down to business, first check if we got any wishes regarding
354
+ # the representation from the client, otherwise we will assume he wants
355
+ # html.
356
+ #
357
+ # @param [String] path
358
+ #
359
+ # @return [nil, Action]
360
+ #
361
+ # @api external
362
+ # @see Node::find_provide Node::update_method_arities Node::find_action
363
+ # @author manveru
364
+ def resolve(path)
365
+ name, wish, engine = find_provide(path)
366
+ node = (respond_to?(:ancestors) && respond_to?(:new)) ? self : self.class
367
+ action = Action.create(:node => node, :wish => wish, :engine => engine, :path => path)
368
+
369
+ if content_type = node.ancestral_trait["#{wish}_content_type"]
370
+ action.options = {:content_type => content_type}
371
+ end
372
+
373
+ node.update_method_arities
374
+ node.update_template_mappings
375
+ node.fill_action(action, name)
376
+ end
377
+
378
+ # Resolve possible provides for the given +path+ from {provides}.
379
+ #
380
+ # @param [String] path
381
+ #
382
+ # @return [Array] with name, wish, engine
383
+ #
384
+ # @api internal
385
+ # @see Node::provide Node::provides
386
+ # @author manveru
387
+ def find_provide(path)
388
+ pr = provides
389
+
390
+ name, wish, engine = path, 'html', pr['html_handler']
391
+
392
+ pr.find do |key, value|
393
+ key = key[/(.*)_handler$/, 1]
394
+ next unless path =~ /^(.+)\.#{key}$/i
395
+ name, wish, engine = $1, key, value
396
+ end
397
+
398
+ return name, wish, engine
399
+ end
400
+
401
+ # Now we're talking {Action}, we try to find a matching template and
402
+ # method, if we can't find either we go to the next pattern, otherwise we
403
+ # answer with an {Action} with everything we know so far about the demands
404
+ # of the client.
405
+ #
406
+ # @param [String] given_name the name extracted from REQUEST_PATH
407
+ # @param [String] wish
408
+ #
409
+ # @return [Action, nil]
410
+ #
411
+ # @api internal
412
+ # @see Node#find_method Node#find_view Node#find_layout Node#patterns_for
413
+ # Action#wish Action#merge!
414
+ # @author manveru
415
+ def fill_action(action, given_name)
416
+ needs_method = self.needs_method?
417
+ wish = action.wish
418
+
419
+ patterns_for(given_name) do |name, params|
420
+ method = find_method(name, params)
421
+
422
+ next unless method if needs_method
423
+ next unless method if params.any?
424
+ next unless (view = find_view(name, wish)) or method
425
+
426
+ params.map!{|param| Rack::Utils.unescape(param) }
427
+
428
+ action.merge!(:method => method, :view => view, :params => params,
429
+ :layout => find_layout(name, wish))
430
+ end
431
+ end
432
+
433
+ # Try to find a suitable value for the layout. This may be a template or
434
+ # the name of a method.
435
+ #
436
+ # If a layout could be found, an Array with two elements is returned, the
437
+ # first indicating the kind of layout (:layout|:view|:method), the second
438
+ # the found value, which may be a String or Symbol.
439
+ #
440
+ # @param [String] name
441
+ # @param [String] wish
442
+ #
443
+ # @return [Array, nil]
444
+ #
445
+ # @api external
446
+ # @see Node#to_layout Node#find_method Node#find_view
447
+ # @author manveru
448
+ #
449
+ # @todo allow layouts combined of method and view... hairy :)
450
+ def find_layout(name, wish)
451
+ return unless layout = ancestral_trait[:layout]
452
+ return unless layout = layout.call(name, wish) if layout.respond_to?(:call)
453
+
454
+ if found = to_layout(layout, wish)
455
+ [:layout, found]
456
+ elsif found = find_view(layout, wish)
457
+ [:view, found]
458
+ elsif found = find_method(layout, [])
459
+ [:method, found]
460
+ end
461
+ end
462
+
463
+ # We check arity if possible, but will happily dispatch to any method that
464
+ # has default parameters.
465
+ # If you don't want your method to be responsible for messing up a request
466
+ # you should think twice about the arguments you specify due to limitations
467
+ # in Ruby.
468
+ #
469
+ # So if you want your method to take only one parameter which may have a
470
+ # default value following will work fine:
471
+ #
472
+ # def index(foo = "bar", *rest)
473
+ #
474
+ # But following will respond to /arg1/arg2 and then fail due to ArgumentError:
475
+ #
476
+ # def index(foo = "bar")
477
+ #
478
+ # Here a glance at how parameters are expressed in arity:
479
+ #
480
+ # def index(a) # => 1
481
+ # def index(a = :a) # => -1
482
+ # def index(a, *r) # => -2
483
+ # def index(a = :a, *r) # => -1
484
+ #
485
+ # def index(a, b) # => 2
486
+ # def index(a, b, *r) # => -3
487
+ # def index(a, b = :b) # => -2
488
+ # def index(a, b = :b, *r) # => -2
489
+ #
490
+ # def index(a = :a, b = :b) # => -1
491
+ # def index(a = :a, b = :b, *r) # => -1
492
+ #
493
+ # @param [String, Symbol] name
494
+ # @param [Array] params
495
+ #
496
+ # @return [String, Symbol]
497
+ #
498
+ # @api external
499
+ # @see Node#fill_action Node#find_layout
500
+ # @author manveru
501
+ #
502
+ # @todo Once 1.9 is mainstream we can use Method#parameters to do accurate
503
+ # prediction
504
+ def find_method(name, params)
505
+ return unless arity = method_arities[name]
506
+ name if arity == params.size or arity < 0
507
+ end
508
+
509
+ # Answer with a hash, keys are method names, values are method arities.
510
+ #
511
+ # Note that this will be executed once for every request, once we have
512
+ # settled things down a bit more we can switch to update based on Reloader
513
+ # hooks and update once on startup.
514
+ # However, that may cause problems with dynamically created methods, so
515
+ # let's play it safe for now.
516
+ #
517
+ # @example
518
+ #
519
+ # Hi.update_method_arities
520
+ # # => {'index' => 0, 'foo' => -1, 'bar => 2}
521
+ #
522
+ # @api internal
523
+ # @see Node#resolve
524
+ # @return [Hash] mapping the name of the methods to their arity
525
+ def update_method_arities
526
+ @method_arities = {}
527
+
528
+ exposed = ancestors & Helper::EXPOSE.to_a
529
+ higher = ancestors.select{|a| a < Innate::Node }
530
+
531
+ (higher + exposed).reverse_each do |ancestor|
532
+ ancestor.public_instance_methods(false).each do |im|
533
+ @method_arities[im.to_s] = ancestor.instance_method(im).arity
534
+ end
535
+ end
536
+
537
+ @method_arities
538
+ end
539
+
540
+ # Try to find the best template for the given basename and wish and respect
541
+ # aliased views.
542
+ #
543
+ # @param [#to_s] action_name
544
+ # @param [#to_s] wish
545
+ #
546
+ # @return [String, nil] depending whether a template could be found
547
+ #
548
+ # @api external
549
+ # @see Node#to_template Node#find_aliased_view
550
+ # @author manveru
551
+ def find_view(action_name, wish)
552
+ aliased = find_aliased_view(action_name, wish)
553
+ return aliased if aliased
554
+
555
+ to_view(action_name, wish)
556
+ end
557
+
558
+ # Try to find the best template for the given basename and wish.
559
+ #
560
+ # This method is mostly here for symetry with {to_layout} and to allow you
561
+ # overriding the template lookup easily.
562
+ #
563
+ # @param [#to_s] action_name
564
+ # @param [#to_s] wish
565
+ #
566
+ # @return [String, nil] depending whether a template could be found
567
+ #
568
+ # @api external
569
+ # @see {Node#find_view} {Node#to_template} {Node#root_mappings}
570
+ # {Node#view_mappings} {Node#to_template}
571
+ # @author manveru
572
+ def to_view(action_name, wish)
573
+ return unless files = view_templates[wish.to_s]
574
+ files[action_name.to_s]
575
+ end
576
+
577
+ # Aliasing one view from another.
578
+ # The aliases are inherited, and the optional third +node+ parameter
579
+ # indicates the Node to take the view from.
580
+ #
581
+ # The argument order is identical with `alias` and `alias_method`, which
582
+ # quite honestly confuses me, but at least we stay consistent.
583
+ #
584
+ # @example
585
+ # class Foo
586
+ # include Innate::Node
587
+ #
588
+ # # Use the 'foo' view when calling 'bar'
589
+ # alias_view 'bar', 'foo'
590
+ #
591
+ # # Use the 'foo' view from FooBar node when calling 'bar'
592
+ # alias_view 'bar', 'foo', FooBar
593
+ # end
594
+ #
595
+ # Note that the parameters have been simplified in comparision with
596
+ # Ramaze::Controller::template where the second parameter may be a
597
+ # Controller or the name of the template. We take that now as an optional
598
+ # third parameter.
599
+ #
600
+ # @param [#to_s] to view that should be replaced
601
+ # @param [#to_s] from view to use or Node.
602
+ # @param [#nil?, Node] node optionally obtain view from this Node
603
+ #
604
+ # @api external
605
+ # @see Node::find_aliased_view
606
+ # @author manveru
607
+ def alias_view(to, from, node = nil)
608
+ trait[:alias_view] || trait(:alias_view => {})
609
+ trait[:alias_view][to.to_s] = node ? [from.to_s, node] : from.to_s
610
+ end
611
+
612
+ # Resolve one level of aliasing for the given +action_name+ and +wish+.
613
+ #
614
+ # @param [String] action_name
615
+ # @param [String] wish
616
+ #
617
+ # @return [nil, String] the absolute path to the aliased template or nil
618
+ #
619
+ # @api internal
620
+ # @see Node::alias_view Node::find_view
621
+ # @author manveru
622
+ def find_aliased_view(action_name, wish)
623
+ aliased_name, aliased_node = ancestral_trait[:alias_view][action_name]
624
+ return unless aliased_name
625
+
626
+ aliased_node ||= self
627
+ aliased_node.update_view_mappings
628
+ aliased_node.find_view(aliased_name, wish)
629
+ end
630
+
631
+ # Find the best matching action_name for the layout, if any.
632
+ #
633
+ # This is mostly an abstract method that you might find handy if you want
634
+ # to do vastly different layout lookup.
635
+ #
636
+ # @param [String] action_name
637
+ # @param [String] wish
638
+ #
639
+ # @return [nil, String] the absolute path to the template or nil
640
+ #
641
+ # @api external
642
+ # @see {Node#to_template} {Node#root_mappings} {Node#layout_mappings}
643
+ # @author manveru
644
+ def to_layout(action_name, wish)
645
+ return unless files = layout_templates[wish.to_s]
646
+ files[action_name.to_s]
647
+ end
648
+
649
+ # Define a layout to use on this Node.
650
+ #
651
+ # A Node can only have one layout, although the template being chosen can
652
+ # depend on {provides}.
653
+ #
654
+ # @param [String, #to_s] name basename without extension of the layout to use
655
+ # @param [Proc, #call] block called on every dispatch if no name given
656
+ #
657
+ # @return [Proc, String] The assigned name or block
658
+ #
659
+ # @api external
660
+ # @see Node#find_layout Node#layout_paths Node#to_layout Node#app_layout
661
+ # @author manveru
662
+ #
663
+ # NOTE:
664
+ # The behaviour of Node#layout changed significantly from Ramaze, instead
665
+ # of multitudes of obscure options and methods like deny_layout we simply
666
+ # take a block and use the returned value as the name for the layout. No
667
+ # layout will be used if the block returns nil.
668
+ def layout(name = nil, &block)
669
+ if name and block
670
+ # default name, but still check with block
671
+ trait(:layout => lambda{|n, w| name if block.call(n, w) })
672
+ elsif name
673
+ # name of a method or template
674
+ trait(:layout => name.to_s)
675
+ elsif block
676
+ # call block every request with name and wish, returned value is name
677
+ # of layout template or method
678
+ trait(:layout => block)
679
+ else
680
+ # remove layout for this node
681
+ trait(:layout => nil)
682
+ end
683
+
684
+ return ancestral_trait[:layout]
685
+ end
686
+
687
+ # The innate beauty in Nitro, Ramaze, and {Innate}.
688
+ #
689
+ # Will yield the name of the action and parameter for the action method in
690
+ # order of significance.
691
+ #
692
+ # def foo__bar # responds to /foo/bar
693
+ # def foo(bar) # also responds to /foo/bar
694
+ #
695
+ # But foo__bar takes precedence because it's more explicit.
696
+ #
697
+ # The last fallback will always be the index action with all of the path
698
+ # turned into parameters.
699
+ #
700
+ # @example yielding possible combinations of action names and params
701
+ #
702
+ # class Foo; include Innate::Node; map '/'; end
703
+ #
704
+ # Foo.patterns_for('/'){|action, params| p action => params }
705
+ # # => {"index"=>[]}
706
+ #
707
+ # Foo.patterns_for('/foo/bar'){|action, params| p action => params }
708
+ # # => {"foo__bar"=>[]}
709
+ # # => {"foo"=>["bar"]}
710
+ # # => {"index"=>["foo", "bar"]}
711
+ #
712
+ # Foo.patterns_for('/foo/bar/baz'){|action, params| p action => params }
713
+ # # => {"foo__bar__baz"=>[]}
714
+ # # => {"foo__bar"=>["baz"]}
715
+ # # => {"foo"=>["bar", "baz"]}
716
+ # # => {"index"=>["foo", "bar", "baz"]}
717
+ #
718
+ # @param [String, #split] path usually the PATH_INFO
719
+ #
720
+ # @return [Action] it actually returns the first non-nil/false result of yield
721
+ #
722
+ # @api internal
723
+ # @see Node#fill_action
724
+ # @author manveru
725
+ def patterns_for(path)
726
+ atoms = path.split('/')
727
+ atoms.delete('')
728
+ result = nil
729
+
730
+ atoms.size.downto(0) do |len|
731
+ action_name = atoms[0...len].join('__')
732
+ params = atoms[len..-1]
733
+ action_name = 'index' if action_name.empty? and params != ['index']
734
+
735
+ return result if result = yield(action_name, params)
736
+ end
737
+
738
+ return nil
739
+ end
740
+
741
+ # Try to find a template at the given +path+ for +wish+.
742
+ #
743
+ # Since Innate supports multiple paths to templates the +path+ has to be an
744
+ # Array that may be nested one level.
745
+ # The +path+ is then translated by {Node#path_glob} and the +wish+ by
746
+ # {Node#ext_glob}.
747
+ #
748
+ # @example Usage to find available templates
749
+ #
750
+ # # This assumes following files:
751
+ # # view/foo.erb
752
+ # # view/bar.erb
753
+ # # view/bar.rss.erb
754
+ # # view/bar.yaml.erb
755
+ #
756
+ # class FooBar
757
+ # Innate.node('/')
758
+ # end
759
+ #
760
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'html')
761
+ # # => "./view/foo.erb"
762
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'yaml')
763
+ # # => "./view/foo.erb"
764
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'rss')
765
+ # # => "./view/foo.erb"
766
+ #
767
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'html')
768
+ # # => "./view/bar.erb"
769
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'yaml')
770
+ # # => "./view/bar.yaml.erb"
771
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'rss')
772
+ # # => "./view/bar.rss.erb"
773
+ #
774
+ # @param [Array<Array<String>>, Array<String>] path
775
+ # array containing strings and nested (1 level) arrays containing strings
776
+ # @param [String] wish
777
+ #
778
+ # @return [nil, String] relative path to the first template found
779
+ #
780
+ # @api external
781
+ # @see Node#find_view Node#to_layout Node#find_aliased_view
782
+ # Node#path_glob Node#ext_glob
783
+ # @author manveru
784
+ def to_template(path, wish)
785
+ to_view(path, wish) || to_layout(path, wish)
786
+ end
787
+
788
+ def update_template_mappings
789
+ update_view_mappings
790
+ update_layout_mappings
791
+ end
792
+
793
+ def update_view_mappings
794
+ paths = possible_paths_for(view_mappings)
795
+ @view_templates = update_mapping_shared(paths)
796
+ end
797
+
798
+ def update_layout_mappings
799
+ paths = possible_paths_for(layout_mappings)
800
+ @layout_templates = update_mapping_shared(paths)
801
+ end
802
+
803
+ def update_mapping_shared(paths)
804
+ mapping = {}
805
+
806
+ provides.each do |wish_key, engine|
807
+ wish = wish_key[/(.*)_handler/, 1]
808
+ ext_glob = ext_glob(wish)
809
+
810
+ paths.reverse_each do |path|
811
+ ::Dir.glob(::File.join(path, "/**/*.#{ext_glob}")) do |file|
812
+ case file.sub(path, '').gsub('/', '__')
813
+ when /^(.*)\.(.*)\.(.*)$/
814
+ action_name, wish_ext, engine_ext = $1, $2, $3
815
+ when /^(.*)\.(.*)$/
816
+ action_name, wish_ext, engine_ext = $1, wish, $2
817
+ end
818
+
819
+ mapping[wish_ext] ||= {}
820
+ mapping[wish_ext][action_name] = file
821
+ end
822
+ end
823
+ end
824
+
825
+ return mapping
826
+ end
827
+
828
+ def possible_paths_for(mappings)
829
+ root_mappings.map{|root_mapping|
830
+ mappings.first.map{|outer_mapping|
831
+ mappings.last.map{|inner_mapping|
832
+ File.join(root_mapping, outer_mapping, inner_mapping, '/')
833
+ }
834
+ }
835
+ }.flatten
836
+ end
837
+
838
+ # Produce a glob that can be processed by Dir::[] matching the extensions
839
+ # associated with the given +wish+.
840
+ #
841
+ # @param [#to_s] wish the extension (no leading '.')
842
+ #
843
+ # @return [String] glob matching the valid exts for the given +wish+
844
+ #
845
+ # @api internal
846
+ # @see Node#to_template View::exts_of Node#provides
847
+ # @author manveru
848
+ def ext_glob(wish)
849
+ pr = provides
850
+ return unless engine = pr["#{wish}_handler"]
851
+ engine_exts = View.exts_of(engine).join(',')
852
+ represented = [*wish].map{|k| "#{k}." }.join(',')
853
+ "{%s,}{%s}" % [represented, engine_exts]
854
+ end
855
+
856
+ # For compatibility with new Kernel#binding behaviour in 1.9
857
+ #
858
+ # @return [Binding] binding of the instance being rendered.
859
+ # @see Action#binding
860
+ # @author manveru
861
+ def binding; super end
862
+
863
+ # make sure this is an Array and a new instance so modification on the
864
+ # wrapping array doesn't affect the original option.
865
+ # [*arr].object_id == arr.object_id if arr is an Array
866
+ #
867
+ # @return [Array] list of root directories
868
+ #
869
+ # @api external
870
+ # @author manveru
871
+ def root_mappings
872
+ [*options.roots].flatten
873
+ end
874
+
875
+ # Set the paths for lookup below the Innate.options.views paths.
876
+ #
877
+ # @param [String, Array<String>] locations
878
+ # Any number of strings indicating the paths where view templates may be
879
+ # located, relative to Innate.options.roots/Innate.options.views
880
+ #
881
+ # @return [Node] self
882
+ #
883
+ # @api external
884
+ # @see {Node#view_mappings}
885
+ # @author manveru
886
+ def map_views(*locations)
887
+ trait :views => locations.flatten.uniq
888
+ self
889
+ end
890
+
891
+ # Combine Innate.options.views with either the `ancestral_trait[:views]`
892
+ # or the {Node#mapping} if the trait yields an empty Array.
893
+ #
894
+ # @return [Array<String>, Array<Array<String>>]
895
+ #
896
+ # @api external
897
+ # @see {Node#map_views}
898
+ # @author manveru
899
+ def view_mappings
900
+ paths = [*ancestral_trait[:views]]
901
+ paths = [mapping] if paths.empty?
902
+
903
+ [[*options.views].flatten, [*paths].flatten]
904
+ end
905
+
906
+ # Set the paths for lookup below the Innate.options.layouts paths.
907
+ #
908
+ # @param [String, Array<String>] locations
909
+ # Any number of strings indicating the paths where layout templates may
910
+ # be located, relative to Innate.options.roots/Innate.options.layouts
911
+ #
912
+ # @return [Node] self
913
+ #
914
+ # @api external
915
+ # @see {Node#layout_mappings}
916
+ # @author manveru
917
+ def map_layouts(*locations)
918
+ trait :layouts => locations.flatten.uniq
919
+ self
920
+ end
921
+
922
+ # Combine Innate.options.layouts with either the `ancestral_trait[:layouts]`
923
+ # or the {Node#mapping} if the trait yields an empty Array.
924
+ #
925
+ # @return [Array<String>, Array<Array<String>>]
926
+ #
927
+ # @api external
928
+ # @see {Node#map_layouts}
929
+ # @author manveru
930
+ def layout_mappings
931
+ paths = [*ancestral_trait[:layouts]]
932
+ paths = [mapping] if paths.empty?
933
+
934
+ [[*options.layouts].flatten, [*paths].flatten]
935
+ end
936
+
937
+ def options
938
+ Innate.options
939
+ end
940
+
941
+ # Whether an {Action} can be built without a method.
942
+ #
943
+ # The default is to allow actions that use only a view template, but you
944
+ # might want to turn this on, for example if you have partials in your view
945
+ # directories.
946
+ #
947
+ # @example turning needs_method? on
948
+ #
949
+ # class Foo
950
+ # Innate.node('/')
951
+ # end
952
+ #
953
+ # Foo.needs_method? # => true
954
+ # Foo.trait :needs_method => false
955
+ # Foo.needs_method? # => false
956
+ #
957
+ # @return [true, false] (false)
958
+ #
959
+ # @api external
960
+ # @see {Node#fill_action}
961
+ # @author manveru
962
+ def needs_method?
963
+ ancestral_trait[:needs_method]
964
+ end
965
+
966
+ # This will return true if the only provides set are by {Node::included}.
967
+ #
968
+ # The reasoning behind this is to determine whether the user has touched
969
+ # the provides at all, in which case we will not override the provides in
970
+ # subclasses.
971
+ #
972
+ # @return [true, false] (false)
973
+ #
974
+ # @api internal
975
+ # @see {Node::included}
976
+ # @author manveru
977
+ def provide_set?
978
+ ancestral_trait[:provide_set]
979
+ end
980
+ end
981
+
982
+ module SingletonMethods
983
+ # Convenience method to include the Node module into +node+ and map to a
984
+ # +location+.
985
+ #
986
+ # @param [#to_s] location where the node is mapped to
987
+ # @param [Node, nil] node the class that will be a node, will try to
988
+ # look it up if not given
989
+ #
990
+ # @return [Class, Module] the node argument or detected class will be
991
+ # returned
992
+ #
993
+ # @api external
994
+ # @see SingletonMethods::node_from_backtrace
995
+ # @author manveru
996
+ def node(location, node = nil)
997
+ node ||= node_from_backtrace(caller)
998
+ node.__send__(:include, Node)
999
+ node.map(location)
1000
+ node
1001
+ end
1002
+
1003
+ # Cheap hack that works reasonably well to avoid passing self all the time
1004
+ # to Innate::node
1005
+ # We simply search the file that Innate::node was called in for the first
1006
+ # class definition above the line that Innate::node was called and look up
1007
+ # the constant.
1008
+ # If there are any problems with this (filenames containing ':' or
1009
+ # metaprogramming) just pass the node parameter explicitly to Innate::node
1010
+ #
1011
+ # @param [Array<String>, #[]] backtrace
1012
+ #
1013
+ # @return [Class, Module]
1014
+ #
1015
+ # @api internal
1016
+ # @see SingletonMethods::node
1017
+ # @author manveru
1018
+ def node_from_backtrace(backtrace)
1019
+ filename, lineno = backtrace[0].split(':', 2)
1020
+ regexp = /^\s*class\s+(\S+)/
1021
+ File.readlines(filename)[0..lineno.to_i].reverse.find{|l| l =~ regexp }
1022
+ const_get($1)
1023
+ end
1024
+ end
1025
+ end