roda 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3c4531548815188387cfc8a1cd6eb4803ca52228
4
- data.tar.gz: f25f081bd7ed74c63d5b4da6ca87d8ad3a413c42
3
+ metadata.gz: 9bbbb565f4b5766e8e11f78d322a5446da6358bd
4
+ data.tar.gz: 4e7b782d3d4a2292e9461c47053156a18d56bec2
5
5
  SHA512:
6
- metadata.gz: 4fd832236f0c0a59129f10303c56c82f3ec9e63d27f8c85cd5080e0ac1cd3477cd3321f309618a6c2d4690b43b79ab5d687c9243957156d61532e472118943a1
7
- data.tar.gz: ca178d1d5b1945ad3f94a9d4273d9cbbfd8726f5f5f924d7eefd9949d0be732eb028e6f2fd1f7f2e7eee526ae04ef796bb2899ebbcbb690bac4a13e0021241af
6
+ metadata.gz: 864fd659136483c2867b91e6512df778fd9f331c60e30b8166f1146afda0810cb1f08d56b38f2c01f50d7fcd477b9914def3d0eb1479de2533f0db8f94dc3dc3
7
+ data.tar.gz: 79dcb04899e305ed875f439d3ebb57cbaf9092bba1614e8fcbbb4b15da55c9b10c74086f3bf24e7b543b9dcaaf9f3042009ab611a9e7b7f0aa0b0077a91a32b0
data/CHANGELOG CHANGED
@@ -1,3 +1,23 @@
1
+ = 2.2.0 (2015-04-13)
2
+
3
+ * Add :escaper render plugin option to support custom escaping of <%= %> tags when :escape is used (jeremyevans)
4
+
5
+ * Add :escape_safe_classes render plugin option, to not escape certain string subclasses when :escape is used (jeremyevans)
6
+
7
+ * Split partials method from padrino_render plugin into partials plugin (kematzy) (#19)
8
+
9
+ * Add shared_vars plugin, for sharing variables between multiple Roda apps (jeremyevans)
10
+
11
+ * Add delay method to chunked plugin, for delaying a block execution until right before content template rendering (jeremyevans)
12
+
13
+ * Have default Content-Type header when using the default_headers plugin (jeremyevans)
14
+
15
+ * Add :by_name option to the path plugin, for registering classes by name, useful when reloading code (jeremyevans)
16
+
17
+ * Add Roda.path_block to get the block related to the given class used for Road#path (jeremyevans)
18
+
19
+ * Make Roda#path work correctly in subclasses (jeremyevans)
20
+
1
21
  = 2.1.0 (2015-03-13)
2
22
 
3
23
  * Have add_file in the mailer plugin support blocks, which are called after the file has been added (jeremyevans)
@@ -1,5 +1,6 @@
1
1
  Copyright (c) 2014-2015 Jeremy Evans
2
- Copyright (c) 2010, 2011 Michel Martens, Damian Janowski and Cyril David
2
+ Copyright (c) 2010-2014 Michel Martens, Damian Janowski and Cyril David
3
+ Copyright (c) 2008-2009 Christian Neukirchen
3
4
 
4
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
5
6
  of this software and associated documentation files (the "Software"), to deal
@@ -744,8 +744,49 @@ to escape by default, so that in your templates:
744
744
  <%= '<>' %> # outputs &lt;&gt;
745
745
  <%== '<>' %> # outputs <>
746
746
 
747
+ You can also provide a +:escape_safe_classes+ option, which will
748
+ make <tt><%= %></tt> not escape certain string subclasses, useful
749
+ if you have helpers that already return escaped output using a
750
+ string subclass instance.
751
+
747
752
  This support requires {Erubis}[http://www.kuwata-lab.com/erubis/].
748
753
 
754
+ === Security Related HTTP Headers
755
+
756
+ You may want to look into setting the following HTTP headers, which
757
+ can be done at the web server level, but can also be done at the
758
+ application level using using the +default_headers+ plugin:
759
+
760
+ Content-Security-Policy/X-Content-Security-Policy :: Defines policy for how javascript and other
761
+ types of content can be used on the page.
762
+ Frame-Options/X-Frame-Options :: Provides click-jacking projection by not allowing usage inside
763
+ a frame.
764
+ Strict-Transport-Security :: Enforces SSL/TLS Connections to the application.
765
+ X-Content-Type-Options :: Forces some browsers to respect a declared Content-Type header.
766
+ X-XSS-Protection :: Enables an XSS mitigation filter in some browsers.
767
+
768
+ Example:
769
+
770
+ class App < Roda
771
+ plugin :default_headers,
772
+ 'Content-Type'=>'text/html',
773
+ 'Content-Security-Policy'=>"default-src 'self'",
774
+ 'Strict-Transport-Security'=>'max-age=16070400;',
775
+ 'X-Frame-Options'=>'deny',
776
+ 'X-Content-Type-Options'=>'nosniff',
777
+ 'X-XSS-Protection'=>'1; mode=block'
778
+ end
779
+
780
+ == Reloading
781
+
782
+ Most rack-based reloaders will work with Roda, including:
783
+
784
+ * {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader]
785
+ * {rerun}[https://github.com/alexch/rerun]
786
+ * {shotgun}[https://github.com/rtomayko/shotgun]
787
+ * {Rack::Reloader}[https://github.com/rack/rack/blob/master/lib/rack/reloader.rb]
788
+ * {pistol}[https://github.com/monk/pistol]
789
+
749
790
  == Plugins
750
791
 
751
792
  By design, Roda has a very small core, providing only the essentials.
data/Rakefile CHANGED
@@ -68,15 +68,16 @@ end
68
68
 
69
69
  begin
70
70
  begin
71
- # RSpec 1
72
- require "spec/rake/spectask"
73
- spec_class = Spec::Rake::SpecTask
74
- spec_files_meth = :spec_files=
75
- rescue LoadError
71
+ raise LoadError if ENV['RSPEC1']
76
72
  # RSpec 2
77
73
  require "rspec/core/rake_task"
78
74
  spec_class = RSpec::Core::RakeTask
79
75
  spec_files_meth = :pattern=
76
+ rescue LoadError
77
+ # RSpec 1
78
+ require "spec/rake/spectask"
79
+ spec_class = Spec::Rake::SpecTask
80
+ spec_files_meth = :spec_files=
80
81
  end
81
82
 
82
83
  spec = lambda do |name, files, d|
@@ -80,7 +80,7 @@
80
80
  path(Track){|track| "/albums/#{track.album_id}/tracks/#{track.number}"}
81
81
 
82
82
  route do
83
- r.get 'tracks/:id' |track_id|
83
+ r.get 'tracks/:id' do |track_id|
84
84
  r.redirect(path(Track[track_id]))
85
85
  end
86
86
  end
@@ -0,0 +1,97 @@
1
+ = New Plugins
2
+
3
+ * A shared_vars plugin has been added, for sharing variables between
4
+ multiple Roda apps. Example:
5
+
6
+ class API < Roda
7
+ plugin :shared_vars
8
+ route do |r|
9
+ user = shared[:user]
10
+ # ...
11
+ end
12
+ end
13
+
14
+ class App < Roda
15
+ plugin :shared_vars
16
+
17
+ route do |r|
18
+ r.on :user_id do |user_id|
19
+ shared[:user] = User[user_id]
20
+ r.run API
21
+ end
22
+ end
23
+ end
24
+
25
+ If you pass a hash to shared, it will update the shared
26
+ variables with the content of the hash:
27
+
28
+ route do |r|
29
+ r.on :user_id do |user_id|
30
+ shared(:user => User[user_id])
31
+ r.run API
32
+ end
33
+ end
34
+
35
+ You can also pass a block to shared, which will set the
36
+ shared variables only for the given block, restoring the
37
+ previous shared variables afterward:
38
+
39
+ route do |r|
40
+ r.on :user_id do |user_id|
41
+ shared(:user => User[user_id]) do
42
+ r.run API
43
+ end
44
+ end
45
+ end
46
+
47
+ * The partials method added by the padrino_render plugin has been
48
+ extracted into a partials plugin, for users who want to use the
49
+ partials method without having render use a layout by default.
50
+
51
+ = Other New Features
52
+
53
+ * The render plugin when using the :escape option now additionally
54
+ supports an :escape_safe_classes option, which accepts a class or
55
+ array of classes that should not be automatically escaped when using
56
+ <%= %> tags. This makes easier to integrate with helpers or
57
+ external libraries that return already html escaped strings, without
58
+ using <%== %> tags.
59
+
60
+ For complete control, an :escaper option is also supported, which
61
+ will be used for escaping <%= %> strings. The value of this option
62
+ should be an object that responds to escape_xml with a single
63
+ argument and returns an output string.
64
+
65
+ * A Roda#delay method has been added to the chunked plugin, which
66
+ delays execution of the given block until right before the content
67
+ template is rendered. This allows you to continue to use the
68
+ routing tree to DRY up your code even when streaming template
69
+ rendering. Example:
70
+
71
+ r.on 'albums/:d' do |album_id|
72
+ delay do
73
+ @album = Album[album_id]
74
+ end
75
+ r.get 'info' do
76
+ chunked(:info)
77
+ end
78
+ r.get 'tracks' do
79
+ chunked(:tracks)
80
+ end
81
+ end
82
+
83
+ * The path plugin now supports a :by_name option, which makes lookup
84
+ of the class be by name as opposed to by reference. This is
85
+ designed for use in development when doing code reloading.
86
+
87
+ * Roda.path_block has been added to the path plugin for returning the
88
+ block associated with the given class.
89
+
90
+ = Other Improvements
91
+
92
+ * The default_headers plugin now defaults to the same headers that
93
+ Roda uses by default (just the Content-Type header with text/html
94
+ value). Previously, it did not set a Content-Type header if you
95
+ didn't specify one.
96
+
97
+ * In the path plugin, Roda#path now works correctly in subclasses.
@@ -2,23 +2,50 @@ require 'erubis'
2
2
 
3
3
  class Roda
4
4
  module RodaPlugins
5
- # The _erubis_escaping plugin is an internal plugin that provides a
6
- # subclass of Erubis::EscapedEruby with a bugfix and an optimization.
5
+ # The _erubis_escaping plugin handles escaping of <tt><%= %></tt> inside
6
+ # ERB templates. It is an internal plugin that should not be loaded
7
+ # directlyn by user code.
7
8
  module ErubisEscaping
8
- # Optimized subclass that fixes escaping of postfix conditionals.
9
+ # Escaper which escapes by default, but does not escape instances of
10
+ # classes marked as safe.
11
+ class UnsafeClassEscaper
12
+ # Default escaper if the string needs to be escaped.
13
+ Escaper = Erubis::XmlHelper
14
+
15
+ # Record the classes to consider safe.
16
+ def initialize(safe_classes)
17
+ @safe_classes = Array(safe_classes).freeze
18
+ freeze
19
+ end
20
+
21
+ # If the string given is not an instance of one of the safe
22
+ # classes, escape it, otherwise return it verbatim. If the
23
+ # given object is not already a string, convert it to a string first.
24
+ def escape_xml(string)
25
+ unless string.is_a?(String)
26
+ string = string.to_s
27
+ end
28
+
29
+ if @safe_classes.any?{|c| string.is_a?(c)}
30
+ string
31
+ else
32
+ Escaper.escape_xml(string)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Subclass that works with specified escaper, also handling
38
+ # postfix conditionals inside <tt><%= %></tt> tags.
9
39
  class Eruby < Erubis::EscapedEruby
10
- # Set escaping class to a local variable, so you don't need a
11
- # constant lookup per escape.
40
+ # Set escaping object to a local variable
12
41
  def convert_input(codebuf, input)
13
- codebuf << '_erubis_xml_helper = Erubis::XmlHelper;'
42
+ codebuf << '_erubis_escaper = render_opts[:escaper];'
14
43
  super
15
44
  end
16
45
 
17
- # Fix bug in Erubis::EscapedEruby where postfix conditionals inside
18
- # <%= %> are broken (e.g. <%= foo if bar %> ), and optimize by using
19
- # a local variable instead of a constant lookup.
46
+ # Use escaping object to escape the code, and handle postfix conditionals.
20
47
  def add_expr_escaped(src, code)
21
- src << " #{@bufvar} << _erubis_xml_helper.escape_xml((" << code << '));'
48
+ src << " " << @bufvar << " << _erubis_escaper.escape_xml((" << code << "));"
22
49
  end
23
50
  end
24
51
  end
@@ -37,6 +37,23 @@ class Roda
37
37
  # end
38
38
  # end
39
39
  #
40
+ # You can also call delay manually with a block, and the execution of the
41
+ # block will be delayed until rendering the content template. This is
42
+ # useful if you want to delay execution for all routes under a branch:
43
+ #
44
+ # r.on 'albums/:d' do |album_id|
45
+ # delay do
46
+ # @album = Album[album_id]
47
+ # end
48
+ # r.get 'info' do
49
+ # chunked(:info)
50
+ # end
51
+ # r.get 'tracks' do
52
+ # chunked(:tracks)
53
+ # end
54
+ # end
55
+ #
56
+ #
40
57
  # If you want to chunk all responses, pass the :chunk_by_default option
41
58
  # when loading the plugin:
42
59
  #
@@ -192,15 +209,19 @@ class Roda
192
209
  end
193
210
 
194
211
  # Render a response to the user in chunks. See Chunked for
195
- # an overview.
212
+ # an overview. If a block is given, it is passed to #delay.
196
213
  def chunked(template, opts=OPTS, &block)
197
214
  unless defined?(@_chunked)
198
215
  @_chunked = env[HTTP_VERSION] == HTTP11
199
216
  end
200
217
 
218
+ if block
219
+ delay(&block)
220
+ end
221
+
201
222
  unless @_chunked
202
223
  # If chunking is disabled, do a normal rendering of the view.
203
- yield if block
224
+ run_delayed_blocks
204
225
  return view(template, opts)
205
226
  end
206
227
 
@@ -214,7 +235,7 @@ class Roda
214
235
 
215
236
  # Hack so that the arguments don't need to be passed
216
237
  # through the response and body objects.
217
- @_each_chunk_args = [template, opts, block]
238
+ @_each_chunk_args = [template, opts]
218
239
 
219
240
  res = response
220
241
  headers = res.headers
@@ -226,11 +247,18 @@ class Roda
226
247
  throw :halt, res.finish_with_body(Body.new(self))
227
248
  end
228
249
 
250
+ # Delay the execution of the block until right before the
251
+ # content template is to be rendered.
252
+ def delay(&block)
253
+ raise RodaError, "must pass a block to Roda#delay" unless block
254
+ (@_delays ||= []) << block
255
+ end
256
+
229
257
  # Yield each chunk of the template rendering separately.
230
258
  def each_chunk
231
259
  response.body.each{|s| yield s}
232
260
 
233
- template, opts, block = @_each_chunk_args
261
+ template, opts = @_each_chunk_args
234
262
 
235
263
  # Use a lambda for the flusher, so that a call to flush
236
264
  # by a template can result in this method yielding a chunk
@@ -243,12 +271,12 @@ class Roda
243
271
  if layout_opts = view_layout_opts(opts)
244
272
  @_out_buf = render_template(layout_opts) do
245
273
  flush
246
- block.call if block
274
+ run_delayed_blocks
247
275
  yield opts[:content] || render_template(template, opts)
248
276
  nil
249
277
  end
250
278
  else
251
- yield if block
279
+ run_delayed_blocks
252
280
  yield view(template, opts)
253
281
  end
254
282
 
@@ -268,6 +296,14 @@ class Roda
268
296
  def flush
269
297
  @_flusher.call if @_flusher
270
298
  end
299
+
300
+ private
301
+
302
+ # Run all delayed blocks
303
+ def run_delayed_blocks
304
+ return unless @_delays
305
+ @_delays.each(&:call)
306
+ end
271
307
  end
272
308
  end
273
309
 
@@ -20,7 +20,7 @@ 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.opts[:default_headers] = (app.opts[:default_headers] || {}).merge(headers).freeze
23
+ app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
24
24
  end
25
25
 
26
26
  module ClassMethods
@@ -133,7 +133,7 @@ class Roda
133
133
  # Freeze the namespaced routes so that there can be no thread safety issues at runtime.
134
134
  def freeze
135
135
  opts[:namespaced_routes].freeze
136
- opts[:namespaced_routes].each_value{|v| v.freeze}
136
+ opts[:namespaced_routes].each_value(&:freeze)
137
137
  super
138
138
  end
139
139
 
@@ -5,7 +5,9 @@ class Roda
5
5
  # rendering supports is supported by this plugin (yet), it
6
6
  # currently handles enough to be a drop in replacement for
7
7
  # some applications.
8
- #
8
+ #
9
+ # plugin :padrino_render, :views => 'path/2/views'
10
+ #
9
11
  # Most notably, this makes the +render+ method default to
10
12
  # using the layout, similar to how the +view+ method works
11
13
  # in the render plugin. If you want to call render and not
@@ -15,22 +17,14 @@ class Roda
15
17
  # render('test') # layout
16
18
  # render('test', :layout=>false) # no layout
17
19
  #
18
- # This also adds a +partial+ method, which renders templates
19
- # without the layout, but prefixes the template filename to
20
- # use with an underscore:
21
- #
22
- # partial('test') # uses _test.erb
23
- # partial('dir/test') # uses dir/_test.erb
24
- #
25
- #
20
+ # Note that this plugin loads the :partials plugin.
26
21
  module PadrinoRender
27
22
  OPTS = {}.freeze
28
- SLASH = '/'.freeze
29
23
 
30
24
  # Depend on the render plugin, since this overrides
31
25
  # some of its methods.
32
26
  def self.load_dependencies(app, opts=OPTS)
33
- app.plugin :render, opts
27
+ app.plugin :partials, opts
34
28
  end
35
29
 
36
30
  module InstanceMethods
@@ -39,19 +33,6 @@ class Roda
39
33
  def render(template, opts=OPTS)
40
34
  view(template, opts)
41
35
  end
42
-
43
- # Renders the given template without a layout, but
44
- # prefixes the template filename to use with an
45
- # underscore.
46
- def partial(template, opts=OPTS)
47
- opts = parse_template_opts(template, opts)
48
- if opts[:template]
49
- template = opts[:template].split(SLASH)
50
- template[-1] = "_#{template[-1]}"
51
- opts[:template] = template.join(SLASH)
52
- end
53
- render_template(opts)
54
- end
55
36
  end
56
37
  end
57
38
 
@@ -0,0 +1,47 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The partials plugin adds a +partial+ method, which renders
4
+ # templates without the layout.
5
+ #
6
+ # plugin :partials, :views => 'path/2/views'
7
+ #
8
+ # Template files are prefixed with an underscore:
9
+ #
10
+ # partial('test') # uses _test.erb
11
+ # partial('dir/test') # uses dir/_test.erb
12
+ #
13
+ # This is basically equivalent to:
14
+ #
15
+ # render('_test')
16
+ # render('dir/_test')
17
+ #
18
+ # Note that this plugin automatically loads the :render plugin.
19
+ module Partials
20
+ OPTS = {}.freeze
21
+ SLASH = '/'.freeze
22
+
23
+ # Depend on the render plugin, since this overrides
24
+ # some of its methods.
25
+ def self.load_dependencies(app, opts=OPTS)
26
+ app.plugin :render, opts
27
+ end
28
+
29
+ module InstanceMethods
30
+ # Renders the given template without a layout, but
31
+ # prefixes the template filename to use with an
32
+ # underscore.
33
+ def partial(template, opts=OPTS)
34
+ opts = parse_template_opts(template, opts)
35
+ if opts[:template]
36
+ template = opts[:template].split(SLASH)
37
+ template[-1] = "_#{template[-1]}"
38
+ opts[:template] = template.join(SLASH)
39
+ end
40
+ render_template(opts)
41
+ end
42
+ end
43
+ end
44
+
45
+ register_plugin(:partials, Partials)
46
+ end
47
+ end
@@ -61,11 +61,15 @@ class Roda
61
61
  DEFAULT_PORTS = {'http' => 80, 'https' => 443}.freeze
62
62
  OPTS = {}.freeze
63
63
 
64
- # Initialize the path classes when loading the plugin
65
- def self.configure(app)
64
+ # Initialize the path classes when loading the plugin. Options:
65
+ # :by_name :: Register classes by name, which is friendlier when reloading code.
66
+ def self.configure(app, opts=OPTS)
66
67
  app.instance_eval do
67
- @path_classes ||= {}
68
- unless @path_classes[String]
68
+ if opts.has_key?(:by_name)
69
+ self.opts[:path_class_by_name] = opts[:by_name]
70
+ end
71
+ self.opts[:path_classes] ||= {}
72
+ unless path_block(String)
69
73
  path(String){|str| str}
70
74
  end
71
75
  end
@@ -73,11 +77,13 @@ class Roda
73
77
 
74
78
  module ClassMethods
75
79
  # Hash of recognizes classes for path instance method. Keys are classes, values are procs.
76
- attr_reader :path_classes
80
+ def path_classes
81
+ opts[:path_classes]
82
+ end
77
83
 
78
84
  # Freeze the path classes when freezing the app.
79
85
  def freeze
80
- @path_classes.freeze
86
+ path_classes.freeze
81
87
  super
82
88
  end
83
89
 
@@ -86,7 +92,10 @@ class Roda
86
92
  if name.is_a?(Class)
87
93
  raise RodaError, "can't provide path or options when calling path with a class" unless path.nil? && opts.empty?
88
94
  raise RodaError, "must provide a block when calling path with a class" unless block
89
- @path_classes[name] = block
95
+ if self.opts[:path_class_by_name]
96
+ name = name.name
97
+ end
98
+ path_classes[name] = block
90
99
  return
91
100
  end
92
101
 
@@ -145,6 +154,14 @@ class Roda
145
154
 
146
155
  nil
147
156
  end
157
+
158
+ # Return the block related to the given class, or nil if there is no block.
159
+ def path_block(klass)
160
+ if opts[:path_class_by_name]
161
+ klass = klass.name
162
+ end
163
+ path_classes[klass]
164
+ end
148
165
  end
149
166
 
150
167
  module InstanceMethods
@@ -153,13 +170,13 @@ class Roda
153
170
  # :add_script_name option is true, this prepends the SCRIPT_NAME to the path.
154
171
  def path(obj, *args)
155
172
  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
173
+ unless blk = app.path_block(obj.class)
161
174
  raise RodaError, "unrecognized object given to Roda#path: #{obj.inspect}"
162
175
  end
176
+
177
+ path = instance_exec(obj, *args, &blk)
178
+ path = request.script_name.to_s + path if app.opts[:add_script_name]
179
+ path
163
180
  end
164
181
  end
165
182
  end
@@ -31,6 +31,11 @@ class Roda
31
31
  # :escape :: Use Roda's Erubis escaping support, which makes <tt><%= %></tt> escape output,
32
32
  # <tt><%== %></tt> not escape output, and handles postfix conditions inside
33
33
  # <tt><%= %></tt> tags.
34
+ # :escape_safe_classes :: String subclasses that should not be HTML escaped when used in
35
+ # <tt><%= %></tt> tags, when :escape is used. Can be an array for multiple classes.
36
+ # :escaper :: Object used for escaping output of <tt><%= %></tt>, when :escape is used,
37
+ # overriding the default. If given, object should respond to +escape_xml+ with
38
+ # a single argument and return an output string.
34
39
  # :ext :: The file extension to assume for view files, defaults to the :engine
35
40
  # option.
36
41
  # :layout :: The base name of the layout file, defaults to 'layout'.
@@ -119,6 +124,12 @@ class Roda
119
124
  end
120
125
  if opts[:escape]
121
126
  template_opts[:engine_class] = ErubisEscaping::Eruby
127
+
128
+ opts[:escaper] ||= if opts[:escape_safe_classes]
129
+ ErubisEscaping::UnsafeClassEscaper.new(opts[:escape_safe_classes])
130
+ else
131
+ ::Erubis::XmlHelper
132
+ end
122
133
  end
123
134
  opts[:cache] = app.thread_safe_cache if opts.fetch(:cache, ENV['RACK_ENV'] != 'development')
124
135
  opts[:layout_opts].freeze
@@ -0,0 +1,85 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The shared_vars plugin adds a shared method for storing
4
+ # shared variables across nested Roda apps.
5
+ #
6
+ # class API < Roda
7
+ # plugin :shared_vars
8
+ # route do |r|
9
+ # user = shared[:user]
10
+ # # ...
11
+ # end
12
+ # end
13
+ #
14
+ # class App < Roda
15
+ # plugin :shared_vars
16
+ #
17
+ # route do |r|
18
+ # r.on :user_id do |user_id|
19
+ # shared[:user] = User[user_id]
20
+ # r.run API
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # If you pass a hash to shared, it will update the shared
26
+ # vars with the content of the hash:
27
+ #
28
+ # route do |r|
29
+ # r.on :user_id do |user_id|
30
+ # shared(:user => User[user_id])
31
+ # r.run API
32
+ # end
33
+ # end
34
+ #
35
+ # You can also pass a block to shared, which will set the
36
+ # shared variables only for the given block, restoring the
37
+ # previous shared variables afterward:
38
+ #
39
+ # route do |r|
40
+ # r.on :user_id do |user_id|
41
+ # shared(:user => User[user_id]) do
42
+ # r.run API
43
+ # end
44
+ # end
45
+ # end
46
+ module SharedVars
47
+ KEY = 'roda.shared'.freeze
48
+
49
+ module InstanceMethods
50
+ # Returns the current shared vars for the request. These are
51
+ # stored in the request's environment, so they will be implicitly
52
+ # shared with other apps using this plugin.
53
+ #
54
+ # If the +vars+ argument is given, it should be a hash that will be
55
+ # merged into the current shared vars.
56
+ #
57
+ # If a block is given, a +vars+ argument must be provided, and it will
58
+ # only make the changes to the shared vars for the duration of the
59
+ # block, restoring the previous shared vars before the block returns.
60
+ def shared(vars=nil)
61
+ h = env[KEY] ||= {}
62
+
63
+ if block_given?
64
+ if vars
65
+ begin
66
+ env[KEY] = h.merge(vars)
67
+ yield
68
+ ensure
69
+ env[KEY] = h
70
+ end
71
+ else
72
+ raise RodaError, "must pass a vars hash when calling shared with a block"
73
+ end
74
+ elsif vars
75
+ h.merge!(vars)
76
+ end
77
+
78
+ h
79
+ end
80
+ end
81
+ end
82
+
83
+ register_plugin(:shared_vars, SharedVars)
84
+ end
85
+ end
@@ -119,7 +119,7 @@ class Roda
119
119
  def close
120
120
  return if closed?
121
121
  @closed = true
122
- @scheduler.schedule{@callbacks.each{|c| c.call}}
122
+ @scheduler.schedule{@callbacks.each(&:call)}
123
123
  end
124
124
 
125
125
  # Whether the connection has already been closed.
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 2
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 1
7
+ RodaMinorVersion = 2
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -12,7 +12,7 @@ rescue LoadError
12
12
  warn "tilt or erubis not installed, skipping _erubis_escaping plugin test"
13
13
  else
14
14
  describe "_erubis_escaping plugin" do
15
- before do
15
+ it "should escape inside <%= %> and not inside <%== %>, and handle postfix conditionals" do
16
16
  app(:bare) do
17
17
  plugin :render, :escape=>true
18
18
 
@@ -20,10 +20,39 @@ describe "_erubis_escaping plugin" do
20
20
  render(:inline=>'<%= "<>" %> <%== "<>" %><%= "<>" if false %>')
21
21
  end
22
22
  end
23
+
24
+ body.should == '&lt;&gt; <>'
23
25
  end
24
26
 
25
- it "should escape inside <%= %> and not inside <%== %>, and handle postfix conditionals" do
27
+ it "should consider classes in :escape_safe_classes as safe" do
28
+ c = Class.new(String)
29
+ c2 = Class.new(String)
30
+ app(:bare) do
31
+ plugin :render, :escape=>true, :escape_safe_classes=>c
32
+
33
+ route do |r|
34
+ @c, @c2 = c, c2
35
+ render(:inline=>'<%= @c2.new("<>") %> <%= @c.new("<>") %>')
36
+ end
37
+ end
38
+
26
39
  body.should == '&lt;&gt; <>'
27
40
  end
41
+
42
+ it "should allow use of custom :escaper" do
43
+ escaper = Object.new
44
+ def escaper.escape_xml(s)
45
+ s.gsub("'", "''")
46
+ end
47
+ app(:bare) do
48
+ plugin :render, :escape=>true, :escaper=>escaper
49
+
50
+ route do |r|
51
+ render(:inline=>'<%= "ab\'1" %> <%== "ab\'1" %>')
52
+ end
53
+ end
54
+
55
+ body.should == "ab''1 ab'1"
56
+ end
28
57
  end
29
58
  end
@@ -7,9 +7,7 @@ rescue LoadError
7
7
  else
8
8
  describe "chunked plugin" do
9
9
  def cbody(env={})
10
- b = ''
11
- req({'HTTP_VERSION'=>'HTTP/1.1'}.merge(env))[2].each{|s| b << s}
12
- b
10
+ body({'HTTP_VERSION'=>'HTTP/1.1'}.merge(env))
13
11
  end
14
12
 
15
13
  it "streams templates in chunked encoding only if HTTP 1.1 is used" do
@@ -42,6 +40,26 @@ describe "chunked plugin" do
42
40
  body.should == "hmht"
43
41
  end
44
42
 
43
+ it "has delay accept block that is called after layout yielding but before content when streaming" do
44
+ app(:chunked) do |r|
45
+ delay do
46
+ @h << 'i'
47
+ end
48
+ @h = 'h'
49
+ chunked(:inline=>'m<%= @h %>', :layout=>{:inline=>'<%= @h %><%= yield %>t'}) do
50
+ @h << 'j'
51
+ end
52
+ end
53
+
54
+ cbody.should == "1\r\nh\r\n4\r\nmhij\r\n1\r\nt\r\n0\r\n\r\n"
55
+ body.should == "hijmhijt"
56
+ end
57
+
58
+ it "has delay raise if not given a block" do
59
+ app(:chunked){|r| delay}
60
+ proc{body}.should raise_error(Roda::RodaError)
61
+ end
62
+
45
63
  it "works when a layout is not used" do
46
64
  app(:chunked) do |r|
47
65
  chunked(:inline=>'m', :layout=>nil)
@@ -43,6 +43,20 @@ describe "default_headers plugin" do
43
43
  req[1].should == {'Content-Type'=>'text/json', 'Foo'=>'baz'}
44
44
  end
45
45
 
46
+ it "should have a default Content-Type header" do
47
+ h = {'Foo'=>'bar'}
48
+
49
+ app(:bare) do
50
+ plugin :default_headers, h
51
+
52
+ route do |r|
53
+ r.halt response.finish_with_body([])
54
+ end
55
+ end
56
+
57
+ req[1].should == {'Content-Type'=>'text/html', 'Foo'=>'bar'}
58
+ end
59
+
46
60
  it "should work correctly in subclasses" do
47
61
  h = {'Content-Type'=>'text/json', 'Foo'=>'bar'}
48
62
 
@@ -11,18 +11,7 @@ describe "padrino_render plugin" do
11
11
  plugin :padrino_render, :views=>"./spec/views"
12
12
 
13
13
  route do |r|
14
- r.is "partial" do
15
- partial("test", :locals=>{:title => "About Roda"})
16
- end
17
-
18
- r.is "partial/subdir" do
19
- partial("about/test", :locals=>{:title => "About Roda"})
20
- end
21
-
22
- r.is "partial/inline" do
23
- partial(:inline=>"Hello <%= name %>", :locals=>{:name => "Agent Smith"})
24
- end
25
-
14
+
26
15
  r.is "render" do
27
16
  render(:content=>'bar', :layout_opts=>{:locals=>{:title=>"Home"}})
28
17
  end
@@ -34,18 +23,6 @@ describe "padrino_render plugin" do
34
23
  end
35
24
  end
36
25
 
37
- it "partial renders without layout, and prepends _ to template" do
38
- body("/partial").strip.should == "<h1>About Roda</h1>"
39
- end
40
-
41
- it "partial renders without layout, and prepends _ to template" do
42
- body("/partial/subdir").strip.should == "<h1>Subdir: About Roda</h1>"
43
- end
44
-
45
- it "partial handles inline partials" do
46
- body("/partial/inline").strip.should == "Hello Agent Smith"
47
- end
48
-
49
26
  it "render uses layout by default" do
50
27
  body("/render").strip.should == "<title>Roda: Home</title>\nbar"
51
28
  end
@@ -0,0 +1,43 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ begin
4
+ require 'tilt/erb'
5
+ rescue LoadError
6
+ warn "tilt not installed, skipping partials plugin test"
7
+ else
8
+ describe "partials plugin" do
9
+ before do
10
+ app(:bare) do
11
+ plugin :partials, :views=>"./spec/views"
12
+
13
+ route do |r|
14
+ r.is "partial" do
15
+ partial("test", :locals=>{:title => "About Roda"})
16
+ end
17
+
18
+ r.is "partial/subdir" do
19
+ partial("about/test", :locals=>{:title => "About Roda"})
20
+ end
21
+
22
+ r.is "partial/inline" do
23
+ partial(:inline=>"Hello <%= name %>", :locals=>{:name => "Agent Smith"})
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+
30
+ it "partial renders without layout, and prepends _ to template" do
31
+ body("/partial").strip.should == "<h1>About Roda</h1>"
32
+ end
33
+
34
+ it "partial renders without layout, and prepends _ to template" do
35
+ body("/partial/subdir").strip.should == "<h1>Subdir: About Roda</h1>"
36
+ end
37
+
38
+ it "partial handles inline partials" do
39
+ body("/partial/inline").strip.should == "Hello Agent Smith"
40
+ end
41
+
42
+ end
43
+ end
@@ -152,6 +152,49 @@ describe "path plugin" do
152
152
  body('path'=>[@obj, 'foo', 'bar'], 'SCRIPT_NAME'=>'/baz').should == '/baz/d/1/foo/bar'
153
153
  end
154
154
 
155
+ it "Roda#path works in subclasses" do
156
+ old_app = @app
157
+ @app = Class.new(@app)
158
+ @app.route{|r| path('/a')}
159
+ body.should == '/a'
160
+
161
+ @app.path(String){|b| "/foo#{b}"}
162
+ body.should == '/foo/a'
163
+
164
+ @app = old_app
165
+ body('path'=>'/a').should == '/a'
166
+ end
167
+
168
+ it "registers classes by reference by default" do
169
+ c1 = Class.new
170
+ def c1.name; 'C'; end
171
+ c2 = Class.new
172
+ def c2.name; 'C'; end
173
+ @app.path(c1){'/c'}
174
+ @app.route{|r| path(r.env['c'])}
175
+ body('c'=>c1.new).should == '/c'
176
+ proc{body('c'=>c2.new)}.should raise_error(Roda::RodaError)
177
+ end
178
+
179
+ it ":by_name => option registers classes by name" do
180
+ c1 = Class.new
181
+ def c1.name; 'C'; end
182
+ c2 = Class.new
183
+ def c2.name; 'C'; end
184
+ @app.plugin :path, :by_name=>true
185
+ @app.path(c1){'/c'}
186
+ @app.route{|r| path(r.env['c'])}
187
+ body('c'=>c1.new).should == '/c'
188
+ body('c'=>c2.new).should == '/c'
189
+ end
190
+
191
+ it "Roda.path_block returns the block used" do
192
+ c = Class.new
193
+ b = proc{|x| x.to_s}
194
+ @app.path(c, &b)
195
+ @app.path_block(c).should == b
196
+ end
197
+
155
198
  it "Roda.path doesn't work with classes without blocks" do
156
199
  proc{app.path(Class.new)}.should raise_error(Roda::RodaError)
157
200
  end
@@ -0,0 +1,45 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "shared_vars plugin" do
4
+ it "adds shared method for sharing variables across multiple apps" do
5
+ app(:shared_vars) {|r| shared[:c]}
6
+ old_app = app
7
+ app(:shared_vars) do |r|
8
+ shared[:c] = 'c'
9
+ r.run old_app
10
+ end
11
+
12
+ body.should == 'c'
13
+ end
14
+
15
+ it "adds shared with hash merges the hash into the shared vars" do
16
+ app(:shared_vars) do |r|
17
+ shared(:c=>'c')
18
+ shared[:c]
19
+ end
20
+
21
+ body.should == 'c'
22
+ end
23
+
24
+ it "calling shared with hash and a block sets shared variables only for that block" do
25
+ app(:shared_vars) do |r|
26
+ c = nil
27
+ d = nil
28
+ shared[:c] = 'b'
29
+ shared(:c=>'c', :d=>'d') do
30
+ c = shared[:c]
31
+ d = shared[:d]
32
+ end
33
+ "#{shared[:c]}:#{shared[:d]}:#{c}:#{d}"
34
+ end
35
+
36
+ body.should == 'b::c:d'
37
+ end
38
+
39
+ it "calling shared with no arguments and a block raises an error" do
40
+ app(:shared_vars) do |r|
41
+ shared{}
42
+ end
43
+ proc{body}.should raise_error(Roda::RodaError)
44
+ end
45
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-13 00:00:00.000000000 Z
11
+ date: 2015-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -138,6 +138,7 @@ extra_rdoc_files:
138
138
  - doc/release_notes/1.3.0.txt
139
139
  - doc/release_notes/2.0.0.txt
140
140
  - doc/release_notes/2.1.0.txt
141
+ - doc/release_notes/2.2.0.txt
141
142
  files:
142
143
  - CHANGELOG
143
144
  - MIT-LICENSE
@@ -150,6 +151,7 @@ files:
150
151
  - doc/release_notes/1.3.0.txt
151
152
  - doc/release_notes/2.0.0.txt
152
153
  - doc/release_notes/2.1.0.txt
154
+ - doc/release_notes/2.2.0.txt
153
155
  - lib/roda.rb
154
156
  - lib/roda/plugins/_erubis_escaping.rb
155
157
  - lib/roda/plugins/all_verbs.rb
@@ -190,12 +192,14 @@ files:
190
192
  - lib/roda/plugins/not_found.rb
191
193
  - lib/roda/plugins/padrino_render.rb
192
194
  - lib/roda/plugins/param_matchers.rb
195
+ - lib/roda/plugins/partials.rb
193
196
  - lib/roda/plugins/pass.rb
194
197
  - lib/roda/plugins/path.rb
195
198
  - lib/roda/plugins/path_matchers.rb
196
199
  - lib/roda/plugins/per_thread_caching.rb
197
200
  - lib/roda/plugins/render.rb
198
201
  - lib/roda/plugins/render_each.rb
202
+ - lib/roda/plugins/shared_vars.rb
199
203
  - lib/roda/plugins/sinatra_helpers.rb
200
204
  - lib/roda/plugins/slash_path_empty.rb
201
205
  - lib/roda/plugins/static.rb
@@ -255,12 +259,14 @@ files:
255
259
  - spec/plugin/not_found_spec.rb
256
260
  - spec/plugin/padrino_render_spec.rb
257
261
  - spec/plugin/param_matchers_spec.rb
262
+ - spec/plugin/partials_spec.rb
258
263
  - spec/plugin/pass_spec.rb
259
264
  - spec/plugin/path_matchers_spec.rb
260
265
  - spec/plugin/path_spec.rb
261
266
  - spec/plugin/per_thread_caching_spec.rb
262
267
  - spec/plugin/render_each_spec.rb
263
268
  - spec/plugin/render_spec.rb
269
+ - spec/plugin/shared_vars_spec.rb
264
270
  - spec/plugin/sinatra_helpers_spec.rb
265
271
  - spec/plugin/slash_path_empty_spec.rb
266
272
  - spec/plugin/static_spec.rb