roda 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +70 -0
  3. data/README.rdoc +261 -302
  4. data/Rakefile +1 -1
  5. data/doc/release_notes/1.2.0.txt +406 -0
  6. data/lib/roda.rb +206 -124
  7. data/lib/roda/plugins/all_verbs.rb +11 -10
  8. data/lib/roda/plugins/assets.rb +5 -5
  9. data/lib/roda/plugins/backtracking_array.rb +12 -5
  10. data/lib/roda/plugins/caching.rb +10 -8
  11. data/lib/roda/plugins/class_level_routing.rb +94 -0
  12. data/lib/roda/plugins/content_for.rb +6 -0
  13. data/lib/roda/plugins/default_headers.rb +4 -11
  14. data/lib/roda/plugins/delay_build.rb +42 -0
  15. data/lib/roda/plugins/delegate.rb +64 -0
  16. data/lib/roda/plugins/drop_body.rb +33 -0
  17. data/lib/roda/plugins/empty_root.rb +48 -0
  18. data/lib/roda/plugins/environments.rb +68 -0
  19. data/lib/roda/plugins/error_email.rb +1 -2
  20. data/lib/roda/plugins/error_handler.rb +1 -1
  21. data/lib/roda/plugins/halt.rb +7 -5
  22. data/lib/roda/plugins/head.rb +4 -2
  23. data/lib/roda/plugins/header_matchers.rb +17 -9
  24. data/lib/roda/plugins/hooks.rb +16 -32
  25. data/lib/roda/plugins/json.rb +4 -10
  26. data/lib/roda/plugins/mailer.rb +233 -0
  27. data/lib/roda/plugins/match_affix.rb +48 -0
  28. data/lib/roda/plugins/multi_route.rb +9 -11
  29. data/lib/roda/plugins/multi_run.rb +81 -0
  30. data/lib/roda/plugins/named_templates.rb +93 -0
  31. data/lib/roda/plugins/not_allowed.rb +43 -48
  32. data/lib/roda/plugins/path.rb +63 -2
  33. data/lib/roda/plugins/render.rb +79 -48
  34. data/lib/roda/plugins/render_each.rb +6 -0
  35. data/lib/roda/plugins/sinatra_helpers.rb +523 -0
  36. data/lib/roda/plugins/slash_path_empty.rb +25 -0
  37. data/lib/roda/plugins/static_path_info.rb +64 -0
  38. data/lib/roda/plugins/streaming.rb +1 -1
  39. data/lib/roda/plugins/view_subdirs.rb +12 -8
  40. data/lib/roda/version.rb +1 -1
  41. data/spec/integration_spec.rb +33 -0
  42. data/spec/plugin/backtracking_array_spec.rb +24 -18
  43. data/spec/plugin/class_level_routing_spec.rb +138 -0
  44. data/spec/plugin/delay_build_spec.rb +23 -0
  45. data/spec/plugin/delegate_spec.rb +20 -0
  46. data/spec/plugin/drop_body_spec.rb +20 -0
  47. data/spec/plugin/empty_root_spec.rb +14 -0
  48. data/spec/plugin/environments_spec.rb +31 -0
  49. data/spec/plugin/h_spec.rb +1 -3
  50. data/spec/plugin/header_matchers_spec.rb +14 -0
  51. data/spec/plugin/hooks_spec.rb +3 -5
  52. data/spec/plugin/mailer_spec.rb +191 -0
  53. data/spec/plugin/match_affix_spec.rb +22 -0
  54. data/spec/plugin/multi_run_spec.rb +31 -0
  55. data/spec/plugin/named_templates_spec.rb +65 -0
  56. data/spec/plugin/path_spec.rb +66 -2
  57. data/spec/plugin/render_spec.rb +46 -1
  58. data/spec/plugin/sinatra_helpers_spec.rb +534 -0
  59. data/spec/plugin/slash_path_empty_spec.rb +22 -0
  60. data/spec/plugin/static_path_info_spec.rb +50 -0
  61. data/spec/request_spec.rb +23 -0
  62. data/spec/response_spec.rb +12 -1
  63. metadata +48 -6
@@ -6,10 +6,9 @@ class Roda
6
6
  # trace, unlink.
7
7
  #
8
8
  # These methods operate just like Roda's default get and post
