manveru-innate 2009.02.25 → 2009.03.24

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGELOG +383 -0
  2. data/MANIFEST +17 -8
  3. data/README.md +222 -136
  4. data/Rakefile +7 -2
  5. data/example/provides.rb +28 -0
  6. data/innate.gemspec +19 -10
  7. data/lib/innate/action.rb +21 -47
  8. data/lib/innate/adapter.rb +65 -56
  9. data/lib/innate/cache.rb +16 -8
  10. data/lib/innate/dynamap.rb +39 -29
  11. data/lib/innate/helper/aspect.rb +60 -0
  12. data/lib/innate/helper/cgi.rb +2 -0
  13. data/lib/innate/helper/link.rb +43 -11
  14. data/lib/innate/helper/partial.rb +6 -5
  15. data/lib/innate/helper.rb +10 -13
  16. data/lib/innate/log/hub.rb +1 -0
  17. data/lib/innate/log.rb +3 -6
  18. data/lib/{rack → innate}/middleware_compiler.rb +19 -12
  19. data/lib/innate/mock.rb +3 -2
  20. data/lib/innate/node.rb +573 -179
  21. data/lib/innate/options/dsl.rb +46 -6
  22. data/lib/innate/options/stub.rb +7 -0
  23. data/lib/innate/options.rb +14 -93
  24. data/lib/innate/request.rb +21 -7
  25. data/lib/innate/response.rb +12 -0
  26. data/lib/innate/route.rb +2 -3
  27. data/lib/innate/session.rb +37 -20
  28. data/lib/innate/spec.rb +4 -0
  29. data/lib/innate/state/fiber.rb +14 -7
  30. data/lib/innate/state/thread.rb +10 -2
  31. data/lib/innate/state.rb +8 -11
  32. data/lib/innate/traited.rb +14 -6
  33. data/lib/innate/trinity.rb +0 -4
  34. data/lib/innate/version.rb +1 -1
  35. data/lib/innate/view/erb.rb +4 -2
  36. data/lib/innate/view/none.rb +2 -2
  37. data/lib/innate/view.rb +14 -21
  38. data/lib/innate.rb +49 -30
  39. data/spec/helper.rb +8 -0
  40. data/spec/innate/action/layout.rb +9 -6
  41. data/spec/innate/cache/common.rb +3 -3
  42. data/spec/innate/helper/aspect.rb +3 -5
  43. data/spec/innate/helper/flash.rb +1 -1
  44. data/spec/innate/helper/link.rb +45 -2
  45. data/spec/innate/helper/partial.rb +34 -10
  46. data/spec/innate/helper/view/loop.erb +1 -1
  47. data/spec/innate/helper/view/recursive.erb +1 -1
  48. data/spec/innate/node/mapping.rb +37 -0
  49. data/spec/innate/node/node.rb +142 -0
  50. data/spec/innate/node/resolve.rb +82 -0
  51. data/spec/innate/node/{another_layout → view/another_layout}/another_layout.erb +0 -0
  52. data/spec/innate/node/{bar.html → view/bar.erb} +0 -0
  53. data/spec/innate/node/{foo.html.erb → view/foo.html.erb} +0 -0
  54. data/spec/innate/node/{only_view.html → view/only_view.erb} +0 -0
  55. data/spec/innate/node/view/with_layout.erb +1 -0
  56. data/spec/innate/node/wrap_action_call.rb +83 -0
  57. data/spec/innate/options.rb +28 -6
  58. data/spec/innate/provides/list.html.erb +1 -0
  59. data/spec/innate/provides/list.txt.erb +1 -0
  60. data/spec/innate/provides.rb +99 -0
  61. data/spec/innate/request.rb +23 -10
  62. data/spec/innate/route.rb +2 -4
  63. data/spec/innate/session.rb +1 -1
  64. data/spec/innate/state/fiber.rb +57 -0
  65. data/spec/innate/state/thread.rb +40 -0
  66. metadata +20 -11
  67. data/lib/rack/reloader.rb +0 -192
  68. data/spec/innate/node/with_layout.erb +0 -3
  69. data/spec/innate/node.rb +0 -224
data/lib/innate/node.rb CHANGED
@@ -22,24 +22,36 @@ module Innate
22
22
  # {Innate::Session} instances, and all the standard helper methods as well as
23
23
  # the ability to simply add other helpers.
24
24
  #
25
- # NOTE:
26
- # * Although I tried to minimize the amount of code in here there is still
27
- # quite a number of methods left in order to do ramaze-style lookups.
28
- # Those methods, and all other methods occurring in the ancestors after
29
- # {Innate::Node} will not be considered valid action methods and will be
30
- # ignored.
31
- # * This also means that method_missing will not see any of the requests
32
- # coming in.
33
- # * If you want an action to act as a catch-all, use `def index(*args)`.
34
-
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.
35
28
  module Node
36
29
  include Traited
37
30
 
38
31
  DEFAULT_HELPERS = %w[aspect cgi flash link partial redirect send_file]
39
32
  NODE_LIST = Set.new
40
33
 
