roda 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +26 -0
  3. data/README.rdoc +83 -22
  4. data/Rakefile +1 -1
  5. data/doc/release_notes/2.1.0.txt +124 -0
  6. data/lib/roda/plugins/assets.rb +17 -9
  7. data/lib/roda/plugins/class_level_routing.rb +5 -2
  8. data/lib/roda/plugins/delegate.rb +6 -3
  9. data/lib/roda/plugins/indifferent_params.rb +7 -0
  10. data/lib/roda/plugins/mailer.rb +18 -1
  11. data/lib/roda/plugins/multi_route.rb +2 -1
  12. data/lib/roda/plugins/path.rb +75 -6
  13. data/lib/roda/plugins/render.rb +33 -14
  14. data/lib/roda/plugins/static.rb +35 -0
  15. data/lib/roda/plugins/view_options.rb +161 -0
  16. data/lib/roda/plugins/view_subdirs.rb +6 -63
  17. data/lib/roda/version.rb +1 -1
  18. data/spec/composition_spec.rb +12 -0
  19. data/spec/matchers_spec.rb +34 -0
  20. data/spec/plugin/assets_spec.rb +112 -17
  21. data/spec/plugin/delete_empty_headers_spec.rb +12 -0
  22. data/spec/plugin/mailer_spec.rb +46 -3
  23. data/spec/plugin/module_include_spec.rb +17 -0
  24. data/spec/plugin/multi_route_spec.rb +10 -0
  25. data/spec/plugin/named_templates_spec.rb +6 -0
  26. data/spec/plugin/not_found_spec.rb +1 -1
  27. data/spec/plugin/path_spec.rb +76 -0
  28. data/spec/plugin/render_each_spec.rb +6 -0
  29. data/spec/plugin/render_spec.rb +40 -1
  30. data/spec/plugin/sinatra_helpers_spec.rb +5 -0
  31. data/spec/plugin/static_spec.rb +30 -0
  32. data/spec/plugin/view_options_spec.rb +117 -0
  33. data/spec/spec_helper.rb +5 -1
  34. data/spec/views/multiple-layout.erb +1 -0
  35. data/spec/views/multiple.erb +1 -0
  36. metadata +10 -4
  37. data/spec/plugin/static_path_info_spec.rb +0 -56
  38. data/spec/plugin/view_subdirs_spec.rb +0 -44
@@ -21,21 +21,24 @@ class Roda
21
21
  #
22
22
  # # /hello branch
23
23
  # on "hello" do
24
+ # # Set variable for all routes in /hello branch
25
+ # @greeting = 'Hello'
26
+ #
24
27
  # # GET /hello/world request
25
28
  # get "world" do
26
- # "Hello world!"
29
+ # "#{@greeting} world!"
27
30
  # end
28
31
  #
29
32
  # # /hello request
30
33
  # is do
31
34
  # # GET /hello request
32
35
  # get do
33
- # "Hello!"
36
+ # "#{@greeting}!"
34
37
  # end
35
38
  #
36
39
  # # POST /hello request
37
40
  # post do
38
- # puts "Someone said hello!"
41
+ # puts "Someone said #{@greeting}!"
39
42
  # redirect
40
43
  # end
41
44
  # end
@@ -14,6 +14,13 @@ class Roda
14
14
  # The params hash is initialized lazily, so you only pay
15
15
  # the penalty of copying the request params if you call
16
16
  # the +params+ method.
17
+ #
18
+ # Note that there is a rack-indifferent gem that
19
+ # automatically makes rack use indifferent params. Using
20
+ # rack-indifferent is faster and has some other minor
21
+ # advantages over the indifferent_params plugin, though
22
+ # it affects rack itself instead of just the Roda app that
23
+ # you load the plugin into.
17
24
  module IndifferentParams
18
25
  module InstanceMethods
19
26
  # A copy of the request params that will automatically
@@ -168,6 +168,10 @@ class Roda
168
168
  header_content_type = @headers.delete(CONTENT_TYPE)
169
169
  m.headers(@headers)
170
170
  m.body(@body.join) unless @body.empty?
171
+ mail_attachments.each do |a, block|
172
+ m.add_file(*a)
173
+ block.call if block
174
+ end
171
175
 
172
176
  if content_type = header_content_type || roda_class.opts[:mailer][:content_type]
173
177
  if mail.multipart?
@@ -188,11 +192,16 @@ class Roda
188
192
  super
189
193
  end
190
194
  end
195
+
196
+ # The attachments related to the current mail.
197
+ def mail_attachments
198
+ @mail_attachments ||= []
199
+ end
191
200
  end
