roda 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +709 -0
  5. data/Rakefile +124 -0
  6. data/lib/roda.rb +608 -0
  7. data/lib/roda/plugins/all_verbs.rb +48 -0
  8. data/lib/roda/plugins/default_headers.rb +50 -0
  9. data/lib/roda/plugins/error_handler.rb +69 -0
  10. data/lib/roda/plugins/flash.rb +62 -0
  11. data/lib/roda/plugins/h.rb +24 -0
  12. data/lib/roda/plugins/halt.rb +79 -0
  13. data/lib/roda/plugins/header_matchers.rb +57 -0
  14. data/lib/roda/plugins/hooks.rb +106 -0
  15. data/lib/roda/plugins/indifferent_params.rb +47 -0
  16. data/lib/roda/plugins/middleware.rb +88 -0
  17. data/lib/roda/plugins/multi_route.rb +77 -0
  18. data/lib/roda/plugins/not_found.rb +62 -0
  19. data/lib/roda/plugins/pass.rb +34 -0
  20. data/lib/roda/plugins/render.rb +217 -0
  21. data/lib/roda/plugins/streaming.rb +165 -0
  22. data/spec/composition_spec.rb +19 -0
  23. data/spec/env_spec.rb +11 -0
  24. data/spec/integration_spec.rb +63 -0
  25. data/spec/matchers_spec.rb +658 -0
  26. data/spec/module_spec.rb +29 -0
  27. data/spec/opts_spec.rb +42 -0
  28. data/spec/plugin/all_verbs_spec.rb +29 -0
  29. data/spec/plugin/default_headers_spec.rb +63 -0
  30. data/spec/plugin/error_handler_spec.rb +67 -0
  31. data/spec/plugin/flash_spec.rb +59 -0
  32. data/spec/plugin/h_spec.rb +13 -0
  33. data/spec/plugin/halt_spec.rb +62 -0
  34. data/spec/plugin/header_matchers_spec.rb +61 -0
  35. data/spec/plugin/hooks_spec.rb +97 -0
  36. data/spec/plugin/indifferent_params_spec.rb +13 -0
  37. data/spec/plugin/middleware_spec.rb +52 -0
  38. data/spec/plugin/multi_route_spec.rb +98 -0
  39. data/spec/plugin/not_found_spec.rb +99 -0
  40. data/spec/plugin/pass_spec.rb +23 -0
  41. data/spec/plugin/render_spec.rb +148 -0
  42. data/spec/plugin/streaming_spec.rb +52 -0
  43. data/spec/plugin_spec.rb +61 -0
  44. data/spec/redirect_spec.rb +24 -0
  45. data/spec/request_spec.rb +55 -0
  46. data/spec/response_spec.rb +131 -0
  47. data/spec/session_spec.rb +35 -0
  48. data/spec/spec_helper.rb +89 -0
  49. data/spec/version_spec.rb +8 -0
  50. metadata +148 -0