41
- trait(:layout => nil, :alias_view => {}, :provide => {},
42
- :method_arities => {}, :wrap => [:aspect_wrap], :provide_set => false)
34
+ # These traits are inherited into ancestors, changing a trait in an
35
+ # ancestor doesn't affect the higher ones.
36
+ #
37
+ # class Foo; include Innate::Node; end
38
+ # class Bar < Foo; end
39
+ #
40
+ # Foo.trait[:wrap] == Bar.trait[:wrap] # => true
41
+ # Bar.trait(:wrap => [:cache_wrap])
42
+ # Foo.trait[:wrap] == Bar.trait[:wrap] # => false
43
+
44
+ trait :views => []
45
+ trait :layouts => []
46
+ trait :layout => nil
47
+ trait :alias_view => {}
48
+ trait :provide => {}
49
+
50
+ # @see wrap_action_call
51
+ trait :wrap => SortedSet.new
52
+ trait :provide_set => false
53
+ trait :needs_method => false
54
+ trait :skip_node_map => false
43
55
 
44
56
  # Upon inclusion we make ourselves comfortable.
45
57
  def self.included(into)
@@ -50,90 +62,172 @@ module Innate
50
62
 
51
63
  NODE_LIST << into
52
64
 
53
- return if into.ancestral_trait[:provide_set]
54
- into.provide(:html => :erb, :yaml => :yaml, :json => :json)
65
+ return if into.provide_set?
66
+ into.provide(:html, :ERB)
55
67
  into.trait(:provide_set => false)
56
68
  end
57
69
 
70
+ # node mapping procedure
71
+ #
72
+ # when Node is included into an object, it's added to NODE_LIST
73
+ # when object::map(location) is sent, it maps the object into DynaMap
74
+ # when Innate.start is issued, it calls Node::setup
75
+ # Node::setup iterates NODE_LIST and maps all objects not in DynaMap by
76
+ # using Node::generate_mapping(object.name) as location
77
+ #
78
+ # when object::map(nil) is sent, the object will be skipped in Node::setup
79
+
58
80
  def self.setup
59
- NODE_LIST.each{|node| Innate.map(node.mapping, node) }
60
- Log.debug("Mapped Nodes: %p" % DynaMap::MAP)
81
+ NODE_LIST.each{|node|
82
+ node.map(generate_mapping(node.name)) unless node.trait[:skip_node_map]
83
+ }
84
+ # Log.debug("Mapped Nodes: %p" % DynaMap.to_hash) unless NODE_LIST.empty?
85
+ end
86
+
87
+ def self.generate_mapping(object_name = self.name)
88
+ return '/' if NODE_LIST.size == 1
89
+ parts = object_name.split('::').map{|part|
90
+ part.gsub(/^[A-Z]+/){|sub| sub.downcase }.gsub(/[A-Z]+[^A-Z]/, '_\&')
91
+ }
92
+ '/' << parts.join('/').downcase
61
93
  end
62
94
 
95
+ # Tries to find the relative url that this {Node} is mapped to.
96
+ # If it cannot find one it will instead generate one based on the
97
+ # snake_cased name of itself.
98
+ #
99
+ # @example Usage:
100
+ #
101
+ # class FooBar
102
+ # include Innate::Node
103
+ # end
104
+ # FooBar.mapping # => '/foo_bar'
105
+ #
63
106
  # @return [String] the relative path to the node
107
+ #
108
+ # @api external
109
+ # @see Innate::SingletonMethods#to
110
+ # @author manveru
64
111
  def mapping
65
- mapped = Innate.to(self)
66
- return mapped if mapped
67
- return '/' if NODE_LIST.size == 1
68
- "/" << self.name.gsub(/\B[A-Z][^A-Z]/, '_\&').downcase
112
+ Innate.to(self)
69
113
  end
70
114
 
71
- # Shortcut to map or remap this Node
115
+ # Shortcut to map or remap this Node.
116
+ #
117
+ # @example Usage for explicit mapping:
118
+ #
119
+ # class FooBar
120
+ # include Innate::Node
121
+ # map '/foo_bar'
122
+ # end
123
+ #
124
+ # Innate.to(FooBar) # => '/foo_bar'
125
+ #
126
+ # @example Usage for automatic mapping:
127
+ #
128
+ # class FooBar
129
+ # include Innate::Node
130
+ # map mapping
131
+ # end
132
+ #
133
+ # Innate.to(FooBar) # => '/foo_bar'
134
+ #
72
135
  # @param [#to_s] location
136
+ #
137
+ # @api external
138
+ # @see Innate::SingletonMethods::map
139
+ # @author manveru
73
140
  def map(location)
74
- Innate.map(location, self)
141
+ trait :skip_node_map => true
142
+ Innate.map(location, self) if location
75
143
  end
76
144
 
77
- # This little piece of nasty looking code enables you to provide different
78
- # content from a single action.
145
+ # Specify which way contents are provided and processed.
79
146
  #
80
- # @example
147
+ # Use this to set a templating engine, custom Content-Type, or pass a block
148
+ # to take over the processing of the {Action} and template yourself.
81
149
  #
82
- # class Feeds
83
- # include Innate::Node
84
- # map '/feed'
150
+ # Provides set via this method will be inherited into subclasses.
85
151
  #
86
- # provide :html => :erb, :rss => :erb, :atom => :erb
152
+ # The +format+ is extracted from the PATH_INFO, it simply represents the
153
+ # last extension name in the path.
87
154
  #
88
- # def index
89
- # @feed = build_some_feed
90
- # end
91
- # end
155
+ # The provide also has influence on the chosen templates for the {Action}.
156
+ #
157
+ # @example providing RSS with ERB templating
158
+ #
159
+ # provide :rss, :engine => :ERB
160
+ #
161
+ # Given a request to `/list.rss` the template lookup first tries to find
162
+ # `list.rss.erb`, if that fails it falls back to `list.erb`.
163
+ # If neither of these are available it will try to use the return value of
164
+ # the method in the {Action} as template.
165
+ #
166
+ # A request to `/list.yaml` would match the format 'yaml'
92
167
  #
