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
         |