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 +4 -4
- data/CHANGELOG +12 -0
- data/Rakefile +1 -1
- data/doc/release_notes/2.4.0.txt +55 -0
- data/lib/roda.rb +2 -1
- data/lib/roda/plugins/assets.rb +93 -7
- data/lib/roda/plugins/hooks.rb +3 -3
- data/lib/roda/plugins/not_found.rb +9 -25
- data/lib/roda/plugins/status_handler.rb +57 -0
- data/lib/roda/plugins/websockets.rb +103 -0
- data/lib/roda/version.rb +1 -1
- data/spec/freeze_spec.rb +5 -5
- data/spec/integration_spec.rb +1 -2
- data/spec/plugin/assets_spec.rb +50 -1
- data/spec/plugin/class_level_routing_spec.rb +1 -1
- data/spec/plugin/multi_route_spec.rb +1 -1
- data/spec/plugin/multi_run_spec.rb +1 -1
- data/spec/plugin/named_templates_spec.rb +1 -1
- data/spec/plugin/path_rewriter_spec.rb +1 -1
- data/spec/plugin/path_spec.rb +2 -2
- data/spec/plugin/status_handler_spec.rb +141 -0
- data/spec/plugin/websockets_spec.rb +80 -0
- data/spec/plugin_spec.rb +1 -1
- data/spec/spec_helper.rb +0 -3
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4196dbdea5add49bdf94cb6c59d5f10f3d2d6464
|
4
|
+
data.tar.gz: 23b683d38973ae9c47d99033e35f71be7ae89e68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/roda/plugins/assets.rb
CHANGED
@@ -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
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
data/lib/roda/plugins/hooks.rb
CHANGED
@@ -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
|
-
|
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
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
|
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
|
14
|
-
proc{app.route{}}.must_raise
|
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
|
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
|
22
|
+
proc{Class.new(app)}.must_raise
|
23
23
|
end
|
24
24
|
|
25
25
|
it "should freeze app" do
|
data/spec/integration_spec.rb
CHANGED
@@ -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
|
-
|
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)
|
data/spec/plugin/assets_spec.rb
CHANGED
@@ -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
|
@@ -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
|
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
|
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
|
data/spec/plugin/path_spec.rb
CHANGED
@@ -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
|
-
|
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
|
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
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.
|
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-
|
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.
|
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.
|
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
|