93
- # This will do following to these requests:
168
+ # @example providing a yaml version of actions
94
169
  #
95
- # /feed # => call Feeds#index with template /view/feed/index.erb
96
- # /feed.atom # => call Feeds#index with template /view/feed/index.atom.erb
97
- # /feed.rss # => call Feeds#index with template /view/feed/index.rss.erb
170
+ # class Articles
171
+ # include Innate::Node
172
+ # map '/article'
98
173
  #
99
- # If index.atom.erb isn't available we fall back to /view/feed/index.erb
174
+ # provide(:yaml, :type => 'text/yaml'){|action, value| value.to_yaml }
100
175
  #
101
- # So it's really easy to add your own content representation.
176
+ # def list
177
+ # @articles = Article.list
178
+ # end
179
+ # end
102
180
  #
103
- # If no matching provider is found for the given extension it will fall
104
- # back to the one specified for html.
181
+ # @example providing plain text inspect version
105
182
  #
106
- # The correct templating engine is selected by matching the last extension
107
- # of the template itself to the one set in Innate::View.
183
+ # class Articles
184
+ # include Innate::Node
185
+ # map '/article'
108
186
  #
109
- # If you don't want that your response is passed through a templating
110
- # engine, use :none like:
187
+ # provide(:txt, :type => 'text/plain'){|action, value| value.inspect }
111
188
  #
112
- # provide :txt => :none
189
+ # def list
190
+ # @articles = Article.list
191
+ # end
192
+ # end
113
193
  #
114
- # So a request to
194
+ # @param [Proc] block
195
+ # upon calling the action, [action, value] will be passed to it and its
196
+ # return value becomes the response body.
115
197
  #
116
- # /feed.txt # => call Feeds#index with template /view/feed/index.txt.erb
198
+ # @option param :engine [Symbol String]
199
+ # Name of an engine for View::get
200
+ # @option param :type [String]
201
+ # default Content-Type if none was set in Response
117
202
  #
118
- # NOTE: provides also have effect on the chosen layout for the action.
203
+ # @raise [ArgumentError] if neither a block nor an engine was given
119
204
  #
120
- # Given a Node at '/' with `layout('default')`:
121
- # /layout/default.erb
122
- # /layout/default.rss.erb
123
- # /view/index.erb
124
- # /view/feed.rss.erb
205
+ # @api external
206
+ # @see View::get Node#provides
207
+ # @author manveru
125
208
  #
126
- # /feed.rss will wrap /view/feed.rss.erb in /layout/default.rss.erb
127
- # /index will wrap /view/index.erb in /layout/default.erb
209
+ # @todo
210
+ # The comment of this method may be too short for the effects it has on
211
+ # the rest of Innate, if you feel something is missing please let me
212
+ # know.
128
213
 
129
- def provide(formats = {})
130
- return ancestral_trait[:provide] if formats.empty?
214
+ def provide(format, param = {}, &block)
215
+ if param.respond_to?(:to_hash)
216
+ param = param.to_hash
217
+ handler = block || View.get(param[:engine])
218
+ content_type = param[:type]
219
+ else
220
+ handler = View.get(param)
221
+ end
222
+
223
+ raise(ArgumentError, "Need an engine or block") unless handler
131
224
 
132
- hash = {}
133
- formats.each{|pr, as| hash[pr.to_s] = Array[*as].map{|a| a.to_s } }
134
- trait(:provide => hash, :provide_set => true)
225
+ trait("#{format}_handler" => handler, :provide_set => true)
226
+ trait("#{format}_content_type" => content_type) if content_type
227
+ end
135
228
 
136
- ancestral_trait[:provide]
229
+ def provides
230
+ ancestral_trait.reject{|k,v| k !~ /_handler$/ }
137
231
  end
138
232
 
139
233
  # This makes the Node a valid application for Rack.
@@ -150,12 +244,18 @@ module Innate
150
244
  # We do however log errors at some vital points in order to provide you
151
245
  # with feedback in your logs.
152
246
  #
153
- # NOTE:
154
- # * A lot of functionality in here relies on the fact that call is
155
- # executed within Innate::STATE.wrap which populates the variables used
156
- # by Trinity.
157
- # * If you use the Node directly as a middleware make sure that you #use
158
- # Innate::Current as a middleware before it.
247
+ # A lot of functionality in here relies on the fact that call is executed
248
+ # within Innate::STATE.wrap which populates the variables used by Trinity.
249
+ # So if you use the Node directly as a middleware make sure that you #use
250
+ # Innate::Current as a middleware before it.
251
+ #
252
+ # @param [Hash] env
253
+ #
254
+ # @return [Array]
255
+ #
256
+ # @api external
257
+ # @see Response#reset Node#try_resolve Session#flush
258
+ # @author manveru
159
259
 
160
260
  def call(env)
161
261
  path = env['PATH_INFO']
@@ -163,7 +263,6 @@ module Innate
163
263
 
164
264
  response.reset
165
265
  response = try_resolve(path)
166
- response['Content-Type'] ||= 'text/html'
167
266
 
168
267
  Current.session.flush(response)
169
268
 
@@ -171,29 +270,42 @@ module Innate
171
270
  end
172
271
 
