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
@@ -0,0 +1,48 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The match_affix plugin allows changing the default prefix and suffix used for
4
+ # match patterns. Roda's default behavior for a match pattern like <tt>"albums"</tt>
5
+ # is to use the pattern <tt>/\A\/(?:albums)(?=\/|\z)/</tt>. This prefixes the pattern
6
+ # with +/+ and suffixes it with <tt>(?=\/|\z)</tt>. With the match_affix plugin, you
7
+ # can change the prefix and suffix to use. So if you want to be explicit and require
8
+ # a leading +/+ in patterns, you can set the prefix to <tt>""</tt>. If you want to
9
+ # consume a trailing slash instead of leaving it, you can set the suffix to <tt>(\/|\z)</tt>.
10
+ #
11
+ # You set the prefix and suffix to use by passing arguments when loading the plugin:
12
+ #
13
+ # plugin :match_affix, ""
14
+ #
15
+ # will load the plugin and use an empty prefix.
16
+ #
17
+ # plugin :match_affix, "", /(\/|\z)/
18
+ #
19
+ # will use an empty prefix and change the suffix to consume a trailing slash.
20
+ #
21
+ # plugin :match_affix, nil, /(\/|\z)/
22
+ #
23
+ # will not modify the prefix and will change the suffix to consume a trailing slash.
24
+ module MatchAffix
25
+ PREFIX = "/".freeze
26
+ SUFFIX = "(?=\/|\z)".freeze
27
+
28
+ # Set the default prefix and suffix to use in match patterns, if a non-nil value
29
+ # is given.
30
+ def self.configure(app, prefix, suffix=nil)
31
+ app.opts[:match_prefix] = prefix if prefix
32
+ app.opts[:match_suffix] = suffix if suffix
33
+ end
34
+
35
+ module RequestClassMethods
36
+ private
37
+
38
+ # Use the match prefix and suffix provided when loading the plugin, or fallback
39
+ # to Roda's default prefix/suffix if one was not provided.
40
+ def consume_pattern(pattern)
41
+ /\A#{roda_class.opts[:match_prefix] || PREFIX}(?:#{pattern})#{roda_class.opts[:match_suffix] || SUFFIX}/
42
+ end
43
+ end
44
+ end
45
+
46
+ register_plugin(:match_affix, MatchAffix)
47
+ end
48
+ end
@@ -125,29 +125,27 @@ class Roda
125
125
  module MultiRoute
126
126
  # Initialize storage for the named routes.
127
127
  def self.configure(app)
128
- app.instance_exec do
129
- @namespaced_routes ||= {}
130
- app::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
131
- end
128
+ app.opts[:namespaced_routes] ||= {}
129
+ app::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
132
130
  end
133
131
 
134
132
  module ClassMethods
135
133
  # Copy the named routes into the subclass when inheriting.
136
134
  def inherited(subclass)
137
135
  super
138
- nsr = subclass.instance_variable_set(:@namespaced_routes, {})
139
- @namespaced_routes.each{|k, v| nsr[k] = v.dup}
136
+ nsr = subclass.opts[:namespaced_routes]
137
+ opts[:namespaced_routes].each{|k, v| nsr[k] = v.dup}
140
138
  subclass::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
141
139
  end
142
140
 
143
141
  # The names for the currently stored named routes
144
142
  def named_routes(namespace=nil)
145
- @namespaced_routes[namespace].keys
143
+ opts[:namespaced_routes][namespace].keys
146
144
  end
147
145
 
148
146
  # Return the named route with the given name.
149
147
  def named_route(name, namespace=nil)
150
- @namespaced_routes[namespace][name]
148
+ opts[:namespaced_routes][namespace][name]
151
149
  end
152
150
 
153
151
  # If the given route has a name, treat it as a named route and
@@ -155,8 +153,8 @@ class Roda
155
153
  # call super.
156
154
  def route(name=nil, namespace=nil, &block)
157
155
  if name
158
- @namespaced_routes[namespace] ||= {}
159
- @namespaced_routes[namespace][name] = block
156
+ opts[:namespaced_routes][namespace] ||= {}
157
+ opts[:namespaced_routes][namespace][name] = block
160
158
  self::RodaRequest.clear_named_route_regexp!(namespace)
161
159
  else
162
160
  super(&block)
@@ -199,7 +197,7 @@ class Roda
199
197
 
200
198
  # Dispatch to the named route with the given name.
201
199
  def route(name, namespace=nil)
