roda 2.16.0 → 2.17.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: 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