173
272
  # Let's try to find some valid action for given +path+.
174
- # Otherwise we dispatch to action_missing
273
+ # Otherwise we dispatch to {action_missing}.
274
+ #
275
+ # @param [String] path from env['PATH_INFO']
175
276
  #
176
- # @param [String] path from request['REQUEST_PATH']
277
+ # @return [Response]
278
+ #
279
+ # @api external
280
+ # @see Node#resolve Node#action_found Node#action_missing
281
+ # @author manveru
177
282
  def try_resolve(path)
178
283
  action = resolve(path)
179
284
  action ? action_found(action) : action_missing(path)
180
285
  end
181
286
 
182
- # Executed once an Action has been found.
287
+ # Executed once an {Action} has been found.
288
+ #
183
289
  # Reset the {Innate::Response} instance, catch :respond and :redirect.
184
290
  # {Action#call} has to return a String.
185
291
  #
186
- # @param [Innate::Action] action
292
+ # @param [Action] action
293
+ #
187
294
  # @return [Innate::Response]
295
+ #
296
+ # @api external
297
+ # @see Action#call Innate::Response
298
+ # @author manveru
188
299
  def action_found(action)
189
- result = catch(:respond){ catch(:redirect){ action.call }}
300
+ response = catch(:respond){ catch(:redirect){ action.call }}
190
301
 
191
- if result.respond_to?(:finish)
192
- return result
193
- else
194
- Current.response.write(result)
195
- return Current.response
302
+ unless response.respond_to?(:finish)
303
+ self.response.write(response)
304
+ response = self.response
196
305
  end
306
+
307
+ response['Content-Type'] ||= action.options[:content_type]
308
+ response
197
309
  end
198
310
 
199
311
  # The default handler in case no action was found, kind of method_missing.
@@ -227,7 +339,10 @@ module Innate
227
339
  # end
228
340
  #
229
341
  # @param [String] path
230
- # @see Innate::Response Node::try_resolve
342
+ #
343
+ # @api external
344
+ # @see Innate::Response Node#try_resolve
345
+ # @author manveru
231
346
  def action_missing(path)
232
347
  response.status = 404
233
348
  response['Content-Type'] = 'text/plain'
@@ -241,57 +356,93 @@ module Innate
241
356
  # html.
242
357
  #
243
358
  # @param [String] path
244
- # @return [nil Action]
359
+ #
360
+ # @return [nil, Action]
361
+ #
362
+ # @api external
245
363
  # @see Node::find_provide Node::update_method_arities Node::find_action
246
364
  # @author manveru
247
365
  def resolve(path)
248
- name, wish = find_provide(path)
366
+ name, wish, engine = find_provide(path)
367
+ action = Action.create(:node => self, :wish => wish, :engine => engine)
368
+
369
+ if content_type = ancestral_trait["#{wish}_content_type"]
370
+ action.options = {:content_type => content_type}
371
+ end
372
+
249
373
  update_method_arities
250
- find_action(name, wish)
374
+ fill_action(action, name)
251
375
  end
252
376
 
377
+ # Resolve possible provides for the given +path+ from {provides}.
378
+ #
253
379
  # @param [String] path
254
- # @return [Array] with name and wish
255
- # @see Node::provide
380
+ #
381
+ # @return [Array] with name, wish, engine
382
+ #
383
+ # @api internal
384
+ # @see Node::provide Node::provides
256
385
  # @author manveru
257
386
  def find_provide(path)
258
- name, wish = path, 'html'
387
+ pr = provides
388
+
389
+ name, wish, engine = path, 'html', pr['html_handler']
259
390
 
260
- provide.find do |key, value|
391
+ pr.find do |key, value|
392
+ key = key[/(.*)_handler$/, 1]
261
393
  next unless path =~ /^(.+)\.#{key}$/i
262
- name, wish = $1, key
394
+ name, wish, engine = $1, key, value
263
395
  end
264
396
 
265
- return name, wish
397
+ return name, wish, engine
266
398
  end
267
399
 
268
- # Now we're talking Action, we try to find a matching template and method,
269
- # if we can't find either we go to the next pattern, otherwise we answer
270
- # with an Action with everything we know so far about the demands of the
271
- # client.
400
+ # Now we're talking {Action}, we try to find a matching template and
401
+ # method, if we can't find either we go to the next pattern, otherwise we
402
+ # answer with an {Action} with everything we know so far about the demands
403
+ # of the client.
272
404
  #
273
405
  # @param [String] given_name the name extracted from REQUEST_PATH
274
406
  # @param [String] wish
407
+ #
408
+ # @return [Action, nil]
409
+ #
410
+ # @api internal
411
+ # @see Node#find_method Node#find_view Node#find_layout Node#patterns_for
412
+ # Action#wish Action#merge!
275
413
  # @author manveru
276
- def find_action(given_name, wish)
277
- needs_method = Innate.options.action.needs_method
414
+ def fill_action(action, given_name)
415
+ needs_method = self.needs_method?
416
+ wish = action.wish
278
417
 
279
418
  patterns_for(given_name) do |name, params|
280
419
  method = find_method(name, params)
281
- view = find_view(name, wish)
282
420
 
283
- next unless view or method
284
421
  next unless method if needs_method
285
422
  next unless method if params.any?
423
+ next unless (view = find_view(name, wish)) or method
286
424
 