192
201
 
193
202
  module InstanceMethods
194
203
  # Add delegates for common email methods.
195
- [:from, :to, :cc, :bcc, :subject, :add_file].each do |meth|
204
+ [:from, :to, :cc, :bcc, :subject].each do |meth|
196
205
  define_method(meth) do |*args|
197
206
  env[RODA_MAIL].send(meth, *args)
198
207
  nil
@@ -215,6 +224,14 @@ class Roda
215
224
  end
216
225
  end
217
226
 
227
+ # Delay adding a file to the message until after the message body has been set.
228
+ # If a block is given, the block is called after the file has been added, and you
229
+ # can access the attachment via <tt>response.mail.attachments.last</tt>.
230
+ def add_file(*a, &block)
231
+ response.mail_attachments << [a, block]
232
+ nil
233
+ end
234
+
218
235
  private
219
236
 
220
237
  # Set the text_part or html_part (depending on the method) in the related email,
@@ -147,7 +147,8 @@ class Roda
147
147
 
148
148
  # The names for the currently stored named routes
149
149
  def named_routes(namespace=nil)
150
- opts[:namespaced_routes][namespace].keys
150
+ routes = opts[:namespaced_routes][namespace]
151
+ routes ? routes.keys : []
151
152
  end
152
153
 
153
154
  # Return the named route with the given name.
@@ -3,40 +3,93 @@ class Roda
3
3
  # The path plugin adds support for named paths. Using the +path+ class method, you can
4
4
  # easily create <tt>*_path</tt> instance methods for each named path. Those instance
5
5
  # methods can then be called if you need to get the path for a form action, link,
6
- # redirect, or anything else. Example:
6
+ # redirect, or anything else.
7
+ #
8
+ # Additionally, you can call the +path+ class method with a class and a block, and it will register
9
+ # the class. You can then call the +path+ instance method with an instance of that class, and it will
10
+ # instance_exec the block with the arguments provided to path.
11
+ #
12
+ # Example:
7
13
  #
8
14
  # plugin :path
9
15
  # path :foo, '/foo'
10
16
  # path :bar do |bar|
11
17
  # "/bar/#{bar.id}"
12
18
  # end
19
+ # path Baz do |baz, *paths|
20
+ # "/baz/#{baz.id}/#{paths.join('/')}"
21
+ # end
22
+ # path Quux |quux, path|
23
+ # "/quux/#{quux.id}/#{path}"
24
+ # end
13
25
  #
14
26
  # route do |r|
27
+ # r.post 'foo' do
28
+ # r.redirect foo_path # /foo
29
+ # end
30
+ #
15
31
  # r.post 'bar' do
16
32
  # bar = Bar.create(r.params['bar'])
17
- # r.redirect bar_path(bar)
33
+ # r.redirect bar_path(bar) # /bar/1
34
+ # end
35
+ #
36
+ # r.post 'baz' do
37
+ # bar = Baz[1]
38
+ # r.redirect path(baz, 'c', 'd') # /baz/1/c/d
39
+ # end
40
+ #
41
+ # r.post 'quux' do
42
+ # bar = Quux[1]
43
+ # r.redirect path(quux, '/bar') # /quux/1/bar
18
44
  # end
19
45
  # end
20
46
  #
21
- # The path method accepts the following options:
47
+ # The path method accepts the following options when not called with a class:
22
48
  #
23
- # :add_script_name :: Prefix the path generated with SCRIPT_NAME.
49
+ # :add_script_name :: Prefix the path generated with SCRIPT_NAME. This defaults to the app's
50
+ # :add_script_name option.
24
51
  # :name :: Provide a different name for the method, instead of using <tt>*_path</tt>.
25
52
  # :url :: Create a url method in addition to the path method, which will prefix the string generated
26
53
  # with the appropriate scheme, host, and port. If true, creates a <tt>*_url</tt>
27
54
  # method. If a Symbol or String, uses the value as the url method name.
28
55
  # :url_only :: Do not create a path method, just a url method.
29
56
  #
30
- # Note that if :add_script_name, :url, or :url_only is used, this will also create a <tt>_*_path</tt>
57
+ # Note that if :add_script_name, :url, or :url_only is used, will also create a <tt>_*_path</tt>
31
58
  # method. This is necessary in order to support path methods that accept blocks, as you can't pass
32
59
  # a block to a block that is instance_execed.
33
60
  module Path
34
61
  DEFAULT_PORTS = {'http' => 80, 'https' => 443}.freeze