9
- # methods other that the http verb used, so using them without
10
- # any arguments just checks for the request method, while
11
- # using them with any arguments also checks that the arguments
12
- # match the full path.
9
+ # methods, so using them without any arguments just checks for
10
+ # the request method, while using them with any arguments also
11
+ # checks that the arguments match the full path.
13
12
  #
14
13
  # Example:
15
14
  #
@@ -30,12 +29,14 @@ class Roda
30
29
  # The verb methods are defined via metaprogramming, so there
31
30
  # isn't documentation for the individual methods created.
32
31
  module AllVerbs
33
- def self.configure(app)
34
- %w'delete head options link patch put trace unlink'.each do |v|
35
- if ::Rack::Request.method_defined?("#{v}?")
36
- app.request_module do
37
- app::RodaRequest.def_verb_method(self, v)
38
- end
32
+ module RequestMethods
33
+ %w'delete head options link patch put trace unlink'.each do |verb|
34
+ if ::Rack::Request.method_defined?("#{verb}?")
35
+ class_eval(<<-END, __FILE__, __LINE__+1)
36
+ def #{verb}(*args, &block)
37
+ _verb(args, &block) if #{verb}?
38
+ end
39
+ END
39
40
  end
40
41
  end
41
42
  end
@@ -197,7 +197,7 @@ class Roda
197
197
  # and compressing files (default: false)
198
198
  # :css_dir :: Directory name containing your css source, inside :path (default: 'css')
199
199
  # :css_headers :: A hash of additional headers for your rendered css files
200
- # :css_opts :: Options to pass to the render plugin when rendering css assets
200
+ # :css_opts :: Template options to pass to the render plugin (via :opts) when rendering css assets
201
201
  # :css_route :: Route under :prefix for css assets (default: :css_dir)
202
202
  # :dependencies :: A hash of dependencies for your asset files. Keys should be paths to asset files,
203
203
  # values should be arrays of paths your asset files depends on. This is used to
@@ -207,7 +207,7 @@ class Roda
207
207
  # :headers :: A hash of additional headers for both js and css rendered files
208
208
  # :js_dir :: Directory name containing your javascript source, inside :path (default: 'js')
209
209
  # :js_headers :: A hash of additional headers for your rendered javascript files
210
- # :js_opts :: Options to pass to the render plugin when rendering javascript assets
210
+ # :js_opts :: Template options to pass to the render plugin (via :opts) when rendering javascript assets
211
211
  # :js_route :: Route under :prefix for javascript assets (default: :js_dir)
212
212
  # :path :: Path to your asset source directory (default: 'assets')
213
213
  # :prefix :: Prefix for assets path in your URL/routes (default: 'assets')
@@ -520,7 +520,7 @@ class Roda
520
520
  if file.end_with?(".#{type}")
521
521
  ::File.read(file)
522
522
  else
523
- render_asset_file(file, self.class.assets_opts[:"#{type}_opts"])
523
+ render_asset_file(file, :opts=>self.class.assets_opts[:"#{type}_opts"])
524
524
  end
525
525
  end
526
526
 
@@ -541,8 +541,8 @@ class Roda
541
541
  # a 304 response immediately. Otherwise, add the appropriate
542
542
  # type-specific headers.
543
543
  def check_asset_request(file, type, mtime)
544
- request.last_modified(mtime)
545
- response.headers.merge!(self.class.assets_opts[:"#{type}_headers"])
544
+ @_request.last_modified(mtime)
545
+ @_response.headers.merge!(self.class.assets_opts[:"#{type}_headers"])
546
546
  end
547
547
 
548
548
  # Render the given asset file using the render plugin, with the given options.
@@ -35,10 +35,13 @@ class Roda
35
35
  # entry in the array.
36
36
  def _match_array(arg, rest=nil)
37
37
  return super unless rest
38
- env = @env
39
38
 
40
- script = env[SCRIPT_NAME]
41
- path = env[PATH_INFO]
39
+ unless path = @remaining_path
40
+ e = @env
41
+ script = e[SCRIPT_NAME]
42
+ path = e[PATH_INFO]
43
+ end
44
+ captures = @captures
42
45
  caps = captures.dup
43
46
  arg.each do |v|
44
47
  if match(v, rest)
@@ -52,8 +55,12 @@ class Roda
52
55
 
53
56
  # Matching all remaining elements failed, reset state
54
57
  captures.replace(caps)