287
- layout = find_layout(name, wish)
288
- params ||= []
289
-
290
- Action.create(:method => method, :params => params, :layout => layout,
291
- :node => self, :view => view, :wish => wish)
425
+ action.merge!(:method => method, :view => view, :params => params,
426
+ :layout => find_layout(name, wish))
292
427
  end
293
428
  end
294
429
 
430
+ # Try to find a suitable value for the layout. This may be a template or
431
+ # the name of a method.
432
+ #
433
+ # If a layout could be found, an Array with two elements is returned, the
434
+ # first indicating the kind of layout (:layout|:view|:method), the second
435
+ # the found value, which may be a String or Symbol.
436
+ #
437
+ # @param [String] name
438
+ # @param [String] wish
439
+ #
440
+ # @return [Array, nil]
441
+ #
442
+ # @api external
443
+ # @see Node#to_layout Node#find_method Node#find_view
444
+ # @author manveru
445
+ #
295
446
  # @todo allow layouts combined of method and view... hairy :)
296
447
  def find_layout(name, wish)
297
448
  return unless layout = ancestral_trait[:layout]
@@ -306,8 +457,8 @@ module Innate
306
457
  end
307
458
  end
308
459
 
309
- # I hope this method talks for itself, we check arity if possible, but will
310
- # happily dispatch to any method that has default parameters.
460
+ # We check arity if possible, but will happily dispatch to any method that
461
+ # has default parameters.
311
462
  # If you don't want your method to be responsible for messing up a request
312
463
  # you should think twice about the arguments you specify due to limitations
313
464
  # in Ruby.
@@ -336,10 +487,19 @@ module Innate
336
487
  # def index(a = :a, b = :b) # => -1
337
488
  # def index(a = :a, b = :b, *r) # => -1
338
489
  #
490
+ # @param [String, Symbol] name
491
+ # @param [Array] params
492
+ #
493
+ # @return [String, Symbol]
494
+ #
495
+ # @api external
496
+ # @see Node#fill_action Node#find_layout
497
+ # @author manveru
498
+ #
339
499
  # @todo Once 1.9 is mainstream we can use Method#parameters to do accurate
340
500
  # prediction
341
501
  def find_method(name, params)
342
- return unless arity = trait[:method_arities][name]
502
+ return unless arity = method_arities[name]
343
503
  name if arity == params.size or arity < 0
344
504
  end
345
505
 
@@ -356,40 +516,61 @@ module Innate
356
516
  # Hi.update_method_arities
357
517
  # # => {'index' => 0, 'foo' => -1, 'bar => 2}
358
518
  #
359
- # @see Node::resolve
519
+ # @api internal
520
+ # @see Node#resolve
360
521
  # @return [Hash] mapping the name of the methods to their arity
361
522
  def update_method_arities
362
- arities = {}
363
- trait(:method_arities => arities)
523
+ @method_arities = {}
364
524
 
365
525
  exposed = ancestors & Helper::EXPOSE.to_a
366
526
  higher = ancestors.select{|a| a < Innate::Node }
367
527
 
368
528
  (higher + exposed).reverse_each do |ancestor|
369
529
  ancestor.public_instance_methods(false).each do |im|
370
- arities[im.to_s] = ancestor.instance_method(im).arity
530
+ @method_arities[im.to_s] = ancestor.instance_method(im).arity
371
531
  end
372
532
  end
373
533
 
374
- arities
534
+ @method_arities
375
535
  end
376
536
 
377
- # Try to find the best template for the given basename and wish.
378
- # Also, having extraordinarily much fun with globs.
537
+ attr_reader :method_arities
538
+
539
+ # Try to find the best template for the given basename and wish and respect
540
+ # aliased views.
541
+ #
542
+ # @param [#to_s] file
543
+ # @param [#to_s] wish
544
+ #
545
+ # @return [String, nil] depending whether a template could be found
546
+ #
547
+ # @api external
548
+ # @see Node#to_template Node#find_aliased_view
549
+ # @author manveru
379
550
  def find_view(file, wish)
380
551
  aliased = find_aliased_view(file, wish)
381
552
  return aliased if aliased
382
553
 
383
- path = [Innate.options.app.root, Innate.options.app.view, view_root, file]
384
- to_template(path, wish)
554
+ to_view(file, wish)
385
555
  end
386
556
 
387
- # This is done to make you feel more at home, pass an absolute path or a
388
- # path relative to your application root to set it, otherwise you'll get
389
- # the current mapping.
390
- def view_root(location = nil)
391
- return @view_root = location if location
392
- @view_root ||= Innate.to(self)
557
+ # Try to find the best template for the given basename and wish.
558
+ #
559
+ # This method is mostly here for symetry with {to_layout} and to allow you
560
+ # overriding the template lookup easily.
561
+ #
562
+ # @param [#to_s] file
563
+ # @param [#to_s] wish
564
+ #
565
+ # @return [String, nil] depending whether a template could be found
566
+ #
567
+ # @api external
568
+ # @see {Node#find_view} {Node#to_template} {Node#root_mappings}
569
+ # {Node#view_mappings} {Node#to_template}
570
+ # @author manveru
571
+ def to_view(file, wish)
572
+ path = root_mappings.concat(view_mappings) << file
573
+ to_template(path, wish)
393
574
  end
394
575
 
395
576
  # Aliasing one view from another.
@@ -417,7 +598,9 @@ module Innate
417
598
  #
418
599
  # @param [#to_s] to view that should be replaced
419
600
  # @param [#to_s] from view to use or Node.
