roda 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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