roda 2.16.0 → 2.17.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: c3051f95d0665c935a0aa391b7f5b3d4ec3905ec
4
- data.tar.gz: c3a81d9c6384dc8708c941fcb8171295fcaff6bc
3
+ metadata.gz: f728447f69ff9f50896fb6dca7a0e709f2df26e1
4
+ data.tar.gz: 521dd8fa68fe85421eab4d410619991da0655cb8
5
5
  SHA512:
6
- metadata.gz: e6479fd6fffd9fdcefc3c09e3543142668b7cd15b3f52af8fda896d438a174b54c1a8350c5aea6a5745188f5eaec78b5fe3a11405365a1318ff38a0098acd307
7
- data.tar.gz: 1a837206ad2a4a158e5c939c0a52107752f3e9c9108255e843db681c1f2a62265ee5ca54ba96071b441ca760de7ee2871cf58822ce9f12ed704fa283fcb786b2
6
+ metadata.gz: 150c34299dd44322d1d36523f83aa948a61740b8b0bc5374de85ce94feb67ab9ced97262b7982330aa91d1433362ace6bab7a1924689a3dd99b3576db478fdc2
7
+ data.tar.gz: 68d485ef5b650992d224ac98af823153d25bc7c4ff9fb71d087a44f7c792f9a2431d2d93722433ebf553ad3f387d74848125f88144ef88dad52dfdaf50dab13b
data/CHANGELOG CHANGED
@@ -1,3 +1,17 @@
1
+ = 2.17.0 (2016-08-13)
2
+
3
+ * Add :postprocessor option to assets plugin, for postprocessing assets (e.g. autoprefixing CSS) (celsworth) (#86)
4
+
5
+ * Fix path passed to rack apps when using r.run and the type_routing plugin (jeremyevans) (#82)
6
+
7
+ * Support :classes option to error_handler plugin for overriding which exception classes to rescue (jeremyevans)
8
+
9
+ * Support :layout_opts=>:merge_locals option in render plugin for merging view template locals into layout template locals (jeremyevans) (#80)
10
+
11
+ * Support :sri option to assets plugin to enable subresource integrity (jeremyevans)
12
+
13
+ * Add run_append_slash plugin, so r.run uses "/" instead of "" for app's PATH_INFO (kenaniah) (#77)
14
+
1
15
  = 2.16.0 (2016-07-13)
2
16
 
3
17
  * Add type_routing plugin, for routing based on path extensions and Accept headers (Papierkorb, jeremyevans) (#75)
@@ -46,7 +46,7 @@ to get the default behavior.
46
46
 
47
47
  Roda has low per-request overhead, and the use of a routing tree
48
48
  and intelligent caching of internal datastructures makes it
49
- significantly faster than popular ruby web frameworks.
49
+ significantly faster than other popular ruby web frameworks.
50
50
 
51
51
  == Usage
52
52
 
@@ -443,6 +443,15 @@ via the +status+ attribute for the response.
443
443
  end
444
444
  end
445
445
 
446
+ When redirecting, the response will use a 302 status code by default.
447
+ You can change this by passing a second argument to +r.redirect+:
448
+
449
+ route do |r|
450
+ r.get "hello" do
451
+ r.redirect "/other", 301 # use 301 Moved Permanently
452
+ end
453
+ end
454
+
446
455
  == Verb Methods
447
456
 
448
457
  As displayed above, Roda has +r.get+ and +r.post+ methods
@@ -601,8 +610,8 @@ and still have access to them inside the +api+ +route+ block.
601
610
 
602
611
  It is very easy to test Roda with {Rack::Test}[https://github.com/brynary/rack-test]
603
612
  or {Capybara}[https://github.com/jnicklas/capybara].
604
- Roda's own tests use {RSpec}[http://rspec.info].
605
- The default Rake task will run the specs for Roda, if RSpec is installed.
613
+ Roda's own tests use {minitest/spec}[https://github.com/seattlerb/minitest].
614
+ The default Rake task will run the specs for Roda.
606
615
 
607
616
  == Settings
608
617
 
@@ -743,7 +752,7 @@ to escape by default, so that in your templates:
743
752
  <%= '<>' %> # outputs &lt;&gt;
744
753
  <%== '<>' %> # outputs <>
745
754
 
746
- When using the +:escape_option, you will need to ensure that your layouts
755
+ When using the +:escape+ option, you will need to ensure that your layouts
747
756
  are not escaping the output of the content template:
748
757
 
749
758
  <%== yield %> # not <%= yield %>
@@ -809,15 +818,50 @@ Roda 3. Note that when specifying the +:path+ option when rendering a template,
809
818
  not check paths, as it assumes that users and libraries that use this option will be checking
810
819
  such paths manually.
811
820
 
812
- == Reloading
813
-
814
- Most rack-based reloaders will work with Roda, including:
815
-
816
- * {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader]
817
- * {rerun}[https://github.com/alexch/rerun]
818
- * {shotgun}[https://github.com/rtomayko/shotgun]
819
- * {Rack::Reloader}[https://github.com/rack/rack/blob/master/lib/rack/reloader.rb]
820
- * {pistol}[https://github.com/monk/pistol]
821
+ == Code Reloading
822
+
823
+ Roda does not ship with integrated support for code reloading, as it is a toolkit and not a
824
+ framework, but there are rack-based reloaders that will work with Roda apps.
825
+
826
+ For most applications, {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader]
827
+ is probably the fastest approach to reloading while still being fairly safe, as it
828
+ reloads just files that have been modified, and unloads constants defined in the files
829
+ before reloading them. However, it requires modifying your application code to use
830
+ rack-unreloader specific APIs.
831
+
832
+ A similar solution that reloads files and unloads constants is ActiveSupport::Dependencies.
833
+ ActiveSupport::Dependencies doesn't require modifying your application code, but it modifies
834
+ some core methods, including +require+ and +const_missing+. It requires less configuration,
835
+ but depends that you follow Rails' file and class naming conventions. It also provides
836
+ autoloading (on the fly) of files when a missing constant is accessed. If your application
837
+ does not rely on autoloading then +require_dependency+ must be used to require the dependencies
838
+ or they won't be reloaded.
839
+
840
+ {AutoReloader}[https://github.com/rosenfeld/auto_reloader] provides transparent reloading for
841
+ all files reached from one of the +reloadable_paths+ option entries, by detecting new top-level
842
+ constants and removing them when any of the reloadable loaded files changes. It overrides
843
+ +require+ and +require_relative+ when activated (usually in the development environment). No
844
+ configurations other than +reloadable_paths+ are required.
845
+
846
+ Both {rerun}[https://github.com/alexch/rerun] and
847
+ {shotgun}[https://github.com/rtomayko/shotgun] use a fork/exec approach for loading new
848
+ versions of your app. rerun is faster as it only reloads the app on changes, whereas
849
+ shotgun reloads the app on every request. Both work without any changes to application
850
+ code, but may be slower as they have to reload the entire application on every change.
851
+ However, for small apps that load quickly, either may be a good approach.
852
+
853
+ {Rack::Reloader}[https://github.com/rack/rack/blob/master/lib/rack/reloader.rb] ships
854
+ with rack and just reloads monitored files when they change, without unloading constants.
855
+ It's fast but may cause issues in cases where you remove classes, constants, or methods,
856
+ or when you are not clearing out cached data manually when files are reloaded.
857
+
858
+ There is no one reloading solution that is the best for all applications and development
859
+ approaches. Consider your needs and the the tradeoffs of each of the reloading approaches,
860
+ and pick the one you think will work best.
861
+
862
+ If you are unsure where to start, it may be best to start with rerun or shotgun
863
+ (unless you're running on JRuby or Windows), and only consider other options if rerun or
864
+ shotgun are not fast enough.
821
865
 
822
866
  == Plugins
823
867
 
@@ -0,0 +1,62 @@
1
+ = New Plugins
2
+
3
+ * A run_append_slash plugin has been added, which automatically uses
4
+ "/" as the path instead of an empty path when calling rack
5
+ applications with r.run:
6
+
7
+ route do |r|
8
+ r.on "a" do
9
+ r.run App
10
+ end
11
+ end
12
+
13
+ # without run_append_slash:
14
+ # GET /a => App gets "" as PATH_INFO
15
+ # GET /a/ => App gets "/" as PATH_INFO
16
+
17
+ # with run_append_slash:
18
+ # GET /a => App gets "/" as PATH_INFO
19
+ # GET /a/ => App gets "/" as PATH_INFO
20
+
21
+ By default, the path is modified directly, but if you want to
22
+ redirect instead, you can pass a :use_redirects option when loading
23
+ the plugin.
24
+
25
+ = New Features
26
+
27
+ * The assets plugin now supports an :sri option to enable subresource
28
+ integrity on the link/script tags generated by the assets helper.
29
+ This option takes either :sha256, :sha384, or :sha512 values,
30
+ specifying the hash algorithm to use. Roda 3 will default to using
31
+ :sri => :sha256.
32
+
33
+ * The assets plugin now supports a :postprocessor option, which should
34
+ be a callable object. If the option is given, it will be called with
35
+ the filename, type, and rendered asset output of the file (CSS/JS),
36
+ and should return the postprocessed content to use. This allows any
37
+ type of custom postprocessing to be done, such as CSS autoprefixing.
38
+
39
+ * The render plugin now supports a :layout_opts=>:merge_locals option,
40
+ which will automatically merge view template locals into layout
41
+ template locals. This is useful if you want to use the same local
42
+ variable for both templates.
43
+
44
+ * The error_handler plugin now supports a :classes option, allowing
45
+ you to override which exception classes to handle. This allows it
46
+ to be used with libraries which use exception classes that
47
+ subclass from Exception instead of StandardError.
48
+
49
+ = Other Improvements
50
+
51
+ * The type_routing plugin now works correctly when using r.run.
52
+ Previously, if the type routing plugin recognized and removed
53
+ the file extension used in the requested path, it would not
54
+ add the file extension back to the path when passing the request
55
+ to another rack app.
56
+
57
+ = Backwards Compatibility
58
+
59
+ * In the assets plugin, Roda::RodaRequest.assets_matchers now uses
60
+ symbols instead of strings as the first argument in each entry.
61
+ This should not affect you unless you were accessing the values
62
+ directly.
@@ -182,6 +182,26 @@ class Roda
182
182
  # and it will run in compiled mode, assuming that the compiled asset files
183
183
  # already exist.
184
184
  #
185
+ # ==== Postprocessing
186
+ #
187
+ # If you pass a callable object to the :postprocessor option, it will be called
188
+ # before an asset is served.
189
+ # If the assets are to be compiled, the object will be called at compilation time.
190
+ #
191
+ # It is passed three arguments; the name of the asset file, the type of the
192
+ # asset file (which is a symbol, either :css or :js), and the asset contents.
193
+ #
194
+ # It should return the new content for the asset.
195
+ #
196
+ # You can use this to call Autoprefixer on your CSS:
197
+ #
198
+ # plugin :assets, {
199
+ # :css => [ 'style.scss' ],
200
+ # :postprocessor => lambda do |file, type, content|
201
+ # type == :css ? AutoprefixerRails.process(content).css : content
202
+ # end
203
+ # }
204
+ #
185
205
  # ==== On Heroku
186
206
  #
187
207
  # Heroku supports precompiling the assets when using Roda. You just need to
@@ -245,12 +265,19 @@ class Roda
245
265
  # :js_route :: Route under :prefix for javascript assets (default: :js_dir)
246
266
  # :path :: Path to your asset source directory (default: 'assets'). Relative
247
267
  # paths will be considered relative to the application's :root option.
268
+ # :postprocessor :: A block which should accept three arguments (asset name, asset type,
269
+ # content). This block can be used to hook into the asset system and
270
+ # make your own modifications before the asset is served. If the asset
271
+ # is to be compiled, the block is called at compile time.
248
272
  # :prefix :: Prefix for assets path in your URL/routes (default: 'assets')
249
273
  # :precompiled :: Path to the compiled asset metadata file. If the file exists, will use compiled
250
274
  # mode using the metadata in the file. If the file does not exist, will use
251
275
  # non-compiled mode, but will write the metadata to the file if compile_assets is called.
252
276
  # :public :: Path to your public folder, in which compiled files are placed (default: 'public'). Relative
253
277
  # paths will be considered relative to the application's :root option.
278
+ # :sri :: Enables subresource integrity when setting up references to compiled assets. The value should be
279
+ # :sha256, :sha384, or :sha512 depending on which hash algorithm you want to use. This changes the
280
+ # hash algorithm that Roda will use when naming compiled asset files.
254
281
  module Assets
255
282
  DEFAULTS = {
256
283
  :compiled_name => 'app'.freeze,
@@ -546,8 +573,15 @@ class Roda
546
573
  # a different digest type or to return a static string if you don't
547
574
  # want to use a unique value.
548
575
  def asset_digest(content)
549
- require 'digest/sha1'
550
- ::Digest::SHA1.hexdigest(content)
576
+ klass = if algo = assets_opts[:sri]
577
+ require 'digest/sha2'
578
+ ::Digest.const_get(algo.to_s.upcase)
579
+ else
580
+ require 'digest/sha1'
581
+ ::Digest::SHA1
582
+ end
583
+
584
+ klass.hexdigest(content)
551
585
  end
552
586
  end
553
587
 
@@ -566,9 +600,9 @@ class Roda
566
600
  o = self.class.assets_opts
567
601
  type, *dirs = type if type.is_a?(Array)
568
602
  stype = type.to_s
603
+ ru = Rack::Utils
569
604
 
570
605
  attrs = if attrs
571
- ru = Rack::Utils
572
606
  attrs.map{|k,v| "#{k}=\"#{ru.escape_html(v.to_s)}\""}.join(SPACE)
573
607
  else
574
608
  EMPTY_STRING
@@ -590,15 +624,19 @@ class Roda
590
624
  if dirs && !dirs.empty?
591
625
  key = dirs.join(DOT)
592
626
  ckey = "#{stype}.#{key}"
593
- if ukey = compiled[ckey]
627
+ if hash = ukey = compiled[ckey]
594
628
  ukey = "#{key}.#{ukey}"
595
629
  end
596
630
  else
597
- ukey = compiled[stype]
631
+ hash = ukey = compiled[stype]
598
632
  end
599
633
 
600
634
  if ukey
601
- "#{tag_start}#{asset_host}#{url_prefix}/#{o[:"compiled_#{stype}_prefix"]}.#{ukey}.#{stype}#{tag_end}"
635
+ if algo = o[:sri]
636
+ integrity = "\" integrity=\"#{algo}-#{ru.escape_html([[hash].pack('H*')].pack('m').tr("\n", EMPTY_STRING))}"
637
+ end
638
+
639
+ "#{tag_start}#{asset_host}#{url_prefix}/#{o[:"compiled_#{stype}_prefix"]}.#{ukey}.#{stype}#{integrity}#{tag_end}"
602
640
  end
603
641
  else
604
642
  asset_dir = o[type]
@@ -640,11 +678,15 @@ class Roda
640
678
  # Otherwise, render the file using the render plugin. +file+ should be
641
679
  # the relative path to the file from the current directory.
642
680
  def read_asset_file(file, type)
643
- if file.end_with?(".#{type}")
681
+ o = self.class.assets_opts
682
+
683
+ content = if file.end_with?(".#{type}")
644
684
  ::File.read(file)
645
685
  else
646
- render_asset_file(file, :template_opts=>self.class.assets_opts[:"#{type}_opts"])
686
+ render_asset_file(file, :template_opts=>o[:"#{type}_opts"])
647
687
  end
688
+
689
+ o[:postprocessor] ? o[:postprocessor].call(file, type, content) : content
648
690
  end
649
691
 
650
692
  private
@@ -680,7 +722,7 @@ class Roda
680
722
  # handled.
681
723
  def assets_matchers
682
724
  @assets_matchers ||= [:css, :js].map do |t|
683
- [t.to_s.freeze, assets_regexp(t)].freeze if roda_class.assets_opts[t]
725
+ [t, assets_regexp(t)].freeze if roda_class.assets_opts[t]
684
726
  end.compact.freeze
685
727
  end
686
728
 
@@ -29,10 +29,21 @@ class Roda
29
29
  # The error handler can change the response status if necessary,
30
30
  # as well set headers and/or write to the body, just like a regular
31
31
  # request.
32
+ #
33
+ #
34
+ # By default, this plugin handles StandardError and ScriptError.
35
+ # To override the exception classes it will handle, pass a :classes
36
+ # option to the plugin:
37
+ #
38
+ # plugin :error_handler, :classes=>[StandardError, ScriptError, NoMemoryError]
32
39
  module ErrorHandler
40
+ DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze
41
+
33
42
  # If a block is given, automatically call the +error+ method on
34
43
  # the Roda class with it.
35
- def self.configure(app, &block)
44
+ def self.configure(app, opts={}, &block)
45
+ app.opts[:error_handler_classes] = (opts[:classes] || app.opts[:error_handler_classes] || DEFAULT_ERROR_HANDLER_CLASSES).dup.freeze
46
+
36
47
  if block
37
48
  app.error(&block)
38
49
  end
@@ -53,7 +64,7 @@ class Roda
53
64
  # the error handler.
54
65
  def call
55
66
  super
56
- rescue StandardError, ScriptError => e
67
+ rescue *opts[:error_handler_classes] => e
57
68
  res = @_response = self.class::RodaResponse.new
58
69
  res.status = 500
59
70
  super{handle_error(e)}
@@ -15,15 +15,16 @@ class Roda
15
15
  #
16
16
  # plugin :match_affix, ""
17
17
  #
18
- # will load the plugin and use an empty prefix.
18
+ # will load the plugin and use an empty prefix (instead of a slash).
19
19
  #
20
20
  # plugin :match_affix, "", /(\/|\z)/
21
21
  #
22
22
  # will use an empty prefix and change the suffix to consume a trailing slash.
23
23
  #
24
- # plugin :match_affix, nil, /(\/|\z)/
24
+ # plugin :match_affix, nil, /(?:\/\z|(?=\/|\z))/
25
25
  #
26
- # will not modify the prefix and will change the suffix to consume a trailing slash.
26
+ # will not modify the prefix and will change the suffix so that it consumes a trailing slash
27
+ # at the end of the path only.
27
28
  module MatchAffix
28
29
  PREFIX = "/".freeze
29
30
  SUFFIX = "(?=\/|\z)".freeze
@@ -36,9 +36,9 @@ class Roda
36
36
  # Patterns can be rewritten dynamically by providing a block accepting a MatchData
37
37
  # object and evaluating to the replacement.
38
38
  #
39
- # rewrite_path(/\A\/a/(\w+)/){|match| match[1].capitalize}
39
+ # rewrite_path(/\A\/a/(\w+)/){|match| "/a/#{match[1].capitalize}"}
40
40
  # # PATH_INFO '/a/moo' => remaining_path '/a/Moo'
41
- # rewrite_path(/\A\/a/(\w+)/, :path_info => true){|match| match[1].capitalize}
41
+ # rewrite_path(/\A\/a/(\w+)/, :path_info => true){|match| "/a/#{match[1].capitalize}"}
42
42
  # # PATH_INFO '/a/moo' => PATH_INFO '/a/Moo'
43
43
  #
44
44
  # All path rewrites are applied in order, so if a path is rewritten by one rewrite,
@@ -20,6 +20,24 @@ class Roda
20
20
  # end
21
21
  # end
22
22
  #
23
+ # The +render+ and +view+ methods just return strings, they do not have
24
+ # side effects (unless the templates themselves have side effects).
25
+ # As Roda uses the routing block return value as the body of the response,
26
+ # in most cases you will call these methods as the last expression in a
27
+ # routing block # to have the response body be the result of the template
28
+ # rendering.
29
+ #
30
+ # Because +render+ and +view+ just return strings, you can call them inside
31
+ # templates (i.e. for subtemplates/partials), or multiple times in the
32
+ # same route and combine the results together:
33
+ #
34
+ # route do |r|
35
+ # r.is 'foo-bars' do
36
+ # @bars = Bar.where(:foo).map{|b| render(:bar, :locals=>{:bar=>b})}.join
37
+ # view('foo')
38
+ # end
39
+ # end
40
+ #
23
41
  # You can provide options to the plugin method:
24
42
  #
25
43
  # plugin :render, :engine=>'haml', :views=>'admin_views'
@@ -46,9 +64,12 @@ class Roda
46
64
  # :escaper :: Object used for escaping output of <tt><%= %></tt>, when :escape is used,
47
65
  # overriding the default. If given, object should respond to +escape_xml+ with
48
66
  # a single argument and return an output string.
49
- # :layout :: The base name of the layout file, defaults to 'layout'.
67
+ # :layout :: The base name of the layout file, defaults to 'layout'. This can be provided as a hash
68
+ # with the :template or :inline options.
50
69
  # :layout_opts :: The options to use when rendering the layout, if different
51
- # from the default options.
70
+ # from the default options. To pass local variables to the layout, include a :locals
71
+ # option inside :layout_opts. To automatically merge the view template locals into
72
+ # the layout template locals, include a :merge_locals option inside :layout_opts.
52
73
  # :template_opts :: The tilt options used when rendering all templates. defaults to:
53
74
  # <tt>{:outvar=>'@_out_buf', :default_encoding=>Encoding.default_external}</tt>.
54
75
  # :engine_opts :: The tilt options to use per template engine. Keys are
@@ -154,6 +175,11 @@ class Roda
154
175
 
155
176
  opts[:layout_opts] = (opts[:layout_opts] || {}).dup
156
177
  opts[:layout_opts][:_is_layout] = true
178
+
179
+ if opts[:layout_opts][:merge_locals] && opts[:locals]
180
+ opts[:layout_opts][:locals] = opts[:locals].merge(opts[:layout_opts][:locals] || {})
181
+ end
182
+
157
183
  if layout = opts.fetch(:layout, true)
158
184
  opts[:layout] = true unless opts.has_key?(:layout)
159
185
 
@@ -373,16 +399,31 @@ class Roda
373
399
  # used, return nil.
374
400
  def view_layout_opts(opts)
375
401
  if layout = opts.fetch(:layout, render_opts[:layout])
402
+
376
403
  layout_opts = render_layout_opts
377
- if l_opts = opts[:layout_opts]
378
- if (l_locals = l_opts[:locals]) && (layout_locals = layout_opts[:locals])
379
- set_locals = Hash[layout_locals].merge!(l_locals)
380
- end
381
- layout_opts.merge!(l_opts)
382
- if set_locals
383
- layout_opts[:locals] = set_locals
384
- end
404
+ merge_locals = layout_opts[:merge_locals]
405
+
406
+ if method_layout_opts = opts[:layout_opts]
407
+ method_layout_locals = method_layout_opts[:locals]
408
+ merge_locals = method_layout_opts[:merge_locals] if method_layout_opts.has_key?(:merge_locals)
409
+ end
410
+
411
+ locals = {}
412
+ if merge_locals && (plugin_locals = render_opts[:locals])
413
+ locals.merge!(plugin_locals)
385
414
  end
415
+ if layout_locals = layout_opts[:locals]
416
+ locals.merge!(layout_locals)
417
+ end
418
+ if merge_locals && (method_locals = opts[:locals])
419
+ locals.merge!(method_locals)
420
+ end
421
+ if method_layout_locals
422
+ locals.merge!(method_layout_locals)
423
+ end
424
+
425
+ layout_opts.merge!(method_layout_opts) if method_layout_opts
426
+ layout_opts[:locals] = locals unless locals.empty?
386
427
 
387
428
  case layout
388
429
  when Hash
@@ -0,0 +1,52 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The run_append_slash plugin makes +r.run+ use +/+ as the +PATH_INFO+
7
+ # when calling the rack application if +PATH_INFO+ would be empty.
8
+ # Example:
9
+ #
10
+ # route do |r|
11
+ # r.on "a" do
12
+ # r.run App
13
+ # end
14
+ # end
15
+ #
16
+ # # without run_append_slash:
17
+ # # GET /a => App gets "" as PATH_INFO
18
+ # # GET /a/ => App gets "/" as PATH_INFO
19
+ #
20
+ # # with run_append_slash:
21
+ # # GET /a => App gets "/" as PATH_INFO
22
+ # # GET /a/ => App gets "/" as PATH_INFO
23
+ module RunAppendSlash
24
+ OPTS = {}.freeze
25
+ # Set plugin specific options. Options:
26
+ # :use_redirects :: Whether to issue 302 redirects when appending the
27
+ # trailing slash.
28
+ def self.configure(app, opts=OPTS)
29
+ app.opts[:run_append_slash_redirect] = !!opts[:use_redirects]
30
+ end
31
+
32
+ module RequestMethods
33
+ # Calls the given rack app. If the path matches the root of the app but
34
+ # does not contain a trailing slash, a trailing slash is appended to the
35
+ # path internally, or a redirect is issued when configured with
36
+ # <tt>use_redirects: true</tt>.
37
+ def run(app)
38
+ if remaining_path.empty?
39
+ if scope.opts[:run_append_slash_redirect]
40
+ redirect("#{path}/")
41
+ else
42
+ @remaining_path += '/'
43
+ end
44
+ end
45
+ super
46
+ end
47
+ end
48
+ end
49
+
50
+ register_plugin(:run_append_slash, RunAppendSlash)
51
+ end
52
+ end
@@ -159,6 +159,16 @@ class Roda
159
159
  @requested_type ||= opts[:default_type]
160
160
  end
161
161
 
162
+ # Add the type routing extension back to the remaining path
163
+ # if it was removed from the path when the application was
164
+ # initialized.
165
+ def run(_)
166
+ if defined?(@type_routing_extension)
167
+ @remaining_path += ".#{@type_routing_extension}"
168
+ end
169
+ super
170
+ end
171
+
162
172
  private
163
173
 
164
174
  # Removes a trailing file extension from the path, and sets
@@ -169,7 +179,7 @@ class Roda
169
179
 
170
180
  if opts[:use_extension]
171
181
  if m = path.match(opts[:extension_regexp])
172
- @requested_type = m[2].to_sym
182
+ @type_routing_extension = @requested_type = m[2].to_sym
173
183
  path = m[1]
174
184
  end
175
185
  end
@@ -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 = 16
7
+ RodaMinorVersion = 17
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -54,6 +54,7 @@ if run_tests
54
54
  after(:all) do
55
55
  FileUtils.rm_r('spec/assets/tmp') if File.directory?('spec/assets/tmp')
56
56
  FileUtils.rm_r('spec/public') if File.directory?('spec/public')
57
+ FileUtils.rm(Dir['spec/assets/app.*.{js,css}*'])
57
58
  end
58
59
 
59
60
  def gunzip(body)
@@ -266,6 +267,31 @@ if run_tests
266
267
  js.must_include('console.log')
267
268
  end
268
269
 
270
+ [[:sha256, 64, 44], [:sha384, 96, 64], [:sha512, 128, 88]].each do |algo, hex_length, base64_length|
271
+ it 'should handle :sri option for subresource integrity when compiling assets' do
272
+ app.plugin :assets, :sri=>algo
273
+ app.compile_assets
274
+ html = body('/test')
275
+ html.scan(/<link/).length.must_equal 1
276
+ html =~ %r|integrity="#{algo}-([^"]+)" />|
277
+ css_hash = $1.gsub('&#x2F;', '/')
278
+ css_hash.length.must_equal base64_length
279
+ html =~ %r|href="(/assets/app\.[a-f0-9]{#{hex_length}}\.css)"|
280
+ css = body($1)
281
+ [Digest.const_get(algo.to_s.upcase).digest(css)].pack('m').tr("\n", "").must_equal css_hash
282
+ html.scan(/<script/).length.must_equal 1
283
+ html =~ %r|integrity="#{algo}-([^"]+)"></script>|
284
+ js_hash = $1.gsub('&#x2F;', '/')
285
+ js_hash.length.must_equal base64_length
286
+ html =~ %r|src="(/assets/app\.head\.[a-f0-9]{#{hex_length}}\.js)"|
287
+ js = body($1)
288
+ [Digest.const_get(algo.to_s.upcase).digest(js)].pack('m').tr("\n", "").must_equal js_hash
289
+ css.must_match(/color: ?red/)
290
+ css.must_match(/color: ?blue/)
291
+ js.must_include('console.log')
292
+ end
293
+ end
294
+
269
295
  it 'should handle linking to compiled assets when a compiled asset host is used' do
270
296
  app.plugin :assets, :compiled_asset_host=>'https://cdn.example.com'
271
297
  app.compile_assets
@@ -519,7 +545,7 @@ if run_tests
519
545
  a = app::RodaRequest.assets_matchers
520
546
  a.length.must_equal 1
521
547
  a.first.length.must_equal 2
522
- a.first.first.must_equal 'js'
548
+ a.first.first.must_equal :js
523
549
  'assets/js/head/app.js'.must_match a.first.last
524
550
  'assets/js/head/app2.js'.wont_match a.first.last
525
551
  end
@@ -529,6 +555,30 @@ if run_tests
529
555
  app::RodaRequest.assets_matchers.must_equal []
530
556
  end
531
557
 
558
+ it 'should support :postprocessor option' do
559
+ postprocessor = lambda do |file, type, content|
560
+ "file=#{file} type=#{type} tc=#{type.class} #{content.sub('color', 'font')}"
561
+ end
562
+
563
+ app.plugin :assets, :path=>'spec', :js_dir=>nil, :css_dir=>nil, :prefix=>nil,
564
+ :postprocessor=>postprocessor,
565
+ :css=>{:assets=>{:css=>%w'app.scss'}}
566
+ app.route do |r|
567
+ r.assets
568
+ r.is 'test' do
569
+ "#{assets([:css, :assets, :css])}"
570
+ end
571
+ end
572
+ html = body('/test')
573
+ html.scan(/<link/).length.must_equal 1
574
+ html =~ %r{href="(/assets/css/app\.scss)"}
575
+ css = body($1)
576
+ css.must_match(/app\.scss/)
577
+ css.must_match(/type=css/)
578
+ css.must_match(/tc=Symbol/)
579
+ css.must_match(/font: red;/)
580
+ end
581
+
532
582
  it 'should support :precompiled option' do
533
583
  app.plugin :assets, :precompiled=>metadata_file
534
584
  File.exist?(metadata_file).must_equal false
@@ -47,6 +47,31 @@ describe "error_handler plugin" do
47
47
  status.must_equal 500
48
48
  end
49
49
 
50
+ it "executes on custom exception classes" do
51
+ app(:bare) do
52
+ plugin :error_handler, :classes=>[StandardError]
53
+
54
+ error do |e|
55
+ e.message
56
+ end
57
+
58
+ route do |r|
59
+ r.on "a" do
60
+ raise 'foo'
61
+ end
62
+
63
+ raise SyntaxError, 'bad idea'
64
+ end
65
+ end
66
+
67
+ proc{body}.must_raise SyntaxError
68
+ body("/a").must_equal 'foo'
69
+
70
+ @app = Class.new(@app)
71
+ proc{body}.must_raise SyntaxError
72
+ body("/a").must_equal 'foo'
73
+ end
74
+
50
75
  it "can override status inside error block" do
51
76
  app(:bare) do
52
77
  plugin :error_handler do |e|
@@ -19,4 +19,25 @@ describe "match_affix plugin" do
19
19
  body("/albums/a/1").must_equal 'albums-/'
20
20
  body("/albums/b/1").must_equal 'b-/-1-""'
21
21
  end
22
+
23
+ it "handles extra trailing slash only" do
24
+ app(:bare) do
25
+ plugin :match_affix, nil, /(?:\/\z|(?=\/|\z))/
26
+
27
+ route do |r|
28
+ r.on "albums" do
29
+ r.on "b" do
30
+ "albums/b:#{r.remaining_path}"
31
+ end
32
+
33
+ "albums:#{r.remaining_path}"
34
+ end
35
+ end
36
+ end
37
+
38
+ body("/albums/a").must_equal 'albums:/a'
39
+ body("/albums/a/").must_equal 'albums:/a/'
40
+ body("/albums/b").must_equal 'albums/b:'
41
+ body("/albums/b/").must_equal 'albums/b:'
42
+ end
22
43
  end
@@ -68,6 +68,34 @@ describe "render plugin" do
68
68
  end
69
69
  end
70
70
 
71
+ describe "render plugin with :layout_opts=>{:merge_locals=>true}" do
72
+ h = {:a=>1, :b=>2, :c=>3, :d=>4}
73
+
74
+ before do
75
+ app(:bare) do
76
+ plugin :render, :views=>"./spec/views", :check_paths=>true, :locals=>{:a=>1, :b=>2, :c=>3, :d=>4, :e=>5}, :layout_opts=>{:inline=>'<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>|<%= f %>|<%= yield %>', :merge_locals=>true, :locals=>{:a=>-1, :f=>6}}
77
+
78
+ route do |r|
79
+ r.on "base" do
80
+ view(:inline=>'(<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>)')
81
+ end
82
+ r.on "override" do
83
+ view(:inline=>'(<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>)', :locals=>{:b=>-2, :d=>-4, :f=>-6}, :layout_opts=>{:locals=>{:d=>0, :c=>-3, :e=>-5}})
84
+ end
85
+ r.on "no_merge" do
86
+ view(:inline=>'(<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>)', :locals=>{:b=>-2, :d=>-4, :f=>-6}, :layout_opts=>{:merge_locals=>false, :locals=>{:d=>0, :c=>-3, :e=>-5}})
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ it "should choose method opts before plugin opts, and layout specific before locals" do
93
+ body("/base").must_equal '-1|2|3|4|5|6|(1|2|3|4|5)'
94
+ body("/override").must_equal '-1|-2|-3|0|-5|-6|(1|-2|3|-4|5)'
95
+ body("/no_merge").must_equal '-1|2|-3|0|-5|6|(1|-2|3|-4|5)'
96
+ end
97
+ end
98
+
71
99
  describe "render plugin" do
72
100
  it "simple layout support" do
73
101
  app(:bare) do
@@ -0,0 +1,77 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "run_append_slash plugin" do
4
+ before do
5
+ sub2 = app do |r|
6
+ r.root do
7
+ 'sub-bar-root'
8
+ end
9
+
10
+ r.get 'baz' do
11
+ 'sub-bar-baz'
12
+ end
13
+ end
14
+
15
+ sub1 = app(:run_append_slash) do |r|
16
+ r.root do
17
+ 'sub-root'
18
+ end
19
+
20
+ r.get 'foo' do
21
+ 'sub-foo'
22
+ end
23
+
24
+ r.on 'bar' do
25
+ r.run sub2
26
+ end
27
+ end
28
+
29
+ app(:bare) do
30
+ route do |r|
31
+ r.root do
32
+ 'root'
33
+ end
34
+
35
+ r.on 'sub' do
36
+ r.run sub1
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ it "internally appends a missing trailing slash to #run sub apps" do
43
+ # Without append slash
44
+ body.must_equal 'root'
45
+ status('/sub').must_equal 404
46
+ body('/sub/').must_equal 'sub-root'
47
+ body('/sub/foo').must_equal 'sub-foo'
48
+ status('/sub/foo/').must_equal 404
49
+ body('/sub/bar/').must_equal 'sub-bar-root'
50
+ body('/sub/bar/baz').must_equal 'sub-bar-baz'
51
+ status('/sub/bar/baz/').must_equal 404
52
+
53
+ # With append slash
54
+ app.plugin :run_append_slash
55
+ body('/sub').must_equal 'sub-root'
56
+ body('/sub/').must_equal 'sub-root'
57
+ body('/sub/foo').must_equal 'sub-foo'
58
+ status('/sub/foo/').must_equal 404
59
+ body('/sub/bar').must_equal 'sub-bar-root'
60
+ body('/sub/bar/').must_equal 'sub-bar-root'
61
+ body('/sub/bar/baz').must_equal 'sub-bar-baz'
62
+ status('/sub/bar/baz/').must_equal 404
63
+ end
64
+
65
+ it "redirects #run sub apps when trailing slash is missing" do
66
+ app.plugin :run_append_slash, :use_redirects => true
67
+ status('/sub').must_equal 302
68
+ header('Location', '/sub').must_equal '/sub/'
69
+ body('/sub/').must_equal 'sub-root'
70
+ body('/sub/foo').must_equal 'sub-foo'
71
+ status('/sub/foo/').must_equal 404
72
+ body('/sub/bar').must_equal 'sub-bar-root'
73
+ body('/sub/bar/').must_equal 'sub-bar-root'
74
+ body('/sub/bar/baz').must_equal 'sub-bar-baz'
75
+ status('/sub/bar/baz/').must_equal 404
76
+ end
77
+ end
@@ -50,6 +50,18 @@ describe "type_routing plugin" do
50
50
  body('/a.html', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML: html'
51
51
  end
52
52
 
53
+ it "works correctly in sub apps" do
54
+ sup_app = app
55
+ b = sup_app.route_block
56
+ @app = Class.new(sup_app)
57
+ app.route do |r|
58
+ r.run(sup_app)
59
+ end
60
+
61
+ body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON: json'
62
+ body('/a.xml', 'HTTP_ACCEPT' => 'application/json').must_equal 'XML: xml'
63
+ body('/a.html', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML: html'
64
+ end
53
65
 
54
66
  it "uses the default if neither file extension nor Accept header are given" do
55
67
  body('/a').must_equal 'HTML: html'
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.16.0
4
+ version: 2.17.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: 2016-07-13 00:00:00.000000000 Z
11
+ date: 2016-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -182,6 +182,7 @@ extra_rdoc_files:
182
182
  - doc/release_notes/2.14.0.txt
183
183
  - doc/release_notes/2.15.0.txt
184
184
  - doc/release_notes/2.16.0.txt
185
+ - doc/release_notes/2.17.0.txt
185
186
  files:
186
187
  - CHANGELOG
187
188
  - MIT-LICENSE
@@ -201,6 +202,7 @@ files:
201
202
  - doc/release_notes/2.14.0.txt
202
203
  - doc/release_notes/2.15.0.txt
203
204
  - doc/release_notes/2.16.0.txt
205
+ - doc/release_notes/2.17.0.txt
204
206
  - doc/release_notes/2.2.0.txt
205
207
  - doc/release_notes/2.3.0.txt
206
208
  - doc/release_notes/2.4.0.txt
@@ -268,6 +270,7 @@ files:
268
270
  - lib/roda/plugins/render_each.rb
269
271
  - lib/roda/plugins/request_headers.rb
270
272
  - lib/roda/plugins/response_request.rb
273
+ - lib/roda/plugins/run_append_slash.rb
271
274
  - lib/roda/plugins/run_handler.rb
272
275
  - lib/roda/plugins/shared_vars.rb
273
276
  - lib/roda/plugins/sinatra_helpers.rb
@@ -353,6 +356,7 @@ files:
353
356
  - spec/plugin/render_spec.rb
354
357
  - spec/plugin/request_headers_spec.rb
355
358
  - spec/plugin/response_request_spec.rb
359
+ - spec/plugin/run_append_slash_spec.rb
356
360
  - spec/plugin/run_handler_spec.rb
357
361
  - spec/plugin/shared_vars_spec.rb
358
362
  - spec/plugin/sinatra_helpers_spec.rb