420
- # @param [#nil? Node] node optionally obtain view from this Node
601
+ # @param [#nil?, Node] node optionally obtain view from this Node
602
+ #
603
+ # @api external
421
604
  # @see Node::find_aliased_view
422
605
  # @author manveru
423
606
  def alias_view(to, from, node = nil)
@@ -425,9 +608,14 @@ module Innate
425
608
  trait[:alias_view][to.to_s] = node ? [from.to_s, node] : from.to_s
426
609
  end
427
610
 
611
+ # Resolve one level of aliasing for the given +file+ and +wish+.
612
+ #
428
613
  # @param [String] file
429
614
  # @param [String] wish
430
- # @return [nil String] the absolute path to the aliased template or nil
615
+ #
616
+ # @return [nil, String] the absolute path to the aliased template or nil
617
+ #
618
+ # @api internal
431
619
  # @see Node::alias_view Node::find_view
432
620
  # @author manveru
433
621
  def find_aliased_view(file, wish)
@@ -438,53 +626,55 @@ module Innate
438
626
 
439
627
  # Find the best matching file for the layout, if any.
440
628
  #
629
+ # This is mostly an abstract method that you might find handy if you want
630
+ # to do vastly different layout lookup.
631
+ #
441
632
  # @param [String] file
442
633
  # @param [String] wish
443
- # @return [nil String] the absolute path to the template or nil
444
- # @see Node::to_template
634
+ #
635
+ # @return [nil, String] the absolute path to the template or nil
636
+ #
637
+ # @api external
638
+ # @see {Node#to_template} {Node#root_mappings} {Node#layout_mappings}
445
639
  # @author manveru
446
640
  def to_layout(file, wish)
447
- path = [Innate.options.app.root, Innate.options.app.layout, file]
641
+ path = root_mappings.concat(layout_mappings) << file
448
642
  to_template(path, wish)
449
643
  end
450
644
 
451
- # @param [String] file
452
- # @param [String] wish
453
- # @return [nil String] the absolute path to the template or nil
454
- # @see Node::find_view Node::to_layout Node::find_aliased_view
455
- # @author manveru
456
- def to_template(path, wish)
457
- return unless path.all?
458
-
459
- path = File.join(*path.map{|pa| pa.to_s.split('__') }.flatten)
460
- exts = (Array[provide[wish]] + provide.keys).flatten.compact.uniq.join(',')
461
- glob = "#{path}.{#{wish}.,#{wish},}{#{exts},}"
462
- found = Dir[glob].uniq
463
-
464
- if found.size > 1
465
- Log.warn("%d views found for %p | %p" % [found.size, path, wish])
466
- end
467
-
468
- found.first
469
- end
470
-
471
645
  # Define a layout to use on this Node.
472
646
  #
473
- # @param [String #to_s] name basename without extension of the layout to use
474
- # @param [Proc #call] block called on every dispatch if no name given
475
- # @return [Proc String] The assigned name or block
647
+ # A Node can only have one layout, although the template being chosen can
648
+ # depend on {provides}.
476
649
  #
477
- # @note The behaviour of Node#layout changed significantly from Ramaze,
478
- # instead of multitudes of obscure options and methods like deny_layout
479
- # we simply take a block and use the returned value as the name for the
480
- # layout. No layout will be used if the block returns nil.
650
+ # @param [String, #to_s] name basename without extension of the layout to use
651
+ # @param [Proc, #call] block called on every dispatch if no name given
652
+ #
653
+ # @return [Proc, String] The assigned name or block
654
+ #
655
+ # @api external
656
+ # @see Node#find_layout Node#layout_paths Node#to_layout Node#app_layout
657
+ # @author manveru
658
+ #
659
+ # NOTE:
660
+ # The behaviour of Node#layout changed significantly from Ramaze, instead
661
+ # of multitudes of obscure options and methods like deny_layout we simply
662
+ # take a block and use the returned value as the name for the layout. No
663
+ # layout will be used if the block returns nil.
481
664
  def layout(name = nil, &block)
482
665
  if name and block
666
+ # default name, but still check with block
483
667
  trait(:layout => lambda{|n, w| name if block.call(n, w) })
484
668
  elsif name
669
+ # name of a method or template
485
670
  trait(:layout => name.to_s)
486
671
  elsif block
672
+ # call block every request with name and wish, returned value is name
673
+ # of layout template or method
487
674
  trait(:layout => block)
675
+ else
676
+ # remove layout for this node
677
+ trait(:layout => nil)
488
678
  end
489
679
 
490
680
  return ancestral_trait[:layout]
@@ -503,7 +693,8 @@ module Innate
503
693
  # The last fallback will always be the index action with all of the path
504
694
  # turned into parameters.
505
695
  #
506
- # @usage
696
+ # @example yielding possible combinations of action names and params
697
+ #
507
698
  # class Foo; include Innate::Node; map '/'; end
508
699
  #
509
700
  # Foo.patterns_for('/'){|action, params| p action => params }
@@ -519,6 +710,14 @@ module Innate
519
710
  # # => {"foo__bar"=>["baz"]}
520
711
  # # => {"foo"=>["bar", "baz"]}
521
712
  # # => {"index"=>["foo", "bar", "baz"]}
713
+ #
714
+ # @param [String, #split] path usually the PATH_INFO
715
+ #
716
+ # @return [Action] it actually returns the first non-nil/false result of yield
717
+ #
718
+ # @api internal
719
+ # @see Node#fill_action
720
+ # @author manveru
522
721
  def patterns_for(path)