35
62
  OPTS = {}.freeze
36
63
 
64
+ # Initialize the path classes when loading the plugin
65
+ def self.configure(app)
66
+ app.instance_eval do
67
+ @path_classes ||= {}
68
+ unless @path_classes[String]
69
+ path(String){|str| str}
70
+ end
71
+ end
72
+ end
73
+
37
74
  module ClassMethods
75
+ # Hash of recognizes classes for path instance method. Keys are classes, values are procs.
76
+ attr_reader :path_classes
77
+
78
+ # Freeze the path classes when freezing the app.
79
+ def freeze
80
+ @path_classes.freeze
81
+ super
82
+ end
83
+
38
84
  # Create a new instance method for the named path. See plugin module documentation for options.
39
85
  def path(name, path=nil, opts=OPTS, &block)
86
+ if name.is_a?(Class)
87
+ raise RodaError, "can't provide path or options when calling path with a class" unless path.nil? && opts.empty?
88
+ raise RodaError, "must provide a block when calling path with a class" unless block
89
+ @path_classes[name] = block
90
+ return
91
+ end
92
+
40
93
  if path.is_a?(Hash)
41
94
  raise RodaError, "cannot provide two option hashses to Roda.path" unless opts.empty?
42
95
  opts = path
@@ -53,7 +106,7 @@ class Roda
53
106
 
54
107
  meth = opts[:name] || "#{name}_path"
55
108
  url = opts[:url]
56
- add_script_name = opts[:add_script_name]
109
+ add_script_name = opts.fetch(:add_script_name, self.opts[:add_script_name])
57
110
 
58
111
  if add_script_name || url || opts[:url_only]
59
112
  _meth = "_#{meth}"
@@ -93,6 +146,22 @@ class Roda
93
146
  nil
94
147
  end
95
148
  end
149
+
150
+ module InstanceMethods
151
+ # Return a path based on the class of the object. The object passed must have
152
+ # had its class previously registered with the application. If the app's
153
+ # :add_script_name option is true, this prepends the SCRIPT_NAME to the path.
154
+ def path(obj, *args)
155
+ app = self.class
156
+ if blk = app.path_classes[obj.class]
157
+ path = instance_exec(obj, *args, &blk)
158
+ path = request.script_name.to_s + path if app.opts[:add_script_name]
159
+ path
160
+ else
161
+ raise RodaError, "unrecognized object given to Roda#path: #{obj.inspect}"
162
+ end
163
+ end
164
+ end
96
165
  end
97
166
 
98
167
  register_plugin(:path, Path)
@@ -38,8 +38,8 @@ class Roda
38
38
  # from the default options.
39
39
  # :template_opts :: The tilt options used when rendering templates, defaults to
40
40
  # <tt>{:outvar=>'@_out_buf', :default_encoding=>Encoding.default_external}</tt>.
41
- # :views :: The directory holding the view files, defaults to 'views' in the
42
- # current directory.
41
+ # :views :: The directory holding the view files, defaults to the 'views' subdirectory of the
42
+ # application's :root option (the process's working directory by default).
43
43
  #
44
44
  # Most of these options can be overridden at runtime by passing options
45
45
  # to the +view+ or +render+ methods:
@@ -86,20 +86,20 @@ class Roda
86
86
 
87
87
  # Setup default rendering options. See Render for details.
88
88
  def self.configure(app, opts=OPTS)
89
- orig_opts = opts
90
89
  if app.opts[:render]
91
- app.opts[:render] = app.opts[:render].merge(opts)
92
- else
93
- app.opts[:render] = opts.dup
90
+ opts = app.opts[:render][:orig_opts].merge(opts)
94
91
  end
92
+ app.opts[:render] = opts.dup
93
+ app.opts[:render][:orig_opts] = opts
95
94
 
96
95
  opts = app.opts[:render]
97
96
  opts[:engine] ||= "erb"
98
97
  opts[:ext] = nil unless opts.has_key?(:ext)
99
- opts[:views] ||= File.expand_path("views", Dir.pwd)
98
+ opts[:views] = File.expand_path(opts[:views]||"views", app.opts[:root])
100
99
  opts[:layout_opts] = (opts[:layout_opts] || {}).dup
100
+ opts[:layout_opts][:_is_layout] = true
101
101
 
102
- if layout = orig_opts.fetch(:layout, true)
102
+ if layout = opts.fetch(:layout, true)
103
103
  opts[:layout] = true unless opts.has_key?(:layout)
104
104
 
105
105
  case layout
@@ -196,8 +196,8 @@ class Roda
196
196
  end