@@ -0,0 +1,47 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The indifferent_params plugin adds a +params+ instance
4
+ # method which returns a copy of the request params hash
5
+ # that will automatically convert symbols to strings.
6
+ # Example:
7
+ #
8
+ # plugin :indifferent_params
9
+ #
10
+ # route do |r|
11
+ # params[:foo]
12
+ # end
13
+ #
14
+ # The params hash is initialized lazily, so you only pay
15
+ # the penalty of copying the request params if you call
16
+ # the +params+ method.
17
+ module IndifferentParams
18
+ module InstanceMethods
19
+ # A copy of the request params that will automatically
20
+ # convert symbols to strings.
21
+ def params
22
+ @_params ||= indifferent_params(request.params)
23
+ end
24
+
25
+ private
26
+
27
+ # Recursively process the request params and convert
28
+ # hashes to support indifferent access, leaving
29
+ # other values alone.
30
+ def indifferent_params(params)
31
+ case params
32
+ when Hash
33
+ h = Hash.new{|h, k| h[k.to_s] if Symbol === k}
34
+ params.each{|k, v| h[k] = indifferent_params(v)}
35
+ h
36
+ when Array
37
+ params.map{|x| indifferent_params(x)}
38
+ else
39
+ params
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ register_plugin(:indifferent_params, IndifferentParams)
46
+ end
47
+ end
@@ -0,0 +1,88 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The middleware plugin allows the Roda app to be used as
4
+ # rack middleware.
5
+ #
6
+ # In the example below, requests to /mid will return Mid
7
+ # by the Mid middleware, and requests to /app will not be
8
+ # matched by the Mid middleware, so they will be forwarded
9
+ # to App.
10
+ #
11
+ # class Mid < Roda
12
+ # plugin :middleware
13
+ #
14
+ # route do |r|
15
+ # r.is "mid" do
16
+ # "Mid"
17
+ # end
18
+ # end
19
+ # end
20
+ #
21
+ # class App < Roda
22
+ # use Mid
23
+ #
24
+ # route do |r|
25
+ # r.is "app" do
26
+ # "App"
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # run App
32
+ #
33
+ # Note that once you use the middleware plugin, you can only use the
34
+ # Roda app as middleware, and you will get errors if you attempt to
35
+ # use it as a regular app.
36
+ module Middleware
37
+ # Forward instances are what is actually used as middleware.
38
+ class Forwarder
39
+ # Store the current middleware and the next middleware to call.
40
+ def initialize(mid, app)
41
+ @mid = mid.app
42
+ @app = app
43
+ end
44
+
45
+ # When calling the middleware, first call the current middleware.
46
+ # If this returns a result, return that result directly. Otherwise,
47
+ # pass handling of the request to the next middleware.
48
+ def call(env)
49
+ res = nil
50
+
51
+ call_next = catch(:next) do
52
+ res = @mid.call(env)
53
+ false
54
+ end
55
+
56
+ if call_next
57
+ @app.call(env)
58
+ else
59
+ res
60
+ end
61
+ end
62
+ end
63
+
64
+ module ClassMethods
65
+ # If an argument is given, this is a middleware app, so create a Forwarder.
66
+ # Otherwise, this is a usual instance creation, so call super.
67
+ def new(app=nil)
68
+ if app
69
+ Forwarder.new(self, app)
70
+ else
71
+ super()
72
+ end
73
+ end
74
+
75
+ # Override the route block so that if no route matches, we throw so
76
+ # that the next middleware is called.
77
+ def route(&block)
78
+ super do |r|
79
+ instance_exec(r, &block)
80
+ throw :next, true
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ register_plugin(:middleware, Middleware)
87
+ end
88
+ end
@@ -0,0 +1,77 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The multi_route plugin allows for multiple named routes, which the
4
+ # main route block can dispatch to by name at any point. If the named
5
+ # route doesn't handle the request, execution will continue, and if the
6
+ # named route does handle the request, the response by the named route
7
+ # will be returned.
8
+ #
9
+ # Example:
10
+ #
11
+ # plugin :multi_route
12
+ #
13
+ # route(:foo) do |r|
14
+ # r.is 'bar' do
15
+ # '/foo/bar'
16
+ # end
17
+ # end
18
+ #
19
+ # route(:bar) do |r|
20
+ # r.is 'foo' do
21
+ # '/bar/foo'
22
+ # end
23
+ # end
24
+ #
25
+ # route do |r|
26
+ # r.on "foo" do
27
+ # route :foo
28
+ # end
29
+ #
30
+ # r.on "bar" do
31
+ # route :bar
32
+ # end
33
+ # end
34
+ #
35
+ # Note that in multi-threaded code, you should not attempt to add a
36
+ # named route after accepting requests.
37
+ module MultiRoute
38
+ # Initialize storage for the named routes.
39
+ def self.configure(app)
40
+ app.instance_exec{@named_routes ||= {}}
41
+ end
42
+
43
+ module ClassMethods
44
+ # Copy the named routes into the subclass when inheriting.
45
+ def inherited(subclass)
46
+ super
47
+ subclass.instance_variable_set(:@named_routes, @named_routes.dup)
48
+ end
49
+
50
+ # Return the named route with the given name.
51
+ def named_route(name)
52
+ @named_routes[name]
53
+ end
54
+
55
+ # If the given route has a named, treat it as a named route and
56
+ # store the route block. Otherwise, this is the main route, so
57
+ # call super.
58
+ def route(name=nil, &block)
59
+ if name
60
+ @named_routes[name] = block
61
+ else
62
+ super(&block)
63
+ end
64
+ end
65
+ end
66
+
67
+ module InstanceMethods
68
+ # Dispatch to the named route with the given name.
69
+ def route(name)
70
+ instance_exec(request, &self.class.named_route(name))
71
+ end
72
+ end
73
+ end
74
+
75
+ register_plugin(:multi_route, MultiRoute)
76
+ end
77
+ end
@@ -0,0 +1,62 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The not_found plugin adds a +not_found+ class method which sets
4
+ # a block that is called whenever a 404 response with an empty body
5
+ # would be returned. The usual use case for this is the desire for
6
+ # nice error pages if the page is not found.
7
+ #
8
+ # You can provide the block with the plugin call:
9
+ #
10
+ # plugin :not_found do
11
+ # "Where did it go?"
12
+ # end
13
+ #
14
+ # Or later via a separate call to +not_found+:
15
+ #
16
+ # plugin :not_found
17
+ #
18
+ # not_found do
19
+ # "Where did it go?"
20
+ # end
21
+ module NotFound
22
+ # If a block is given, install the block as the not_found handler.
23
+ def self.configure(app, &block)
24
+ if block
25
+ app.not_found(&block)
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ # Install the given block as the not_found handler.
31
+ def not_found(&block)
32
+ define_method(:not_found, &block)
33
+ private :not_found
34
+ end
35
+ end
36
+
37
+ module InstanceMethods
38
+ private
39
+
40
+ # If routing returns a 404 response with an empty body, call
41
+ # the not_found handler.
42
+ def _route
43
+ result = super
44
+
45
+ if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
46
+ super{not_found}
47
+ else
48
+ result
49
+ end
50
+ end
51
+
52
+ # Use an empty not_found_handler by default, so that loading
53
+ # the plugin without defining a not_found handler doesn't
54
+ # break things.
55
+ def not_found
56
+ end
57
+ end
58
+ end
59
+
60
+ register_plugin(:not_found, NotFound)
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The pass plugin adds a request +pass+ method to skip the current +on+
4
+ # block as if it did not match.
5
+ #
6
+ # plugin :pass
7
+ #
8
+ # route do |r|
9
+ # r.on "foo/:bar" do |bar|
10
+ # pass if bar == 'baz'
11
+ # "/foo/#{bar} (not baz)"
12
+ # end
13
+ #
14
+ # r.on "foo/baz" do
15
+ # "/foo/baz"
16
+ # end
17
+ # end
18
+ module Pass
19
+ module RequestMethods
20
+ # Handle passing inside the current block.
21
+ def on(*)
22
+ catch(:pass){super}
23
+ end
24
+
25
+ # Skip the current #on block as if it did not match.
26
+ def pass
27
+ throw :pass
28
+ end
29
+ end
30
+ end
31
+
32
+ register_plugin(:pass, Pass)
33
+ end
34
+ end
@@ -0,0 +1,217 @@
1
+ require "tilt"
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The render plugin adds support for template rendering using the tilt
6
+ # library. Two methods are provided for template rendering, +view+
7
+ # (which uses the layout) and +render+ (which does not).
8
+ #
9
+ # plugin :render
10
+ #
11
+ # route do |r|
12
+ # r.is 'foo' do
13
+ # view('foo') # renders views/foo.erb inside views/layout.erb
14
+ # end
15
+ #
16
+ # r.is 'bar' do
17
+ # render('bar') # renders views/bar.erb
18
+ # end
19
+ # end
20
+ #
21
+ # You can provide options to the plugin method, or later by modifying
22
+ # +render_opts+.
23
+ #
24
+ # plugin :render, :engine=>'haml'
25
+ #
26
+ # render_opts[:views] = 'admin_views'
27
+ #
28
+ # The following options are supported:
29
+ #
30
+ # :cache :: A specific cache to store templates in, or nil/false to not
31
+ # cache templates (useful for development), defaults to true to
32
+ # automatically use the default template cache.
33
+ # :engine :: The tilt engine to use for rendering, defaults to 'erb'.
34
+ # :ext :: The file extension to assume for view files, defaults to the :engine
35
+ # option.
36
+ # :layout :: The base name of the layout file, defaults to 'layout'.
37
+ # :layout_opts :: The options to use when rendering the layout, if different
38
+ # from the default options.
39
+ # :opts :: The tilt options used when rendering templates, defaults to
40
+ # {:outvar=>'@_out_buf'}.
41
+ # :views :: The directory holding the view files, defaults to 'views' in the
42
+ # current directory.
43
+ #
44
+ # Most of these options can be overridden at runtime by passing options
45
+ # to the +view+ or +render+ methods:
46
+ #
47
+ # view('foo', :ext=>'html.erb')
48
+ # render('foo', :views=>'admin_views')
49
+ #
50
+ # There are a couple of additional options to +view+ and +render+ that are
51
+ # available at runtime:
52
+ #
53
+ # :inline :: Use the value given as the template code, instead of looking
54
+ # for template code in a file.
55
+ # :locals :: Hash of local variables to make available inside the template.
56
+ # :path :: Use the value given as the full pathname for the file, instead
57
+ # of using the :views and :ext option in combination with the
58
+ # template name.
59
+ #
60
+ # Here's how those options are used:
61
+ #
62
+ # view(:inline=>'<%= @foo %>')
63
+ # render(:path=>'/path/to/template.erb')
64
+ #
65
+ # If you pass a hash as the first argument to +view+ or +render+, it should
66
+ # have either +:inline+ or +:path+ as one of the keys.
67
+ module Render
68
+ # Default template cache. Thread-safe so that multiple threads can
69
+ # simultaneously use the cache.
70
+ class Cache
71
+ # Mutex used to synchronize access to the cache. Uses a
72
+ # singleton mutex to reduce memory.
73
+ MUTEX = ::Mutex.new
74
+
75
+ # Initialize the cache.
76
+ def initialize
77
+ MUTEX.synchronize{@cache = {}}
78
+ end
79
+
80
+ # Clear the cache.
81
+ alias clear initialize
82
+
83
+ # If the template is found in the cache under the given key,
84
+ # return it, otherwise yield to get the template, and
85
+ # store the template under the given key
86
+ def fetch(key)
87
+ unless template = MUTEX.synchronize{@cache[key]}
88
+ template = yield
89
+ MUTEX.synchronize{@cache[key] = template}
90
+ end
91
+
92
+ template
93
+ end
94
+ end
95
+
96
+ # Setup default rendering options. See Render for details.
97
+ def self.configure(app, opts={})
98
+ if app.opts[:render]
99
+ app.opts[:render].merge!(opts)
100
+ else
101
+ app.opts[:render] = opts.dup
102
+ end
103
+
104
+ opts = app.opts[:render]
105
+ opts[:engine] ||= "erb"
106
+ opts[:ext] = nil unless opts.has_key?(:ext)
107
+ opts[:views] ||= File.expand_path("views", Dir.pwd)
108
+ opts[:layout] = "layout" unless opts.has_key?(:layout)
109
+ opts[:layout_opts] ||= (opts[:layout_opts] || {}).dup
110
+ opts[:opts] ||= (opts[:opts] || {}).dup
111
+ opts[:opts][:outvar] ||= '@_out_buf'
112
+ if RUBY_VERSION >= "1.9"
113
+ opts[:opts][:default_encoding] ||= Encoding.default_external
114
+ end
115
+ cache = opts.fetch(:cache, true)
116
+ opts[:cache] = Cache.new if cache == true
117
+ end
118
+
119
+ module ClassMethods
120
+ # Copy the rendering options into the subclass, duping
121
+ # them as necessary to prevent changes in the subclass
122
+ # affecting the parent class.
123
+ def inherited(subclass)
124
+ super
125
+ opts = subclass.opts[:render] = render_opts.dup
126
+ opts[:layout_opts] = opts[:layout_opts].dup
127
+ opts[:opts] = opts[:opts].dup
128
+ opts[:cache] = Cache.new if opts[:cache]
129
+ end
130
+
131
+ # Return the render options for this class.
132
+ def render_opts
133
+ opts[:render]
134
+ end
135
+ end
136
+
137
+ module InstanceMethods
138
+ # Render the given template. See Render for details.
139
+ def render(template, opts = {}, &block)
140
+ if template.is_a?(Hash)
141
+ if opts.empty?
142
+ opts = template
143
+ else
144
+ opts = opts.merge(template)
145
+ end
146
+ end
147
+ render_opts = render_opts()
148
+
149
+ if content = opts[:inline]
150
+ path = content
151
+ template_block = Proc.new{content}
152
+ template_class = ::Tilt[opts[:engine] || render_opts[:engine]]
153
+ else
154
+ template_class = ::Tilt
155
+ unless path = opts[:path]
156
+ path = template_path(template, opts)
157
+ end
158
+ end
159
+
160
+ cached_template(path) do
161
+ template_class.new(path, 1, render_opts[:opts].merge(opts), &template_block)
162
+ end.render(self, opts[:locals], &block)
163
+ end
164
+
165
+ # Return the render options for the instance's class.
166
+ def render_opts
167
+ self.class.render_opts
168
+ end
169
+
170
+ # Render the given template. If there is a default layout
171
+ # for the class, take the result of the template rendering
172
+ # and render it inside the layout. See Render for details.
173
+ def view(template, opts={})
174
+ if template.is_a?(Hash)
175
+ if opts.empty?
176
+ opts = template
177
+ else
178
+ opts = opts.merge(template)
179
+ end
180
+ end
181
+
182
+ content = render(template, opts)
183
+
184
+ if layout = opts.fetch(:layout, render_opts[:layout])
185
+ if layout_opts = opts[:layout_opts]
186
+ layout_opts = render_opts[:layout_opts].merge(layout_opts)
187
+ end
188
+
189
+ content = render(layout, layout_opts||{}){content}
190
+ end
191
+
192
+ content
193
+ end
194
+
195
+ private
196
+
197
+ # If caching templates, attempt to retrieve the template from the cache. Otherwise, just yield
198
+ # to get the template.
199
+ def cached_template(path, &block)
200
+ if cache = render_opts[:cache]
201
+ cache.fetch(path, &block)
202
+ else
203
+ yield
204
+ end
205
+ end
206
+
207
+ # The path for the given template.
208
+ def template_path(template, opts)
209
+ render_opts = render_opts()
210
+ "#{opts[:views] || render_opts[:views]}/#{template}.#{opts[:ext] || render_opts[:ext] || render_opts[:engine]}"
211
+ end
212
+ end
213
+ end
214
+
215
+ register_plugin(:render, Render)
216
+ end
217
+ end