roda 2.0.0 → 2.1.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 (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