roda 2.3.0 → 2.4.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: 9c94569f43d36e67b81fa0f67d739a24f9b5a594
4
- data.tar.gz: 8420ceb5dafb0f76e840fdeff028e46f4b0842f9
3
+ metadata.gz: 4196dbdea5add49bdf94cb6c59d5f10f3d2d6464
4
+ data.tar.gz: 23b683d38973ae9c47d99033e35f71be7ae89e68
5
5
  SHA512:
6
- metadata.gz: ab68ffe4810164018008821e8a8ae7a66469a58884ff5a25599e0cac03330e393d9b62cf5d8712ceea8e9a870afd431f52a072294c710285a7e69d1a243326b0
7
- data.tar.gz: ab79ec088d85b8daa3f705fb92bcd9e64be19ae9a5bf131f9414fca72c53a89f00a9c82465ed8db68440bf120a783c1ad4e521e295f15868a4a372463ecf8a06
6
+ metadata.gz: 79a29e4ad2df5b81307e3db2bd768c10937f9a704f93d5702b97779ee1f6821b223e60b5011ce3c482220bf56d993d3aa67473e1bd6a8b68869135123ee29897
7
+ data.tar.gz: 7ffc00c161bf098279cf9b5e61bd02aa5e99531e9c4bd1072ab7b97a191ff77e1c417a037e8f1c7b2d8d2dc757fc15d667d57d622aa001a3e568c467700ce8be
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ = 2.4.0 (2015-06-15)
2
+
3
+ * Add websockets plugin, for integration with faye-websocket (jeremyevans)
4
+
5
+ * Add status_handler plugin, similar to not_found but for any status code (celsworth) (#29)
6
+
7
+ * Support Closure Compiler, Uglifier, and MinJS for compressing javascript in the assets plugin (jeremyevans)
8
+
9
+ * Make Roda.plugin always return nil (jeremyevans)
10
+
11
+ * Add :gzip option to assets plugin (jeremyevans)
12
+
1
13
  = 2.3.0 (2015-05-13)
2
14
 
3
15
  * Make assets plugin work better with json plugin when r.assets is the last method called in a route block (jeremyevans) (#27)
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ VERS = lambda do
6
6
  require File.expand_path("../lib/roda/version.rb", __FILE__)
7
7
  Roda::RodaVersion
8
8
  end
9
- CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc"]
9
+ CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc", "spec/assets/app.*.css", "spec/assets/app.*.js", "spec/assets/app.*.css.gz", "spec/assets/app.*.js.gz"]
10
10
 
11
11
  # Gem Packaging and Release
12
12
 
@@ -0,0 +1,55 @@
1
+ = New Plugins
2
+
3
+ * A websocket plugin has been added, for websocket support using
4
+ faye-websocket. Here's an example of a simple echo service using
5
+ websockets:
6
+
7
+ plugin :websocket
8
+
9
+ route do |r|
10
+ r.get "echo" do
11
+ r.websocket do |ws|
12
+ # Routing block taken for a websocket request to /echo
13
+
14
+ # ws is a Faye::WebSocket instance, so you can use the
15
+ # Faye::WebSocket API
16
+ ws.on(:message) do |event|
17
+ ws.send(event.data)
18
+ end
19
+ end
20
+
21
+ # View rendered if a regular GET request to /echo
22
+ view "echo"
23
+ end
24
+ end
25
+
26
+ * A status_handler plugin has been added, which allows Roda to
27
+ specially handle arbitrary status codes. Usage is similar to the
28
+ not_found plugin (which now uses status_handler internally):
29
+
30
+ plugin :status_handler
31
+
32
+ status_handler 403 do
33
+ "You are forbidden from seeing that!"
34
+ end
35
+ status_handler 404 do
36
+ "Where did it go?"
37
+ end
38
+
39
+ = Other New Features
40
+
41
+ * The assets plugin now supports a :gzip option, which will save
42
+ gzipped versions when compiling assets. When serving compiled
43
+ assets, if the request accepts gzip encoding, it will serve
44
+ the gzipped version. This also plays nicely with nginx's
45
+ gzip_static support.
46
+
47
+ * The assets plugin now supports Google Closure Compiler, Uglifier,
48
+ and MinJS for minifying javascript. You can now specify which
49
+ css and js compressors to use via the :css_compressor and
50
+ :js_compressor options.
51
+
52
+ = Backwards Compatibility
53
+
54
+ * Roda.plugin now always returns nil. Previously the return value
55
+ could be non-nil if the plugin used a configure method.
data/lib/roda.rb CHANGED
@@ -161,7 +161,7 @@ class Roda
161
161
 
162
162
  # Load a new plugin into the current class. A plugin can be a module
163
163
  # which is used directly, or a symbol represented a registered plugin
164
- # which will be required and then used.
164
+ # which will be required and then used. Returns nil.
165
165
  #
166
166
  # Roda.plugin PluginModule
167
167
  # Roda.plugin :csrf
@@ -176,6 +176,7 @@ class Roda
176
176
  self::RodaResponse.send(:include, plugin::ResponseMethods) if defined?(plugin::ResponseMethods)
177
177
  self::RodaResponse.extend(plugin::ResponseClassMethods) if defined?(plugin::ResponseClassMethods)
178
178
  plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
179
+ nil
179
180
  end
180
181
 
181
182
  # Setup routing tree for the current Roda application, and build the
@@ -199,6 +199,8 @@ class Roda
199
199
  # :compiled_path:: Path inside public folder in which compiled files are stored (default: :prefix)
200
200
  # :concat_only :: Whether to just concatenate instead of concatentating
201
201
  # and compressing files (default: false)
202
+ # :css_compressor :: Compressor to use for compressing CSS, either :yui, :none, or nil (the default, which will try
203
+ # :yui if available, but not fail if it is not available)
202
204
  # :css_dir :: Directory name containing your css source, inside :path (default: 'css')
203
205
  # :css_headers :: A hash of additional headers for your rendered css files
204
206
  # :css_opts :: Template options to pass to the render plugin (via :template_opts) when rendering css assets
@@ -208,7 +210,11 @@ class Roda
208
210
  # detect changes in your asset files.
209
211
  # :group_subdirs :: Whether a hash used in :css and :js options requires the assets for the
210
212
  # related group are contained in a subdirectory with the same name (default: true)
213
+ # :gzip :: Store gzipped compiled assets files, and serve those to clients who accept gzip encoding.
211
214
  # :headers :: A hash of additional headers for both js and css rendered files
215
+ # :js_compressor :: Compressor to use for compressing javascript, either :yui, :closure, :uglifier, :minjs,
216
+ # :none, or nil (the default, which will try :yui, :closure, :uglifier, then :minjs, but
217
+ # not fail if any of them is not available)
212
218
  # :js_dir :: Directory name containing your javascript source, inside :path (default: 'js')
213
219
  # :js_headers :: A hash of additional headers for your rendered javascript files
214
220
  # :js_opts :: Template options to pass to the render plugin (via :template_opts) when rendering javascript assets
@@ -243,6 +249,13 @@ class Roda
243
249
  EMPTY_STRING = ''.freeze
244
250
  JS_SUFFIX = '.js'.freeze
245
251
  CSS_SUFFIX = '.css'.freeze
252
+ HTTP_ACCEPT_ENCODING = 'HTTP_ACCEPT_ENCODING'.freeze
253
+ CONTENT_ENCODING = 'Content-Encoding'.freeze
254
+ GZIP = 'gzip'.freeze
255
+ DOTGZ = '.gz'.freeze
256
+
257
+ # Internal exception raised when a compressor cannot be found
258
+ CompressorNotFound = Class.new(RodaError)
246
259
 
247
260
  # Load the render and caching plugins plugins, since the assets plugin
248
261
  # depends on them.
@@ -420,6 +433,14 @@ class Roda
420
433
  path = "#{o[:"compiled_#{type}_path"]}#{suffix}.#{unique_id}.#{type}"
421
434
  ::FileUtils.mkdir_p(File.dirname(path))
422
435
  ::File.open(path, 'wb'){|f| f.write(content)}
436
+
437
+ if o[:gzip]
438
+ require 'zlib'
439
+ Zlib::GzipWriter.open("#{path}.gz") do |gz|
440
+ gz.write(content)
441
+ end
442
+ end
443
+
423
444
  nil
424
445
  end
425
446
 
@@ -428,22 +449,81 @@ class Roda
428
449
  # a java runtime. This method can be overridden by the application
429
450
  # to use a different compressor.
430
451
  def compress_asset(content, type)
431
- require 'yuicompressor'
432
- # :nocov:
433
- content = YUICompressor.send("compress_#{type}", content, :munge => true)
434
- # :nocov:
435
- rescue LoadError, Errno::ENOENT
436
- # yuicompressor or java not available, just use concatenated, uncompressed output
452
+ case compressor = assets_opts[:"#{type}_compressor"]
453
+ when :none
454
+ return content
455
+ when nil
456
+ # default, try different compressors
457
+ else
458
+ return send("compress_#{type}_#{compressor}", content)
459
+ end
460
+
461
+ compressors = if type == :js
462
+ [:yui, :closure, :uglifier, :minjs]
463
+ else
464
+ [:yui]
465
+ end
466
+
467
+ compressors.each do |comp|
468
+ begin
469
+ if c = send("compress_#{type}_#{comp}", content)
470
+ return c
471
+ end
472
+ rescue LoadError, CompressorNotFound
473
+ end
474
+ end
475
+
437
476
  content
438
477
  end
439
478
 
479
+ # Compress the CSS using YUI Compressor, requires java runtime
480
+ def compress_css_yui(content)
481
+ compress_yui(content, :compress_css)
482
+ end
483
+
484
+ # Compress the JS using Google Closure Compiler, requires java runtime
485
+ def compress_js_closure(content)
486
+ require 'closure-compiler'
487
+
488
+ begin
489
+ ::Closure::Compiler.new.compile(content)
490
+ rescue ::Closure::Error => e
491
+ raise CompressorNotFound, "#{e.class}: #{e.message}", e.backtrace
492
+ end
493
+ end
494
+
495
+ # Compress the JS using MinJS, a pure ruby compressor
496
+ def compress_js_minjs(content)
497
+ require 'minjs'
498
+ ::Minjs::Compressor.new(:debug => false).compress(content)
499
+ end
500
+
501
+ # Compress the JS using Uglifier, requires javascript runtime
502
+ def compress_js_uglifier(content)
503
+ require 'uglifier'
504
+ Uglifier.compile(content)
505
+ end
506
+
507
+ # Compress the CSS using YUI Compressor, requires java runtime
508
+ def compress_js_yui(content)
509
+ compress_yui(content, :compress_js)
510
+ end
511
+
512
+ # Compress the CSS/JS using YUI Compressor, requires java runtime
513
+ def compress_yui(content, meth)
514
+ require 'yuicompressor'
515
+ ::YUICompressor.send(meth, content, :munge => true)
516
+ rescue ::Errno::ENOENT => e
517
+ raise CompressorNotFound, "#{e.class}: #{e.message}", e.backtrace
518
+ end
519
+
440
520
  # Return a unique id for the given content. By default, uses the
441
521
  # SHA1 hash of the content. This method can be overridden to use
442
522
  # a different digest type or to return a static string if you don't
443
523
  # want to use a unique value.
444
524
  def asset_digest(content)
445
525
  require 'digest/sha1'
446
- Digest::SHA1.hexdigest(content)
526
+ ::Digest::SHA1.hexdigest(content)
447
527
  end
448
528
  end
449
529
 
@@ -512,6 +592,12 @@ class Roda
512
592
  o = self.class.assets_opts
513
593
  if o[:compiled]
514
594
  file = "#{o[:"compiled_#{type}_path"]}#{file}"
595
+
596
+ if o[:gzip] && env[HTTP_ACCEPT_ENCODING] =~ /\bgzip\b/
597
+ @_response[CONTENT_ENCODING] = GZIP
598
+ file << DOTGZ
599
+ end
600
+
515
601
  check_asset_request(file, type, ::File.stat(file).mtime)
516
602
  ::File.read(file)
517
603
  else
@@ -21,13 +21,13 @@ class Roda
21
21
  # # ...
22
22
  # end
23
23
  #
24
+ # However, this code makes it easier to write after hooks, as well as
25
+ # handle cases where before hooks are added after the route block.
26
+ #
24
27
  # Note that the after hook is called with the rack response array
25
28
  # of status, headers, and body. If it wants to change the response,
26
29
  # it must mutate this argument, calling <tt>response.status=</tt> inside
27
30
  # an after block will not affect the returned status.
28
- #
29
- # However, this code makes it easier to write after hooks, as well as
30
- # handle cases where before hooks are added after the route block.
31
31
  module Hooks
32
32
  def self.configure(app)
33
33
  app.opts[:before_hook] ||= nil
@@ -23,7 +23,15 @@ class Roda
23
23
  # will be cleared. So if you want to be sure the headers are set
24
24
  # even in a not_found block, you need to reset them in the
25
25
  # not_found block.
26
+ #
27
+ # This plugin is now a wrapper around the +status_handler+ plugin and
28
+ # still exists mainly for backward compatibility.
26
29
  module NotFound
30
+ # Require the status_handler plugin
31
+ def self.load_dependencies(app)
32
+ app.plugin :status_handler
33
+ end
34
+
27
35
  # If a block is given, install the block as the not_found handler.
28
36
  def self.configure(app, &block)
29
37
  if block
@@ -34,31 +42,7 @@ class Roda
34
42
  module ClassMethods
35
43
  # Install the given block as the not_found handler.
36
44
  def not_found(&block)
37
- define_method(:not_found, &block)
38
- private :not_found
39
- end
40
- end
41
-
42
- module InstanceMethods
43
- # If routing returns a 404 response with an empty body, call
44
- # the not_found handler.
45
- def call
46
- result = super
47
-
48
- if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
49
- @_response.headers.clear
50
- super{not_found}
51
- else
52
- result
53
- end
54
- end
55
-
56
- private
57
-
58
- # Use an empty not_found_handler by default, so that loading
59
- # the plugin without defining a not_found handler doesn't
60
- # break things.
61
- def not_found
45
+ status_handler(404, &block)
62
46
  end
63
47
  end
64
48
  end
@@ -0,0 +1,57 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The status_handler plugin adds a +status_handler+ method which sets a
4
+ # block that is called whenever a response with the relevant response code
5
+ # with an empty body would be returned.
6
+ #
7
+ # This plugin does not support providing the blocks with the plugin call;
8
+ # you must provide them to status_handler calls afterwards:
9
+ #
10
+ # plugin :status_handler
11
+ #
12
+ # status_handler(403) do
13
+ # "You are forbidden from seeing that!"
14
+ # end
15
+ # status_handler(404) do
16
+ # "Where did it go?"
17
+ # end
18
+ #
19
+ # Before a block is called, any existing headers on the response will be
20
+ # cleared. So if you want to be sure the headers are set even in your block,
21
+ # you need to reset them in the block.
22
+ module StatusHandler
23
+ def self.configure(app)
24
+ app.opts[:status_handler] ||= {}
25
+ end
26
+
27
+ module ClassMethods
28
+ # Install the given block as a status handler for the given HTTP response code.
29
+ def status_handler(code, &block)
30
+ opts[:status_handler][code] = block
31
+ end
32
+
33
+ # Freeze the hash of status handlers so that there can be no thread safety issues at runtime.
34
+ def freeze
35
+ opts[:status_handler].freeze
36
+ super
37
+ end
38
+ end
39
+
40
+ module InstanceMethods
41
+ # If routing returns a response we have a handler for, call that handler.
42
+ def call
43
+ result = super
44
+
45
+ if (block = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
46
+ @_response.headers.clear
47
+ super(&block)
48
+ else
49
+ result
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ register_plugin(:status_handler, StatusHandler)
56
+ end
57
+ end
@@ -0,0 +1,103 @@
1
+ require 'faye/websocket'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The websocket plugin adds integration support for websockets.
6
+ # Currently, only 'faye-websocket' is supported, so eventmachine
7
+ # is required for websockets. See the
8
+ # {faye-websocket documentation}[https://github.com/faye/faye-websocket-ruby]
9
+ # for details on the faye-websocket API. Note that faye-websocket
10
+ # is only supported on ruby 1.9.3+, so the websockets plugin only works
11
+ # on ruby 1.9.3+.
12
+ #
13
+ # Here's a simplified example for a basic multi-user,
14
+ # multi-room chat server, where a message from any user in a room
15
+ # is sent to all other users in the same room, using a websocket
16
+ # per room:
17
+ #
18
+ # plugin :websockets, :adapter=>:thin, :opts=>{:ping=>45}
19
+ #
20
+ # MUTEX = Mutex.new
21
+ # ROOMS = {}
22
+ #
23
+ # def sync
24
+ # MUTEX.synchronize{yield}
25
+ # end
26
+ #
27
+ # route do |r|
28
+ # r.get "room/:d" do |room_id|
29
+ # room = sync{ROOMS[room_id] ||= []}
30
+ #
31
+ # r.websocket do |ws|
32
+ # # Routing block taken if request is a websocket request,
33
+ # # yields a Faye::WebSocket instance
34
+ #
35
+ # ws.on(:message) do |event|
36
+ # sync{room.dup}.each{|user| user.send(event.data)}
37
+ # end
38
+ #
39
+ # ws.on(:close) do |event|
40
+ # sync{room.delete(ws)}
41
+ # sync{room.dup}.each{|user| user.send("Someone left")}
42
+ # end
43
+ #
44
+ # sync{room.dup}.each{|user| user.send("Someone joined")}
45
+ # sync{room.push(ws)}
46
+ # end
47
+ #
48
+ # # If the request is not a websocket request, execution
49
+ # # continues, similar to how routing in general works.
50
+ # view 'room'
51
+ # end
52
+ # end
53
+ module Websockets
54
+ WebSocket = ::Faye::WebSocket
55
+ OPTS = {}.freeze
56
+
57
+ # Add default opions used for websockets. These options are
58
+ # passed to Faye:WebSocket.new, except that the following
59
+ # options are handled separately.
60
+ #
61
+ # :adapter :: Calls Faye::WebSocket.load adapter with the given
62
+ # value, used to set the adapter to load, if Faye
63
+ # requires an adapter to work with the webserver.
64
+ # Possible options: :thin, :rainbows, :goliath
65
+ #
66
+ # See RequestMethods#websocket for additional supported options.
67
+ def self.configure(app, opts=OPTS)
68
+ opts = app.opts[:websockets_opts] = (app.opts[:websockets_opts] || {}).merge(opts || {})
69
+ if adapter = opts.delete(:adapter)
70
+ WebSocket.load_adapter(adapter.to_s)
71
+ end
72
+ opts.freeze
73
+ end
74
+
75
+ module RequestMethods
76
+ # True if this request is a websocket request, false otherwise.
77
+ def websocket?
78
+ WebSocket.websocket?(env)
79
+ end
80
+
81
+ # If the request is a websocket request, yield a websocket to the
82
+ # block, and return the appropriate rack response after the block
83
+ # returns. +opts+ is an options hash used when creating the
84
+ # websocket, except the following options are handled specially:
85
+ #
86
+ # :protocols :: Set the protocols to accept, should be an array
87
+ # of strings.
88
+ def websocket(opts=OPTS)
89
+ if websocket?
90
+ always do
91
+ opts = Hash[roda_class.opts[:websockets_opts]].merge!(opts)
92
+ ws = WebSocket.new(env, opts.delete(:protocols), opts)
93
+ yield ws
94
+ halt ws.rack_response
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ register_plugin(:websockets, Websockets)
102
+ end
103
+ end
data/lib/roda/version.rb CHANGED
@@ -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 = 3
7
+ RodaMinorVersion = 4
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
data/spec/freeze_spec.rb CHANGED
@@ -6,20 +6,20 @@ describe "Roda.freeze" do
6
6
  end
7
7
 
8
8
  it "should make opts not be modifiable after calling finalize!" do
9
- proc{app.opts[:foo] = 'bar'}.must_raise FrozenError
9
+ proc{app.opts[:foo] = 'bar'}.must_raise
10
10
  end
11
11
 
12
12
  it "should make use and route raise errors" do
13
- proc{app.use Class.new}.must_raise FrozenError
14
- proc{app.route{}}.must_raise FrozenError
13
+ proc{app.use Class.new}.must_raise
14
+ proc{app.route{}}.must_raise
15
15
  end
16
16
 
17
17
  it "should make plugin raise errors" do
18
- proc{app.plugin Module.new}.must_raise Roda::RodaError
18
+ proc{app.plugin Module.new}.must_raise
19
19
  end
20
20
 
21
21
  it "should make subclassing raise errors" do
22
- proc{Class.new(app)}.must_raise Roda::RodaError
22
+ proc{Class.new(app)}.must_raise
23
23
  end
24
24
 
25
25
  it "should freeze app" do
@@ -176,8 +176,7 @@ describe "integration" do
176
176
  it "should have app return the rack application to call" do
177
177
  app(:bare){}.app.must_equal nil
178
178
  app.route{|r|}
179
- # Work around minitest bug
180
- assert_kind_of Proc, app.app
179
+ app.app.must_be_kind_of(Proc)
181
180
  c = Class.new{def initialize(app) @app = app end; def call(env) @app.call(env) end}
182
181
  app.use c
183
182
  app.app.must_be_kind_of(c)
@@ -30,7 +30,9 @@ if run_tests
30
30
  :js => { :head => ['app.js'] },
31
31
  :path => 'spec/assets',
32
32
  :public => 'spec',
33
- :css_opts => {:cache=>false}
33
+ :css_opts => {:cache=>false},
34
+ :css_compressor => :none,
35
+ :js_compressor => :none
34
36
 
35
37
  route do |r|
36
38
  r.assets
@@ -51,6 +53,10 @@ if run_tests
51
53
  FileUtils.rm_r('spec/public') if File.directory?('spec/public')
52
54
  end
53
55
 
56
+ def gunzip(body)
57
+ Zlib::GzipReader.wrap(StringIO.new(body), &:read)
58
+ end
59
+
54
60
  it 'assets_opts should use correct paths given options' do
55
61
  fpaths = [:js_path, :css_path, :compiled_js_path, :compiled_css_path]
56
62
  rpaths = [:js_prefix, :css_prefix, :compiled_js_prefix, :compiled_css_prefix]
@@ -224,6 +230,25 @@ if run_tests
224
230
  js.must_include('console.log')
225
231
  end
226
232
 
233
+ it 'should handle compressing using different libraries' do
234
+ try_compressor = proc do |css, js|
235
+ app.plugin :assets, :css_compressor=>css, :js_compressor=>js
236
+ begin
237
+ app.compile_assets
238
+ rescue LoadError, Roda::RodaPlugins::Assets::CompressorNotFound
239
+ next
240
+ end
241
+ File.read("spec/assets/app.#{app.assets_opts[:compiled]['css']}.css").must_match(/color: ?blue/)
242
+ File.read("spec/assets/app.head.#{app.assets_opts[:compiled]['js.head']}.js").must_include('console.log')
243
+ end
244
+
245
+ try_compressor.call(nil, nil)
246
+ try_compressor.call(:yui, :yui)
247
+ try_compressor.call(:none, :closure)
248
+ try_compressor.call(:none, :uglifier)
249
+ try_compressor.call(:none, :minjs)
250
+ end
251
+
227
252
  it 'should handle compiling assets, linking to them, and accepting requests for them' do
228
253
  app.compile_assets
229
254
  html = body('/test')
@@ -238,6 +263,30 @@ if run_tests
238
263
  js.must_include('console.log')
239
264
  end
240
265
 
266
+ it 'should handle compiling assets, linking to them, and accepting requests for them when :gzip is set' do
267
+ app.plugin :assets, :gzip=>true
268
+ app.compile_assets
269
+ html = body('/test')
270
+ html.scan(/<link/).length.must_equal 1
271
+ html =~ %r{href="(/assets/app\.[a-f0-9]{40}\.css)"}
272
+ css_path = $1
273
+ html.scan(/<script/).length.must_equal 1
274
+ html =~ %r{src="(/assets/app\.head\.[a-f0-9]{40}\.js)"}
275
+ js_path = $1
276
+
277
+ css = body(css_path)
278
+ js = body(js_path)
279
+ css.must_match(/color: ?red/)
280
+ css.must_match(/color: ?blue/)
281
+ js.must_include('console.log')
282
+
283
+ css = gunzip(body(css_path, 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip'))
284
+ js = gunzip(body(js_path, 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip'))
285
+ css.must_match(/color: ?red/)
286
+ css.must_match(/color: ?blue/)
287
+ js.must_include('console.log')
288
+ end
289
+
241
290
  it 'should handle compiling assets, linking to them, and accepting requests for them when :add_script_name app option is used' do
242
291
  app.opts[:add_script_name] = true
243
292
  app.plugin :assets
@@ -159,6 +159,6 @@ describe "class_level_routing plugin" do
159
159
  status.must_equal 200
160
160
  status("/asdfa/asdf").must_equal 404
161
161
 
162
- proc{app.on{}}.must_raise FrozenError
162
+ proc{app.on{}}.must_raise
163
163
  end
164
164
  end
@@ -87,7 +87,7 @@ describe "multi_route plugin" do
87
87
  status('/c').must_equal 404
88
88
  status('/c', 'REQUEST_METHOD'=>'POST').must_equal 404
89
89
 
90
- proc{app.route("foo"){}}.must_raise FrozenError
90
+ proc{app.route("foo"){}}.must_raise
91
91
  end
92
92
 
93
93
  it "uses multi_route to dispatch to any named route" do
@@ -45,7 +45,7 @@ describe "multi_run plugin" do
45
45
  body("/b/a").must_equal 'b2'
46
46
  body.must_equal 'c'
47
47
 
48
- proc{app.run "a", Class.new(Roda).class_eval{route{"a1"}; app}}.must_raise FrozenError
48
+ proc{app.run "a", Class.new(Roda).class_eval{route{"a1"}; app}}.must_raise
49
49
  end
50
50
 
51
51
  it "works when subclassing" do
@@ -52,7 +52,7 @@ describe "named_templates plugin" do
52
52
  app.freeze
53
53
  body.must_equal 'bar13-foo12-baz'
54
54
 
55
- proc{app.template(:b){"a"}}.must_raise FrozenError
55
+ proc{app.template(:b){"a"}}.must_raise
56
56
  end
57
57
 
58
58
  it "works with the view_subdirs plugin" do
@@ -32,6 +32,6 @@ describe "path_rewriter plugin" do
32
32
 
33
33
  app.freeze
34
34
  body('/a').must_equal '/a:/b'
35
- proc{app.rewrite_path '/a', '/b'}.must_raise FrozenError
35
+ proc{app.rewrite_path '/a', '/b'}.must_raise
36
36
  end
37
37
  end
@@ -193,7 +193,7 @@ describe "path plugin" do
193
193
  b = proc{|x| x.to_s}
194
194
  @app.path(c, &b)
195
195
  # Work around minitest bug
196
- assert_equal @app.path_block(c), b
196
+ app.path_block(c).must_equal b
197
197
  end
198
198
 
199
199
  it "Roda.path doesn't work with classes without blocks" do
@@ -207,7 +207,7 @@ describe "path plugin" do
207
207
 
208
208
  it "Roda.path doesn't work after freezing the app" do
209
209
  app.freeze
210
- proc{app.path(Class.new){|obj| ''}}.must_raise FrozenError
210
+ proc{app.path(Class.new){|obj| ''}}.must_raise
211
211
  end
212
212
  end
213
213
 
@@ -0,0 +1,141 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "status_handler plugin" do
4
+ it "executes on no arguments" do
5
+ app(:bare) do
6
+ plugin :status_handler
7
+
8
+ status_handler(404) do
9
+ "not found"
10
+ end
11
+
12
+ route do |r|
13
+ r.on "a" do
14
+ "found"
15
+ end
16
+ end
17
+ end
18
+
19
+ body.must_equal 'not found'
20
+ status.must_equal 404
21
+ body("/a").must_equal 'found'
22
+ status("/a").must_equal 200
23
+ end
24
+
25
+ it "allows overriding status inside status_handler" do
26
+ app(:bare) do
27
+ plugin :status_handler
28
+
29
+ status_handler(404) do
30
+ response.status = 403
31
+ "not found"
32
+ end
33
+
34
+ route do |r|
35
+ end
36
+ end
37
+
38
+ status.must_equal 403
39
+ end
40
+
41
+ it "calculates correct Content-Length" do
42
+ app(:bare) do
43
+ plugin :status_handler
44
+
45
+ status_handler(404) do
46
+ "a"
47
+ end
48
+
49
+ route{}
50
+ end
51
+
52
+ header('Content-Length').must_equal "1"
53
+ end
54
+
55
+ it "clears existing headers" do
56
+ app(:bare) do
57
+ plugin :status_handler
58
+
59
+ status_handler(404) do
60
+ "a"
61
+ end
62
+
63
+ route do |r|
64
+ response['Content-Type'] = 'text/pdf'
65
+ response['Foo'] = 'bar'
66
+ nil
67
+ end
68
+ end
69
+
70
+ header('Content-Type').must_equal 'text/html'
71
+ header('Foo').must_equal nil
72
+ end
73
+
74
+ it "does not modify behavior if status_handler is not called" do
75
+ app(:status_handler) do |r|
76
+ r.on "a" do
77
+ "found"
78
+ end
79
+ end
80
+
81
+ body.must_equal ''
82
+ body("/a").must_equal 'found'
83
+ end
84
+
85
+ it "does not modify behavior if body is not an array" do
86
+ app(:bare) do
87
+ plugin :status_handler
88
+
89
+ status_handler(404) do
90
+ "not found"
91
+ end
92
+
93
+ o = Object.new
94
+ def o.each; end
95
+ route do |r|
96
+ r.halt [404, {}, o]
97
+ end
98
+ end
99
+
100
+ body.must_equal ''
101
+ end
102
+
103
+ it "does not modify behavior if body is not an empty array" do
104
+ app(:bare) do
105
+ plugin :status_handler
106
+
107
+ status_handler(404) do
108
+ "not found"
109
+ end
110
+
111
+ route do |r|
112
+ response.status = 404
113
+ response.write 'a'
114
+ end
115
+ end
116
+
117
+ body.must_equal 'a'
118
+ end
119
+
120
+ it "does not allow further status handlers to be added after freezing" do
121
+ app(:bare) do
122
+ plugin :status_handler
123
+
124
+ status_handler(404) do
125
+ "not found"
126
+ end
127
+
128
+ route{}
129
+ end
130
+
131
+ app.freeze
132
+
133
+ body.must_equal 'not found'
134
+ status.must_equal 404
135
+
136
+ proc{app.status_handler(404) { "blah" }}.must_raise
137
+
138
+ body.must_equal 'not found'
139
+ end
140
+
141
+ end
@@ -0,0 +1,80 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ if RUBY_VERSION >= '1.9.3'
4
+ begin
5
+ lib = nil
6
+ for lib in %w'faye/websocket thin' do
7
+ require lib
8
+ end
9
+ rescue LoadError
10
+ warn "#{lib} not installed, skipping websockets plugin test"
11
+ else
12
+ describe "websockets plugin" do
13
+ it "supports regular requests" do
14
+ app(:websockets) do |r|
15
+ r.websocket{}
16
+ "a"
17
+ end
18
+ body.must_equal 'a'
19
+ end
20
+ end
21
+
22
+ describe "websockets plugin" do
23
+ before do
24
+ events = @events = []
25
+ app(:bare) do
26
+ plugin :websockets, :adapter=>:thin
27
+ route do |r|
28
+ r.websocket do |ws|
29
+ ws.on(:open) do |event|
30
+ events << 'open'
31
+ end
32
+ ws.on(:message) do |event|
33
+ events << event.data
34
+ ws.send(event.data.reverse)
35
+ end
36
+ ws.on(:close) do |event|
37
+ events << 'close'
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ @port = 9791
44
+ q = Queue.new
45
+ Thread.new do
46
+ Thin::Logging.silent = true
47
+ Rack::Handler.get('thin').run(app, :Port => @port) do |s|
48
+ @server = s
49
+ q.push nil
50
+ end
51
+ end
52
+ q.pop
53
+ end
54
+ after do
55
+ @server.stop
56
+ end
57
+
58
+ it "supports websocket requests" do
59
+ ws = Faye::WebSocket::Client.new("ws://localhost:#{@port}")
60
+ msg = nil
61
+ ws.on(:open){|event| msg = true}
62
+ t = Time.now
63
+ sleep 0.01 until msg || Time.now - t > 5
64
+ msg.must_equal true
65
+
66
+ msg = nil
67
+ ws.on(:message){|event| msg = event.data}
68
+ ws.send("hello")
69
+ t = Time.now
70
+ sleep 0.01 until msg || Time.now - t > 5
71
+ msg.must_equal 'olleh'
72
+
73
+ ws.close
74
+ t = Time.now
75
+ sleep 0.01 until @events == %w'open hello close' || Time.now - t > 5
76
+ @events.must_equal %w'open hello close'
77
+ end
78
+ end
79
+ end
80
+ end
data/spec/plugin_spec.rb CHANGED
@@ -48,7 +48,7 @@ describe "plugins" do
48
48
  end
49
49
 
50
50
  app(:bare) do
51
- plugin c, "Foo "
51
+ plugin(c, "Foo ").must_equal nil
52
52
 
53
53
  route do |r|
54
54
  r.hello do
data/spec/spec_helper.rb CHANGED
@@ -24,9 +24,6 @@ require "stringio"
24
24
  gem 'minitest'
25
25
  require "minitest/autorun"
26
26
 
27
- # Work around minitest issue requiring specific exception class
28
- FrozenError = RUBY_VERSION >= '1.9' ? RuntimeError : TypeError
29
-
30
27
  #def (Roda::RodaPlugins).warn(s); end
31
28
 
32
29
  class Minitest::Spec
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.3.0
4
+ version: 2.4.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-05-13 00:00:00.000000000 Z
11
+ date: 2015-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 5.6.1
33
+ version: 5.7.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 5.6.1
40
+ version: 5.7.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: tilt
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -140,6 +140,7 @@ extra_rdoc_files:
140
140
  - doc/release_notes/2.1.0.txt
141
141
  - doc/release_notes/2.2.0.txt
142
142
  - doc/release_notes/2.3.0.txt
143
+ - doc/release_notes/2.4.0.txt
143
144
  files:
144
145
  - CHANGELOG
145
146
  - MIT-LICENSE
@@ -154,6 +155,7 @@ files:
154
155
  - doc/release_notes/2.1.0.txt
155
156
  - doc/release_notes/2.2.0.txt
156
157
  - doc/release_notes/2.3.0.txt
158
+ - doc/release_notes/2.4.0.txt
157
159
  - lib/roda.rb
158
160
  - lib/roda/plugins/_erubis_escaping.rb
159
161
  - lib/roda/plugins/all_verbs.rb
@@ -210,11 +212,13 @@ files:
210
212
  - lib/roda/plugins/slash_path_empty.rb
211
213
  - lib/roda/plugins/static.rb
212
214
  - lib/roda/plugins/static_path_info.rb
215
+ - lib/roda/plugins/status_handler.rb
213
216
  - lib/roda/plugins/streaming.rb
214
217
  - lib/roda/plugins/symbol_matchers.rb
215
218
  - lib/roda/plugins/symbol_views.rb
216
219
  - lib/roda/plugins/view_options.rb
217
220
  - lib/roda/plugins/view_subdirs.rb
221
+ - lib/roda/plugins/websockets.rb
218
222
  - lib/roda/version.rb
219
223
  - spec/assets/css/app.scss
220
224
  - spec/assets/css/no_access.css
@@ -280,10 +284,12 @@ files:
280
284
  - spec/plugin/sinatra_helpers_spec.rb
281
285
  - spec/plugin/slash_path_empty_spec.rb
282
286
  - spec/plugin/static_spec.rb
287
+ - spec/plugin/status_handler_spec.rb
283
288
  - spec/plugin/streaming_spec.rb
284
289
  - spec/plugin/symbol_matchers_spec.rb
285
290
  - spec/plugin/symbol_views_spec.rb
286
291
  - spec/plugin/view_options_spec.rb
292
+ - spec/plugin/websockets_spec.rb
287
293
  - spec/plugin_spec.rb
288
294
  - spec/redirect_spec.rb
289
295
  - spec/request_spec.rb