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
@@ -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