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 +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
|