197
197
  end
198
198
 
199
- # Given the template name and options, return the template class, template path/content,
200
- # and template block to use for the render.
199
+ # Given the template name and options, set the template class, template path/content,
200
+ # template block, and locals to use for the render in the passed options.
201
201
  def find_template(opts)
202
202
  if content = opts[:inline]
203
203
  path = opts[:path] = content
@@ -221,6 +221,14 @@ class Roda
221
221
  opts[:key] = key
222
222
  end
223
223
 
224
+ if !opts[:_is_layout] && (r_locals = render_opts[:locals])
225
+ opts[:locals] = if locals = opts[:locals]
226
+ r_locals.merge(locals)
227
+ else
228
+ r_locals
229
+ end
230
+ end
231
+
224
232
  opts
225
233
  end
226
234
 
@@ -230,6 +238,12 @@ class Roda
230
238
  opts.merge(template)
231
239
  end
232
240
 
241
+ # The default render options to use. These set defaults that can be overridden by
242
+ # providing a :layout_opts option to the view/render method.
243
+ def render_layout_opts
244
+ render_opts[:layout_opts].dup
245
+ end
246
+
233
247
  # The name to use for the template. By default, just converts the :template option to a string.
234
248
  def template_name(opts)
235
249
  opts[:template].to_s
@@ -246,10 +260,15 @@ class Roda
246
260
  # used, return nil.
247
261
  def view_layout_opts(opts)
248
262
  if layout = opts.fetch(:layout, render_opts[:layout])
249
- layout_opts = if opts[:layout_opts]
250
- opts[:layout_opts].merge(render_opts[:layout_opts])
251
- else
252
- render_opts[:layout_opts].dup
263
+ layout_opts = render_layout_opts
264
+ if l_opts = opts[:layout_opts]
265
+ if (l_locals = l_opts[:locals]) && (layout_locals = layout_opts[:locals])
266
+ set_locals = layout_locals.merge(l_locals)
267
+ end
268
+ layout_opts.merge!(l_opts)
269
+ if set_locals
270
+ layout_opts[:locals] = set_locals
271
+ end
253
272
  end
254
273
 
255
274
  case layout