55
- env[SCRIPT_NAME] = script
56
- env[PATH_INFO] = path
58
+ if @remaining_path
59
+ @remaining_path = path
60
+ else
61
+ e[SCRIPT_NAME] = script
62
+ e[PATH_INFO] = path
63
+ end
57
64
  end
58
65
  end
59
66
  false
@@ -87,18 +87,19 @@ class Roda
87
87
  # with a 412 status.
88
88
  def last_modified(time)
89
89
  return unless time
90
- response[LAST_MODIFIED] = time.httpdate
90
+ res = response
91
91
  e = env
92
+ res[LAST_MODIFIED] = time.httpdate
92
93
  return if e[HTTP_IF_NONE_MATCH]
93
- status = response.status
94
+ status = res.status
94
95
 
95
96
  if (!status || status == 200) && (ims = time_from_header(e[HTTP_IF_MODIFIED_SINCE])) && ims >= time.to_i
96
- response.status = 304
97
+ res.status = 304
97
98
  halt
98
99
  end
99
100
 
100
101
  if (!status || (status >= 200 && status < 300) || status == 412) && (ius = time_from_header(e[HTTP_IF_UNMODIFIED_SINCE])) && ius < time.to_i
101
- response.status = 412
102
+ res.status = 412
102
103
  halt
103
104
  end
104
105
  end
@@ -122,19 +123,20 @@ class Roda
122
123
  weak = opts[:weak]
123
124
  new_resource = opts.fetch(:new_resource){post?}
124
125
 
125
- response[ETAG] = etag = "#{'W/' if weak}\"#{value}\""
126
- status = response.status
126
+ res = response
127
127
  e = env
128
+ res[ETAG] = etag = "#{'W/' if weak}\"#{value}\""
129
+ status = res.status
128
130
 
129
131
  if (!status || (status >= 200 && status < 300) || status == 304)
130
132
  if etag_matches?(e[HTTP_IF_NONE_MATCH], etag, new_resource)
131
- response.status = (request_method =~ /\AGET|HEAD|OPTIONS|TRACE\z/i ? 304 : 412)
133
+ res.status = (request_method =~ /\AGET|HEAD|OPTIONS|TRACE\z/i ? 304 : 412)
132
134
  halt
133
135
  end
134
136
 
135
137
  if ifm = e[HTTP_IF_MATCH]
136
138
  unless etag_matches?(ifm, etag, new_resource)
137
- response.status = 412
139
+ res.status = 412
138
140
  halt
139
141
  end
140
142
  end
