roda 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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