roda 1.1.0 → 1.2.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 (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