roda 2.3.0 → 2.4.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: 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