523
722
  atoms = path.split('/')
524
723
  atoms.delete('')
@@ -527,7 +726,7 @@ module Innate
527
726
  atoms.size.downto(0) do |len|
528
727
  action = atoms[0...len].join('__')
529
728
  params = atoms[len..-1]
530
- action = 'index' if action.empty?
729
+ action = 'index' if action.empty? and params != ['index']
531
730
 
532
731
  return result if result = yield(action, params)
533
732
  end
@@ -535,24 +734,93 @@ module Innate
535
734
  return nil
536
735
  end
537
736
 
538
- # This awesome piece of hackery implements action AOP, methods may register
539
- # themself in the trait[:wrap] and will be called in left-to-right order,
540
- # each being passed the action instance and a block that they have to yield
541
- # to continue the chain.
737
+ # Try to find a template at the given +path+ for +wish+.
738
+ #
739
+ # Since Innate supports multiple paths to templates the +path+ has to be an
740
+ # Array that may be nested one level.
741
+ # The +path+ is then translated by {Node#path_glob} and the +wish+ by
742
+ # {Node#ext_glob}.
743
+ #
744
+ # @example Usage to find available templates
745
+ #
746
+ # # This assumes following files:
747
+ # # view/foo.erb
748
+ # # view/bar.erb
749
+ # # view/bar.rss.erb
750
+ # # view/bar.yaml.erb
751
+ #
752
+ # class FooBar
753
+ # Innate.node('/')
754
+ # end
755
+ #
756
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'html')
757
+ # # => "./view/foo.erb"
758
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'yaml')
759
+ # # => "./view/foo.erb"
760
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'rss')
761
+ # # => "./view/foo.erb"
762
+ #
763
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'html')
764
+ # # => "./view/bar.erb"
765
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'yaml')
766
+ # # => "./view/bar.yaml.erb"
767
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'rss')
768
+ # # => "./view/bar.rss.erb"
769
+ #
770
+ # @param [Array<Array<String>>, Array<String>] path
771
+ # array containing strings and nested (1 level) arrays containing strings
772
+ # @param [String] wish
773
+ #
774
+ # @return [nil, String] relative path to the first template found
775
+ #
776
+ # @api external
777
+ # @see Node#find_view Node#to_layout Node#find_aliased_view
778
+ # Node#path_glob Node#ext_glob
779
+ # @author manveru
780
+ def to_template(path, wish)
781
+ return unless exts = ext_glob(wish)
782
+ glob = "#{path_glob(*path)}.#{exts}"
783
+ found = Dir[glob].uniq
784
+
785
+ count = found.size
786
+ Log.warn("%d views found for %p" % [count, glob]) if count > 1
787
+
788
+ found.first
789
+ end
790
+
791
+ # Produce a glob that can be processed by Dir::[] matching the possible
792
+ # paths to the given +elements+.
793
+ #
794
+ # The +elements+ are an Array that may be nested one level, take care to
795
+ # splat if you try to pass an existing Array.
796
+ #
797
+ # @return [String] glob matching possible paths to the given +elements+
798
+ #
799
+ # @api internal
800
+ # @see Node#to_template
801
+ # @author manveru
802
+ def path_glob(*elements)
803
+ File.join(elements.map{|element|
804
+ "{%s}" % [*element].map{|e| e.to_s.gsub('__', '/') }.join(',')
805
+ }).gsub(/\/\{\/?\}\//, '/')
806
+ end
807
+
808
+ # Produce a glob that can be processed by Dir::[] matching the extensions
809
+ # associated with the given +wish+.
810
+ #
811
+ # @param [#to_s] wish the extension (no leading '.')
542
812
  #
543
- # This enables things like action logging, caching, aspects,
544
- # authentication, etc...
813
+ # @return [String] glob matching the valid exts for the given +wish+
545
814
  #
546
- # @param [Action] action instance that is being passed to every registered method
547
- # @param [Proc] block contains the instructions to call the action method if any
548
- # @see Action#render
815
+ # @api internal
816
+ # @see Node#to_template View::exts_of Node#provides
549
817
  # @author manveru
550
- def wrap_action_call(action, &block)
551
- wrap = ancestral_trait[:wrap]
552
- head, *tail = wrap
553
- tail.reverse!
554
- combined = tail.inject(block){|s,v| lambda{ __send__(v, action, &s) } }
555
- __send__(head, action, &combined)
818
+ def ext_glob(wish)
819
+ pr = provides
820
+ return unless engine = pr["#{wish}_handler"]
821
+ engine_exts = View.exts_of(engine).join(',')
822
+ represented = [*wish].map{|k| "#{k}." }.join(',')
823
+ "{%s,}{%s}" % [represented, engine_exts]
556
824
  end
557
825
 
558
826
  # For compatibility with new Kernel#binding behaviour in 1.9
@@ -560,7 +828,125 @@ module Innate
560
828
  # @return [Binding] binding of the instance being rendered.
561
829
  # @see Action#binding
562
830
  # @author manveru
563
- def binding; super; end
831
+ def binding; super end
832
+
833
+ # make sure this is an Array and a new instance so modification on the
834
+ # wrapping array doesn't affect the original option.
835
+ # [*arr].object_id == arr.object_id if arr is an Array
836
+ #
837
+ # @return [Array] list of root directories
838
+ #
839
+ # @api external
840
+ # @author manveru
841
+ def root_mappings
842
+ [*options.roots].dup
843
+ end
844
+
845
+ # Set the paths for lookup below the Innate.options.views paths.
846
+ #
847
+ # @param [String, Array<String>] locations
848
+ # Any number of strings indicating the paths where view templates may be
849
+ # located, relative to Innate.options.roots/Innate.options.views
850
+ #
851
+ # @return [Node] self
852
+ #
853
+ # @api external
854
+ # @see {Node#view_mappings}
855
+ # @author manveru
856
+ def map_views(*locations)
857
+ trait :views => locations.flatten.uniq
858
+ self
859
+ end
860
+
861
+ # Combine Innate.options.views with either the `ancestral_trait[:views]`
862
+ # or the {Node#mapping} if the trait yields an empty Array.
863
+ #
864
+ # @return [Array<String>, Array<Array<String>>]
865
+ #
866
+ # @api external
867
+ # @see {Node#map_views}
868
+ # @author manveru
869
+ def view_mappings
870
+ paths = [*ancestral_trait[:views]]
871
+ paths = [mapping] if paths.empty?
872
+
873
+ [*options.views] + paths
874
+ end
875
+
876
+ # Set the paths for lookup below the Innate.options.layouts paths.
877
+ #
878
+ # @param [String, Array<String>] locations
879
+ # Any number of strings indicating the paths where layout templates may
880
+ # be located, relative to Innate.options.roots/Innate.options.layouts
881
+ #
882
+ # @return [Node] self
883
+ #
884
+ # @api external
885
+ # @see {Node#layout_mappings}
886
+ # @author manveru
887
+ def map_layouts(*locations)
888
+ trait :layouts => locations.flatten.uniq
889
+ self
890
+ end
891
+
892
+ # Combine Innate.options.layouts with either the `ancestral_trait[:layouts]`
893
+ # or the {Node#mapping} if the trait yields an empty Array.
894
+ #
895
+ # @return [Array<String>, Array<Array<String>>]
896
+ #
897
+ # @api external
898
+ # @see {Node#map_layouts}
899
+ # @author manveru
900
+ def layout_mappings
901
+ paths = [*ancestral_trait[:layouts]]
902
+ paths = [mapping] if paths.empty?
903
+
904
+ [*options.layouts] + paths
905
+ end
906
+
907
+ def options
908
+ Innate.options
909
+ end
910
+
911
+ # Whether an {Action} can be built without a method.
912
+ #
913
+ # The default is to allow actions that use only a view template, but you
914
+ # might want to turn this on, for example if you have partials in your view
915
+ # directories.
916
+ #
917
+ # @example turning needs_method? on
918
+ #
919
+ # class Foo
920
+ # Innate.node('/')
921
+ # end
922
+ #
923
+ # Foo.needs_method? # => true
924
+ # Foo.trait :needs_method => false
925
+ # Foo.needs_method? # => false
926
+ #
927
+ # @return [true, false] (false)
928
+ #
929
+ # @api external
930
+ # @see {Node#fill_action}
931
+ # @author manveru
932
+ def needs_method?
933
+ ancestral_trait[:needs_method]
934
+ end
935
+
936
+ # This will return true if the only provides set are by {Node::included}.
937
+ #
938
+ # The reasoning behind this is to determine whether the user has touched
939
+ # the provides at all, in which case we will not override the provides in
940
+ # subclasses.
941
+ #
942
+ # @return [true, false] (false)
943
+ #
944
+ # @api internal
945
+ # @see {Node::included}
946
+ # @author manveru
947
+ def provide_set?
948
+ ancestral_trait[:provide_set]
949
+ end
564
950
  end
565
951
 
566
952
  module SingletonMethods
@@ -568,10 +954,14 @@ module Innate
568
954
  # +location+.
569
955
  #
570
956
  # @param [#to_s] location where the node is mapped to
571
- # @param [Node nil] node the class that will be a node, will try to look it
572
- # up if not given
573
- # @return [Class] the node argument or detected class will be returned
574
- # @see Innate::node_from_backtrace
957
+ # @param [Node, nil] node the class that will be a node, will try to
958
+ # look it up if not given
959
+ #
960
+ # @return [Class, Module] the node argument or detected class will be
961
+ # returned
962
+ #
963
+ # @api external
964
+ # @see SingletonMethods::node_from_backtrace
575
965
  # @author manveru
576
966
  def node(location, node = nil)
577
967
  node ||= node_from_backtrace(caller)
@@ -588,13 +978,17 @@ module Innate
588
978
  # If there are any problems with this (filenames containing ':' or
589
979
  # metaprogramming) just pass the node parameter explicitly to Innate::node
590
980
  #
591
- # @param [Array #[]] backtrace
592
- # @see Innate::node
981
+ # @param [Array<String>, #[]] backtrace
982
+ #
983
+ # @return [Class, Module]
984
+ #
985
+ # @api internal
986
+ # @see SingletonMethods::node
593
987
  # @author manveru
594
988
  def node_from_backtrace(backtrace)
595
- file, line = backtrace[0].split(':', 2)
596
- line = line.to_i
597
- File.readlines(file)[0..line].reverse.find{|line| line =~ /^\s*class\s+(\S+)/ }
989
+ filename, lineno = backtrace[0].split(':', 2)
990
+ regexp = /^\s*class\s+(\S+)/
991
+ File.readlines(filename)[0..lineno.to_i].reverse.find{|l| l =~ regexp }
598
992
  const_get($1)
599
993
  end
600
994
  end