202
- scope.instance_exec(self, &self.class.roda_class.named_route(name, namespace))
200
+ scope.instance_exec(self, &roda_class.named_route(name, namespace))
203
201
  end
204
202
  end
205
203
  end
@@ -0,0 +1,81 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The multi_run plugin provides the ability to easily dispatch to other
4
+ # rack applications based on the request path prefix.
5
+ # First, load the plugin:
6
+ #
7
+ # class App < Roda
8
+ # plugin :multi_run
9
+ # end
10
+ #
11
+ # Then, other rack applications can register with the multi_run plugin:
12
+ #
13
+ # App.run "ra", PlainRackApp
14
+ # App.run "ro", OtherRodaApp
15
+ # App.run "si", SinatraApp
16
+ #
17
+ # Inside your route block, you can call r.multi_run to dispatch to all
18
+ # three rack applications based on the prefix:
19
+ #
20
+ # App.route do |r|
21
+ # r.multi_run
22
+ # end
23
+ #
24
+ # This will dispatch routes starting with +/ra+ to +PlainRackApp+, routes
25
+ # starting with +/ro+ to +OtherRodaApp+, and routes starting with +/si+ to
26
+ # SinatraApp.
27
+ #
28
+ # The multi_run plugin is similar to the multi_route plugin, with the difference
29
+ # being the multi_route plugin keeps all routing subtrees in the same Roda app/class,
30
+ # while multi_run dispatches to other rack apps. If you want to isolate your routing
31
+ # subtrees, multi_run is a better approach, but it does not let you set instance
32
+ # variables in the main Roda app and have those instance variables usable in
33
+ # the routing subtrees.
34
+ module MultiRun
35
+ # Initialize the storage for the dispatched applications
36
+ def self.configure(app)
37
+ app.instance_eval do
38
+ @multi_run_apps ||= {}
39
+ end
40
+ end
41
+
42
+ module ClassMethods
43
+ # Hash storing rack applications to dispatch to, keyed by the prefix
44
+ # for the application.
45
+ attr_reader :multi_run_apps
46
+
47
+ # Add a rack application to dispatch to for the given prefix when
48
+ # r.multi_run is called.
49
+ def run(prefix, app)
50
+ @multi_run_apps[prefix.to_s] = app
51
+ self::RodaRequest.refresh_multi_run_regexp!
52
+ end
53
+ end
54
+
55
+ module RequestClassMethods
56
+ # Refresh the multi_run_regexp, using the stored route prefixes,
57
+ # preferring longer routes before shorter routes.
58
+ def refresh_multi_run_regexp!
59
+ @multi_run_regexp = /(#{Regexp.union(roda_class.multi_run_apps.keys.sort.reverse)})/
60
+ end
61
+
62
+ # Refresh the multi_run_regexp if it hasn't been loaded yet.
63
+ def multi_run_regexp
64
+ @multi_run_regexp || refresh_multi_run_regexp!
65
+ end
66
+ end
67
+
68
+ module RequestMethods
69
+ # If one of the stored route prefixes match the current request,
70
+ # dispatch the request to the stored rack application.
71
+ def multi_run
72
+ on self.class.multi_run_regexp do |prefix|
73
+ run scope.class.multi_run_apps[prefix]
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ register_plugin(:multi_run, MultiRun)
80
+ end
81
+ end
@@ -0,0 +1,93 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The named_templates plugin allows you to specify templates by name,
4
+ # providing the template code to use for a given name:
5
+ #
6
+ # plugin :named_templates
7
+ #
8
+ # template :layout do
9
+ # "<html><body><%= yield %></body></html>"
10
+ # end
11
+ # template :index do
12
+ # "<p>Hello <%= @user %>!</p>"
13
+ # end
14
+ #
15
+ # route do |r|
16
+ # @user = 'You'
17
+ # render(:index)
18
+ # end
19
+ # # => "<html><body><p>Hello You!</p></body></html>"
20
+ #
21
+ # You can provide options for the template, for example to change the
22
+ # engine that the template uses:
23
+ #
24
+ # template :index, :engine=>:str do
25
+ # "<p>Hello #{@user}!</p>"
26
+ # end
27
+ #
28
+ # The block you use is reevaluted on every call, allowing you to easily
29
+ # include additional setup logic:
30
+ #
31
+ # template :index do
32
+ # greeting = ['hello', 'hi', 'howdy'].sample
33
+ # @user.downcase!
34
+ # "<p>#{greating} <%= @user %>!</p>"
35
+ # end
36
+ #
37
+ # This plugin also works with the view_subdirs plugin, as long as you
38
+ # prefix the template name with the view subdirectory:
39
+ #
40
+ # template "main/index" do
41
+ # "<html><body><%= yield %></body></html>"
42
+ # end
43
+ #
44
+ # route do |r|
45
+ # set_view_subdir("main")
46
+ # @user = 'You'
47
+ # render(:index)
48
+ # end
49
+ module NamedTemplates
50
+ # Depend on the render plugin
51
+ def self.load_dependencies(app)
52
+ app.plugin :render
53
+ end
54
+
55
+ # Initialize the storage for named templates.
56
+ def self.configure(app)
57
+ app.opts[:named_templates] ||= {}
58
+ end
59
+
60
+ module ClassMethods
61
+ # Store a new template block and options for the given template name.
62
+ def template(name, options=nil, &block)
63
+ opts[:named_templates][name.to_s] = [options, block]
64
+ nil
65
+ end
66
+ end
67
+
68
+ module InstanceMethods
69
+ private
70
+
71
+ # If a template name is given and it matches a named template, call
72
+ # the named template block to get the inline template to use.
73
+ def find_template(options)
74
+ if options[:template] && (template_opts, block = opts[:named_templates][template_name(options)]; block)
75
+ if template_opts
76
+ options = template_opts.merge(options)
77
+ else
78
+ options = options.dup
79
+ end
80
+
81
+ options[:inline] = instance_exec(&block)
82
+
83
+ super(options)
84
+ else
85
+ super
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ register_plugin(:named_templates, NamedTemplates)
92
+ end
93
+ end
@@ -59,48 +59,15 @@ class Roda
59
59
  #
60
60
  # In all cases where it uses a 405 response, it also sets the +Allow+
61
61
  # header in the response to contain the request methods supported.
62
- #
63
- # To make this affect the verb methods added by the all_verbs plugin,
64
- # load this plugin first.
62
+ #
63
+ # This plugin depends on the all_verbs plugin. It works by overriding
64
+ # the verb methods, so it wouldn't work if loaded after the all_verbs
65
+ # plugin.
65
66
  module NotAllowed
66
- # Redefine the +r.get+ and +r.post+ methods when loading the plugin.
67
- def self.configure(app)
68
- app.request_module do
69
- app::RodaRequest.def_verb_method(self, :get)
70
- app::RodaRequest.def_verb_method(self, :post)
71
- end
72
- end
73
-
74
- module RequestClassMethods
75
- # Define a method named +verb+ in the given module which will
76
- # return a 405 response if the method is called with any
77
- # arguments and the arguments terminally match but the
78
- # request method does not.
79
- #
80
- # If called without any arguments, check to see if the call
81
- # is inside a terminal match, and in that case record the
82
- # request method used.
83
- def def_verb_method(mod, verb)
84
- mod.class_eval(<<-END, __FILE__, __LINE__+1)
85
- def #{verb}(*args, &block)
86
- if args.empty?
87
- @_is_verbs << "#{verb.to_s.upcase}" if defined?(@_is_verbs)
88
- always(&block) if #{verb == :get ? :is_get : verb}?
89
- else
90
- args << ::Roda::RodaPlugins::Base::RequestMethods::TERM
91
- if_match(args) do |*a|
92
- if #{verb == :get ? :is_get : verb}?
93
- block_result(yield(*a))
94
- throw :halt, response.finish
95
- end
96
- response.status = 405
97
- response['Allow'] = '#{verb.to_s.upcase}'
98
- nil
99
- end
100
- end
101
- end
102
- END
103
- end
67
+ # Depend on the all_verbs plugin, as this plugin overrides methods
68
+ # defined by it and calls super.
69
+ def self.load_dependencies(app)
70
+ app.plugin :all_verbs
104
71
  end
105
72
 
106
73
  module RequestMethods
@@ -110,20 +77,19 @@ class Roda
110
77
  # since there was already a successful terminal match, the
111
78
  # request method must not be allowed, so return a 405
112
79
  # response in that case.
113
- def is(*verbs)
114
- super(*verbs) do
80
+ def is(*args)
81
+ super(*args) do
115
82
  begin
116
- @_is_verbs = []
83
+ is_verbs = @_is_verbs = []
117
84
 
118
- ret = if verbs.empty?
85
+ ret = if args.empty?
119
86
  yield
120
87
  else
121
88
  yield(*captures)
122
89
  end
123
90
 
124
- unless @_is_verbs.empty?
125
- response.status = 405
126
- response['Allow'] = @_is_verbs.join(', ')
91
+ unless is_verbs.empty?
92
+ method_not_allowed(is_verbs.join(', '))
127
93
  end
128
94
 
129
95
  ret
@@ -132,6 +98,35 @@ class Roda
132
98
  end
133
99
  end
134
100
  end
101
+
102
+ # Setup methods for all verbs. If inside an is block and not given
103
+ # arguments, record the verb used. If given an argument, add an is
104
+ # check with the argu
105
+ %w'get post delete head options link patch put trace unlink'.each do |verb|
106
+ if ::Rack::Request.method_defined?("#{verb}?")
107
+ class_eval(<<-END, __FILE__, __LINE__+1)
108
+ def #{verb}(*args, &block)
109
+ if (empty = args.empty?) && @_is_verbs
110
+ @_is_verbs << "#{verb.to_s.upcase}"
111
+ end
112
+ super
113
+ unless empty
114
+ is(*args){method_not_allowed("#{verb.to_s.upcase}")}
115
+ end
116
+ end
117
+ END
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ # Set the response status to 405 (Method Not Allowed), and set the Allow header
124
+ # to the given string of allowed request methods.
125
+ def method_not_allowed(verbs)
126
+ res = response
127
+ res.status = 405
128
+ res['Allow'] = verbs
129
+ end
135
130
  end
136
131
  end
137
132
 
@@ -17,9 +17,31 @@ class Roda
17
17
  # r.redirect bar_path(bar)
18
18
  # end
19
19
  # end
20
+ #
21
+ # The path method accepts the following options:
22
+ #
23
+ # :add_script_name :: Prefix the path generated with SCRIPT_NAME.
24
+ # :name :: Provide a different name for the method, instead of using <tt>*_path</tt>.
25
+ # :url :: Create a url method in addition to the path method, which will prefix the string generated
26
+ # with the appropriate scheme, host, and port. If true, creates a <tt>*_url</tt>
27
+ # method. If a Symbol or String, uses the value as the url method name.
28
+ # :url_only :: Do not create a path method, just a url method.
29
+ #
30
+ # Note that if :add_script_name, :url, or :url_only is used, this will also create a <tt>_*_path</tt>
31
+ # method. This is necessary in order to support path methods that accept blocks, as you can't pass
32
+ # a block to a block that is instance_execed.
20
33
  module Path
34
+ DEFAULT_PORTS = {'http' => 80, 'https' => 443}.freeze
35
+
21
36
  module ClassMethods
22
- def path(name, path=nil, &block)
37
+ # Create a new instance method for the named path. See plugin module documentation for options.
38
+ def path(name, path=nil, opts={}, &block)
39
+ if path.is_a?(Hash)
40
+ raise RodaError, "cannot provide two option hashses to Roda.path" unless opts.empty?
41
+ opts = path
42
+ path = nil
43
+ end
44
+
23
45
  raise RodaError, "cannot provide both path and block to Roda.path" if path && block
24
46
  raise RodaError, "must provide either path or block to Roda.path" unless path || block
25
47
 
@@ -28,7 +50,46 @@ class Roda
28
50
  block = lambda{path}
29
51
  end
30
52
 
31
- define_method("#{name}_path", &block)
53
+ meth = opts[:name] || "#{name}_path"
54
+ url = opts[:url]
55
+ add_script_name = opts[:add_script_name]
56
+
57
+ if add_script_name || url || opts[:url_only]
58
+ _meth = "_#{meth}"
59
+ define_method(_meth, &block)
60
+ end
61
+
62
+ unless opts[:url_only]
63
+ if add_script_name
64
+ define_method(meth) do |*a, &blk|
65
+ request.script_name.to_s + send(_meth, *a, &blk)
66
+ end
67
+ else
68
+ define_method(meth, &block)
69
+ end
70
+ end
71
+
72
+ if url || opts[:url_only]
73
+ url_meth = if url.is_a?(String) || url.is_a?(Symbol)
74
+ url
75
+ else
76
+ "#{name}_url"
77
+ end
78
+
79
+ url_block = lambda do |*a, &blk|
80
+ r = request
81
+ scheme = r.scheme
82
+ port = r.port
83
+ uri = ["#{scheme}://#{r.host}#{":#{port}" unless DEFAULT_PORTS[scheme] == port}"]
84
+ uri << request.script_name.to_s if add_script_name
85
+ uri << send(_meth, *a, &blk)
86
+ File.join(uri)
87
+ end
88
+
89
+ define_method(url_meth, &url_block)
90
+ end
91
+
92
+ nil
32
93
  end
33
94
  end
34
95
  end