@@ -0,0 +1,35 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The static plugin loads the Rack::Static middleware into the application.
4
+ # It mainly exists to make serving static files simpler, by supplying
5
+ # defaults to Rack::Static that are appropriate for Roda.
6
+ #
7
+ # The static plugin recognizes the application's :root option, and by default
8
+ # sets the Rack::Static +:root+ option to the +public+ subfolder of the application's
9
+ # +:root+ option. Additionally, if a relative path is provided as the :root
10
+ # option to the plugin, it will be considered relative to the application's
11
+ # +:root+ option.
12
+ #
13
+ # Since the :urls option for Rack::Static is always required, the static plugin
14
+ # uses a separate option for it.
15
+ #
16
+ # Examples:
17
+ #
18
+ # opts[:root] = '/path/to/app'
19
+ # plugin :static, ['/js', '/css'] # path: /path/to/app/public
20
+ # plugin :static, ['/images'], :root=>'pub' # path: /path/to/app/pub
21
+ # plugin :static, ['/media'], :root=>'/path/to/public' # path: /path/to/public
22
+ module Static
23
+ # Load the Rack::Static middleware. Use the paths given as the :urls option,
24
+ # and set the :root option to be relative to the application's :root option.
25
+ def self.configure(app, paths, opts={})
26
+ opts = opts.dup
27
+ opts[:urls] = paths
28
+ opts[:root] = File.expand_path(opts[:root]||"public", app.opts[:root])
29
+ app.use ::Rack::Static, opts
30
+ end
31
+ end
32
+
33
+ register_plugin(:static, Static)
34
+ end
35
+ end
@@ -0,0 +1,161 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The view_options plugin allows you to override view and layout
4
+ # options and locals for specific branches and routes.
5
+ #
6
+ # plugin :render
7
+ # plugin :view_options
8
+ #
9
+ # route do |r|
10
+ # r.on "users" do
11
+ # set_layout_options :template=>'users_layout'
12
+ # set_layout_locals :title=>'Users'
13
+ # set_view_options :engine=>'haml'
14
+ # set_view_locals :footer=>'(c) Roda'
15
+ #
16
+ # # ...
17
+ # end
18
+ # end
19
+ #
20
+ # The options and locals you specify have higher precedence than
21
+ # the render plugin options, but lower precedence than options
22
+ # you directly pass to the view/render methods.
23
+ #
24
+ # The view_options plugin also has special support for sites
25
+ # that have outgrown a flat view directory and use subdirectories
26
+ # for views. It allows you to set the view directory to
27
+ # use, and template names that do not contain a slash will
28
+ # automatically use that view subdirectory. Example:
29
+ #
30
+ # plugin :render, :layout=>'./layout'
31
+ # plugin :view_options
32
+ #
33
+ # route do |r|
34
+ # r.on "users" do
35
+ # set_view_subdir 'users'
36
+ #
37
+ # r.get :id do
38
+ # append_view_subdir 'profile'
39
+ # view 'index' # uses ./views/users/profile/index.erb
40
+ # end
41
+ #
42
+ # r.get 'list' do
43
+ # view 'lists/users' # uses ./views/lists/users.erb
44
+ # end
45
+ # end
46
+ # end
47
+ #
48
+ # Note that when a view subdirectory is set, the layout will
49
+ # also be looked up in the subdirectory unless it contains
50
+ # a slash. So if you want to use a view subdirectory for
51
+ # templates but have a shared layout, you should make sure your
52
+ # layout contains a slash, similar to the example above.
53
+ module ViewOptions
54
+ # Load the render plugin before this plugin, since this plugin
55
+ # works by overriding methods in the render plugin.
56
+ def self.load_dependencies(app)
57
+ app.plugin :render
58
+ end
59
+
60
+ # The following methods are created via metaprogramming:
61
+ # set_layout_locals :: Set locals to use in the layout
62
+ # set_layout_options :: Set options to use when rendering the layout
63
+ # set_view_locals :: Set locals to use in the view
64
+ # set_view_options :: Set options to use when rendering the view
65
+ module InstanceMethods
66
+ %w'layout view'.each do |type|
67
+ %w'locals options'.each do |var|
68
+ v = "_#{type}_#{var}"
69
+ module_eval(<<-END, __FILE__, __LINE__+1)
70
+ def set#{v}(opts)
71
+ if @#{v}
72
+ @#{v} = @#{v}.merge(opts)
73
+ else
74
+ @#{v} = opts
75
+ end
76
+ end
77
+ END
78
+ end
79
+ end
80
+
81
+ # Append a view subdirectory to use. If there hasn't already
82
+ # been a view subdirectory set, this just sets it to the argument.
83
+ # If there has already been a view subdirectory set, this sets
84
+ # the view subdirectory to a subdirectory of the existing
85
+ # view subdirectory.
86
+ def append_view_subdir(v)
87
+ if subdir = @_view_subdir
88
+ set_view_subdir("#{subdir}/#{v}")
89
+ else
90
+ set_view_subdir(v)
91
+ end
92
+ end
93
+
94
+ # Set the view subdirectory to use. This can be set to nil
95
+ # to not use a view subdirectory.
96
+ def set_view_subdir(v)
97
+ @_view_subdir = v
98
+ end
99
+
100
+ private
101
+
102
+ # If view options or locals have been set and this
103
+ # template isn't a layout template, merge the options
104
+ # and locals into the returned hash.
105
+ def parse_template_opts(template, opts)
106
+ t_opts = super
107
+
108
+ unless t_opts[:_is_layout]
109
+ if v_opts = @_view_options
110
+ t_opts.merge!(v_opts)
111
+ end
112
+
113
+ if v_locals = @_view_locals
114
+ t_opts[:locals] = if t_locals = t_opts[:locals]
115
+ v_locals.merge(t_locals)
116
+ else
117
+ v_locals
118
+ end
119
+ end
120
+ end
121
+
122
+ t_opts
123
+ end
124
+
125
+ # If layout options or locals have been set,
126
+ # merge the options and locals into the returned hash.
127
+ def render_layout_opts
128
+ opts = super
129
+
130
+ if l_opts = @_layout_options
131
+ opts.merge!(l_opts)
132
+ end
133
+
134
+ if l_locals = @_layout_locals
135
+ opts[:locals] = if o_locals = opts[:locals]
136
+ o_locals.merge(l_locals)
137
+ else
138
+ l_locals
139
+ end
140
+ end
141
+
142
+ opts
143
+ end
144
+
145
+ # Override the template name to use the view subdirectory if the
146
+ # there is a view subdirectory and the template name does not
147
+ # contain a slash.
148
+ def template_name(opts)
149
+ name = super
150
+ if (v = @_view_subdir) && name !~ /\//
151
+ "#{v}/#{name}"
152
+ else
153
+ name
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ register_plugin(:view_options, ViewOptions)
160
+ end
161
+ end