innate 2009.04

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