manveru-innate 2009.02.06

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/CHANGELOG +1409 -0
  2. data/COPYING +18 -0
  3. data/MANIFEST +100 -0
  4. data/README.md +485 -0
  5. data/Rakefile +139 -0
  6. data/example/app/retro_games.rb +57 -0
  7. data/example/app/whywiki_erb/layout/wiki.html.erb +15 -0
  8. data/example/app/whywiki_erb/spec/wiki.rb +19 -0
  9. data/example/app/whywiki_erb/start.rb +45 -0
  10. data/example/app/whywiki_erb/view/edit.html.erb +6 -0
  11. data/example/app/whywiki_erb/view/index.html.erb +10 -0
  12. data/example/custom_middleware.rb +43 -0
  13. data/example/error_handling.rb +31 -0
  14. data/example/hello.rb +12 -0
  15. data/example/howto_spec.rb +60 -0
  16. data/example/link.rb +35 -0
  17. data/example/providing_hash.rb +46 -0
  18. data/example/session.rb +42 -0
  19. data/innate.gemspec +118 -0
  20. data/lib/innate.rb +191 -0
  21. data/lib/innate/action.rb +156 -0
  22. data/lib/innate/adapter.rb +89 -0
  23. data/lib/innate/cache.rb +117 -0
  24. data/lib/innate/cache/api.rb +106 -0
  25. data/lib/innate/cache/drb.rb +58 -0
  26. data/lib/innate/cache/file_based.rb +39 -0
  27. data/lib/innate/cache/marshal.rb +17 -0
  28. data/lib/innate/cache/memory.rb +22 -0
  29. data/lib/innate/cache/yaml.rb +17 -0
  30. data/lib/innate/core_compatibility/basic_object.rb +9 -0
  31. data/lib/innate/core_compatibility/string.rb +3 -0
  32. data/lib/innate/current.rb +37 -0
  33. data/lib/innate/dynamap.rb +81 -0
  34. data/lib/innate/helper.rb +195 -0
  35. data/lib/innate/helper/aspect.rb +62 -0
  36. data/lib/innate/helper/cgi.rb +39 -0
  37. data/lib/innate/helper/flash.rb +36 -0
  38. data/lib/innate/helper/link.rb +55 -0
  39. data/lib/innate/helper/partial.rb +90 -0
  40. data/lib/innate/helper/redirect.rb +85 -0
  41. data/lib/innate/helper/send_file.rb +18 -0
  42. data/lib/innate/log.rb +23 -0
  43. data/lib/innate/log/color_formatter.rb +43 -0
  44. data/lib/innate/log/hub.rb +72 -0
  45. data/lib/innate/mock.rb +49 -0
  46. data/lib/innate/node.rb +471 -0
  47. data/lib/innate/options.rb +91 -0
  48. data/lib/innate/options/dsl.rb +155 -0
  49. data/lib/innate/request.rb +165 -0
  50. data/lib/innate/response.rb +18 -0
  51. data/lib/innate/route.rb +109 -0
  52. data/lib/innate/session.rb +104 -0
  53. data/lib/innate/session/flash.rb +94 -0
  54. data/lib/innate/setup.rb +23 -0
  55. data/lib/innate/spec.rb +42 -0
  56. data/lib/innate/state.rb +22 -0
  57. data/lib/innate/state/accessor.rb +130 -0
  58. data/lib/innate/state/fiber.rb +68 -0
  59. data/lib/innate/state/thread.rb +39 -0
  60. data/lib/innate/traited.rb +20 -0
  61. data/lib/innate/trinity.rb +22 -0
  62. data/lib/innate/version.rb +3 -0
  63. data/lib/innate/view.rb +67 -0
  64. data/lib/innate/view/erb.rb +17 -0
  65. data/lib/innate/view/none.rb +9 -0
  66. data/lib/rack/middleware_compiler.rb +62 -0
  67. data/lib/rack/reloader.rb +192 -0
  68. data/spec/example/hello.rb +14 -0
  69. data/spec/example/link.rb +29 -0
  70. data/spec/helper.rb +2 -0
  71. data/spec/innate/cache/common.rb +45 -0
  72. data/spec/innate/cache/marshal.rb +5 -0
  73. data/spec/innate/cache/memory.rb +5 -0
  74. data/spec/innate/cache/yaml.rb +5 -0
  75. data/spec/innate/dynamap.rb +22 -0
  76. data/spec/innate/helper.rb +66 -0
  77. data/spec/innate/helper/aspect.rb +80 -0
  78. data/spec/innate/helper/cgi.rb +37 -0
  79. data/spec/innate/helper/flash.rb +148 -0
  80. data/spec/innate/helper/link.rb +82 -0
  81. data/spec/innate/helper/partial.rb +66 -0
  82. data/spec/innate/helper/redirect.rb +148 -0
  83. data/spec/innate/helper/send_file.rb +21 -0
  84. data/spec/innate/helper/view/aspect_hello.erb +1 -0
  85. data/spec/innate/helper/view/locals.erb +1 -0
  86. data/spec/innate/helper/view/loop.erb +4 -0
  87. data/spec/innate/helper/view/num.erb +1 -0
  88. data/spec/innate/helper/view/partial.erb +1 -0
  89. data/spec/innate/helper/view/recursive.erb +8 -0
  90. data/spec/innate/mock.rb +84 -0
  91. data/spec/innate/node.rb +180 -0
  92. data/spec/innate/node/bar.html +1 -0
  93. data/spec/innate/node/foo.html.erb +1 -0
  94. data/spec/innate/node/with_layout.erb +3 -0
  95. data/spec/innate/options.rb +90 -0
  96. data/spec/innate/parameter.rb +154 -0
  97. data/spec/innate/request.rb +73 -0
  98. data/spec/innate/route.rb +129 -0
  99. data/spec/innate/session.rb +59 -0
  100. data/spec/innate/traited.rb +55 -0
  101. metadata +160 -0