@@ -0,0 +1,94 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The class_level_routing plugin adds routing methods at the class level, which can
4
+ # be used instead of or in addition to using the normal +route+ method to start the
5
+ # routing tree. If a request is not matched by the normal routing tree, the class
6
+ # level routes will be tried. This offers a more Sinatra-like API, while
7
+ # still allowing you to use a routing tree inside individual actions.
8
+ #
9
+ # Here's the first example from the README, modified to use the class_level_routing
10
+ # plugin:
11
+ #
12
+ # class App < Roda
13
+ # plugin :class_level_routing
14
+ #
15
+ # # GET / request
16
+ # root do
17
+ # request.redirect "/hello"
18
+ # end
19
+ #
20
+ # # GET /hello/world request
21
+ # get "hello/world" do
22
+ # "Hello world!"
23
+ # end
24
+ #
25
+ # # /hello request
26
+ # is "hello" do
27
+ # # GET /hello request
28
+ # request.get do
29
+ # "Hello!"
30
+ # end
31
+ #
32
+ # # POST /hello request
33
+ # request.post do
34
+ # puts "Someone said hello!"
35
+ # request.redirect
36
+ # end
37
+ # end
38
+ # end
39
+ #
40
+ # When using the the class_level_routing plugin with nested routes, you may also want to use the
41
+ # delegate plugin to delegate certain instance methods to the request object, so you don't have
42
+ # to continually use +request.+ in your routing blocks.
43
+ #
44
+ # Note that class level routing is implemented via a simple array of routes, so routing performance
45
+ # will degrade linearly as the number of routes increases. For best performance, you should use
46
+ # the normal +route+ class method to define your routing tree. This plugin does make it simpler to
47
+ # add additional routes after the routing tree has already been defined, though.
48
+ module ClassLevelRouting
49
+ # Initialize the class_routes array when the plugin is loaded. Also, if the application doesn't
50
+ # currently have a routing block, setup an empty routing block so that things will still work if
51
+ # a routing block isn't added.
52
+ def self.configure(app)
53
+ app.route{} unless app.route_block
54
+ app.opts[:class_level_routes] ||= []
55
+ end
56
+
57
+ module ClassMethods
58
+ # Define routing methods that will store class level routes.
59
+ [:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
60
+ define_method(meth) do |*args, &block|
61
+ opts[:class_level_routes] << [meth, args, block]
62
+ end
63
+ end
64
+ end
65
+
66
+ module InstanceMethods
67
+ private
68
+
69
+ # If the normal routing tree doesn't handle an action, try each class level route
70
+ # to see if it matches.
71
+ def _route(&block)
72
+ result = super
73
+
74
+ if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
75
+ # Reset the response so it doesn't inherit the status or any headers from
76
+ # the original response.
77
+ @_response = self.class::RodaResponse.new
78
+ super do |r|
79
+ opts[:class_level_routes].each do |meth, args, blk|
80
+ r.send(meth, *args) do |*a|
81
+ instance_exec(*a, &blk)
82
+ end
83
+ end
84
+ end
85
+ else
86
+ result
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ register_plugin(:class_level_routing, ClassLevelRouting)
93
+ end
94
+ end
@@ -21,6 +21,12 @@ class Roda
21
21
  #
22
22
  # <%= content_for :foo %>
23
23
  module ContentFor
24
+ # Depend on the render plugin, since this plugin only makes
25
+ # sense when the render plugin is used.
26
+ def self.load_dependencies(app)
27
+ app.plugin :render
28
+ end
29
+
24
30
  module InstanceMethods
25
31
  # If called with a block, store content enclosed by block
26
32
  # under the given key. If called without a block, retrieve
@@ -20,27 +20,20 @@ class Roda
20
20
  module DefaultHeaders
21
21
  # Merge the given headers into the existing default headers, if any.
22
22
  def self.configure(app, headers={})
23
- app.instance_eval do
24
- @default_headers ||= {}
25
- @default_headers.merge!(headers)
26
- end
23
+ (app.opts[:default_headers] ||= {}).merge!(headers)
27
24
  end
28
25
 
29
26
  module ClassMethods
30
27
  # The default response headers to use for the current class.
31
- attr_reader :default_headers
32
-
33
- # Copy the default headers into the subclass when inheriting.
34
- def inherited(subclass)
35
- super
36
- subclass.instance_variable_set(:@default_headers, default_headers.dup)
28
+ def default_headers
29
+ opts[:default_headers]
37
30
  end
38
31
  end
39
32
 
40
33
  module ResponseMethods
41
34
  # Get the default headers from the related roda class.
42
35
  def default_headers
43
- self.class.roda_class.default_headers.dup
36
+ roda_class.default_headers.dup
44
37
  end
45
38
  end
46
39
  end
@@ -0,0 +1,42 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The delay_build plugin does not build the rack app until
4
+ # Roda.app is called, and only rebuilds the rack app if Roda.build!
5
+ # is called. This differs from Roda's default behavior, which
6
+ # rebuilds the rack app every time the route block changes and
7
+ # every time middleware is added if a route block has already
8
+ # been defined.
9
+ #
10
+ # If you are loading hundreds of middleware after a
11
+ # route block has already been defined, this can fix a possible
12
+ # performance issue, turning an O(n^2) calculation into an
13
+ # O(n) calculation, where n is the number of middleware used.
14
+ module DelayBuild
15
+ module ClassMethods
16
+ # If the app is not been defined yet, build the app.
17
+ def app
18
+ @app || build!
19
+ end
20
+
21
+ # Rebuild the application.
22
+ def build!
23
+ @build_app = true
24
+ build_rack_app
25
+ @app
26
+ ensure
27
+ @build_app = false
28
+ end
29
+
30
+ private
31
+
32
+ # Do not build the rack app automatically, wait for an
33
+ # explicit call to build!.
34
+ def build_rack_app
35
+ super if @build_app
36
+ end
37
+ end
38
+ end
39
+
40
+ register_plugin(:delay_build, DelayBuild)
41
+ end
42
+ end
@@ -0,0 +1,64 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The delegate plugin allows you to easily setup instance methods in
4
+ # the scope of the route block that call methods on the related
5
+ # request or response, which may offer a simpler API in some cases.
6
+ # Roda doesn't automatically setup such delegate methods because
7
+ # it pollutes the application's method namespace, but this plugin
8
+ # allows the user to do so.
9
+ #
10
+ # Here's an example based on the README's initial example, using the
11
+ # request_delegate method to simplify the DSL:
12
+ #
13
+ # plugin :delegate
14
+ # request_delegate :root, :on, :is, :get, :post, :redirect
15
+ #
16
+ # route do |r|
17
+ # # GET / request
18
+ # root do
19
+ # redirect "/hello"
20
+ # end
21
+ #
22
+ # # /hello branch
23
+ # on "hello" do
24
+ # # GET /hello/world request
25
+ # get "world" do
26
+ # "Hello world!"
27
+ # end
28
+ #
29
+ # # /hello request
30
+ # is do
31
+ # # GET /hello request
32
+ # get do
33
+ # "Hello!"
34
+ # end
35
+ #
36
+ # # POST /hello request
37
+ # post do
38
+ # puts "Someone said hello!"
39
+ # redirect
40
+ # end
41
+ # end
42
+ # end
43
+ # end
44
+ module Delegate
45
+ module ClassMethods
46
+ # Delegate the given methods to the request
47
+ def request_delegate(*meths)
48
+ meths.each do |meth|
49
+ define_method(meth){|*a, &block| @_request.send(meth, *a, &block)}
50
+ end
51
+ end
52
+
53
+ # Delegate the given methods to the response
54
+ def response_delegate(*meths)
55
+ meths.each do |meth|
56
+ define_method(meth){|*a, &block| @_response.send(meth, *a, &block)}
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ register_plugin(:delegate, Delegate)
63
+ end
64
+ end
@@ -0,0 +1,33 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The drop_body plugin automatically drops the body and
4
+ # Content-Type/Content-Length headers from the response if
5
+ # the response status indicates that the response should
6
+ # not include a body (response statuses 100, 101, 102, 204, 205,
7
+ # and 304).
8
+ module DropBody
9
+ module ResponseMethods
10
+ DROP_BODY_STATUSES = [100, 101, 102, 204, 205, 304].freeze
11
+ EMPTY_BODY = [].freeze
12
+ CONTENT_LENGTH = "Content-Length".freeze
13
+ CONTENT_TYPE = "Content-Type".freeze
14
+
15
+ # If the response status indicates a body should not be
16
+ # returned, use an empty body and remove the Content-Length
17
+ # and Content-Type headers.
18
+ def finish
19
+ r = super
20
+ if DROP_BODY_STATUSES.include?(r[0])
21
+ r[2] = EMPTY_BODY
22
+ h = r[1]
23
+ h.delete(CONTENT_LENGTH)
24
+ h.delete(CONTENT_TYPE)
25
+ end
26
+ r
27
+ end
28
+ end
29
+ end
30
+
31
+ register_plugin(:drop_body, DropBody)
32
+ end
33
+ end
@@ -0,0 +1,48 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The empty_root plugin makes +r.root+ match both on +/+ and
4
+ # on the empty string. This is mostly useful when using multiple
5
+ # rack applications, where the initial PATH_INFO has been moved
6
+ # to SCRIPT_NAME. For example, if you have the following
7
+ # applications:
8
+ #
9
+ # class App1 < Roda
10
+ # on "albums" do
11
+ # run App2
12
+ # end
13
+ # end
14
+ #
15
+ # class App2 < Roda
16
+ # plugin :empty_root
17
+ #
18
+ # route do |r|
19
+ # r.root do
20
+ # "root"
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # Then requests for both +/albums/+ and +/albums+ will return
26
+ # "root". Without this plugin loaded into App2, only requests
27
+ # for +/albums/+ will return "root", since by default, +r.root+
28
+ # matches only when the current PATH_INFO is +/+ and not when
29
+ # it is empty.
30
+ module EmptyRoot
31
+ EMPTY_STRING = ''.freeze
32
+
33
+ module RequestMethods
34
+ # Match when the remaining path is the empty string,
35
+ # in addition to the default behavior of matching when
36
+ # the remaining path is +/+.
37
+ def root(&block)
38
+ super
39
+ if remaining_path == EMPTY_STRING && is_get?
40
+ always(&block)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ register_plugin(:empty_root, EmptyRoot)
47
+ end
48
+ end