@@ -0,0 +1,471 @@
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 Ramaze controller is that every
9
+ # 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
+ # nodes.
19
+ #
20
+ # Upon inclusion, it will also include Innate::Trinity and Innate::Helper to
21
+ # provide you with request/response objects, a session and all the standard
22
+ # helper methods as well as the ability to simply add other helpers.
23
+ #
24
+ # NOTE:
25
+ # * Although I tried to minimize the amount of code in here there is still
26
+ # quite a number of methods left in order to do ramaze-style lookups.
27
+ # Those methods, and all other methods occurring in the ancestors after
28
+ # Innate::Node will not be considered valid action methods and will be
29
+ # ignored.
30
+ # * This also means that method_missing will not see any of the requests
31
+ # coming in.
32
+ # * If you want an action to act as a catch-all, use `def index(*args)`.
33
+
34
+ module Node
35
+ include Traited
36
+
37
+ HELPERS = [:aspect, :cgi, :flash, :link, :partial, :redirect, :send_file]
38
+ LIST = Set.new
39
+
40
+ trait(:layout => nil, :alias_view => {}, :provide => {},
41
+ :method_arities => {}, :wrap => [:aspect_wrap])
42
+
43
+ # Upon inclusion we make ourselves comfortable.
44
+ def self.included(obj)
45
+ obj.__send__(:include, Helper)
46
+ obj.helper(*HELPERS)
47
+
48
+ obj.extend(Trinity, self)
49
+
50
+ LIST << obj
51
+
52
+ return if obj.provide.any?
53
+ # provide .html with no interpolation
54
+ obj.provide(:html => :erb, :yaml => :yaml, :json => :json)
55
+ end
56
+
57
+ def self.setup
58
+ LIST.each{|node| Innate.map(node.mapping, node) }
59
+ Log.debug("Mapped Nodes: %p" % DynaMap::MAP)
60
+ end
61
+
62
+ def mapping
63
+ mapped = Innate.to(self)
64
+ return mapped if mapped
65
+ return '/' if Innate::Node::LIST.size == 1
66
+ "/" << self.name.gsub(/\B[A-Z][^A-Z]/, '_\&').downcase
67
+ end
68
+
69
+ # Shortcut to map or remap this Node
70
+
71
+ def map(location)
72
+ Innate.map(location, self)
73
+ end
74
+
75
+ # This little piece of nasty looking code enables you to provide different
76
+ # content from a single action.
77
+ #
78
+ # Usage:
79
+ #
80
+ # class Feeds
81
+ # include Innate::Node
82
+ # map '/feed'
83
+ #
84
+ # provide :html => :erb, :rss => :erb, :atom => :erb
85
+ #
86
+ # def index
87
+ # @feed = build_some_feed
88
+ # end
89
+ # end
90
+ #
91
+ # This will do following to these requests:
92
+ #
93
+ # /feed # => call Feeds#index with template /view/feed/index.erb
94
+ # /feed.atom # => call Feeds#index with template /view/feed/index.atom.erb
95
+ # /feed.rss # => call Feeds#index with template /view/feed/index.rss.erb
96
+ #
97
+ # If index.atom.erb isn't available we fall back to /view/feed/index.erb
98
+ #
99
+ # So it's really easy to add your own content representation.
100
+ #
101
+ # If no matching provider is found for the given extension it will fall
102
+ # back to the one specified for html.
103
+ #
104
+ # The correct templating engine is selected by matching the last extension
105
+ # of the template itself to the one set in Innate::View.
106
+ #
107
+ # If you don't want that your response is passed through a templating
108
+ # engine, use :none like:
109
+ #
110
+ # provide :txt => :none
111
+ #
112
+ # So a request to
113
+ #
114
+ # /feed.txt # => call Feeds#index with template /view/feed/index.txt.erb
115
+ #
116
+ # NOTE: provides also have effect on the chosen layout for the action.
117
+ #
118
+ # Given a Node at '/' with `layout('default')`:
119
+ # /layout/default.erb
120
+ # /layout/default.rss.erb
121
+ # /view/index.erb
122
+ # /view/feed.rss.erb
123
+ #
124
+ # /feed.rss will wrap /view/feed.rss.erb in /layout/default.rss.erb
125
+ # /index will wrap /view/index.erb in /layout/default.erb
126
+
127
+ def provide(formats = {})
128
+ return ancestral_trait[:provide] if formats.empty?
129
+
130
+ trait[:provide] ||= {}
131
+ formats.each{|pr, as| trait[:provide][pr.to_s] = as.to_s }
132
+
133
+ ancestral_trait[:provide]
134
+ end
135
+
136
+ # This makes the Node a valid application for Rack.
137
+ # +env+ is the environment hash passed from the Rack::Handler
138
+ #
139
+ # We rely on correct PATH_INFO.
140
+ #
141
+ # As defined by the Rack spec, PATH_INFO may be empty if it wants the root
142
+ # of the application, so we insert '/' to make our dispatcher simple.
143
+ #
144
+ # Innate will not rescue any errors for you or do any error handling, this
145
+ # should be done by an underlying middleware.
146
+ #
147
+ # We do however log errors at some vital points in order to provide you
148
+ # with feedback in your logs.
149
+ #
150
+ # NOTE:
151
+ # * A lot of functionality in here relies on the fact that call is
152
+ # executed within Innate::STATE.wrap which populates the variables used
153
+ # by Trinity.
154
+ # * If you use the Node directly as a middleware make sure that you #use
155
+ # Innate::Current as a middleware before it.
156
+
157
+ def call(env)
158
+ path = env['PATH_INFO']
159
+ path << '/' if path.empty?
160
+
161
+ response.reset
162
+ response = try_resolve(path)
163
+ response['Content-Type'] ||= 'text/html'
164
+
165
+ Current.session.flush(response)
166
+
167
+ response.finish
168
+ end
169
+
170
+ # Let's try to find some valid action for given +path+.
171
+ # Otherwise we dispatch to action_not_found
172
+
173
+ def try_resolve(path)
174
+ action = resolve(path)
175
+ action ? action_found(action) : action_not_found(path)
176
+ end
177
+
178
+ # Executed once an Action has been found.
179
+ # Reset the Response instance, catch :respond and :redirect.
180
+ # Action#call has to return a String.
181
+
182
+ def action_found(action)
183
+ result = catch(:respond){ catch(:redirect){ action.call }}
184
+
185
+ if result.respond_to?(:finish)
186
+ return result
187
+ else
188
+ Current.response.write(result)
189
+ return Current.response
190
+ end
191
+ end
192
+
193
+ # The default handler in case no action was found, kind of method_missing.
194
+ # Must modify the response in order to have any lasting effect.
195
+ #
196
+ # Reasoning:
197
+ # * We are doing this is in order to avoid tons of special error handling
198
+ # code that would impact runtime and make the overall API more
199
+ # complicated.
200
+ # * This cannot be a normal action is that methods defined in Innate::Node
201
+ # will never be considered for actions.
202
+ #
203
+ # To use a normal action with template do following:
204
+ #
205
+ # class Hi
206
+ # include Innate::Node
207
+ # map '/'
208
+ #
209
+ # def action_not_found(path)
210
+ # return if path == '/not_found'
211
+ # # No normal action, runs on bare metal
212
+ # try_resolve('/not_found')
213
+ # end
214
+ #
215
+ # def not_found
216
+ # # Normal action
217
+ # "Sorry, I do not exist"
218
+ # end
219
+ # end
220
+
221
+ def action_not_found(path)
222
+ response.status = 404
223
+ response['Content-Type'] = 'text/plain'
224
+ response.write("Action not found at: %p" % path)
225
+
226
+ response
227
+ end
228
+
229
+ # Let's get down to business, first check if we got any wishes regarding
230
+ # the representation from the client, otherwise we will assume he wants
231
+ # html.
232
+
233
+ def resolve(path)
234
+ name, wish = find_provide(path)
235
+ update_method_arities
236
+ find_action(name, wish)
237
+ end
238
+
239
+ def find_provide(path)
240
+ name, wish = path, 'html'
241
+
242
+ provide.find do |key, value|
243
+ next unless path =~ /^(.+)\.#{key}$/i
244
+ name, wish = $1, key
245
+ end
246
+
247
+ return name, wish
248
+ end
249
+
250
+ # Now we're talking Action, we try to find a matching template and method,
251
+ # if we can't find either we go to the next pattern, otherwise we answer
252
+ # with an Action with everything we know so far about the demands of the
253
+ # client.
254
+
255
+ def find_action(given_name, wish)
256
+ patterns_for(given_name) do |name, params|
257
+ view = find_view(name, wish)
258
+ method = find_method(name, params)
259
+
260
+ next unless view or method
261
+
262
+ layout = find_layout(name, wish)
263
+
264
+ Action.create(
265
+ :node => self, :params => params, :wish => wish, :method => method,
266
+ :view => view, :options => {}, :variables => {}, :layout => layout)
267
+ end
268
+ end
269
+
270
+ # TODO: allow layouts combined of method and view... hairy :)
271
+ def find_layout(name, wish)
272
+ return unless found_layout = layout
273
+
274
+ if found = to_layout(found_layout, wish)
275
+ [:layout, found]
276
+ elsif found = find_view(found_layout, wish)
277
+ [:view, found]
278
+ elsif found = find_method(found_layout, [])
279
+ [:method, found]
280
+ end
281
+ end
282
+
283
+ # I hope this method talks for itself, we check arity if possible, but will
284
+ # happily dispatch to any method that has default parameters.
285
+ # If you don't want your method to be responsible for messing up a request
286
+ # you should think twice about the arguments you specify due to limitations
287
+ # in Ruby.
288
+ #
289
+ # So if you want your method to take only one parameter which may have a
290
+ # default value following will work fine:
291
+ #
292
+ # def index(foo = "bar", *rest)
293
+ #
294
+ # But following will respond to /arg1/arg2 and then fail due to ArgumentError:
295
+ #
296
+ # def index(foo = "bar")
297
+ #
298
+ # Here a glance at how parameters are expressed in arity:
299
+ #
300
+ # def index(a) # => 1
301
+ # def index(a = :a) # => -1
302
+ # def index(a, *r) # => -2
303
+ # def index(a = :a, *r) # => -1
304
+ #
305
+ # def index(a, b) # => 2
306
+ # def index(a, b, *r) # => -3
307
+ # def index(a, b = :b) # => -2
308
+ # def index(a, b = :b, *r) # => -2
309
+ #
310
+ # def index(a = :a, b = :b) # => -1
311
+ # def index(a = :a, b = :b, *r) # => -1
312
+ #
313
+ # NOTE: Once 1.9 is mainstream we can use Method#parameters to do accurate
314
+ # prediction
315
+ def find_method(name, params)
316
+ return unless arity = trait[:method_arities][name]
317
+ name if arity == params.size or arity < 0
318
+ end
319
+
320
+ # Answer with and set the @method_arities Hash, keys are method names,
321
+ # values are method arities.
322
+ #
323
+ # Usually called from Node::resolve
324
+ #
325
+ # NOTE:
326
+ # * This will be executed once for every request, once we have settled
327
+ # things down a bit more we can switch to update based on Reloader
328
+ # hooks and update once on startup.
329
+ # However, that may cause problems with dynamically created methods, so
330
+ # let's play it safe for now.
331
+ #
332
+ # Example:
333
+ #
334
+ # Hi.update_method_arities
335
+ # # => {'index' => 0, 'foo' => -1, 'bar => 2}
336
+ def update_method_arities
337
+ arities = trait[:method_arities] = {}
338
+
339
+ exposed = ancestors & Helper::EXPOSE.to_a
340
+ higher = ancestors.select{|a| a < Innate::Node }
341
+
342
+ (higher + exposed).reverse_each do |ancestor|
343
+ ancestor.public_instance_methods(false).each do |im|
344
+ arities[im.to_s] = ancestor.instance_method(im).arity
345
+ end
346
+ end
347
+
348
+ arities
349
+ end
350
+
351
+ # Try to find the best template for the given basename and wish.
352
+ # Also, having extraordinarily much fun with globs.
353
+ def find_view(file, wish)
354
+ path = [Innate.options.app.root, Innate.options.app.view, view_root, file]
355
+ to_template(path, wish)
356
+ end
357
+
358
+ # This is done to make you feel more at home, pass an absolute path or a
359
+ # path relative to your application root to set it, otherwise you'll get
360
+ # the current mapping.
361
+ def view_root(location = nil)
362
+ return @view_root = location if location
363
+ @view_root ||= Innate.to(self)
364
+ end
365
+
366
+ def alias_view(to, from)
367
+ (trait[:alias_view] ||= {})[to] = from
368
+ end
369
+
370
+ # Find the best matching file for the layout, if any.
371
+ def to_layout(file, wish)
372
+ path = [Innate.options.app.root, Innate.options.app.layout, file]
373
+ to_template(path, wish)
374
+ end
375
+
376
+ def to_template(path, wish)
377
+ return unless path.all?
378
+
379
+ path = File.join(*path.map{|pa| pa.to_s })
380
+ exts = [provide[wish], *provide.keys].flatten.compact.uniq.join(',')
381
+ found = Dir["#{path}.{#{wish}.,#{wish},}{#{exts},}"].uniq
382
+
383
+ if found.size > 1
384
+ Log.warn("%d views found for %p | %p" % [found.size, path, wish])
385
+ end
386
+
387
+ template = found.first
388
+ ancestral_trait[:alias_view][template] || template
389
+ end
390
+
391
+ # Set the +name+ of the layout you want, this takes only the basename
392
+ # without any filename-extension or directory.
393
+ def layout(name = nil)
394
+ name ? trait(:layout => name) : ancestral_trait[:layout]
395
+ end
396
+
397
+ # The innate beauty in Nitro, Ramaze, and Innate.
398
+ #
399
+ # Will yield the name of the action and parameter for the action method in
400
+ # order of significance.
401
+ #
402
+ # def foo__bar # responds to /foo/bar
403
+ # def foo(bar) # also responds to /foo/bar
404
+ #
405
+ # But foo__bar takes precedence because it's more explicit.
406
+ #
407
+ # The last fallback will always be the index action with all of the path
408
+ # turned into parameters.
409
+ #
410
+ # Samples:
411
+ #
412
+ # class Foo; include Innate::Node; map '/'; end
413
+ #
414
+ # Foo.patterns_for('/'){|action, params| p action => params }
415
+ # {"index"=>[]}
416
+ #
417
+ # Foo.patterns_for('/foo/bar'){|action, params| p action => params }
418
+ # {"foo__bar"=>[]}
419
+ # {"foo"=>["bar"]}
420
+ # {"index"=>["foo", "bar"]}
421
+ #
422
+ # Foo.patterns_for('/foo/bar/baz'){|action, params| p action => params }
423
+ # {"foo__bar__baz"=>[]}
424
+ # {"foo__bar"=>["baz"]}
425
+ # {"foo"=>["bar", "baz"]}
426
+ # {"index"=>["foo", "bar", "baz"]}
427
+ def patterns_for(path)
428
+ atoms = path.split('/')
429
+ atoms.delete('')
430
+ result = nil
431
+
432
+ atoms.size.downto(0) do |len|
433
+ action = atoms[0...len].join('__')
434
+ params = atoms[len..-1]
435
+ action = 'index' if action.empty?
436
+
437
+ return result if result = yield(action, params)
438
+ end
439
+
440
+ return nil
441
+ end
442
+
443
+ # This awesome piece of hackery implements action AOP, methods may register
444
+ # themself in the trait[:wrap] and will be called in left-to-right order,
445
+ # each being passed the action instance and a block that they have to yield
446
+ # to continue the chain.
447
+ #
448
+ # This enables things like action logging, caching, aspects,
449
+ # authentication, etc...
450
+ #
451
+ # @param [Action] action instance that is being passed to every registered method
452
+ # @param [Proc] block contains the instructions to call the action method if any
453
+ # @see Action#render
454
+ # @author manveru
455
+
456
+ def wrap_action_call(action, &block)
457
+ wrap = ancestral_trait[:wrap]
458
+
459
+ head, tail = wrap[0], wrap[1..-1].reverse
460
+ combined = tail.inject(block){|s,v| lambda{ __send__(v, action, &s) } }
461
+ __send__(head, action, &combined)
462
+ end
463
+
464
+ # For compatibility with new Kernel#binding behaviour in 1.9
465
+ #
466
+ # @return [Binding] binding of the instance being rendered.
467
+ # @see Action#binding
468
+ # @author manveru
469
+ def binding; super; end
470
+ end
471
+ end