diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..be954557 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: [push, pull_request] + +jobs: + test: + runs-on: "ubuntu-latest" + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + ruby_version: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3"] + experimental: [false] + include: + - ruby_version: "ruby-head" + experimental: true + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby_version }} + bundler-cache: true + - run: "bundle exec rubocop lib/ spec/" + - run: "bundle exec rspec spec/ -b" diff --git a/.gitignore b/.gitignore index c84df18a..da6e7dd4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ Gemfile.lock .rvmrc .SyncID .SyncIgnore +vendor diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..2f434748 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,56 @@ +## +# Plugins +require: + - standard + +## +# Defaults: standardrb +inherit_gem: + standard: config/base.yml + +## +# Rules that break from standardrb defaults +Style/StringLiterals: + EnforcedStyle: single_quotes + +## +# Disabled rules +Lint/AssignmentInCondition: + Enabled: false +Lint/FloatComparison: + Enabled: false +Lint/ConstantDefinitionInBlock: + Enabled: false +Lint/EmptyWhen: + Exclude: + - "lib/webmachine/dispatcher/route.rb" +Lint/DuplicateMethods: + Exclude: + - "lib/webmachine/application.rb" +Lint/UnderscorePrefixedVariableName: + Exclude: + - "lib/webmachine/trace/resource_proxy.rb" + - "spec/webmachine/dispatcher_spec.rb" +Lint/NestedMethodDefinition: + Exclude: + - "spec/webmachine/decision/flow_spec.rb" +Lint/RescueException: + Exclude: + - "spec/webmachine/decision/fsm_spec.rb" +Lint/RaiseException: + Exclude: + - "spec/webmachine/decision/fsm_spec.rb" +Style/MissingRespondToMissing: + Exclude: + - "lib/webmachine/request.rb" +Style/NilComparison: + Exclude: + - "spec/webmachine/decision/falsey_spec.rb" +Style/GlobalVars: + Exclude: + - "lib/webmachine/decision/conneg.rb" + +AllCops: + NewCops: disable + SuggestExtensions: false + TargetRubyVersion: 2.6 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f3dad088..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -rvm: - - 1.9.3 - - 2.0.0 - - 2.1.2 - - ruby-head - - jruby - - rbx-2 - -matrix: - allow_failures: - - rvm: ruby-head - - rvm: jruby - -bundler_args: --without guard docs - -script: bundle exec rspec spec/ -b diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0565a2..34220692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,74 @@ ### HEAD -* decode the value of the header 'Content-MD5' as base64-encoded string. +### 2.0.1 Feb 27, 2024 + +* Don't include the `doc/`, `pkg/`, or `vendor/` directory in the gem package +* Add `base64` as a runtime dependency +* Add support for Ruby 3.3 +* Fix Rubocop issues + +### 2.0.0 Mar 31, 2023 + +* Add support for ruby 3.0, 3.1 and 3.2 +* Drop support for ruby 2.3, 2.4 and 2.5 +* Remove the HTTPkit adapter +* Remove the Reel adapter +* Format cookie 'Expires' timestamps as per RFC 2616 + +### 1.6.0 June 22, 2021 + +* fix: replace missed URI.decode with new Route.rfc3986_percent_decode (#261) +* fix: implement rfc3986 Percent-Encoding decoder +* feat: make rack env available on the webmachine request when using a rack adapter + +### 1.5.0 September 8, 2017 + +* Removed Fixnum/Integer deprecation warnings in Ruby 2.4 +* Fixed multiple cookie setting code +* Added support for named captures +* Improved logic for determining which errors are 'rescuable' by Webmachine, + and which are 'unhandlable'. + +### 1.4.0 March 20, 2015 + +* Added RackMapped adapter which allows Webmachine apps to be mounted + at a non-root path using Rack::Map. + Thanks to [Julian Doherty](https://github.com/madlep) for writing this. + +### 1.3.1 January 15, 2015 + +* Fixed URI construction, including handling IPv6 addresses, when the + adapter did not supply a complete hostname. +* Removed dependency of Rack Adapter on REQUEST_INFO rack environment + variable which was not always present. +* Use IPAddr instead of Addrinfo as rbx does not support Addrinfo. + +### 1.3.0 January 3, 2015 + +1.3.0 is a feature and bugfix release that removes two adapters, +reduces memory usage, fixes bugs, and includes a bunch of new +documentation. Thank you to our new contributor @rpag! + +* Greatly reduced per-request garbage by freezing commonly used + Strings and Regexps into constants. +* Tutorial/example documentation was extracted from the README and + extended in the `documentation` directory. +* HTTPkit adapter was added. +* Hatetepe and Mongrel adapters were removed and adapters no longer + install interrupt handlers. +* The "splat" matcher in path specifications is now a Symbol `:*` + rather than a String `"*"`. Using the String version will result in + a deprecation warning. +* Requests with If-None-Match where the resource does not supply an + ETag will no longer respond with 412 or 500, but follow the success + path. +* Path fragments are now decoded. +* Simplified the interaction between the decision FSM and tracing. +* Updated specs to use RSpec 3. +* Improved handling of IO.copy_stream on Rack servers. +* Updated the Reel adapter. +* Exposed Application instance to the Adapter. +* Decode the value of the header 'Content-MD5' as base64-encoded string. ### 1.2.2 January 8, 2014 diff --git a/Gemfile b/Gemfile index a966da12..3479e339 100644 --- a/Gemfile +++ b/Gemfile @@ -1,43 +1,35 @@ -require 'rbconfig' source 'https://rubygems.org' gemspec group :development do - gem "yard" - gem "rake" + gem 'rake', '~> 12.0' + gem 'standard', '~> 1.21' + gem 'webrick', '~> 1.7' end group :test do - gem "rspec", '~> 3.0.0' - gem "rspec-its" - gem "rack" + gem 'rack', '~> 2.0' + gem 'rack-test', '~> 0.7' + gem 'rspec', '~> 3.0', '>= 3.6.0' + gem 'rspec-its', '~> 1.2' + gem 'websocket_parser', '~>1.0' end -group :webservers do - gem 'reel', '~> 0.5.0' - gem 'http', '~> 0.6.0' - gem 'httpkit', :platform => [:mri, :rbx] +group :docs do + gem 'redcarpet', '~> 3.4', platform: :ruby + gem 'yard', '~> 0.9' end -group :guard do - gem 'guard-rspec' - case RbConfig::CONFIG['host_os'] - when /darwin/ - gem 'rb-fsevent' - # gem 'growl_notify' - gem 'growl' - when /linux/ - gem 'rb-inotify' - gem 'libnotify' - end +platforms :jruby do + gem 'jruby-openssl' end -group :docs do - platform :mri_19, :mri_20 do - gem 'redcarpet' - end +if RUBY_VERSION >= '3.4' + gem 'mutex_m' # TODO: remove this once as-notifications has such a dependency end -platforms :jruby do - gem 'jruby-openssl' +if RUBY_VERSION >= '4.0' + gem 'pstore' + gem 'logger' + gem 'ostruct' end diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 357f38e2..00000000 --- a/Guardfile +++ /dev/null @@ -1,11 +0,0 @@ -gemset = ENV['RVM_GEMSET'] || 'webmachine' -gemset = "@#{gemset}" unless gemset.to_s == '' - -rvms = %W[ 1.8.7 1.9.2 1.9.3 jruby rbx ].map {|v| "#{v}#{gemset}" } - -guard 'rspec', :cli => "--color --profile", :growl => true, :rvm => rvms do - watch(%r{^lib/webmachine/locale/.+$}) { "spec" } - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}){ |m| "spec/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec" } -end diff --git a/README.md b/README.md index eefa9946..d60863f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# webmachine for Ruby [![travis](https://travis-ci.org/seancribbs/webmachine-ruby.png?branch=master)](http://travis-ci.org/seancribbs/webmachine-ruby) +# webmachine for Ruby + [![Gem Version](https://badge.fury.io/rb/webmachine.svg)](https://badge.fury.io/rb/webmachine) + [![Build Status](https://github.com/webmachine/webmachine-ruby/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/webmachine/webmachine-ruby/actions/workflows/test.yml) webmachine-ruby is a port of [Webmachine](https://github.com/basho/webmachine), which is written in @@ -18,8 +20,7 @@ are up to you. requests, and response codes for you. * Provides a base resource with points of extension to let you describe what is relevant about your particular resource. -* Supports WEBrick, Reel, HTTPkit, and a Rack shim. Other host - servers are being investigated. +* Supports WEBrick and a Rack shim. Other host servers are being investigated. * Streaming/chunked response bodies are permitted as Enumerables, Procs, or Fibers! * Unlike the Erlang original, it does real Language negotiation. @@ -163,10 +164,8 @@ the "visual debugger". Learn how to configure it [here][visual-debugger]. ## Related libraries -* [irwebmachine](https://github.com/robgleeson/irwebmachine) - IRB/Pry debugging of Webmachine applications * [webmachine-test](https://github.com/bernd/webmachine-test) - Helpers for testing Webmachine applications * [webmachine-linking](https://github.com/petejohanson/webmachine-linking) - Helpers for linking between Resources, and Web Linking -* [webmachine-sprockets](https://github.com/lgierth/webmachine-sprockets) - Integration with Sprockets assets packaging system * [webmachine-actionview](https://github.com/rgarner/webmachine-actionview) - Integration of some Rails-style view conventions into Webmachine * [jruby-http-kit](https://github.com/nLight/jruby-http-kit) - Includes an adapter for the Clojure-based Ring library/server * [newrelic-webmachine](https://github.com/mdub/newrelic-webmachine) - NewRelic instrumentation diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..4642cb2d --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,21 @@ +1. Open `CHANGELOG.md` and summarize the changes made since the last release (hopefully better than the individual commit messages). The history can be grabbed with a simple git command (assuming the last version was 1.3.0: + + $ git log --pretty=format:' * %s' v1.3.0..HEAD + +2. Edit the version in `lib/webmachine/version.rb` according to semantic versioning rules. +3. Commit both files. + + $ git add CHANGELOG.md lib/webmachine/version.rb + $ git commit -m "chore(release): version $(ruby -r ./lib/webmachine/version.rb -e "puts Webmachine::VERSION")" && git push + +4. Release the gem. + + $ bundle exec rake release + +5. If this is a new major or minor release, push a new stable branch, otherwise merge the commit into the stable branch (or master, depending on where you made the commit). + + $ git push origin HEAD:1.3-stable + # or + $ git checkout 1.3-stable; git merge master; git push origin; git checkout master + +6. YOU'RE DONE! diff --git a/Rakefile b/Rakefile index f88cf434..c9f54e96 100644 --- a/Rakefile +++ b/Rakefile @@ -1,60 +1,44 @@ -require 'rubygems' -require 'rubygems/package_task' +require 'bundler/gem_tasks' begin require 'yard' require 'yard/rake/yardoc_task' YARD::Rake::YardocTask.new do |doc| - doc.files = Dir["lib/**/*.rb"] + ['README.md'] - doc.options = ["-m", "markdown"] + doc.files = Dir['lib/**/*.rb'] + ['README.md'] + doc.options = ['-m', 'markdown'] end rescue LoadError end -def gemspec - $webmachine_gemspec ||= Gem::Specification.load("webmachine.gemspec") +desc 'Validate the gemspec file.' +task :validate_gemspec do + Gem::Specification.load('webmachine.gemspec').validate end -Gem::PackageTask.new(gemspec) do |pkg| - pkg.need_zip = false - pkg.need_tar = false -end - -task :gem => :gemspec - -desc %{Validate the gemspec file.} -task :gemspec do - gemspec.validate -end - -desc %{Release the gem to RubyGems.org} -task :release => :gem do - system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem" -end +task build: :validate_gemspec -desc "Cleans up white space in source files" +desc 'Cleans up white space in source files' task :clean_whitespace do no_file_cleaned = true - Dir["**/*.rb"].each do |file| + Dir['**/*.rb'].each do |file| contents = File.read(file) cleaned_contents = contents.gsub(/([ \t]+)$/, '') unless cleaned_contents == contents no_file_cleaned = false puts " - Cleaned #{file}" - File.open(file, 'w') { |f| f.write(cleaned_contents) } + File.write(file, cleaned_contents) end end if no_file_cleaned - puts "No files with trailing whitespace found" + puts 'No files with trailing whitespace found' end end -require 'rspec/core' require 'rspec/core/rake_task' -desc "Run specs" +desc 'Run specs' RSpec::Core::RakeTask.new(:spec) -task :default => :spec +task default: :spec diff --git a/documentation/adapters.md b/documentation/adapters.md index 78b83b57..3bba6911 100644 --- a/documentation/adapters.md +++ b/documentation/adapters.md @@ -1,7 +1,7 @@ ### Adapters -Webmachine includes adapters for [WEBrick][webrick], [Reel][reel], and -[HTTPkit][httpkit]. Additionally, the [Rack][rack] adapter lets it +Webmachine includes an adapter for [WEBrick][webrick]. +Additionally, the [Rack][rack] adapter lets it run on any webserver that provides a Rack interface. It also lets it run on [Shotgun][shotgun] ([example][shotgun_example]). @@ -10,28 +10,21 @@ run on any webserver that provides a Rack interface. It also lets it run on In order to be compatible with popular deployment stacks, Webmachine has a [Rack](https://github.com/rack/rack) adapter (thanks to Jamis Buck). -Webmachine can be used with Rack middlware features such as Rack::Map and Rack::Cascade as long as: - -1. The Webmachine app is mounted at the root directory. -2. Any requests/responses that are handled by the Webmachine app are not modified by the middleware. The behaviours that are encapsulated in Webmachine assume that no modifications +Webmachine can be used with Rack middlware features such as Rack::Map and Rack::Cascade as long as any requests/responses that are handled by the Webmachine app are **not** modified by the middleware. The behaviours that are encapsulated in Webmachine assume that no modifications are done to requests or response outside of Webmachine. Keep in mind that Webmachine already supports many things that Rack middleware is used for with other HTTP frameworks (eg. etags, specifying supported/preferred Accept and Content-Types). +The base `Webmachine::Adapters::Rack` class assumes the Webmachine application +is mounted at the route path `/` (i.e. not using `Rack::Builder#map` or Rails +`ActionDispatch::Routing::Mapper::Base#mount`). In order to +map to a subpath, use the `Webmachine::Adapters::RackMapped` adapter instead. + For an example of using Webmachine with Rack middleware, see the [Pact Broker][middleware-example]. See the [Rack Adapter API docs][rack-adapter-api-docs] for more information. -#### A Note about MRI 1.9 - -The [Reel][reel] and [HTTPkit][httpkit] -adapters might crash with a `SystemStackError` on MRI 1.9 due to its -limited fiber stack size. If your application is affected by this, the -only known solution is to switch to JRuby, Rubinius or MRI 2.0. - [webrick]: http://rubydoc.info/stdlib/webrick -[reel]: https://github.com/celluloid/reel -[httpkit]: https://github.com/lgierth/httpkit [rack]: https://github.com/rack/rack [shotgun]: https://github.com/rtomayko/shotgun [shotgun_example]: https://gist.github.com/4389220 diff --git a/documentation/examples.md b/documentation/examples.md index 08f25538..3891cc7d 100644 --- a/documentation/examples.md +++ b/documentation/examples.md @@ -92,12 +92,15 @@ end ``` # POST to perform a task -* Override `allowed_methods` and `process_post`. Put all the code to be executed in `process_post`. -* `process_post` must return true, or the HTTP response code -* Response headers like Content-Type will need to be set manually. +* Override `allowed_methods`, `process_post`, and `content_types_provided` (if the response has a content type). +* Rather than providing a method handler in the `content_type_provided` mappings, put all the code to be executed in `process_post`. +* `process_post` must return true, or the HTTP response code. ```ruby class DispatchOrderResource < Webmachine::Resource + def content_types_provided + [["application/json"]] + end def allowed_methods ["POST"] @@ -108,9 +111,8 @@ class DispatchOrderResource < Webmachine::Resource end def process_post - @order.dispatch - response.headers['Content-Type'] = 'text/plain' - response.body = "Successfully dispatched order #{id}" + @order.dispatch(params['some_param']) + response.body = { message: "Successfully dispatched order #{id}" }.to_json true end @@ -119,6 +121,10 @@ class DispatchOrderResource < Webmachine::Resource def id request.path_info[:id] end + + def params + JSON.parse(request.body.to_s) + end end ``` @@ -137,6 +143,8 @@ class OrderResource < Webmachine::Resource [["application/json", :from_json]] end + # Note that returning falsey will NOT result in a 404 for PUT requests. + # See note below. def resource_exists? order end @@ -166,8 +174,10 @@ class OrderResource < Webmachine::Resource end ``` +If you wish to disallow PUT to a non-existent resource, read more [here](https://github.com/webmachine/webmachine-ruby/issues/207#issuecomment-132604379). + # PATCH -* Webmachine does not currently support PATCH requests. See https://github.com/seancribbs/webmachine-ruby/issues/109 for more information and https://github.com/bethesque/pact_broker/blob/2918814e70bbda14df68598a6a41502a5eac4308/lib/pact_broker/api/resources/pacticipant.rb for a dirty hack to make it work if you need to. +* Webmachine does not currently support PATCH requests. See https://github.com/webmachine/webmachine-ruby/issues/109 for more information and https://github.com/bethesque/pact_broker/blob/2918814e70bbda14df68598a6a41502a5eac4308/lib/pact_broker/api/resources/pacticipant.rb for a dirty hack to make it work if you need to. # DELETE * Override `resource_exists?` and `delete_resource` @@ -208,8 +218,7 @@ Thanks to [oestrich][oestrich] for putting together the original example. You ca [oestrich]: https://github.com/oestrich [source]: https://gist.github.com/oestrich/3638605 - + ## What order are the callbacks invoked in? - This question is actually irrelevant if you write your code in a "stateless" way using lazy initialization as the examples do above. As much as possible, think about exposing "facts" about your resource, not writing procedural code that needs to be called in a certain order. See [How it works](/documentation/how-it-works.md) for more information on how the Webmachine state machine works. diff --git a/documentation/how-it-works.md b/documentation/how-it-works.md index 70d49087..323dfd1a 100644 --- a/documentation/how-it-works.md +++ b/documentation/how-it-works.md @@ -71,6 +71,6 @@ end * A collection resource (eg. /orders) should be implemented as a separate class to a single object resource (eg. /orders/1), as the routes represent different underlying objects with different "facts". For example, the orders _collection_ resource probably always exists (but may be empty), however the order with ID 1 may or may not exist. [callbacks]: https://github.com/seancribbs/webmachine-ruby/blob/master/lib/webmachine/resource/callbacks.rb -[diagram]: http://webmachine.basho.com/images/http-headers-status-v3.png +[diagram]: https://webmachine.github.io/images/http-headers-status-v3.png [flow]: https://github.com/seancribbs/webmachine-ruby/blob/master/lib/webmachine/decision/flow.rb [examples]: /documentation/examples.md diff --git a/documentation/routes.md b/documentation/routes.md index bdbd27f0..9dcfd307 100644 --- a/documentation/routes.md +++ b/documentation/routes.md @@ -20,6 +20,21 @@ App = Webmachine::Application.new do |app| # but will not provide any path_info add ["orders", :*], OrderResource + # Will map to any path that matches the given components and regular expression + # Any capture groups specified in the regex will be made available in + # request.path_info[:captures. In this case, you would get one or two + # values in :captures depending on whether your request looked like: + # /orders/1/cancel + # or + # /orders/1/cancel.json + add ["orders", :id, /([^.]*)\.?(.*)?/], OrderResource + + # You can even use named captures with regular expressions. This will + # automatically put the captures into path_info. In the below example, + # you would get :id from the symbol, along with :action and :format + # from the regex. :format in this case would be optional. + add ["orders", :id, /(?)[^.]*)\.?(?.*)?/], OrderResource + # will map to any path add [:*], DefaultResource end @@ -94,4 +109,4 @@ end request.path_info[:foo] => "bar" -``` \ No newline at end of file +``` diff --git a/examples/debugger.rb b/examples/debugger.rb index cff11c90..ec4bffa5 100644 --- a/examples/debugger.rb +++ b/examples/debugger.rb @@ -2,21 +2,23 @@ require 'webmachine/trace' class MyTracedResource < Webmachine::Resource - def trace?; true; end + def trace? + true + end def resource_exists? case request.query['e'] when 'true' true when 'fail' - raise "BOOM" + raise 'BOOM' else false end end def to_html - "You found me." + 'You found me.' end end diff --git a/examples/logging.rb b/examples/logging.rb index 24ceac7f..cda597ee 100644 --- a/examples/logging.rb +++ b/examples/logging.rb @@ -18,7 +18,7 @@ def handle_event(event) resource = event.payload[:resource] code = event.payload[:code] - puts "[%s] method=%s uri=%s code=%d resource=%s time=%.4f" % [ + puts '[%s] method=%s uri=%s code=%d resource=%s time=%.4f' % [ Time.now.iso8601, request.method, request.uri.to_s, code, resource, event.duration ] @@ -34,7 +34,7 @@ def handle_event(event) app.configure do |config| config.adapter = :WEBrick - config.adapter_options = {:AccessLog => [], :Logger => Logger.new('/dev/null')} + config.adapter_options = {AccessLog: [], Logger: Logger.new(File::NULL)} end end diff --git a/examples/webrick.rb b/examples/webrick.rb index 3c899ca8..a6a81268 100644 --- a/examples/webrick.rb +++ b/examples/webrick.rb @@ -6,11 +6,11 @@ def last_modified end def encodings_provided - { "gzip" => :encode_gzip, "identity" => :encode_identity } + {'gzip' => :encode_gzip, 'identity' => :encode_identity} end def to_html - "Hello from WebmachineHello, world!" + 'Hello from WebmachineHello, world!' end end diff --git a/lib/webmachine.rb b/lib/webmachine.rb index 3af9a461..0860ac32 100644 --- a/lib/webmachine.rb +++ b/lib/webmachine.rb @@ -1,4 +1,5 @@ -require 'webmachine/configuration' +require 'webmachine/configuration' +require 'webmachine/constants' require 'webmachine/cookie' require 'webmachine/headers' require 'webmachine/request' diff --git a/lib/webmachine/adapter.rb b/lib/webmachine/adapter.rb index c11aad52..2896a8cc 100644 --- a/lib/webmachine/adapter.rb +++ b/lib/webmachine/adapter.rb @@ -1,10 +1,8 @@ module Webmachine - # The abstract class for definining a Webmachine adapter. # # @abstract Subclass and override {#run} to implement a custom adapter. class Adapter - # @return [Webmachine::Application] returns the application attr_reader :application @@ -25,6 +23,5 @@ def self.run(application) def run raise NotImplementedError end - end end diff --git a/lib/webmachine/adapters.rb b/lib/webmachine/adapters.rb index 37c8e9d3..ab6dc231 100644 --- a/lib/webmachine/adapters.rb +++ b/lib/webmachine/adapters.rb @@ -5,7 +5,5 @@ module Webmachine # Contains classes and modules that connect Webmachine to Ruby # application servers. module Adapters - autoload :Reel, 'webmachine/adapters/reel' - autoload :HTTPkit, 'webmachine/adapters/httpkit' end end diff --git a/lib/webmachine/adapters/httpkit.rb b/lib/webmachine/adapters/httpkit.rb deleted file mode 100644 index 9961f8ff..00000000 --- a/lib/webmachine/adapters/httpkit.rb +++ /dev/null @@ -1,72 +0,0 @@ -require 'httpkit' - -require 'webmachine/version' -require 'webmachine/headers' -require 'webmachine/request' -require 'webmachine/response' -require 'webmachine/dispatcher' -require 'webmachine/chunked_body' - -module Webmachine - module Adapters - class HTTPkit < Adapter - def options - @options ||= { - :address => application.configuration.ip, - :port => application.configuration.port, - :handlers => [ - ::HTTPkit::Server::TimeoutsHandler.new, - ::HTTPkit::Server::KeepAliveHandler.new, - self - ] - } - end - - def run - ::HTTPkit.start do - ::HTTPkit::Server.start(options) - end - end - - # Called by HTTPkit::Server for every request - def serve(request, served) - response = Webmachine::Response.new - application.dispatcher.dispatch(convert_request(request), response) - - served.fulfill(convert_response(response)) - end - - private - - # Converts HTTPkit::Request to Webmachine::Request - def convert_request(request) - Webmachine::Request.new( - request.http_method.to_s.upcase, - request.uri, - Webmachine::Headers[request.headers.dup], - request.body) - end - - # Converts Webmachine::Response to HTTPkit::Response - def convert_response(response) - response.headers["Server"] = - Webmachine::SERVER_STRING + ' HTTPkit/' + ::HTTPkit::VERSION - - ::HTTPkit::Response.new( - response.code.to_i, - response.headers, - convert_body(response.body)) - end - - # HTTPkit::Body accepts strings and enumerables, i.e. Webmachine's - # Callable, Enumerable, IO, and Fiber encoders are supported. - def convert_body(body) - if body.respond_to?(:call) - [body.call] - else - body || '' - end - end - end - end -end diff --git a/lib/webmachine/adapters/lazy_request_body.rb b/lib/webmachine/adapters/lazy_request_body.rb index 08e6dbed..8fd2c069 100644 --- a/lib/webmachine/adapters/lazy_request_body.rb +++ b/lib/webmachine/adapters/lazy_request_body.rb @@ -1,5 +1,4 @@ - -module Webmachine +module Webmachine module Adapters # Wraps a request body so that it can be passed to # {Request} while still lazily evaluating the body. @@ -25,7 +24,7 @@ def empty? # @yield [chunk] # @yieldparam [String] chunk a chunk of the request body def each - @request.body {|chunk| yield chunk } + @request.body { |chunk| yield chunk } end end # class RequestBody end # module Adapters diff --git a/lib/webmachine/adapters/rack.rb b/lib/webmachine/adapters/rack.rb index d1d34fee..b4484d5b 100644 --- a/lib/webmachine/adapters/rack.rb +++ b/lib/webmachine/adapters/rack.rb @@ -1,9 +1,10 @@ +require 'webmachine/adapter' require 'rack' -require 'webmachine/version' +require 'webmachine/constants' require 'webmachine/headers' require 'webmachine/request' require 'webmachine/response' -require 'webmachine/dispatcher' +require 'webmachine/version' require 'webmachine/chunked_body' module Webmachine @@ -11,9 +12,12 @@ module Adapters # A minimal "shim" adapter to allow Webmachine to interface with Rack. The # intention here is to allow Webmachine to run under Rack-compatible # web-servers, like unicorn and pow. + # # The adapter expects your Webmachine application to be mounted at the root path - # it will NOT allow you to nest your Webmachine application at an arbitrary path # eg. map "/api" { run MyWebmachineAPI } + # To use map your Webmachine application at an arbitrary path, use the + # `Webmachine::Adapters::RackMapped` subclass instead. # # To use this adapter, create a config.ru file and populate it like so: # @@ -36,12 +40,16 @@ class Rack < Adapter # Used to override default Rack server options (useful in testing) DEFAULT_OPTIONS = {} + REQUEST_URI = 'REQUEST_URI'.freeze + VERSION_STRING = "#{Webmachine::SERVER_STRING} Rack/#{::Rack.release}".freeze + NEWLINE = "\n".freeze + # Start the Rack adapter def run options = DEFAULT_OPTIONS.merge({ - :app => self, - :Port => application.configuration.port, - :Host => application.configuration.ip + app: self, + Port: application.configuration.port, + Host: application.configuration.ip }).merge(application.configuration.adapter_options) @server = ::Rack::Server.new(options) @@ -54,52 +62,82 @@ def call(env) headers = Webmachine::Headers.from_cgi(env) rack_req = ::Rack::Request.new env - request = Webmachine::Request.new(rack_req.request_method, - env['REQUEST_URI'], - headers, - RequestBody.new(rack_req)) + request = build_webmachine_request(rack_req, headers) response = Webmachine::Response.new application.dispatcher.dispatch(request, response) - response.headers['Server'] = [Webmachine::SERVER_STRING, "Rack/#{::Rack.version}"].join(" ") + response.headers[SERVER] = VERSION_STRING - rack_status = response.code - rack_headers = response.headers.flattened("\n") + rack_status = response.code + rack_headers = response.headers.flattened(NEWLINE) rack_body = case response.body - when String # Strings are enumerable in ruby 1.8 - [response.body] - else - if (io_body = IO.try_convert(response.body)) - io_body - elsif response.body.respond_to?(:call) - Webmachine::ChunkedBody.new(Array(response.body.call)) - elsif response.body.respond_to?(:each) - # This might be an IOEncoder with a Content-Length, which shouldn't be chunked. - if response.headers["Transfer-Encoding"] == "chunked" - Webmachine::ChunkedBody.new(response.body) - else - response.body - end - else - [response.body.to_s] - end - end + when String # Strings are enumerable in ruby 1.8 + [response.body] + else + if (io_body = IO.try_convert(response.body)) + io_body + elsif response.body.respond_to?(:call) + Webmachine::ChunkedBody.new(Array(response.body.call)) + elsif response.body.respond_to?(:each) + # This might be an IOEncoder with a Content-Length, which shouldn't be chunked. + if response.headers[TRANSFER_ENCODING] == 'chunked' + Webmachine::ChunkedBody.new(response.body) + else + response.body + end + else + [response.body.to_s] + end + end rack_res = RackResponse.new(rack_body, rack_status, rack_headers) rack_res.finish end + protected + + def routing_tokens(rack_req) + nil # no-op for default, un-mapped rack adapter + end + + def base_uri(rack_req) + nil # no-op for default, un-mapped rack adapter + end + + private + + def build_webmachine_request(rack_req, headers) + RackRequest.new(rack_req.request_method, + rack_req.url, + headers, + RequestBody.new(rack_req), + routing_tokens(rack_req), + base_uri(rack_req), + rack_req.env) + end + + class RackRequest < Webmachine::Request + attr_reader :env + + def initialize(method, uri, headers, body, routing_tokens, base_uri, env) + super(method, uri, headers, body, routing_tokens, base_uri) + @env = env + end + end + class RackResponse + ONE_FIVE = '1.5'.freeze + def initialize(body, status, headers) - @body = body - @status = status + @body = body + @status = status @headers = headers end def finish - @headers['Content-Type'] ||= 'text/html' if rack_release_enforcing_content_type - @headers.delete('Content-Type') if response_without_body + @headers[CONTENT_TYPE] ||= TEXT_HTML if rack_release_enforcing_content_type + @headers.delete(CONTENT_TYPE) if response_without_body [@status, @headers, @body] end @@ -110,7 +148,7 @@ def response_without_body end def rack_release_enforcing_content_type - ::Rack.release < '1.5' + ::Rack.release < ONE_FIVE end end @@ -151,14 +189,16 @@ def to_s # @yieldparam [String] chunk a chunk of the request body def each if @value - @value.each {|chunk| yield chunk } + @value.each { |chunk| yield chunk } else @value = [] - @request.body.each {|chunk| @value << chunk; yield chunk } + @request.body.each { |chunk| + @value << chunk + yield chunk + } end end end # class RequestBody end # class Rack - end # module Adapters end # module Webmachine diff --git a/lib/webmachine/adapters/rack_mapped.rb b/lib/webmachine/adapters/rack_mapped.rb new file mode 100644 index 00000000..dc87a732 --- /dev/null +++ b/lib/webmachine/adapters/rack_mapped.rb @@ -0,0 +1,41 @@ +require 'webmachine/adapters/rack' + +module Webmachine + module Adapters + # Provides the same functionality as the parent Webmachine::Adapters::Rack + # adapter, but allows the Webmachine application to be hosted at an + # arbitrary path in a parent Rack application (as in Rack `map` or Rails + # routing `mount`) + # + # This functionality is separated out from the parent class to preserve + # backward compatibility in the behaviour of the parent Rack adpater. + # + # To use the adapter in a parent Rack application, map the Webmachine + # application as follows in a rackup file or Rack::Builder: + # + # map '/foo' do + # run SomeotherRackApp + # + # map '/bar' do + # run MyWebmachineApp.adapter + # end + # end + class RackMapped < Rack + protected + + def routing_tokens(rack_req) + routing_match = rack_req.path_info.match(Webmachine::Request::ROUTING_PATH_MATCH) + routing_path = routing_match ? routing_match[1] : '' + routing_path.split(SLASH) + end + + def base_uri(rack_req) + # rack SCRIPT_NAME env var doesn't end with "/". This causes weird + # behavour when URI.join concatenates URI components in + # Webmachine::Decision::Flow#n11 + script_name = rack_req.script_name + SLASH + URI.join(rack_req.base_url, script_name) + end + end # class RackMapped + end # module Adapters +end # module Webmachine diff --git a/lib/webmachine/adapters/reel.rb b/lib/webmachine/adapters/reel.rb deleted file mode 100644 index d76b31d5..00000000 --- a/lib/webmachine/adapters/reel.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'reel' -require 'webmachine/version' -require 'webmachine/headers' -require 'webmachine/request' -require 'webmachine/response' -require 'webmachine/dispatcher' -require 'set' - -module Webmachine - module Adapters - class Reel < Adapter - # Used to override default Reel server options (useful in testing) - DEFAULT_OPTIONS = {} - - def run - @options = DEFAULT_OPTIONS.merge({ - :port => application.configuration.port, - :host => application.configuration.ip - }).merge(application.configuration.adapter_options) - - if extra_verbs = application.configuration.adapter_options[:extra_verbs] - @extra_verbs = Set.new(extra_verbs.map(&:to_s).map(&:upcase)) - else - @extra_verbs = Set.new - end - - if @options[:ssl] - unless @options[:ssl][:cert] && @options[:ssl][:key] - raise ArgumentError, 'Certificate or Private key missing for HTTPS Server' - end - @server = ::Reel::Server::HTTPS.supervise(@options[:host], @options[:port], @options[:ssl], &method(:process)) - else - @server = ::Reel::Server::HTTP.supervise(@options[:host], @options[:port], &method(:process)) - end - - Celluloid::Actor.join(@server) - end - - def process(connection) - connection.each_request do |request| - # Users of the adapter can configure a custom WebSocket handler - if request.websocket? - if handler = @options[:websocket_handler] - handler.call(request.websocket) - else - # Pretend we don't know anything about the WebSocket protocol - # FIXME: This isn't strictly what RFC 6455 would have us do - request.respond :bad_request, "WebSockets not supported" - end - - next - end - - # Optional support for e.g. WebDAV verbs not included in Webmachine's - # state machine. Do the "Railsy" thing and handle them like POSTs - # with a magical parameter - if @extra_verbs.include?(request.method) - method = "POST" - param = "_method=#{request.method}" - uri = request_uri(request.url, request.headers, param) - else - method = request.method - uri = request_uri(request.url, request.headers) - end - - wm_headers = Webmachine::Headers[request.headers.dup] - wm_request = Webmachine::Request.new(method, uri, wm_headers, request.body) - - wm_response = Webmachine::Response.new - application.dispatcher.dispatch(wm_request, wm_response) - - fixup_headers(wm_response) - fixup_callable_encoder(wm_response) - - request.respond ::Reel::Response.new(wm_response.code, - wm_response.headers, - wm_response.body) - end - end - - def request_uri(path, headers, extra_query_params = nil) - path_parts = path.split('?') - uri_hash = {path: path_parts.first} - uri_hash[:query] = path_parts.last if path_parts.length == 2 - - if extra_query_params - if uri_hash[:query] - uri_hash[:query] << "&#{extra_query_params}" - else - uri_hash[:query] = extra_query_params - end - end - - URI::HTTP.build(uri_hash) - end - - def fixup_headers(response) - response.headers.each do |key, value| - if value.is_a?(Array) - response.headers[key] = value.join(", ") - end - end - end - - def fixup_callable_encoder(response) - if response.body.is_a?(Streaming::CallableEncoder) - response.body = [response.body.call] - end - end - end - end -end diff --git a/lib/webmachine/adapters/webrick.rb b/lib/webmachine/adapters/webrick.rb index 18644521..52c6ce78 100644 --- a/lib/webmachine/adapters/webrick.rb +++ b/lib/webmachine/adapters/webrick.rb @@ -1,9 +1,11 @@ +require 'webmachine/adapter' require 'webrick' -require 'webmachine/version' +require 'webmachine/constants' require 'webmachine/headers' +require 'webmachine/adapters/lazy_request_body' require 'webmachine/request' require 'webmachine/response' -require 'webmachine/dispatcher' +require 'webmachine/version' module Webmachine module Adapters @@ -15,9 +17,9 @@ class WEBrick < Adapter # Starts the WEBrick adapter def run options = DEFAULT_OPTIONS.merge({ - :Port => application.configuration.port, - :BindAddress => application.configuration.ip, - :application => application + Port: application.configuration.port, + BindAddress: application.configuration.ip, + application: application }).merge(application.configuration.adapter_options) @server = Server.new(options) @server.start @@ -27,35 +29,35 @@ def run class Server < ::WEBrick::HTTPServer def initialize(options) @application = options[:application] - super(options) + super end # Handles a request def service(wreq, wres) header = Webmachine::Headers.new - wreq.each {|k,v| header[k] = v } + wreq.each { |k, v| header[k] = v } request = Webmachine::Request.new(wreq.request_method, - wreq.request_uri, - header, - LazyRequestBody.new(wreq)) + wreq.request_uri, + header, + LazyRequestBody.new(wreq)) response = Webmachine::Response.new @application.dispatcher.dispatch(request, response) wres.status = response.code.to_i - headers = response.headers.flattened.reject { |k,v| k == 'Set-Cookie' } - headers.each { |k,v| wres[k] = v } + headers = response.headers.flattened.reject { |k, v| k == 'Set-Cookie' } + headers.each { |k, v| wres[k] = v } cookies = [response.headers['Set-Cookie'] || []].flatten cookies.each { |c| wres.cookies << c } - wres['Server'] = [Webmachine::SERVER_STRING, wres.config[:ServerSoftware]].join(" ") + wres[SERVER] = [Webmachine::SERVER_STRING, wres.config[:ServerSoftware]].join(' ') case response.body when String wres.body << response.body when Enumerable - wres.chunked = response.headers['Transfer-Encoding'] == 'chunked' - response.body.each {|part| wres.body << part } + wres.chunked = response.headers[TRANSFER_ENCODING] == 'chunked' + response.body.each { |part| wres.body << part } else if response.body.respond_to?(:call) wres.chunked = true diff --git a/lib/webmachine/application.rb b/lib/webmachine/application.rb index 9cc3a5b7..314f2e8b 100644 --- a/lib/webmachine/application.rb +++ b/lib/webmachine/application.rb @@ -43,7 +43,7 @@ class Application # the Application instance being initialized def initialize(configuration = Configuration.default, dispatcher = Dispatcher.new) @configuration = configuration - @dispatcher = dispatcher + @dispatcher = dispatcher yield self if block_given? end @@ -73,7 +73,7 @@ def adapter_class # # @see Webmachine::Dispatcher#add_route def routes(&block) - if block_given? + if block dispatcher.instance_eval(&block) self else diff --git a/lib/webmachine/chunked_body.rb b/lib/webmachine/chunked_body.rb index a1819ea9..8942ac7f 100644 --- a/lib/webmachine/chunked_body.rb +++ b/lib/webmachine/chunked_body.rb @@ -1,3 +1,5 @@ +require 'webmachine/constants' + module Webmachine # {ChunkedBody} is used to wrap an {Enumerable} object (like an enumerable # {Response#body}) so it yields proper chunks for chunked transfer encoding. @@ -13,11 +15,8 @@ module Webmachine # # This is needed for Ruby webservers which don't do the chunking themselves. class ChunkedBody - # Delimiter for chunked encoding - CRLF = "\r\n" - # Final chunk in any chunked-encoding response - FINAL_CHUNK = "0#{CRLF}#{CRLF}" + FINAL_CHUNK = "0#{CRLF}#{CRLF}".freeze # Creates a new {ChunkedBody} from the given {Enumerable}. # @param [Enumerable] body the enumerable response body @@ -30,7 +29,7 @@ def initialize(body) # parameter. # Returns an {Enumerator} if no block is given. def each - return self.to_enum unless block_given? + return to_enum unless block_given? @body.each do |chunk| size = chunk.bytesize diff --git a/lib/webmachine/configuration.rb b/lib/webmachine/configuration.rb index d6b66ef2..5555a92c 100644 --- a/lib/webmachine/configuration.rb +++ b/lib/webmachine/configuration.rb @@ -5,14 +5,14 @@ module Webmachine # defaults will be filled in when {Webmachine::run} is called. # @attr [String] ip the interface to bind to, defaults to "0.0.0.0" # (all interfaces) - # @attr [Fixnum] port the port to bind to, defaults to 8080 + # @attr [Integer] port the port to bind to, defaults to 8080 # @attr [Symbol] adapter the adapter to use, defaults to :WEBrick # @attr [Hash] adapter_options adapter-specific options, defaults to {} Configuration = Struct.new(:ip, :port, :adapter, :adapter_options) # @return [Configuration] the default configuration def Configuration.default - new("0.0.0.0", 8080, :WEBrick, {}) + new('0.0.0.0', 8080, :WEBrick, {}) end # Yields the current configuration to the passed block. diff --git a/lib/webmachine/constants.rb b/lib/webmachine/constants.rb new file mode 100644 index 00000000..444f4eb8 --- /dev/null +++ b/lib/webmachine/constants.rb @@ -0,0 +1,75 @@ +module Webmachine + # Universal HTTP delimiter + CRLF = "\r\n".freeze + + # HTTP Content-Type + CONTENT_TYPE = 'Content-Type'.freeze + + # Default Content-Type + TEXT_HTML = 'text/html'.freeze + + # HTTP Date + DATE = 'Date'.freeze + + # HTTP Transfer-Encoding + TRANSFER_ENCODING = 'Transfer-Encoding'.freeze + + # HTTP Content-Length + CONTENT_LENGTH = 'Content-Length'.freeze + + # A underscore + UNDERSCORE = '_'.freeze + + # A dash + DASH = '-'.freeze + + # A Slash + SLASH = '/'.freeze + + MATCHES_ALL = '*/*'.freeze + + GET_METHOD = 'GET' + HEAD_METHOD = 'HEAD' + POST_METHOD = 'POST' + PUT_METHOD = 'PUT' + DELETE_METHOD = 'DELETE' + OPTIONS_METHOD = 'OPTIONS' + TRACE_METHOD = 'TRACE' + CONNECT_METHOD = 'CONNECT' + + STANDARD_HTTP_METHODS = [ + GET_METHOD, HEAD_METHOD, POST_METHOD, + PUT_METHOD, DELETE_METHOD, TRACE_METHOD, + CONNECT_METHOD, OPTIONS_METHOD + ].map!(&:freeze) + STANDARD_HTTP_METHODS.freeze + + # A colon + COLON = ':'.freeze + + # http string + HTTP = 'http'.freeze + + # Host string + HOST = 'Host'.freeze + + # HTTP Content-Encoding + CONTENT_ENCODING = 'Content-Encoding'.freeze + + # Charset string + CHARSET = 'Charset'.freeze + + # Comma split match + SPLIT_COMMA = /\s*,\s*/.freeze + + # Star Character + STAR = '*'.freeze + + # HTTP Location + LOCATION = 'Location'.freeze + + # identity Encoding + IDENTITY = 'identity'.freeze + + SERVER = 'Server'.freeze +end diff --git a/lib/webmachine/cookie.rb b/lib/webmachine/cookie.rb index 9d80e26e..dfb07990 100644 --- a/lib/webmachine/cookie.rb +++ b/lib/webmachine/cookie.rb @@ -11,7 +11,7 @@ class Cookie def self.parse(cstr, include_dups = false) cookies = {} (cstr || '').split(/\s*[;,]\s*/n).each { |c| - k,v = c.split(/\s*=\s*/, 2).map { |s| unescape(s) } + k, v = c.split(/\s*=\s*/, 2).map { |s| unescape(s) } case cookies[k] when nil @@ -31,7 +31,7 @@ def self.parse(cstr, include_dups = false) # Allowed keys for the attributes parameter of # {Webmachine::Cookie#initialize} ALLOWED_ATTRIBUTES = [:secure, :httponly, :path, :domain, - :comment, :maxage, :expires, :version] + :comment, :maxage, :expires, :version] # If the cookie is HTTP only def http_only? @@ -83,32 +83,34 @@ def to_s attributes = ALLOWED_ATTRIBUTES.select { |a| @attributes[a] }.map do |a| case a when :httponly - "HttpOnly" if @attributes[a] + 'HttpOnly' if @attributes[a] when :secure - "Secure" if @attributes[a] + 'Secure' if @attributes[a] when :maxage - "Max-Age=" + @attributes[a].to_s + 'Max-Age=' + @attributes[a].to_s when :expires - "Expires=" + rfc2822(@attributes[a]) + 'Expires=' + rfc2822(@attributes[a]) when :comment - "Comment=" + escape(@attributes[a].to_s) + 'Comment=' + escape(@attributes[a].to_s) else - a.to_s.sub(/^\w/) { $&.capitalize } + "=" + @attributes[a].to_s + a.to_s.sub(/^\w/) { $&.capitalize } + '=' + @attributes[a].to_s end end - ([escape(name) + "=" + escape(value)] + attributes).join("; ") + ([escape(name) + '=' + escape(value)] + attributes).join('; ') end private + # Format timestamps for the 'Expires' portion of the cookie string, as per RFC 2822 and 2616. + # + # @see https://www.rfc-editor.org/rfc/rfc2616#section-3.3.1 + # @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires def rfc2822(time) - wday = Time::RFC2822_DAY_NAME[time.wday] - mon = Time::RFC2822_MONTH_NAME[time.mon - 1] - time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT") + time.strftime('%a, %d %b %Y %T GMT') end - if URI.respond_to?(:decode_www_form_component) and defined?(::Encoding) + if URI.respond_to?(:decode_www_form_component) && defined?(::Encoding) # Escape a cookie def escape(s) URI.encode_www_form_component(s) @@ -132,7 +134,7 @@ def self.unescape(s, encoding = Encoding::UTF_8) # @private TBLDECWWWCOMP_ = {} 256.times do |i| - h, l = i>>4, i&15 + h, l = i >> 4, i & 15 TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr @@ -146,9 +148,9 @@ def self.unescape(s, encoding = Encoding::UTF_8) # This decodes + to SP. # # @private - def self.unescape(str, enc=nil) - raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%\h\h|[^%]+)*\z/ =~ str - str.gsub(/\+|%\h\h/){|c| TBLDECWWWCOMP_[c] } + def self.unescape(str, enc = nil) + raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%\h\h|[^%]+)*\z/.match?(str) + str.gsub(/\+|%\h\h/) { |c| TBLDECWWWCOMP_[c] } end # Encode given +str+ to URL-encoded form data. @@ -161,7 +163,7 @@ def self.unescape(str, enc=nil) # # @private def escape(str) - str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/){|c| TBLENCWWWCOMP_[c] } + str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/) { |c| TBLENCWWWCOMP_[c] } end end end diff --git a/lib/webmachine/decision/conneg.rb b/lib/webmachine/decision/conneg.rb index e89d2d9e..204634de 100644 --- a/lib/webmachine/decision/conneg.rb +++ b/lib/webmachine/decision/conneg.rb @@ -1,3 +1,4 @@ +require 'webmachine/constants' require 'webmachine/translation' require 'webmachine/media_type' @@ -13,7 +14,7 @@ module Conneg # appropriate media type. # @api private def choose_media_type(provided, header) - types = Array(header).map{|h| h.split(/\s*,\s*/) }.flatten + types = Array(header).map { |h| h.split(SPLIT_COMMA) }.flatten requested = MediaTypeList.build(types) provided = provided.map do |p| # normalize_provided MediaType.parse(p) @@ -31,9 +32,9 @@ def choose_media_type(provided, header) # @api private def choose_encoding(provided, header) encodings = provided.keys - if encoding = do_choose(encodings, header, "identity") - response.headers['Content-Encoding'] = encoding unless encoding == 'identity' - metadata['Content-Encoding'] = encoding + if encoding = do_choose(encodings, header, IDENTITY) + response.headers[CONTENT_ENCODING] = encoding unless encoding == IDENTITY + metadata[CONTENT_ENCODING] = encoding end end @@ -42,9 +43,9 @@ def choose_encoding(provided, header) # @api private def choose_charset(provided, header) if provided && !provided.empty? - charsets = provided.map {|c| c.first } + charsets = provided.map { |c| c.first } if charset = do_choose(charsets, header, HAS_ENCODING ? Encoding.default_external.name : kcode_charset) - metadata['Charset'] = charset + metadata[CHARSET] = charset end else true @@ -56,22 +57,22 @@ def choose_charset(provided, header) # @api private def choose_language(provided, header) if provided && !provided.empty? - requested = PriorityList.build(header.split(/\s*,\s*/)) - star_priority = requested.priority_of("*") + requested = PriorityList.build(header.split(SPLIT_COMMA)) + star_priority = requested.priority_of(STAR) any_ok = star_priority && star_priority > 0.0 accepted = requested.find do |priority, range| if priority == 0.0 - provided.delete_if {|tag| language_match(range, tag) } + provided.delete_if { |tag| language_match(range, tag) } false else - provided.any? {|tag| language_match(range, tag) } + provided.any? { |tag| language_match(range, tag) } end end chosen = if accepted - provided.find {|tag| language_match(accepted.last, tag) } - elsif any_ok - provided.first - end + provided.find { |tag| language_match(accepted.last, tag) } + elsif any_ok + provided.first + end if chosen metadata['Language'] = chosen response.headers['Content-Language'] = chosen @@ -90,17 +91,17 @@ def choose_language(provided, header) # is "-". # @api private def language_match(range, tag) - range.downcase == tag.downcase || tag =~ /^#{Regexp.escape(range)}\-/i + range.downcase == tag.downcase || tag =~ /^#{Regexp.escape(range)}-/i end # Makes an conneg choice based what is accepted and what is # provided. # @api private def do_choose(choices, header, default) - choices = choices.dup.map {|s| s.downcase } - accepted = PriorityList.build(header.split(/\s*,\s*/)) + choices = choices.dup.map { |s| s.downcase } + accepted = PriorityList.build(header.split(SPLIT_COMMA)) default_priority = accepted.priority_of(default) - star_priority = accepted.priority_of("*") + star_priority = accepted.priority_of(STAR) default_ok = (default_priority.nil? && star_priority != 0.0) || default_priority any_ok = star_priority && star_priority > 0.0 chosen = accepted.find do |priority, acceptable| @@ -111,14 +112,15 @@ def do_choose(choices, header, default) choices.include?(acceptable.downcase) end end - (chosen && chosen.last) || # Use the matching one + chosen&.last || # Use the matching one (any_ok && choices.first) || # Or first if "*" (default_ok && choices.include?(default) && default) # Or default end private + # Matches acceptable items that include 'q' values - CONNEG_REGEX = /^\s*(\S+);\s*q=(\S*)\s*$/ + CONNEG_REGEX = /^\s*(\S+);\s*q=(\S*)\s*$/.freeze # Matches the requested media type (with potential modifiers) # against the provided types (with potential modifiers). @@ -137,13 +139,13 @@ def media_match(requested, provided) def kcode_charset case $KCODE when /^U/i - "UTF-8" + 'UTF-8' when /^S/i - "Shift-JIS" + 'Shift-JIS' when /^B/i - "Big5" - else #when /^A/i, nil - "ASCII" + 'Big5' + else # when /^A/i, nil + 'ASCII' end end @@ -156,7 +158,7 @@ class PriorityList # Given an acceptance list, create a PriorityList from them. def self.build(list) new.tap do |plist| - list.each {|item| plist.add_header_val(item) } + list.each { |item| plist.add_header_val(item) } end end @@ -165,7 +167,7 @@ def self.build(list) # Creates a {PriorityList}. # @see PriorityList::build def initialize - @hash = Hash.new {|h,k| h[k] = [] } + @hash = Hash.new { |h, k| h[k] = [] } @index = {} end @@ -184,8 +186,8 @@ def add(q, choice) def add_header_val(c) if c =~ CONNEG_REGEX choice, q = $1, $2 - q = "0" << q if q =~ /^\./ # handle strange FeedBurner Accept - add(q.to_f,choice) + q = '0' << q if /^\./.match?(q) # handle strange FeedBurner Accept + add(q.to_f, choice) else add(1.0, c) end @@ -211,8 +213,8 @@ def priority_of(choice) # @yieldparam [Float] q the acceptable item's priority # @yieldparam [String] v the acceptable item def each - @hash.to_a.sort.reverse_each do |q,l| - l.each {|v| yield q, v } + @hash.to_a.sort.reverse_each do |q, l| + l.each { |v| yield q, v } end end end @@ -231,10 +233,9 @@ def add_header_val(c) q = mt.params.delete('q') || 1.0 add(q.to_f, mt) rescue ArgumentError - raise MalformedRequest, t('invalid_media_type', :type => c) + raise MalformedRequest, t('invalid_media_type', type: c) end end - end # module Conneg end # module Decision end # module Webmachine diff --git a/lib/webmachine/decision/falsey.rb b/lib/webmachine/decision/falsey.rb index 30a15f56..06d5fe75 100644 --- a/lib/webmachine/decision/falsey.rb +++ b/lib/webmachine/decision/falsey.rb @@ -7,4 +7,3 @@ def Falsey.===(other) end end end - diff --git a/lib/webmachine/decision/flow.rb b/lib/webmachine/decision/flow.rb index a3f2c16f..5ecc8f54 100644 --- a/lib/webmachine/decision/flow.rb +++ b/lib/webmachine/decision/flow.rb @@ -1,6 +1,7 @@ -require 'time' +require 'time' require 'digest/md5' require 'base64' +require 'webmachine/constants' require 'webmachine/decision/conneg' require 'webmachine/decision/falsey' require 'webmachine/translation' @@ -34,7 +35,7 @@ module Flow # Handles standard decisions where halting is allowed def decision_test(test, iftrue, iffalse) case test - when Fixnum # Allows callbacks to "halt" with a given response code + when Integer # Allows callbacks to "halt" with a given response code test when Falsey iffalse @@ -63,7 +64,7 @@ def b10 if resource.allowed_methods.include?(request.method) :b9 else - response.headers["Allow"] = resource.allowed_methods.join(", ") + response.headers['Allow'] = resource.allowed_methods.join(', ') 405 end end @@ -76,18 +77,18 @@ def b9 # Content-MD5 valid? def b9a case valid = resource.validate_content_checksum - when Fixnum + when Integer valid when true :b9b when false - response.body = "Content-MD5 header does not match request body." + response.body = 'Content-MD5 header does not match request body.' 400 else # not_validated if decode64(request.content_md5) == Digest::MD5.hexdigest(request.body) :b9b else - response.body = "Content-MD5 header does not match request body." + response.body = 'Content-MD5 header does not match request body.' 400 end end @@ -104,7 +105,7 @@ def b8 case result when true :b7 - when Fixnum + when Integer result when String response.headers['WWW-Authenticate'] = result @@ -119,9 +120,10 @@ def b7 decision_test(resource.forbidden?, 403, :b6) end + CONTENT = /content-/.freeze # Okay Content-* Headers? def b6 - decision_test(resource.valid_content_headers?(request.headers.grep(/content-/)), :b5, 501) + decision_test(resource.valid_content_headers?(request.headers.grep(CONTENT)), :b5, 501) end # Known Content-Type? @@ -147,7 +149,7 @@ def b3 # Accept exists? def c3 if !request.accept - metadata['Content-Type'] = MediaType.parse(resource.content_types_provided.first.first) + metadata[CONTENT_TYPE] = MediaType.parse(resource.content_types_provided.first.first) :d4 else :c4 @@ -156,12 +158,12 @@ def c3 # Acceptable media type available? def c4 - types = resource.content_types_provided.map {|pair| pair.first } + types = resource.content_types_provided.map { |pair| pair.first } chosen_type = choose_media_type(types, request.accept) if !chosen_type 406 else - metadata['Content-Type'] = chosen_type + metadata[CONTENT_TYPE] = chosen_type :d4 end end @@ -169,7 +171,7 @@ def c4 # Accept-Language exists? def d4 if !request.accept_language - if language = choose_language(resource.languages_provided, "*") + if language = choose_language(resource.languages_provided, STAR) resource.language_chosen(language) :e5 else @@ -193,7 +195,7 @@ def d5 # Accept-Charset exists? def e5 if !request.accept_charset - choose_charset(resource.charsets_provided, "*") ? :f6 : 406 + choose_charset(resource.charsets_provided, STAR) ? :f6 : 406 else :e6 end @@ -207,13 +209,13 @@ def e6 # Accept-Encoding exists? # (also, set content-type header here, now that charset is chosen) def f6 - chosen_type = metadata['Content-Type'] - if chosen_charset = metadata['Charset'] + chosen_type = metadata[CONTENT_TYPE] + if chosen_charset = metadata[CHARSET] chosen_type.params['charset'] = chosen_charset end - response.headers['Content-Type'] = chosen_type.to_s + response.headers[CONTENT_TYPE] = chosen_type.to_s if !request.accept_encoding - choose_encoding(resource.encodings_provided, "identity;q=1.0,*;q=0.5") ? :g7 : 406 + choose_encoding(resource.encodings_provided, 'identity;q=1.0,*;q=0.5') ? :g7 : 406 else :f7 end @@ -227,7 +229,7 @@ def f7 # Resource exists? def g7 # This is the first place after all conneg, so set Vary here - response.headers['Vary'] = variances.join(", ") if variances.any? + response.headers['Vary'] = variances.join(', ') if variances.any? decision_test(resource.resource_exists?, :g8, :h7) end @@ -238,18 +240,18 @@ def g8 # If-Match: * exists? def g9 - quote(request.if_match) == '"*"' ? :h10 : :g11 + (quote(request.if_match) == '"*"') ? :h10 : :g11 end # ETag in If-Match def g11 - request_etags = request.if_match.split(/\s*,\s*/).map {|etag| ETag.new(etag) } + request_etags = request.if_match.split(SPLIT_COMMA).map { |etag| ETag.new(etag) } request_etags.include?(ETag.new(resource.generate_etag)) ? :h10 : 412 end # If-Match exists? def h7 - (request.if_match && unquote(request.if_match) == '*') ? 412 : :i7 + (request.if_match && unquote(request.if_match) == STAR) ? 412 : :i7 end # If-Unmodified-Since exists? @@ -269,16 +271,16 @@ def h11 # Last-Modified > I-UM-S? def h12 - resource.last_modified > metadata['If-Unmodified-Since'] ? 412 : :i12 + (resource.last_modified > metadata['If-Unmodified-Since']) ? 412 : :i12 end # Moved permanently? (apply PUT to different URI) def i4 case uri = resource.moved_permanently? when String, URI - response.headers["Location"] = uri.to_s + response.headers[LOCATION] = uri.to_s 301 - when Fixnum + when Integer uri else :p3 @@ -297,7 +299,7 @@ def i12 # If-none-match: * exists? def i13 - quote(request.if_none_match) == '"*"' ? :j18 : :k13 + (quote(request.if_none_match) == '"*"') ? :j18 : :k13 end # GET or HEAD? @@ -309,9 +311,9 @@ def j18 def k5 case uri = resource.moved_permanently? when String, URI - response.headers["Location"] = uri.to_s + response.headers[LOCATION] = uri.to_s 301 - when Fixnum + when Integer uri else :l5 @@ -325,10 +327,10 @@ def k7 # Etag in if-none-match? def k13 - request_etags = request.if_none_match.split(/\s*,\s*/).map {|etag| ETag.new(etag) } + request_etags = request.if_none_match.split(SPLIT_COMMA).map { |etag| ETag.new(etag) } resource_etag = resource.generate_etag if resource_etag && request_etags.include?(ETag.new(resource_etag)) - :j18 + :j18 else :l13 end @@ -338,9 +340,9 @@ def k13 def l5 case uri = resource.moved_temporarily? when String, URI - response.headers["Location"] = uri.to_s + response.headers[LOCATION] = uri.to_s 307 - when Fixnum + when Integer uri else :m5 @@ -369,12 +371,12 @@ def l14 # IMS > Now? def l15 - metadata['If-Modified-Since'] > Time.now ? :m16 : :l17 + (metadata['If-Modified-Since'] > Time.now) ? :m16 : :l17 end # Last-Modified > IMS? def l17 - resource.last_modified.nil? || resource.last_modified > metadata['If-Modified-Since'] ? :m16 : 304 + (resource.last_modified.nil? || resource.last_modified > metadata['If-Modified-Since']) ? :m16 : 304 end # POST? @@ -413,27 +415,27 @@ def n11 if resource.post_is_create? case uri = resource.create_path when nil - raise InvalidResource, t('create_path_nil', :class => resource.class) + raise InvalidResource, t('create_path_nil', class: resource.class) when URI, String base_uri = resource.base_uri || request.base_uri new_uri = URI.join(base_uri.to_s, uri) request.disp_path = new_uri.path - response.headers['Location'] = new_uri.to_s + response.headers[LOCATION] = new_uri.to_s result = accept_helper - return result if Fixnum === result + return result if Integer === result end else case result = resource.process_post when true encode_body_if_set - when Fixnum + when Integer return result else - raise InvalidResource, t('process_post_invalid', :result => result.inspect) + raise InvalidResource, t('process_post_invalid', result: result.inspect) end end if response.is_redirect? - if response.headers['Location'] + if response.headers[LOCATION] 303 else raise InvalidResource, t('do_redirect') @@ -454,7 +456,7 @@ def o14 409 else res = accept_helper - (Fixnum === res) ? res : :p11 + (Integer === res) ? res : :p11 end end @@ -468,10 +470,10 @@ def o16 def o18 if request.get? || request.head? add_caching_headers - content_type = metadata['Content-Type'] - handler = resource.content_types_provided.find {|ct, _| content_type.type_matches?(MediaType.parse(ct)) }.last + content_type = metadata[CONTENT_TYPE] + handler = resource.content_types_provided.find { |ct, _| content_type.type_matches?(MediaType.parse(ct)) }.last result = resource.send(handler) - if Fixnum === result + if Integer === result result else response.body = result @@ -499,15 +501,14 @@ def p3 409 else res = accept_helper - (Fixnum === res) ? res : :p11 + (Integer === res) ? res : :p11 end end # New resource? def p11 - !response.headers["Location"] ? :o20 : 201 + (!response.headers[LOCATION]) ? :o20 : 201 end - end # module Flow end # module Decision end # module Webmachine diff --git a/lib/webmachine/decision/fsm.rb b/lib/webmachine/decision/fsm.rb index d641a7f2..44ec2484 100644 --- a/lib/webmachine/decision/fsm.rb +++ b/lib/webmachine/decision/fsm.rb @@ -1,6 +1,8 @@ -require 'webmachine/decision/helpers' +require 'webmachine/decision/helpers' require 'webmachine/trace' require 'webmachine/translation' +require 'webmachine/constants' +require 'webmachine/rescueable_exception' module Webmachine module Decision @@ -28,17 +30,17 @@ def run trace_decision(state) result = handle_exceptions { send(state) } case result - when Fixnum # Response code + when Integer # Response code respond(result) break when Symbol # Next state state = result else # You bwoke it - raise InvalidResource, t('fsm_broke', :state => state, :result => result.inspect) + raise InvalidResource, t('fsm_broke', state: state, result: result.inspect) end end - rescue Exception => e - Webmachine.render_error(500, request, response, :message => e.message) + rescue => e + Webmachine.render_error(500, request, response, message: e.message) ensure trace_response(response) end @@ -47,22 +49,22 @@ def run def handle_exceptions yield - rescue MalformedRequest => e - Webmachine.render_error(400, request, response, :message => e.message) - 400 - rescue Exception => e + rescue Webmachine::RescuableException => e resource.handle_exception(e) 500 + rescue MalformedRequest => e + Webmachine.render_error(400, request, response, message: e.message) + 400 end - def respond(code, headers={}) + def respond(code, headers = {}) response.code = code response.headers.merge!(headers) case code when 404 Webmachine.render_error(code, request, response) when 304 - response.headers.delete('Content-Type') + response.headers.delete(CONTENT_TYPE) add_caching_headers end diff --git a/lib/webmachine/decision/helpers.rb b/lib/webmachine/decision/helpers.rb index 6625473c..892ad063 100644 --- a/lib/webmachine/decision/helpers.rb +++ b/lib/webmachine/decision/helpers.rb @@ -1,10 +1,11 @@ -require 'stringio' +require 'stringio' require 'time' require 'webmachine/streaming' require 'webmachine/media_type' require 'webmachine/quoted_string' require 'webmachine/etags' require 'webmachine/header_negotiation' +require 'webmachine/constants' module Webmachine module Decision @@ -28,38 +29,38 @@ def encode_body_if_set # Encodes the body in the selected charset and encoding. def encode_body body = response.body - chosen_charset = metadata['Charset'] - chosen_encoding = metadata['Content-Encoding'] - charsetter = resource.charsets_provided && resource.charsets_provided.find {|c,_| c == chosen_charset }.last || :charset_nop + chosen_charset = metadata[CHARSET] + chosen_encoding = metadata[CONTENT_ENCODING] + charsetter = resource.charsets_provided&.find { |c, _| c == chosen_charset }&.last || :charset_nop encoder = resource.encodings_provided[chosen_encoding] response.body = case body - when String # 1.8 treats Strings as Enumerable - resource.send(encoder, resource.send(charsetter, body)) - when IO, StringIO - IOEncoder.new(resource, encoder, charsetter, body) - when Fiber - FiberEncoder.new(resource, encoder, charsetter, body) - when Enumerable - EnumerableEncoder.new(resource, encoder, charsetter, body) - else - if body.respond_to?(:call) - CallableEncoder.new(resource, encoder, charsetter, body) - else - resource.send(encoder, resource.send(charsetter, body)) - end - end + when String # 1.8 treats Strings as Enumerable + resource.send(encoder, resource.send(charsetter, body)) + when IO, StringIO + IOEncoder.new(resource, encoder, charsetter, body) + when Fiber + FiberEncoder.new(resource, encoder, charsetter, body) + when Enumerable + EnumerableEncoder.new(resource, encoder, charsetter, body) + else + if body.respond_to?(:call) + CallableEncoder.new(resource, encoder, charsetter, body) + else + resource.send(encoder, resource.send(charsetter, body)) + end + end if body_is_fixed_length? ensure_content_length(response) else - response.headers.delete 'Content-Length' - response.headers['Transfer-Encoding'] = 'chunked' + response.headers.delete CONTENT_LENGTH + response.headers[TRANSFER_ENCODING] = 'chunked' end end # Assists in receiving request bodies def accept_helper content_type = MediaType.parse(request.content_type || 'application/octet-stream') - acceptable = resource.content_types_accepted.find {|ct, _| content_type.match?(ct) } + acceptable = resource.content_types_accepted.find { |ct, _| content_type.match?(ct) } if acceptable resource.send(acceptable.last) else @@ -70,10 +71,10 @@ def accept_helper # Computes the entries for the 'Vary' response header def variances resource.variances.tap do |v| - v.unshift "Accept-Language" if resource.languages_provided.size > 1 - v.unshift "Accept-Charset" if resource.charsets_provided && resource.charsets_provided.size > 1 - v.unshift "Accept-Encoding" if resource.encodings_provided.size > 1 - v.unshift "Accept" if resource.content_types_provided.size > 1 + v.unshift 'Accept-Language' if resource.languages_provided.size > 1 + v.unshift 'Accept-Charset' if resource.charsets_provided && resource.charsets_provided.size > 1 + v.unshift 'Accept-Encoding' if resource.encodings_provided.size > 1 + v.unshift 'Accept' if resource.content_types_provided.size > 1 end end @@ -94,7 +95,7 @@ def add_caching_headers # is a String or IO with known size. def body_is_fixed_length? response.body.respond_to?(:bytesize) && - Fixnum === response.body.bytesize + Integer === response.body.bytesize end end # module Helpers end # module Decision diff --git a/lib/webmachine/dispatcher.rb b/lib/webmachine/dispatcher.rb index def8961e..c405369e 100644 --- a/lib/webmachine/dispatcher.rb +++ b/lib/webmachine/dispatcher.rb @@ -1,4 +1,4 @@ -require 'forwardable' +require 'forwardable' require 'webmachine/decision' require 'webmachine/dispatcher/route' @@ -6,6 +6,8 @@ module Webmachine # Handles dispatching incoming requests to the proper registered # resources and initializing the decision logic. class Dispatcher + WM_DISPATCH = 'wm.dispatch'.freeze + # @return [Array] the list of routes that will be # dispatched to # @see #add_route @@ -31,7 +33,7 @@ def add_route(*args, &block) @routes << route route end - alias :add :add_route + alias_method :add, :add_route # Dispatches a request to the appropriate {Resource} in the # dispatch list. If a matching resource is not found, a "404 Not @@ -40,7 +42,7 @@ def add_route(*args, &block) # @param [Response] response the response object def dispatch(request, response) if resource = find_resource(request, response) - Webmachine::Events.instrument('wm.dispatch') do |payload| + Webmachine::Events.instrument(WM_DISPATCH) do |payload| Webmachine::Decision::FSM.new(resource, request, response).run payload[:resource] = resource.class.name @@ -70,10 +72,11 @@ def find_resource(request, response) # Find the first route that matches an incoming request # @param [Request] request the request to match def find_route(request) - @routes.find {|r| r.match?(request) } + @routes.find { |r| r.match?(request) } end private + def prepare_resource(route, request, response) route.apply(request) @resource_creator.call(route, request, response) diff --git a/lib/webmachine/dispatcher/route.rb b/lib/webmachine/dispatcher/route.rb index e36d3b8a..17fca693 100644 --- a/lib/webmachine/dispatcher/route.rb +++ b/lib/webmachine/dispatcher/route.rb @@ -1,12 +1,13 @@ -require 'webmachine/resource' +require 'webmachine/resource' require 'webmachine/translation' +require 'webmachine/constants' module Webmachine class Dispatcher # Pairs URIs with {Resource} classes in the {Dispatcher}. To # create routes, use {Dispatcher#add_route}. class Route - include Webmachine::Translation + include Translation # @return [Class] the resource this route will dispatch to, a # subclass of {Resource} @@ -27,6 +28,21 @@ class Route # String version of MATCH_ALL, deprecated. Use the symbol instead. MATCH_ALL_STR = '*'.freeze + # Decode a string using the scheme described in RFC 3986 2.1. Percent-Encoding (https://www.ietf.org/rfc/rfc3986.txt) + def self.rfc3986_percent_decode(value) + s = StringScanner.new(value) + result = '' + until s.eos? + encoded_val = s.scan(/%([0-9a-fA-F]){2}/) + result << if encoded_val.nil? + s.getch + else + [encoded_val[1..]].pack('H*') + end + end + result + end + # Creates a new Route that will associate a pattern to a # {Resource}. # @@ -57,32 +73,32 @@ class Route # @yield [req] an optional guard block # @yieldparam [Request] req the request object # @see Dispatcher#add_route - def initialize(path_spec, *args) - if args.last.is_a? Hash - bindings = args.pop + def initialize(path_spec, *args, &block) + bindings = if args.last.is_a? Hash + args.pop else - bindings = {} + {} end resource = args.pop guards = args - guards << Proc.new if block_given? + guards << block if block warn t('match_all_symbol') if path_spec.include? MATCH_ALL_STR @path_spec = path_spec - @guards = guards - @resource = resource - @bindings = bindings + @guards = guards + @resource = resource + @bindings = bindings - raise ArgumentError, t('not_resource_class', :class => resource.name) unless resource < Resource + raise ArgumentError, t('not_resource_class', class: resource.name) unless resource < Resource end # Determines whether the given request matches this route and # should be dispatched to the {#resource}. # @param [Reqeust] request the request object def match?(request) - tokens = request.uri.path.match(/^\/(.*)/)[1].split('/') + tokens = request.routing_tokens bind(tokens, {}) && guards.all? { |guard| guard.call(request) } end @@ -90,19 +106,20 @@ def match?(request) # route, including path bindings. # @param [Request] request the request object def apply(request) - request.disp_path = request.uri.path.match(/^\/(.*)/)[1] + request.disp_path = request.routing_tokens.join(SLASH) request.path_info = @bindings.dup - tokens = request.disp_path.split('/') - depth, trailing = bind(tokens, request.path_info) + tokens = request.routing_tokens + _depth, trailing = bind(tokens, request.path_info) request.path_tokens = trailing || [] end private + # Attempts to match the path spec against the path tokens, while # accumulating variable bindings. # @param [Array] tokens the list of path segments # @param [Hash] bindings where path bindings will be stored - # @return [Fixnum, Array, false] either the depth + # @return [Integer, Array, false] either the depth # that the path matched at, the depth and tokens matched by # {MATCH_ALL}, or false if it didn't match. def bind(tokens, bindings) @@ -118,18 +135,30 @@ def bind(tokens, bindings) return [depth, tokens] when tokens.empty? return false + when Regexp === spec.first + matches = spec.first.match Route.rfc3986_percent_decode(tokens.first) + if matches + if spec.first.named_captures.empty? + bindings[:captures] = (bindings[:captures] || []) + matches.captures + else + spec.first.named_captures.each_with_object(bindings) do |(name, idxs), bindings| + bindings[name.to_sym] = matches.captures[idxs.first - 1] + end + end + else + return false + end when Symbol === spec.first - bindings[spec.first] = URI.decode(tokens.first) + bindings[spec.first] = Route.rfc3986_percent_decode(tokens.first) when spec.first == tokens.first else return false end - spec = spec[1..-1] - tokens = tokens[1..-1] + spec = spec[1..] + tokens = tokens[1..] depth += 1 end end - end # class Route end # module Dispatcher end # module Webmachine diff --git a/lib/webmachine/errors.rb b/lib/webmachine/errors.rb index bc93325b..8ebb1c9f 100644 --- a/lib/webmachine/errors.rb +++ b/lib/webmachine/errors.rb @@ -1,5 +1,6 @@ -require 'webmachine/header_negotiation' +require 'webmachine/header_negotiation' require 'webmachine/translation' +require 'webmachine/constants' require 'webmachine/version' module Webmachine @@ -8,28 +9,27 @@ module Webmachine # Renders a standard error message body for the response. The # standard messages are defined in localization files. - # @param [Fixnum] code the response status code + # @param [Integer] code the response status code # @param [Request] req the request object # @param [Response] req the response object # @param [Hash] options keys to override the defaults when rendering # the response body - def self.render_error(code, req, res, options={}) + def self.render_error(code, req, res, options = {}) res.code = code unless res.body title, message = t(["errors.#{code}.title", "errors.#{code}.message"], - { :method => req.method, - :error => res.error}.merge(options)) - res.body = t("errors.standard_body", - {:title => title, - :message => message, - :version => Webmachine::SERVER_STRING}.merge(options)) - res.headers['Content-Type'] = "text/html" + {method: req.method, + error: res.error}.merge(options)) + res.body = t('errors.standard_body', + {title: title, + message: message, + version: Webmachine::SERVER_STRING}.merge(options)) + res.headers[CONTENT_TYPE] = TEXT_HTML end ensure_content_length(res) ensure_date_header(res) end - # Superclass of all errors generated by Webmachine. class Error < ::StandardError; end @@ -38,6 +38,6 @@ class InvalidResource < Error; end # Raised when the client has submitted an invalid request, e.g. in # the case where a request header is improperly formed. Raising this - # exception will result in a 400 response. + # error will result in a 400 response. class MalformedRequest < Error; end end # module Webmachine diff --git a/lib/webmachine/etags.rb b/lib/webmachine/etags.rb index b5ae2b72..4fe28a4a 100644 --- a/lib/webmachine/etags.rb +++ b/lib/webmachine/etags.rb @@ -5,12 +5,13 @@ module Webmachine # This class by itself represents a "strong" entity tag. class ETag include QuotedString + # The pattern for a weak entity tag WEAK_ETAG = /^W\/#{QUOTED_STRING}$/.freeze def self.new(etag) return etag if ETag === etag - klass = etag =~ WEAK_ETAG ? WeakETag : self + klass = WEAK_ETAG.match?(etag) ? WeakETag : self klass.send(:allocate).tap do |obj| obj.send(:initialize, etag) end @@ -53,6 +54,7 @@ def to_s end private + def unquote(str) if str =~ WEAK_ETAG unescape_quotes $1 diff --git a/lib/webmachine/events.rb b/lib/webmachine/events.rb index 1fe73d96..5040bcdc 100644 --- a/lib/webmachine/events.rb +++ b/lib/webmachine/events.rb @@ -55,9 +55,9 @@ def publish(name, *args) # and publish it. Notice that events get sent even if an error occurs # in the passed-in block. # - # If an exception happens during an instrumentation the payload will + # If an error happens during an instrumentation the payload will # have a key `:exception` with an array of two elements as value: - # a string with the name of the exception class, and the exception + # a string with the name of the error class, and the error # message. (when using the default # [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications) # backend) diff --git a/lib/webmachine/header_negotiation.rb b/lib/webmachine/header_negotiation.rb index 4dc292db..6243e199 100644 --- a/lib/webmachine/header_negotiation.rb +++ b/lib/webmachine/header_negotiation.rb @@ -1,22 +1,23 @@ +require 'webmachine/constants' + module Webmachine module HeaderNegotiation def ensure_date_header(res) - if (200..499).include?(res.code) - res.headers['Date'] ||= Time.now.httpdate + if (200..499).cover?(res.code) + res.headers[DATE] ||= Time.now.httpdate end end def ensure_content_length(res) body = res.body - case - when res.headers['Transfer-Encoding'] - return - when [204, 205, 304].include?(res.code) - res.headers.delete 'Content-Length' - when body != nil - res.headers['Content-Length'] = body.respond_to?(:bytesize) ? body.bytesize.to_s : body.length.to_s + if res.headers[TRANSFER_ENCODING] + nil + elsif [204, 205, 304].include?(res.code) + res.headers.delete CONTENT_LENGTH + elsif !body.nil? + res.headers[CONTENT_LENGTH] = body.respond_to?(:bytesize) ? body.bytesize.to_s : body.length.to_s else - res.headers['Content-Length'] = '0' + res.headers[CONTENT_LENGTH] = '0' end end end diff --git a/lib/webmachine/headers.rb b/lib/webmachine/headers.rb index a441b7aa..2de99112 100644 --- a/lib/webmachine/headers.rb +++ b/lib/webmachine/headers.rb @@ -1,15 +1,19 @@ +require 'webmachine/constants' + module Webmachine # Case-insensitive Hash of Request headers class Headers < ::Hash + CGI_HTTP_MATCH = /^HTTP_(\w+)$/.freeze + CONTENT_TYPE_LENGTH_MATCH = /^(CONTENT_(?:TYPE|LENGTH))$/.freeze + # Convert CGI-style Hash into Request headers # @param [Hash] env a hash of CGI-style env/headers # @return [Webmachine::Headers] def self.from_cgi(env) - env.inject(new) do |h,(k,v)| - if k =~ /^HTTP_(\w+)$/ || k =~ /^(CONTENT_(?:TYPE|LENGTH))$/ - h[$1.tr("_", "-")] = v + env.each_with_object(new) do |(k, v), h| + if k =~ CGI_HTTP_MATCH || k =~ CONTENT_TYPE_LENGTH_MATCH + h[$1.tr(UNDERSCORE, DASH)] = v end - h end end @@ -28,22 +32,22 @@ def self.from_cgi(env) # @param [Object] # @return [Webmachine::Headers] def self.[](*args) - super(super(*args).map {|k, v| [k.to_s.downcase, v]}) + super(super.map { |k, v| [k.to_s.downcase, v] }) end # Fetch a header def [](key) - super transform_key(key) + super(transform_key(key)) end # Set a header - def []=(key,value) - super transform_key(key), value + def []=(key, value) + super(transform_key(key), value) end # Returns the value for the given key. If the key can't be found, # there are several options: - # With no other arguments, it will raise a KeyError exception; + # With no other arguments, it will raise a KeyError error; # if default is given, then that will be returned; # if the optional code block is specified, then that will be run and its # result returned. @@ -66,15 +70,16 @@ def fetch(*args, &block) # Delete a header def delete(key) - super transform_key(key) + super(transform_key(key)) end # Select matching headers def grep(pattern) - self.class[select { |k,_| pattern === k }] + self.class[select { |k, _| pattern === k }] end private + def transform_key(key) key.to_s.downcase end diff --git a/lib/webmachine/media_type.rb b/lib/webmachine/media_type.rb index 128ee801..53ee0746 100644 --- a/lib/webmachine/media_type.rb +++ b/lib/webmachine/media_type.rb @@ -1,14 +1,17 @@ -require 'webmachine/translation' +require 'webmachine/translation' +require 'webmachine/constants' +require 'webmachine/dispatcher/route' module Webmachine # Encapsulates a MIME media type, with logic for matching types. class MediaType extend Translation + # Matches valid media types - MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\s*\S+\s*)*)\s*$/ + MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\s*\S+\s*)*)\s*$/.freeze # Matches sub-type parameters - PARAMS_REGEX = /;\s*([^=]+)(=([^;=\s]*))?/ + PARAMS_REGEX = /;\s*([^=]+)(=([^;=\s]*))?/.freeze # Creates a new MediaType by parsing an alternate representation. # @param [MediaType, String, Array] obj the raw type @@ -21,11 +24,11 @@ def self.parse(obj) obj when MEDIA_TYPE_REGEX type, raw_params = $1, $2 - params = Hash[raw_params.scan(PARAMS_REGEX).map { |m| [m[0], m[2].to_s] }] + params = raw_params.scan(PARAMS_REGEX).map { |m| [m[0], m[2].to_s] }.to_h new(type, params) else unless Array === obj && String === obj[0] && Hash === obj[1] - raise ArgumentError, t('invalid_media_type', :type => obj.inspect) + raise ArgumentError, t('invalid_media_type', type: obj.inspect) end type = parse(obj[0]) type.params.merge!(obj[1]) @@ -41,14 +44,14 @@ def self.parse(obj) # @param [String] type the main media type, e.g. application/json # @param [Hash] params the media type parameters - def initialize(type, params={}) + def initialize(type, params = {}) @type, @params = type, params end # Detects whether the {MediaType} represents an open wildcard # type, that is, "*/*" without any {#params}. def matches_all? - @type == "*/*" && @params.empty? + @type == MATCHES_ALL && @params.empty? end # @return [true,false] Are these two types strictly equal? @@ -86,23 +89,23 @@ def match?(other) # @param [Hash] params the requested params # @return [true,false] whether it is an acceptable match def params_match?(other) - other.all? {|k,v| params[k] == v } + other.all? { |k, v| params[k] == v } end # Reconstitutes the type into a String # @return [String] the type as a String def to_s - [type, *params.map {|k,v| "#{k}=#{v}" }].join(";") + [type, *params.map { |k, v| "#{k}=#{v}" }].join(';') end # @return [String] The major type, e.g. "application", "text", "image" def major - type.split("/").first + type.split(SLASH).first end # @return [String] the minor or sub-type, e.g. "json", "html", "jpeg" def minor - type.split("/").last + type.split(SLASH).last end # @param [MediaType] other the other type @@ -110,11 +113,8 @@ def minor # ignoring params and taking into account wildcards def type_matches?(other) other = self.class.parse(other) - if ["*", "*/*", type].include?(other.type) - true - else - other.major == major && other.minor == "*" - end + [Dispatcher::Route::MATCH_ALL_STR, MATCHES_ALL, type].include?(other.type) || + (other.major == major && other.minor == Dispatcher::Route::MATCH_ALL_STR) end end # class MediaType end # module Webmachine diff --git a/lib/webmachine/quoted_string.rb b/lib/webmachine/quoted_string.rb index 7c09a91c..bb039f8e 100644 --- a/lib/webmachine/quoted_string.rb +++ b/lib/webmachine/quoted_string.rb @@ -19,21 +19,21 @@ def unquote(str) # Ensures that quotes exist around a quoted-string def quote(str) - if str =~ QS_ANCHORED + if QS_ANCHORED.match?(str) str else - %Q{"#{escape_quotes str}"} + %("#{escape_quotes str}") end end # Escapes quotes within a quoted string. def escape_quotes(str) - str.gsub(/"/, '\\"') + str.gsub('"', '\\"') end # Unescapes quotes within a quoted string def unescape_quotes(str) - str.gsub(%r{\\}, '') + str.delete('\\') end end end diff --git a/lib/webmachine/request.rb b/lib/webmachine/request.rb index 3ff7d8fd..f8f3b771 100644 --- a/lib/webmachine/request.rb +++ b/lib/webmachine/request.rb @@ -1,28 +1,19 @@ -require 'cgi' +require 'cgi' require 'forwardable' +require 'webmachine/constants' +require 'ipaddr' module Webmachine # Request represents a single HTTP request sent from a client. It # should be instantiated by {Adapters} when a request is received class Request + HTTP_HEADERS_MATCH = /^(?:[a-z0-9])+(?:_[a-z0-9]+)*$/i.freeze + ROUTING_PATH_MATCH = /^\/(.*)/.freeze + extend Forwardable - attr_reader :method, :uri, :headers, :body - attr_accessor :disp_path, :path_info, :path_tokens - GET_METHOD = "GET" - HEAD_METHOD = "HEAD" - POST_METHOD = "POST" - PUT_METHOD = "PUT" - DELETE_METHOD = "DELETE" - OPTIONS_METHOD = "OPTIONS" - TRACE_METHOD = "TRACE" - CONNECT_METHOD = "CONNECT" - - STANDARD_HTTP_METHODS = [ - GET_METHOD, HEAD_METHOD, POST_METHOD, - PUT_METHOD, DELETE_METHOD, TRACE_METHOD, - CONNECT_METHOD, OPTIONS_METHOD - ].map!(&:freeze) + attr_reader :method, :uri, :headers, :body, :routing_tokens, :base_uri + attr_accessor :disp_path, :path_info, :path_tokens # @param [String] method the HTTP request method # @param [URI] uri the requested URI, including host, scheme and @@ -30,9 +21,14 @@ class Request # @param [Headers] headers the HTTP request headers # @param [String,#to_s,#each,nil] body the entity included in the # request, if present - def initialize(method, uri, headers, body) + def initialize(method, uri, headers, body, routing_tokens = nil, base_uri = nil) @method, @headers, @body = method, headers, body @uri = build_uri(uri, headers) + @routing_tokens = routing_tokens || @uri.path.match(ROUTING_PATH_MATCH)[1].split(SLASH) + @base_uri = base_uri || @uri.dup.tap do |u| + u.path = SLASH + u.query = nil + end end def_delegators :headers, :[] @@ -41,9 +37,18 @@ def initialize(method, uri, headers, body) # lowercased-underscored version of the header name, e.g. # `if_unmodified_since`. def method_missing(m, *args, &block) - if m.to_s =~ /^(?:[a-z0-9])+(?:_[a-z0-9]+)*$/i + if HTTP_HEADERS_MATCH.match?(m) # Access headers more easily as underscored methods. - self[m.to_s.tr('_', '-')] + header_name = m.to_s.tr(UNDERSCORE, DASH) + if (header_value = @headers[header_name]) + # Make future lookups faster. + self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{m} + @headers["#{header_name}"] + end + RUBY + end + header_value else super end @@ -54,16 +59,6 @@ def has_body? !(body.nil? || body.empty?) end - # The root URI for the request, ignoring path and query. This is - # useful for calculating relative paths to resources. - # @return [URI] - def base_uri - @base_uri ||= uri.dup.tap do |u| - u.path = "/" - u.query = nil - end - end - # Returns a hash of query parameters (they come after the ? in the # URI). Note that this does NOT work in the same way as Rails, # i.e. it does not support nested arrays and hashes. @@ -71,8 +66,8 @@ def base_uri def query unless @query @query = {} - (uri.query || '').split(/&/).each do |kv| - key, value = kv.split(/=/) + (uri.query || '').split('&').each do |kv| + key, value = kv.split('=') if key && value key, value = CGI.unescape(key), CGI.unescape(value) @query[key] = value @@ -87,9 +82,7 @@ def query # @return [Hash] # {} if no Cookies header set def cookies - unless @cookies - @cookies = Webmachine::Cookie.parse(headers['Cookie']) - end + @cookies ||= Webmachine::Cookie.parse(headers['Cookie']) @cookies end @@ -98,7 +91,7 @@ def cookies # @return [Boolean] # true if this request was made via HTTPS def https? - uri.scheme == "https" + uri.scheme == 'https' end # Is this a GET request? @@ -167,20 +160,34 @@ def options? private - def build_uri(uri, headers) - uri = URI(uri) - - host, _, port = headers.fetch("Host", "").rpartition(":") - return uri if host.empty? + IPV6_MATCH = /\A\[(?
.* )\]:(? \d+ )\z/x.freeze # string like "[::1]:80" + HOST_MATCH = /\A(? [^:]+ ):(? \d+ )\z/x.freeze # string like "www.example.com:80" + + def parse_host(uri, host_string) + # Split host and port number from string. + case host_string + when IPV6_MATCH + uri.host = IPAddr.new($~[:address], Socket::AF_INET6).to_s + uri.port = $~[:port].to_i + when HOST_MATCH + uri.host = $~[:host] + uri.port = $~[:port].to_i + else # string with no port number + uri.host = host_string + end - host = "[#{host}]" if host.include?(":") - port = 80 if port.empty? + uri + end - uri.scheme = "http" - uri.host, uri.port = host, port.to_i + def build_uri(uri, headers) + uri = URI(uri) + uri.port ||= 80 + uri.scheme ||= HTTP + if uri.host + return uri + end - URI.parse(uri.to_s) + parse_host(uri, headers.fetch(HOST)) end - end # class Request end # module Webmachine diff --git a/lib/webmachine/rescueable_exception.rb b/lib/webmachine/rescueable_exception.rb new file mode 100644 index 00000000..0e7d9907 --- /dev/null +++ b/lib/webmachine/rescueable_exception.rb @@ -0,0 +1,62 @@ +module Webmachine::RescuableException + require_relative 'errors' + require 'set' + + UNRESCUABLE_DEFAULTS = [ + Webmachine::MalformedRequest, + NoMemoryError, SystemExit, SignalException + ].freeze + + UNRESCUABLE = Set.new UNRESCUABLE_DEFAULTS.dup + private_constant :UNRESCUABLE + + def self.===(e) + case e + when *UNRESCUABLE then false + else true + end + end + + # + # Remove modifications to Webmachine::RescuableException. + # Restores default list of unrescue-able exceptions. + # + # @return [nil] + # + def self.default! + UNRESCUABLE.replace Set.new(UNRESCUABLE_DEFAULTS.dup) + nil + end + + # + # @return [Array] + # Returns an Array of exceptions that will not be + # rescued by {Webmachine::Resource#handle_exception}. + # + def self.UNRESCUABLEs + UNRESCUABLE.to_a + end + + # + # Add a variable number of exceptions that should be rescued by + # {Webmachine::Resource#handle_exception}. See {UNRESCUABLE_DEFAULTS} + # for a list of exceptions that are not caught by default. + # + # @param (see #remove) + # + def self.add(*exceptions) + exceptions.each { |e| UNRESCUABLE.delete(e) } + end + + # + # Remove a variable number of exceptions from being rescued by + # {Webmachine::Resource#handle_exception}. See {UNRESCUABLE_DEFAULTS} + # for a list of exceptions that are not caught by default. + # + # @param [Exception] *exceptions + # A subclass of Exception. + # + def self.remove(*exceptions) + exceptions.each { |e| UNRESCUABLE.add(e) } + end +end diff --git a/lib/webmachine/resource.rb b/lib/webmachine/resource.rb index 1f4b3cb1..c8fdbd22 100644 --- a/lib/webmachine/resource.rb +++ b/lib/webmachine/resource.rb @@ -59,11 +59,11 @@ def self.run end private + # When no specific charsets are provided, this acts as an identity # on the response body. Probably deserves some refactoring. def charset_nop(x) x end - end # class Resource end # module Webmachine diff --git a/lib/webmachine/resource/authentication.rb b/lib/webmachine/resource/authentication.rb index d3a3e976..6cce84e6 100644 --- a/lib/webmachine/resource/authentication.rb +++ b/lib/webmachine/resource/authentication.rb @@ -23,14 +23,13 @@ module Authentication # @yieldparam [String] user the passed username # @yieldparam [String] password the passed password # @yieldreturn [true,false] whether the username/password is correct - def basic_auth(header, realm="Webmachine") - if header =~ BASIC_HEADER && (yield *$1.unpack('m*').first.split(/:/,2)) + def basic_auth(header, realm = 'Webmachine') + if header =~ BASIC_HEADER && yield(*$1.unpack1('m*').split(':', 2)) true else - %Q[Basic realm="#{realm}"] + %(Basic realm="#{realm}") end end - end # module Authentication end # class Resource end # module Webmachine diff --git a/lib/webmachine/resource/callbacks.rb b/lib/webmachine/resource/callbacks.rb index ecc607cc..6780de42 100644 --- a/lib/webmachine/resource/callbacks.rb +++ b/lib/webmachine/resource/callbacks.rb @@ -1,3 +1,5 @@ +require 'webmachine/constants' + module Webmachine class Resource # These methods are the primary way your {Webmachine::Resource} @@ -101,7 +103,7 @@ def valid_content_headers?(content_headers = nil) # If the entity length on PUT or POST is invalid, this should # return false, which will result in a '413 Request Entity Too # Large' response. Defaults to true. - # @param [Fixnum] length the size of the request body (entity) + # @param [Integer] length the size of the request body (entity) # @return [true,false] Whether the body is a valid length (not too # large) # @api callback @@ -123,7 +125,7 @@ def options # @return [Array] allowed methods on this resource # @api callback def allowed_methods - ['GET', 'HEAD'] + [GET_METHOD, HEAD_METHOD] end # HTTP methods that are known to the resource. Like @@ -134,7 +136,7 @@ def allowed_methods # @return [Array] known methods # @api callback def known_methods - ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS'] + STANDARD_HTTP_METHODS end # This method is called when a DELETE request should be enacted, @@ -190,7 +192,7 @@ def base_uri # If post_is_create? returns false, then this will be called to # process any POST request. If it succeeds, it should return true. - # @return [true,false,Fixnum] Whether the POST was successfully + # @return [true,false,Integer] Whether the POST was successfully # processed, or an alternate response code # @api callback def process_post @@ -209,7 +211,7 @@ def process_post # @return an array of mediatype/handler pairs # @api callback def content_types_provided - [['text/html', :to_html]] + [[TEXT_HTML, :to_html]] end # Similarly to content_types_provided, this should return an array @@ -263,7 +265,7 @@ def language_chosen(lang) # @api callback # @see Encodings def encodings_provided - {"identity" => :encode_identity } + {IDENTITY => :encode_identity} end # If this method is implemented, it should return a list of @@ -358,14 +360,15 @@ def generate_etag # constructed and sent. The return value is ignored, so any effect # of this method must be by modifying the response. # @api callback - def finish_request; end + def finish_request + end # - # This method is called when an exception is raised within a subclass of + # This method is called when an error is raised within a subclass of # {Webmachine::Resource}. # - # @param [Exception] e - # The exception. + # @param [StandardError] e + # The error. # # @return [void] # @@ -387,7 +390,6 @@ def handle_exception(e) def validate_content_checksum nil end - end # module Callbacks end # class Resource end # module Webmachine diff --git a/lib/webmachine/resource/encodings.rb b/lib/webmachine/resource/encodings.rb index 40987f1e..3e6c0cad 100644 --- a/lib/webmachine/resource/encodings.rb +++ b/lib/webmachine/resource/encodings.rb @@ -15,23 +15,17 @@ def encode_identity(data) # The 'deflate' encoding, which uses libz's DEFLATE compression. def encode_deflate(data) # The deflate options were borrowed from Rack and Mongrel1. - Zlib::Deflate.deflate(data, *[Zlib::DEFAULT_COMPRESSION, - # drop the zlib header which causes both Safari and IE to choke - -Zlib::MAX_WBITS, - Zlib::DEF_MEM_LEVEL, - Zlib::DEFAULT_STRATEGY - ]) + Zlib::Deflate.deflate(data, Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS, Zlib::DEF_MEM_LEVEL, Zlib::DEFAULT_STRATEGY) end # The 'gzip' encoding, which uses GNU Zip (via libz). # @note Because of the header/checksum requirements, gzip cannot # be used on streamed responses. def encode_gzip(data) - "".tap do |out| - Zlib::GzipWriter.wrap(StringIO.new(out)){|gz| gz << data } + ''.tap do |out| + Zlib::GzipWriter.wrap(StringIO.new(out)) { |gz| gz << data } end end - end # module Encodings end # class Resource end # module Webmachine diff --git a/lib/webmachine/response.rb b/lib/webmachine/response.rb index 8c45a8b8..73fee48e 100644 --- a/lib/webmachine/response.rb +++ b/lib/webmachine/response.rb @@ -4,7 +4,7 @@ class Response # @return [HeaderHash] Response headers that will be sent to the client attr_reader :headers - # @return [Fixnum] The HTTP status code of the response + # @return [Integer] The HTTP status code of the response attr_accessor :code # @return [String, #each] The response body @@ -35,7 +35,7 @@ def initialize # of the target resource, or manually set the Location header # using {#headers}. # @param [String, URI] location the target of the redirection - def do_redirect(location=nil) + def do_redirect(location = nil) headers['Location'] = location.to_s if location self.redirect = true end @@ -48,16 +48,14 @@ def set_cookie(name, value, attributes = {}) cookie = Webmachine::Cookie.new(name, value, attributes).to_s case headers['Set-Cookie'] when nil - headers['Set-Cookie'] = cookie - when String - headers['Set-Cookie'] = [headers['Set-Cookie'], cookie] + headers['Set-Cookie'] = [cookie] when Array - headers['Set-Cookie'] = headers['Set-Cookie'] + cookie + headers['Set-Cookie'] << cookie end end - alias :is_redirect? :redirect - alias :redirect_to :do_redirect + alias_method :is_redirect?, :redirect + alias_method :redirect_to, :do_redirect # A {Hash} that can flatten array values into single values with a separator class HeaderHash < ::Hash @@ -65,17 +63,15 @@ class HeaderHash < ::Hash # @param [String] The separator used to join Array values # @return [HeaderHash] A new {HeaderHash} with Array values flattened def flattened(separator = ',') - Hash[self.collect { |k,v| + collect { |k, v| case v when Array - [k,v.join(separator)] + [k, v.join(separator)] else - [k,v] + [k, v] end - }] - + }.to_h end end - end # class Response end # module Webmachine diff --git a/lib/webmachine/spec/adapter_lint.rb b/lib/webmachine/spec/adapter_lint.rb index 74244db8..2cbed74f 100644 --- a/lib/webmachine/spec/adapter_lint.rb +++ b/lib/webmachine/spec/adapter_lint.rb @@ -1,159 +1,169 @@ -require "webmachine/spec/test_resource" -require "net/http" +require 'webmachine/spec/test_resource' +require 'net/http' + +ADDRESS = '127.0.0.1' shared_examples_for :adapter_lint do - attr_accessor :client + attr_reader :client + + class TestApplicationNotResponsive < Timeout::Error; end - let(:address) { "127.0.0.1" } - let(:port) { s = TCPServer.new(address, 0); p = s.addr[1]; s.close; p } + def find_free_port + temp_server = TCPServer.new(ADDRESS, 0) + port = temp_server.addr[1] + temp_server.close # only frees Ruby resource, socket is in TIME_WAIT at OS level + # so we can't have our adapter use it too quickly + + sleep(0.1) # 'Wait' for temp_server to *really* close, not just TIME_WAIT + port + end - let(:application) do - application = Webmachine::Application.new - application.dispatcher.add_route ["test"], Test::Resource + def create_test_application(port) + Webmachine::Application.new.tap do |application| + application.dispatcher.add_route ['test'], Test::Resource - application.configure do |c| - c.ip = address - c.port = port + application.configure do |c| + c.ip = ADDRESS + c.port = port + end end + end - application + def run_application(adapter_class, application) + adapter = adapter_class.new(application) + Thread.abort_on_exception = true + Thread.new { adapter.run } end - let(:client) do - client = Net::HTTP.new(application.configuration.ip, port) - # Wait until the server is responsive - timeout(5) do - begin - client.start - rescue Errno::ECONNREFUSED - sleep(0.01) - retry - end + def wait_until_server_responds_to(client) + Timeout.timeout(5, TestApplicationNotResponsive) do + client.start + rescue Errno::ECONNREFUSED + sleep(0.01) + retry end - client end - before do - @adapter = described_class.new(application) + before(:all) do + @port = find_free_port + application = create_test_application(@port) - Thread.abort_on_exception = true - @server_thread = Thread.new { @adapter.run } - sleep(0.01) + adapter_class = described_class + @server_thread = run_application(adapter_class, application) + + @client = Net::HTTP.new(application.configuration.ip, @port) + wait_until_server_responds_to(client) end - after do - client.finish + after(:all) do + @client.finish @server_thread.kill end - it "provides the request URI" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.request_uri" + it 'provides the request URI' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.request_uri' response = client.request(request) - expect(response.body).to eq("http://#{address}:#{port}/test") + expect(response.body).to eq("http://#{ADDRESS}:#{@port}/test") end - context do - let(:address) { "::1" } + # context do + # let(:address) { "::1" } - it "provides the IPv6 request URI" do - if RUBY_VERSION =~ /^2\.(0|1)\./ - skip "Net::HTTP regression in Ruby 2.(0|1)" - end - - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.request_uri" - response = client.request(request) - expect(response.body).to eq("http://[#{address}]:#{port}/test") - end - end + # it "provides the IPv6 request URI" do + # request = Net::HTTP::Get.new("/test") + # request["Accept"] = "test/response.request_uri" + # response = client.request(request) + # expect(response.body).to eq("http://[#{address}]:#{port}/test") + # end + # end - it "provides a string-like request body" do - request = Net::HTTP::Put.new("/test") - request.body = "Hello, World!" - request["Content-Type"] = "test/request.stringbody" + it 'provides a string-like request body' do + request = Net::HTTP::Put.new('/test') + request.body = 'Hello, World!' + request['Content-Type'] = 'test/request.stringbody' response = client.request(request) - expect(response["Content-Length"]).to eq("21") - expect(response.body).to eq("String: Hello, World!") + expect(response['Content-Length']).to eq('21') + expect(response.body).to eq('String: Hello, World!') end - it "provides an enumerable request body" do - request = Net::HTTP::Put.new("/test") - request.body = "Hello, World!" - request["Content-Type"] = "test/request.enumbody" + it 'provides an enumerable request body' do + request = Net::HTTP::Put.new('/test') + request.body = 'Hello, World!' + request['Content-Type'] = 'test/request.enumbody' response = client.request(request) - expect(response["Content-Length"]).to eq("19") - expect(response.body).to eq("Enum: Hello, World!") + expect(response['Content-Length']).to eq('19') + expect(response.body).to eq('Enum: Hello, World!') end - it "handles missing pages" do - request = Net::HTTP::Get.new("/missing") + it 'handles missing pages' do + request = Net::HTTP::Get.new('/missing') response = client.request(request) - expect(response.code).to eq("404") - expect(response["Content-Type"]).to eq("text/html") + expect(response.code).to eq('404') + expect(response['Content-Type']).to eq('text/html') end - it "handles empty response bodies" do - request = Net::HTTP::Post.new("/test") - request.body = "" + it 'handles empty response bodies' do + request = Net::HTTP::Post.new('/test') + request.body = '' response = client.request(request) - expect(response.code).to eq("204") - expect(["0", nil]).to include(response["Content-Length"]) + expect(response.code).to eq('204') + expect(['0', nil]).to include(response['Content-Length']) expect(response.body).to be_nil end - it "handles string response bodies" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.stringbody" + it 'handles string response bodies' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.stringbody' response = client.request(request) - expect(response["Content-Length"]).to eq("20") - expect(response.body).to eq("String response body") + expect(response['Content-Length']).to eq('20') + expect(response.body).to eq('String response body') end - it "handles enumerable response bodies" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.enumbody" + it 'handles enumerable response bodies' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.enumbody' response = client.request(request) - expect(response["Transfer-Encoding"]).to eq("chunked") - expect(response.body).to eq("Enumerable response body") + expect(response['Transfer-Encoding']).to eq('chunked') + expect(response.body).to eq('Enumerable response body') end - it "handles proc response bodies" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.procbody" + it 'handles proc response bodies' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.procbody' response = client.request(request) - expect(response["Transfer-Encoding"]).to eq("chunked") - expect(response.body).to eq("Proc response body") + expect(response['Transfer-Encoding']).to eq('chunked') + expect(response.body).to eq('Proc response body') end - it "handles fiber response bodies" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.fiberbody" + it 'handles fiber response bodies' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.fiberbody' response = client.request(request) - expect(response["Transfer-Encoding"]).to eq("chunked") - expect(response.body).to eq("Fiber response body") + expect(response['Transfer-Encoding']).to eq('chunked') + expect(response.body).to eq('Fiber response body') end - it "handles io response bodies" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.iobody" + it 'handles io response bodies' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.iobody' response = client.request(request) - expect(response["Content-Length"]).to eq("17") + expect(response['Content-Length']).to eq('17') expect(response.body).to eq("IO response body\n") end - it "handles request cookies" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.cookies" - request["Cookie"] = "echo=echocookie" + it 'handles request cookies' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.cookies' + request['Cookie'] = 'echo=echocookie' response = client.request(request) - expect(response.body).to eq("echocookie") + expect(response.body).to eq('echocookie') end - it "handles response cookies" do - request = Net::HTTP::Get.new("/test") - request["Accept"] = "test/response.cookies" + it 'handles response cookies' do + request = Net::HTTP::Get.new('/test') + request['Accept'] = 'test/response.cookies' response = client.request(request) - expect(response["Set-Cookie"]).to eq("cookie=monster, rodeo=clown") + expect(response['Set-Cookie']).to eq('cookie=monster, rodeo=clown') end end diff --git a/lib/webmachine/spec/test_resource.rb b/lib/webmachine/spec/test_resource.rb index e8d2d00e..cc949216 100644 --- a/lib/webmachine/spec/test_resource.rb +++ b/lib/webmachine/spec/test_resource.rb @@ -1,34 +1,35 @@ module Test class Resource < Webmachine::Resource def allowed_methods - ["GET", "PUT", "POST"] + ['GET', 'PUT', 'POST'] end def content_types_accepted [ - ["test/request.stringbody", :from_string], - ["test/request.enumbody", :from_enum] + ['test/request.stringbody', :from_string], + ['test/request.enumbody', :from_enum] ] end def content_types_provided [ - ["test/response.stringbody", :to_string], - ["test/response.enumbody", :to_enum], - ["test/response.procbody", :to_proc], - ["test/response.fiberbody", :to_fiber], - ["test/response.iobody", :to_io_body], - ["test/response.cookies", :to_cookies], - ["test/response.request_uri", :to_request_uri] + ['test/response.stringbody', :to_string], + ['test/response.enumbody', :to_enum], + ['test/response.procbody', :to_proc], + ['test/response.fiberbody', :to_fiber], + ['test/response.iobody', :to_io_body], + ['test/response.cookies', :to_cookies], + ['test/response.request_uri', :to_request_uri], + ['test/response.rack_env', :to_rack_env] ] end def from_string - response.body = "String: #{request.body.to_s}" + response.body = "String: #{request.body}" end def from_enum - response.body = "Enum: " + response.body = 'Enum: ' request.body.each do |part| response.body += part end @@ -40,22 +41,22 @@ def process_post end def to_string - "String response body" + 'String response body' end def to_enum - ["Enumerable ", "response " "body"] + ['Enumerable ', 'response ', 'body'] end def to_proc - Proc.new { "Proc response body" } + proc { 'Proc response body' } end def to_fiber Fiber.new do - Fiber.yield "Fiber " - Fiber.yield "response " - "body" + Fiber.yield 'Fiber ' + Fiber.yield 'response ' + 'body' end end @@ -64,16 +65,20 @@ def to_io_body end def to_cookies - response.set_cookie("cookie", "monster") - response.set_cookie("rodeo", "clown") + response.set_cookie('cookie', 'monster') + response.set_cookie('rodeo', 'clown') # FIXME: Mongrel/WEBrick fail if this method returns nil # Might be a net/http issue. Is this a bug? # @see Flow#o18, Helpers#encode_body_if_set - request.cookies["echo"] || "" + request.cookies['echo'] || '' end def to_request_uri request.uri.to_s end + + def to_rack_env + request.env.to_json + end end end diff --git a/lib/webmachine/streaming/encoder.rb b/lib/webmachine/streaming/encoder.rb index 0ad9f9e3..90322ede 100644 --- a/lib/webmachine/streaming/encoder.rb +++ b/lib/webmachine/streaming/encoder.rb @@ -12,12 +12,13 @@ def initialize(resource, encoder, charsetter, body) end protected + # @return [true, false] whether the stream will be modified by # the encoder and/or charsetter. Only returns true if using the # built-in "encode_identity" and "charset_nop" methods. def is_unencoded? - encoder.to_s == "encode_identity" && - charsetter.to_s == "charset_nop" + encoder.to_s == 'encode_identity' && + charsetter.to_s == 'charset_nop' end end # class Encoder end # module Streaming diff --git a/lib/webmachine/streaming/io_encoder.rb b/lib/webmachine/streaming/io_encoder.rb index 55e069b9..3b41d7a9 100644 --- a/lib/webmachine/streaming/io_encoder.rb +++ b/lib/webmachine/streaming/io_encoder.rb @@ -7,13 +7,14 @@ module Streaming # @api private class IOEncoder < Encoder include Enumerable + CHUNK_SIZE = 8192 # Iterates over the IO, encoding and yielding individual chunks # of the response entity. # @yield [chunk] # @yieldparam [String] chunk a chunk of the response, encoded def each - while chunk = body.read(CHUNK_SIZE) and chunk != "" + while (chunk = body.read(CHUNK_SIZE)) && (chunk != '') yield resource.send(encoder, resource.send(charsetter, chunk)) end end @@ -26,10 +27,10 @@ def copy_stream(outstream) if can_copy_stream? IO.copy_stream(body, outstream) else - each {|chunk| outstream << chunk } + each { |chunk| outstream << chunk } end end - + # Allows the response body to be converted to a IO object. # @return [IO,nil] the body as a IO object, or nil. def to_io @@ -39,7 +40,7 @@ def to_io # Returns the length of the IO stream, if known. Returns nil if # the stream uses an encoder or charsetter that might modify the # length of the stream, or the stream size is unknown. - # @return [Fixnum] the size, in bytes, of the underlying IO, or + # @return [Integer] the size, in bytes, of the underlying IO, or # nil if unsupported def size if is_unencoded? @@ -60,9 +61,10 @@ def empty? size == 0 end - alias bytesize size + alias_method :bytesize, :size private + def can_copy_stream? IO.respond_to?(:copy_stream) && is_unencoded? && !is_string_io? end diff --git a/lib/webmachine/trace.rb b/lib/webmachine/trace.rb index f6aa142b..6481db9b 100644 --- a/lib/webmachine/trace.rb +++ b/lib/webmachine/trace.rb @@ -9,10 +9,11 @@ module Webmachine # Contains means to enable the Webmachine visual debugger. module Trace module_function + # Classes that implement storage for visual debugger traces. TRACE_STORES = { - :memory => Hash, - :pstore => PStoreTraceStore + memory: Hash, + pstore: PStoreTraceStore } DEFAULT_TRACE_SUBSCRIBER = Webmachine::Events.subscribe( @@ -71,10 +72,10 @@ def trace_store=(*args) def trace_store @trace_store ||= begin - opts = Array(@trace_store_opts).dup - type = opts.shift - TRACE_STORES[type].new(*opts) - end + opts = Array(@trace_store_opts).dup + type = opts.shift + TRACE_STORES[type].new(*opts) + end end private :trace_store diff --git a/lib/webmachine/trace/fsm.rb b/lib/webmachine/trace/fsm.rb index 49b24ae1..d01b86ca 100644 --- a/lib/webmachine/trace/fsm.rb +++ b/lib/webmachine/trace/fsm.rb @@ -23,39 +23,46 @@ def trace? # Adds the request to the trace. # @param [Webmachine::Request] request the request to be traced def trace_request(request) - response.trace << { - :type => :request, - :method => request.method, - :path => request.uri.request_uri.to_s, - :headers => request.headers, - :body => request.body.to_s - } if trace? + if trace? + response.trace << { + type: :request, + method: request.method, + path: request.uri.request_uri.to_s, + headers: request.headers, + body: request.body.to_s + } + end end # Adds the response to the trace and then commits the trace to # separate storage which can be discovered by the debugger. # @param [Webmachine::Response] response the response to be traced def trace_response(response) - response.trace << { - :type => :response, - :code => response.code.to_s, - :headers => response.headers, - :body => trace_response_body(response.body) - } if trace? + if trace? + response.trace << { + type: :response, + code: response.code.to_s, + headers: response.headers, + body: trace_response_body(response.body) + } + end ensure - Webmachine::Events.publish('wm.trace.record', { - :trace_id => resource.object_id.to_s, - :trace => response.trace - }) if trace? + if trace? + Webmachine::Events.publish('wm.trace.record', { + trace_id: resource.object_id.to_s, + trace: response.trace + }) + end end # Adds a decision to the trace. # @param [Symbol] decision the decision being processed def trace_decision(decision) - response.trace << {:type => :decision, :decision => decision} if trace? + response.trace << {type: :decision, decision: decision} if trace? end private + # Works around streaming encoders where possible def trace_response_body(body) case body diff --git a/lib/webmachine/trace/resource_proxy.rb b/lib/webmachine/trace/resource_proxy.rb index 71cca9b1..bd6e7de4 100644 --- a/lib/webmachine/trace/resource_proxy.rb +++ b/lib/webmachine/trace/resource_proxy.rb @@ -14,7 +14,7 @@ class ResourceProxy # including body-producing or accepting methods, encoders and # charsetters. CALLBACK_REFERRERS = [:content_types_accepted, :content_types_provided, - :encodings_provided, :charsets_provided] + :encodings_provided, :charsets_provided] # Creates a {ResourceProxy} that decorates the passed # {Webmachine::Resource} such that callbacks invoked by the @@ -46,6 +46,7 @@ def finish_request(*args) end private + # Proxy a given callback to the inner resource, decorating with traces def proxy_callback(callback, *args) # Log inputs and attempt @@ -56,20 +57,20 @@ def proxy_callback(callback, *args) resource.response.trace << result(_result) _result rescue => exc - exc.backtrace.reject! {|s| s.include?(__FILE__) } + exc.backtrace.reject! { |s| s.include?(__FILE__) } resource.response.trace << exception(exc) raise end # Creates a log entry for the entry to a resource callback. def attempt(callback, args) - log = {:type => :attempt} + log = {type: :attempt} method = resource.method(callback) if method.owner == ::Webmachine::Resource::Callbacks log[:name] = "(default)##{method.name}" else log[:name] = "#{method.owner.name}##{method.name}" - log[:source] = method.source_location.join(":") if method.respond_to?(:source_location) + log[:source] = method.source_location.join(':') if method.respond_to?(:source_location) end unless args.empty? log[:args] = args @@ -79,15 +80,15 @@ def attempt(callback, args) # Creates a log entry for the result of a resource callback def result(result) - {:type => :result, :value => result} + {type: :result, value: result} end # Creates a log entry for an exception that was raised from a callback def exception(e) - {:type => :exception, - :class => e.class.name, - :backtrace => e.backtrace, - :message => e.message } + {type: :exception, + class: e.class.name, + backtrace: e.backtrace, + message: e.message} end # Adds proxy methods for callbacks that are dynamically referred to. diff --git a/lib/webmachine/trace/static/http-headers-status-v3.png b/lib/webmachine/trace/static/http-headers-status-v3.png index 23131606..18e754f1 100644 Binary files a/lib/webmachine/trace/static/http-headers-status-v3.png and b/lib/webmachine/trace/static/http-headers-status-v3.png differ diff --git a/lib/webmachine/trace/trace_resource.rb b/lib/webmachine/trace/trace_resource.rb index e8d60d6b..be9855c2 100644 --- a/lib/webmachine/trace/trace_resource.rb +++ b/lib/webmachine/trace/trace_resource.rb @@ -7,15 +7,14 @@ module Trace # includes serving the static files (the PNG flow diagram, CSS and # JS for the UI) and the HTML for the individual traces. class TraceResource < Resource - - MAP_EXTERNAL = %w{static map.png} - MAP_FILE = File.expand_path("../static/http-headers-status-v3.png", __FILE__) - SCRIPT_EXTERNAL = %w{static wmtrace.js} - SCRIPT_FILE = File.expand_path("../#{SCRIPT_EXTERNAL.join '/'}", __FILE__) - STYLE_EXTERNAL = %w{static wmtrace.css} - STYLE_FILE = File.expand_path("../#{STYLE_EXTERNAL.join '/'}", __FILE__) - TRACELIST_ERB = File.expand_path("../static/tracelist.erb", __FILE__) - TRACE_ERB = File.expand_path("../static/trace.erb", __FILE__) + MAP_EXTERNAL = %w[static map.png] + MAP_FILE = File.expand_path('../static/http-headers-status-v3.png', __FILE__) + SCRIPT_EXTERNAL = %w[static wmtrace.js] + SCRIPT_FILE = File.expand_path("../#{SCRIPT_EXTERNAL.join "/"}", __FILE__) + STYLE_EXTERNAL = %w[static wmtrace.css] + STYLE_FILE = File.expand_path("../#{STYLE_EXTERNAL.join "/"}", __FILE__) + TRACELIST_ERB = File.expand_path('../static/tracelist.erb', __FILE__) + TRACE_ERB = File.expand_path('../static/trace.erb', __FILE__) # The ERB template for the trace list def self.tracelist @@ -30,15 +29,15 @@ def self.trace def content_types_provided case request.path_tokens when [] - [["text/html", :produce_list]] + [['text/html', :produce_list]] when MAP_EXTERNAL - [["image/png", :produce_file]] + [['image/png', :produce_file]] when SCRIPT_EXTERNAL - [["text/javascript", :produce_file]] + [['text/javascript', :produce_file]] when STYLE_EXTERNAL - [["text/css", :produce_file]] + [['text/css', :produce_file]] else - [["text/html", :produce_trace]] + [['text/html', :produce_trace]] end end @@ -73,12 +72,12 @@ def produce_file # TODO: Add support for IO objects as response bodies, # allowing server optimizations like sendfile or chunked # downloads - open(@file, "rb") {|io| io.read } + File.binread(@file) end def produce_list - base = request.uri.path.chomp("/") - traces = Trace.traces.map {|t| [ t, "#{base}/#{t}" ] } + base = request.uri.path.chomp('/') + traces = Trace.traces.map { |t| [t, "#{base}/#{t}"] } self.class.tracelist.result(binding) end @@ -96,22 +95,22 @@ def encode_trace(data) tres = data.pop.dup treq.delete :type tres.delete :type - [ MultiJson.dump(treq), MultiJson.dump(tres), MultiJson.dump(encode_decisions(data)) ] + [MultiJson.dump(treq), MultiJson.dump(tres), MultiJson.dump(encode_decisions(data))] end def encode_decisions(decisions) - decisions.inject([]) do |list, event| + decisions.each_with_object([]) do |event, list| case event[:type] when :decision # Don't produce new decisions for sub-steps in the graph - unless event[:decision].to_s =~ /[a-z]$/ + unless /[a-z]$/.match?(event[:decision].to_s) list << {'d' => event[:decision], 'calls' => []} end when :attempt list.last['calls'] << { - "call" => event[:name], - "source" => event[:source], - "input" => event[:args] && event[:args].inspect + 'call' => event[:name], + 'source' => event[:source], + 'input' => event[:args] && event[:args].inspect } when :result list.last['calls'].last['output'] = event[:value].inspect @@ -122,7 +121,6 @@ def encode_decisions(decisions) 'message' => event[:message] } end - list end end end diff --git a/lib/webmachine/translation.rb b/lib/webmachine/translation.rb index 16210db7..f55ed682 100644 --- a/lib/webmachine/translation.rb +++ b/lib/webmachine/translation.rb @@ -1,6 +1,7 @@ +require 'set' require 'i18n' I18n.enforce_available_locales = true if I18n.respond_to?(:enforce_available_locales) -I18n.config.load_path << File.expand_path("../locale/en.yml", __FILE__) +I18n.config.load_path << File.expand_path('../locale/en.yml', __FILE__) module Webmachine # Provides an interface to the I18n library specifically for @@ -11,8 +12,8 @@ module Translation # @param [Hash] options options to pass to I18n, including # variables to interpolate. # @return [String] the interpolated string - def t(key, options={}) - ::I18n.t(key, options.merge(:scope => :webmachine)) + def t(key, options = {}) + ::I18n.t(key, **options.merge(scope: :webmachine)) end end end diff --git a/lib/webmachine/version.rb b/lib/webmachine/version.rb index d1f0cce3..f00d2830 100644 --- a/lib/webmachine/version.rb +++ b/lib/webmachine/version.rb @@ -1,8 +1,8 @@ -module Webmachine +module Webmachine # Library version - VERSION = "1.2.2" + VERSION = '2.0.1'.freeze # String for use in "Server" HTTP response header, which includes # the {VERSION}. - SERVER_STRING = "Webmachine-Ruby/#{VERSION}" + SERVER_STRING = "Webmachine-Ruby/#{VERSION}".freeze end diff --git a/memory_test.rb b/memory_test.rb new file mode 100644 index 00000000..29abc5ff --- /dev/null +++ b/memory_test.rb @@ -0,0 +1,36 @@ +$:.push File.expand_path('../lib', __FILE__) +require 'webmachine' + +class Constantized < Webmachine::Resource + HELLO_WORLD = 'Hello World'.freeze + ALLOWED_METHODS = ['GET'.freeze].freeze + CONTENT_TYPES_PROVIDED = [['text/html'.freeze, :to_html].freeze].freeze + + def allowed_methods + ALLOWED_METHODS + end + + def content_types_provided + CONTENT_TYPES_PROVIDED + end + + def to_html + HELLO_WORLD + end +end + +Webmachine.application.routes do + add ['constantized'], Constantized +end + +require 'webmachine/test' +session = Webmachine::Test::Session.new(Webmachine.application) +CONSTANTIZED = '/constantized'.freeze +require 'memory_profiler' +report = MemoryProfiler.report do + 100.times do + session.get(CONSTANTIZED) + end +end + +report.pretty_print diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index abc5000e..a63215df 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,15 +1,15 @@ -require "bundler/setup" -Bundler.require :default, :test, :webservers +require 'bundler/setup' +Bundler.require :default, :test require 'logger' class NullLogger < Logger - def add(severity, message=nil, progname=nil, &block) + def add(severity, message = nil, progname = nil, &block) end end RSpec.configure do |config| config.mock_with :rspec - config.filter_run :focus => true + config.filter_run focus: true config.run_all_when_everything_filtered = true config.formatter = :documentation if ENV['CI'] if defined?(::Java) @@ -18,10 +18,18 @@ def add(severity, message=nil, progname=nil, &block) config.order = :random end + config.before :each do + Webmachine::RescuableException.remove(RSpec::Mocks::MockExpectationError) + end + + config.after :each do + Webmachine::RescuableException.default! + end + config.before(:suite) do options = { - :Logger => NullLogger.new(STDERR), - :AccessLog => [] + Logger: NullLogger.new($stderr), + AccessLog: [] } Webmachine::Adapters::WEBrick::DEFAULT_OPTIONS.merge! options Webmachine::Adapters::Rack::DEFAULT_OPTIONS.merge! options if defined?(Webmachine::Adapters::Rack) @@ -29,18 +37,18 @@ def add(severity, message=nil, progname=nil, &block) end # For use in specs that need a fully initialized resource -shared_context "default resource" do +shared_context 'default resource' do let(:method) { 'GET' } - let(:uri) { URI.parse("http://localhost/") } + let(:uri) { URI.parse('http://localhost/') } let(:headers) { Webmachine::Headers.new } - let(:body) { "" } + let(:body) { '' } let(:request) { Webmachine::Request.new(method, uri, headers, body) } let(:response) { Webmachine::Response.new } let(:resource_class) do Class.new(Webmachine::Resource) do def to_html - "Hello, world!" + 'Hello, world!' end end end diff --git a/spec/webmachine/adapter_spec.rb b/spec/webmachine/adapter_spec.rb index 28b5d7c3..e949919b 100644 --- a/spec/webmachine/adapter_spec.rb +++ b/spec/webmachine/adapter_spec.rb @@ -1,4 +1,4 @@ -require "spec_helper" +require 'spec_helper' describe Webmachine::Adapter do let(:application) { Webmachine::Application.new } @@ -10,19 +10,19 @@ described_class.new(application) end - describe "#initialize" do - it "stores the provided application" do + describe '#initialize' do + it 'stores the provided application' do expect(adapter.application).to eq(application) end end - describe ".run" do - it "creates a new adapter and runs it" do + describe '.run' do + it 'creates a new adapter and runs it' do adapter = double(described_class) - expect(described_class).to receive(:new). - with(application). - and_return(adapter) + expect(described_class).to receive(:new) + .with(application) + .and_return(adapter) expect(adapter).to receive(:run) @@ -30,10 +30,9 @@ end end - describe "#run" do - it "raises a NotImplementedError" do + describe '#run' do + it 'raises a NotImplementedError' do expect { adapter.run }.to raise_exception(NotImplementedError) end end - end diff --git a/spec/webmachine/adapters/httpkit_spec.rb b/spec/webmachine/adapters/httpkit_spec.rb deleted file mode 100644 index f2ee7dde..00000000 --- a/spec/webmachine/adapters/httpkit_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "spec_helper" -require "webmachine/spec/adapter_lint" - -begin - describe Webmachine::Adapters::HTTPkit do - it_should_behave_like :adapter_lint - end -rescue LoadError - warn "Platform is #{RUBY_PLATFORM}: skipping httpkit adapter spec." -end diff --git a/spec/webmachine/adapters/rack_mapped_spec.rb b/spec/webmachine/adapters/rack_mapped_spec.rb new file mode 100644 index 00000000..ca944639 --- /dev/null +++ b/spec/webmachine/adapters/rack_mapped_spec.rb @@ -0,0 +1,71 @@ +require 'webmachine/adapter' +require 'webmachine/adapters/rack_mapped' +require 'spec_helper' +require 'webmachine/spec/adapter_lint' +require 'rack/test' + +describe Webmachine::Adapters::RackMapped do + it_should_behave_like :adapter_lint do + it 'should set Server header' do + response = client.request(Net::HTTP::Get.new('/test')) + expect(response['Server']).to match(/Webmachine/) + expect(response['Server']).to match(/Rack/) + end + end +end + +describe Webmachine::Adapters::RackMapped do + class CreateResource < Webmachine::Resource + def allowed_methods + ['POST'] + end + + def content_types_accepted + [['application/json', :from_json]] + end + + def content_types_provided + [['application/json', :to_json]] + end + + def post_is_create? + true + end + + def create_path + 'created_path_here/123' + end + + def from_json + response.body = %( {"foo": "bar"} ) + end + end + + let(:app) do + Rack::Builder.new do + map '/some/route' do + run(Webmachine::Application.new do |app| + app.add_route(['test'], Test::Resource) + app.add_route(['create_test'], CreateResource) + app.configure do |config| + config.adapter = :RackMapped + end + end.adapter) + end + end + end + + context 'using Rack::Test' do + include Rack::Test::Methods + + it 'provides the full request URI' do + rack_response = get 'some/route/test', nil, {'HTTP_ACCEPT' => 'test/response.request_uri'} + expect(rack_response.body).to eq 'http://example.org/some/route/test' + end + + it 'provides LOCATION header using custom base_uri when creating from POST request' do + rack_response = post '/some/route/create_test', %({"foo": "bar"}), {'HTTP_ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json'} + expect(rack_response.headers['Location']).to eq('http://example.org/some/route/created_path_here/123') + end + end +end diff --git a/spec/webmachine/adapters/rack_spec.rb b/spec/webmachine/adapters/rack_spec.rb index 0725b12a..e76f3ca0 100644 --- a/spec/webmachine/adapters/rack_spec.rb +++ b/spec/webmachine/adapters/rack_spec.rb @@ -2,35 +2,62 @@ require 'webmachine/adapters/rack' require 'spec_helper' require 'webmachine/spec/adapter_lint' +require 'rack/test' +require 'json' describe Webmachine::Adapters::Rack do it_should_behave_like :adapter_lint do - it "should set Server header" do - response = client.request(Net::HTTP::Get.new("/test")) - expect(response["Server"]).to match(/Webmachine/) - expect(response["Server"]).to match(/Rack/) + it 'should set Server header' do + response = client.request(Net::HTTP::Get.new('/test')) + expect(response['Server']).to match(/Webmachine/) + expect(response['Server']).to match(/Rack/) end end end describe Webmachine::Adapters::Rack::RackResponse do - context "on Rack < 1.5 release" do - before { allow(Rack).to receive_messages(:release => "1.4") } + context 'on Rack < 1.5 release' do + before { allow(Rack).to receive_messages(release: '1.4') } - it "should add Content-Type header on not acceptable response" do + it 'should add Content-Type header on not acceptable response' do rack_response = described_class.new(double(:body), 406, {}) - rack_status, rack_headers, rack_body = rack_response.finish - expect(rack_headers).to have_key("Content-Type") + _rack_status, rack_headers, _rack_body = rack_response.finish + expect(rack_headers).to have_key('Content-Type') end end - context "on Rack >= 1.5 release" do - before { allow(Rack).to receive_messages(:release => "1.5") } + context 'on Rack >= 1.5 release' do + before { allow(Rack).to receive_messages(release: '1.5') } - it "should not add Content-Type header on not acceptable response" do + it 'should not add Content-Type header on not acceptable response' do rack_response = described_class.new(double(:body), 406, {}) - rack_status, rack_headers, rack_body = rack_response.finish - expect(rack_headers).not_to have_key("Content-Type") + _rack_status, rack_headers, _rack_body = rack_response.finish + expect(rack_headers).not_to have_key('Content-Type') + end + end +end + +describe Webmachine::Adapters::Rack do + let(:app) do + Webmachine::Application.new do |app| + app.add_route(['test'], Test::Resource) + app.configure do |config| + config.adapter = :Rack + end + end.adapter + end + + context 'using Rack::Test' do + include Rack::Test::Methods + + it 'provides the full request URI' do + rack_response = get 'test', nil, {'HTTP_ACCEPT' => 'test/response.request_uri'} + expect(rack_response.body).to eq 'http://example.org/test' + end + + it 'provides the rack env on the request' do + rack_response = get 'test', nil, {'HTTP_ACCEPT' => 'test/response.rack_env'} + expect(JSON.parse(rack_response.body).keys).to include 'rack.input' end end end diff --git a/spec/webmachine/adapters/reel_spec.rb b/spec/webmachine/adapters/reel_spec.rb deleted file mode 100644 index 9d954b44..00000000 --- a/spec/webmachine/adapters/reel_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'spec_helper' -require 'webmachine/spec/adapter_lint' -describe Webmachine::Adapters::Reel do - context 'lint' do - it_should_behave_like :adapter_lint - end - - context 'websockets' do - let(:application) { Webmachine::Application.new } - let(:adapter) do - server = TCPServer.new('0.0.0.0', 0) - application.configuration.port = server.addr[1] - server.close - described_class.new(application) - end - - let(:example_host) { "www.example.com" } - let(:example_path) { "/example"} - let(:example_url) { "ws://#{example_host}#{example_path}" } - let :handshake_headers do - { - "Host" => example_host, - "Upgrade" => "websocket", - "Connection" => "Upgrade", - "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", - "Origin" => "http://example.com", - "Sec-WebSocket-Protocol" => "chat, superchat", - "Sec-WebSocket-Version" => "13" - } - end - let(:client_message) { "Hi server!" } - let(:server_message) { "Hi client!" } - - it 'supports websockets' do - application.configuration.adapter_options[:websocket_handler] = proc do |socket| - expect(socket.read).to eq client_message - socket << server_message - end - - reel_server(adapter) do |client| - client << WebSocket::ClientHandshake.new(:get, example_url, handshake_headers).to_data - - # Discard handshake response - # FIXME: hax - client.readpartial(4096) - - client << WebSocket::Message.new(client_message).to_data - parser = WebSocket::Parser.new - parser.append client.readpartial(4096) until message = parser.next_message - - expect(message).to eq server_message - end - end - end - - def reel_server(adptr = adapter) - thread = Thread.new { adptr.run } - begin - timeout(5) do - begin - sock = TCPSocket.new(adptr.application.configuration.ip, adptr.application.configuration.port) - begin - yield(sock) - ensure - sock.close - end - rescue Errno::ECONNREFUSED - Thread.pass - retry - end - end - ensure - thread.kill if thread - end - end -end diff --git a/spec/webmachine/adapters/webrick_spec.rb b/spec/webmachine/adapters/webrick_spec.rb index 2beb0c32..dfdc8489 100644 --- a/spec/webmachine/adapters/webrick_spec.rb +++ b/spec/webmachine/adapters/webrick_spec.rb @@ -1,12 +1,12 @@ -require "spec_helper" -require "webmachine/spec/adapter_lint" +require 'spec_helper' +require 'webmachine/spec/adapter_lint' describe Webmachine::Adapters::WEBrick do it_should_behave_like :adapter_lint do - it "should set Server header" do - response = client.request(Net::HTTP::Get.new("/test")) - expect(response["Server"]).to match(/Webmachine/) - expect(response["Server"]).to match(/WEBrick/) + it 'should set Server header' do + response = client.request(Net::HTTP::Get.new('/test')) + expect(response['Server']).to match(/Webmachine/) + expect(response['Server']).to match(/WEBrick/) end end end diff --git a/spec/webmachine/application_spec.rb b/spec/webmachine/application_spec.rb index e7894bb8..2469b2bb 100644 --- a/spec/webmachine/application_spec.rb +++ b/spec/webmachine/application_spec.rb @@ -4,12 +4,12 @@ let(:application) { described_class.new } let(:test_resource) { Class.new(Webmachine::Resource) } - it "accepts a Configuration when initialized" do + it 'accepts a Configuration when initialized' do config = Webmachine::Configuration.new('1.1.1.1', 9999, :Reel, {}) expect(described_class.new(config).configuration).to be(config) end - it "is yielded into a block provided during initialization" do + it 'is yielded into a block provided during initialization' do yielded_app = nil returned_app = described_class.new do |app| expect(app).to be_kind_of(Webmachine::Application) @@ -18,25 +18,25 @@ expect(returned_app).to be(yielded_app) end - it "is initialized with the default Configration if none is given" do + it 'is initialized with the default Configration if none is given' do expect(application.configuration).to eq(Webmachine::Configuration.default) end - it "returns the receiver from the configure call so you can chain it" do + it 'returns the receiver from the configure call so you can chain it' do expect(application.configure { |c| }).to equal(application) end - it "is configurable" do + it 'is configurable' do application.configure do |config| expect(config).to be_kind_of(Webmachine::Configuration) end end - it "is initialized with an empty Dispatcher" do + it 'is initialized with an empty Dispatcher' do expect(application.dispatcher.routes).to be_empty end - it "can have routes added" do + it 'can have routes added' do route = nil resource = test_resource # overcome instance_eval :/ @@ -50,24 +50,24 @@ expect(application.routes).to eq([route]) end - describe "#adapter" do + describe '#adapter' do let(:adapter_class) { application.adapter_class } it "returns an instance of it's adapter class" do expect(application.adapter).to be_an_instance_of(adapter_class) end - it "is memoized" do + it 'is memoized' do expect(application.adapter).to eql application.adapter end end - it "can be run" do + it 'can be run' do expect(application.adapter).to receive(:run) application.run end - it "can be queried about its configured adapter" do + it 'can be queried about its configured adapter' do expected = Webmachine::Adapters.const_get(application.configuration.adapter) expect(application.adapter_class).to equal(expected) end diff --git a/spec/webmachine/chunked_body_spec.rb b/spec/webmachine/chunked_body_spec.rb index 96bbf163..0a0620f7 100644 --- a/spec/webmachine/chunked_body_spec.rb +++ b/spec/webmachine/chunked_body_spec.rb @@ -2,7 +2,7 @@ require 'webmachine/chunked_body' describe Webmachine::ChunkedBody do - it "builds a proper body" do + it 'builds a proper body' do body = '' Webmachine::ChunkedBody.new(['foo', 'bar', '', 'j', 'webmachine']).each do |chunk| body << chunk @@ -10,8 +10,8 @@ expect(body).to eq("3\r\nfoo\r\n3\r\nbar\r\n1\r\nj\r\na\r\nwebmachine\r\n0\r\n\r\n") end - context "with an empty body" do - it "builds a proper body" do + context 'with an empty body' do + it 'builds a proper body' do body = '' Webmachine::ChunkedBody.new([]).each do |chunk| body << chunk @@ -20,9 +20,9 @@ end end - describe "#each" do - context "without a block given" do - it "returns an Enumerator" do + describe '#each' do + context 'without a block given' do + it 'returns an Enumerator' do expect(Webmachine::ChunkedBody.new([]).each).to respond_to(:next) end end diff --git a/spec/webmachine/configuration_spec.rb b/spec/webmachine/configuration_spec.rb index b97e65c6..4148e2ea 100644 --- a/spec/webmachine/configuration_spec.rb +++ b/spec/webmachine/configuration_spec.rb @@ -3,25 +3,25 @@ describe Webmachine::Configuration do before { Webmachine.configuration = nil } - %w{ip port adapter adapter_options}.each do |field| + %w[ip port adapter adapter_options].each do |field| it { is_expected.to respond_to(field) } it { is_expected.to respond_to("#{field}=") } end - it "should yield configuration to the block" do + it 'should yield configuration to the block' do Webmachine.configure do |config| expect(config).to be_kind_of(described_class) end end - it "should set the global configuration from the yielded instance" do + it 'should set the global configuration from the yielded instance' do Webmachine.configure do |config| @config = config end expect(@config).to eq Webmachine.configuration end - it "should return the module from the configure call so you can chain it" do - expect(Webmachine.configure {|c|}).to eq Webmachine + it 'should return the module from the configure call so you can chain it' do + expect(Webmachine.configure { |c| }).to eq Webmachine end end diff --git a/spec/webmachine/cookie_spec.rb b/spec/webmachine/cookie_spec.rb index ca2d66fc..de815c1f 100644 --- a/spec/webmachine/cookie_spec.rb +++ b/spec/webmachine/cookie_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe Webmachine::Cookie do - describe "creating a cookie" do - let(:name) { "monster" } - let(:value) { "mash" } + describe 'creating a cookie' do + let(:name) { 'monster' } + let(:value) { 'mash' } let(:attributes) { {} } let(:cookie) { Webmachine::Cookie.new(name, value, attributes) } @@ -13,32 +13,32 @@ its(:name) { should == name } its(:value) { should == value } - its(:to_s) { should == "monster=mash" } + its(:to_s) { should == 'monster=mash' } - describe "a cookie with whitespace in name and value" do - let(:name) { "cookie name" } - let(:value) { "cookie value" } + describe 'a cookie with whitespace in name and value' do + let(:name) { 'cookie name' } + let(:value) { 'cookie value' } - its(:to_s) { should == "cookie+name=cookie+value" } + its(:to_s) { should == 'cookie+name=cookie+value' } end - describe "a cookie with attributes set" do - let(:domain) { "www.server.com" } - let(:path) { "/" } - let(:comment) { "comment with spaces" } + describe 'a cookie with attributes set' do + let(:domain) { 'www.server.com' } + let(:path) { '/' } + let(:comment) { 'comment with spaces' } let(:version) { 1 } let(:maxage) { 60 } - let(:expires) { Time.gm(2010,3,14, 3, 14, 0) } + let(:expires) { Time.gm(2010, 3, 14, 3, 14, 0) } let(:attributes) { { - :comment => comment, - :domain => domain, - :path => path, - :secure => true, - :httponly => true, - :version => version, - :maxage => maxage, - :expires => expires + comment: comment, + domain: domain, + path: path, + secure: true, + httponly: true, + version: version, + maxage: maxage, + expires: expires } } @@ -51,49 +51,49 @@ its(:maxage) { should == maxage } its(:expires) { should == expires } - it "should include the attributes in its string version" do + it 'should include the attributes in its string version' do str = subject.to_s - expect(str).to include "Secure" - expect(str).to include "HttpOnly" - expect(str).to include "Comment=comment+with+spaces" - expect(str).to include "Domain=www.server.com" - expect(str).to include "Path=/" - expect(str).to include "Version=1" - expect(str).to include "Max-Age=60" - expect(str).to include "Expires=Sun, 14-Mar-2010 03:14:00 GMT" + expect(str).to include 'Secure' + expect(str).to include 'HttpOnly' + expect(str).to include 'Comment=comment+with+spaces' + expect(str).to include 'Domain=www.server.com' + expect(str).to include 'Path=/' + expect(str).to include 'Version=1' + expect(str).to include 'Max-Age=60' + expect(str).to include 'Expires=Sun, 14 Mar 2010 03:14:00 GMT' end end end - describe "parsing a cookie parameter" do - let(:str) { "cookie = monster" } + describe 'parsing a cookie parameter' do + let(:str) { 'cookie = monster' } subject { Webmachine::Cookie.parse(str) } - it("should have the cookie") { expect(subject).to eq({ "cookie" => "monster" }) } + it('should have the cookie') { expect(subject).to eq({'cookie' => 'monster'}) } - describe "parsing multiple cookie parameters" do - let(:str) { "cookie=monster; monster=mash" } + describe 'parsing multiple cookie parameters' do + let(:str) { 'cookie=monster; monster=mash' } - it("should have both cookies") { expect(subject).to eq({ "cookie" => "monster", "monster" => "mash" }) } + it('should have both cookies') { expect(subject).to eq({'cookie' => 'monster', 'monster' => 'mash'}) } end - describe "parsing an encoded cookie" do - let(:str) { "cookie=yum+yum" } + describe 'parsing an encoded cookie' do + let(:str) { 'cookie=yum+yum' } - it("should decode the cookie") { expect(subject).to eq({ "cookie" => "yum yum" }) } + it('should decode the cookie') { expect(subject).to eq({'cookie' => 'yum yum'}) } end - describe "parsing nil" do + describe 'parsing nil' do let(:str) { nil } - it("should return empty hash") { expect(subject).to eq({}) } + it('should return empty hash') { expect(subject).to eq({}) } end - describe "parsing duplicate cookies" do - let(:str) { "cookie=monster; cookie=yum+yum" } + describe 'parsing duplicate cookies' do + let(:str) { 'cookie=monster; cookie=yum+yum' } - it("should return the first instance of the cookie") { expect(subject).to eq({ "cookie" => "monster" }) } + it('should return the first instance of the cookie') { expect(subject).to eq({'cookie' => 'monster'}) } end end end diff --git a/spec/webmachine/decision/conneg_spec.rb b/spec/webmachine/decision/conneg_spec.rb index 07428b0c..58b9f846 100644 --- a/spec/webmachine/decision/conneg_spec.rb +++ b/spec/webmachine/decision/conneg_spec.rb @@ -1,163 +1,160 @@ require 'spec_helper' describe Webmachine::Decision::Conneg do - include_context "default resource" + include_context 'default resource' subject do Webmachine::Decision::FSM.new(resource, request, response) end - context "choosing a media type" do - it "should not choose a type when none are provided" do - expect(subject.choose_media_type([], "*/*")).to be_nil + context 'choosing a media type' do + it 'should not choose a type when none are provided' do + expect(subject.choose_media_type([], '*/*')).to be_nil end - it "should not choose a type when none are acceptable" do - expect(subject.choose_media_type(["text/html"], "application/json")).to be_nil + it 'should not choose a type when none are acceptable' do + expect(subject.choose_media_type(['text/html'], 'application/json')).to be_nil end - it "should choose the first acceptable type" do - expect(subject.choose_media_type(["text/html", "application/xml"], - "application/xml, text/html, */*")).to eq("application/xml") + it 'should choose the first acceptable type' do + expect(subject.choose_media_type(['text/html', 'application/xml'], + 'application/xml, text/html, */*')).to eq('application/xml') end - it "should choose the type that matches closest when matching subparams" do - expect(subject.choose_media_type(["text/html", - ["text/html", {"charset" => "iso8859-1"}]], - "text/html;charset=iso8859-1, application/xml")). - to eq("text/html;charset=iso8859-1") + it 'should choose the type that matches closest when matching subparams' do + expect(subject.choose_media_type(['text/html', + ['text/html', {'charset' => 'iso8859-1'}]], + 'text/html;charset=iso8859-1, application/xml')) + .to eq('text/html;charset=iso8859-1') end - it "should choose a type more specific than requested when an exact match is not present" do - expect(subject.choose_media_type(["application/json;v=3;foo=bar", "application/json;v=2"], - "text/html, application/json")). - to eq("application/json;v=3;foo=bar") + it 'should choose a type more specific than requested when an exact match is not present' do + expect(subject.choose_media_type(['application/json;v=3;foo=bar', 'application/json;v=2'], + 'text/html, application/json')) + .to eq('application/json;v=3;foo=bar') end - - it "should choose the preferred type over less-preferred types" do - expect(subject.choose_media_type(["text/html", "application/xml"], - "application/xml;q=0.7, text/html, */*")).to eq("text/html") - + it 'should choose the preferred type over less-preferred types' do + expect(subject.choose_media_type(['text/html', 'application/xml'], + 'application/xml;q=0.7, text/html, */*')).to eq('text/html') end - it "should raise an exception when a media-type is improperly formatted" do + it 'should raise an error when a media-type is improperly formatted' do expect { - subject.choose_media_type(["text/html", "application/xml"], - "bah;") + subject.choose_media_type(['text/html', 'application/xml'], + 'bah;') }.to raise_error(Webmachine::MalformedRequest) end - - it "should choose a type when more than one accept header is present" do - expect(subject.choose_media_type(["text/html"], - ["text/html", "text/plain"])).to eq("text/html") + it 'should choose a type when more than one accept header is present' do + expect(subject.choose_media_type(['text/html'], + ['text/html', 'text/plain'])).to eq('text/html') end end - context "choosing an encoding" do - it "should not set the encoding when none are provided" do - subject.choose_encoding({}, "identity, gzip") + context 'choosing an encoding' do + it 'should not set the encoding when none are provided' do + subject.choose_encoding({}, 'identity, gzip') expect(subject.metadata['Content-Encoding']).to be_nil expect(subject.response.headers['Content-Encoding']).to be_nil end - it "should not set the Content-Encoding header when it is identity" do - subject.choose_encoding({"gzip"=> :encode_gzip, "identity" => :encode_identity}, "identity") + it 'should not set the Content-Encoding header when it is identity' do + subject.choose_encoding({'gzip' => :encode_gzip, 'identity' => :encode_identity}, 'identity') expect(subject.metadata['Content-Encoding']).to eq('identity') expect(response.headers['Content-Encoding']).to be_nil end - it "should choose the first acceptable encoding" do - subject.choose_encoding({"gzip" => :encode_gzip}, "identity, gzip") + it 'should choose the first acceptable encoding' do + subject.choose_encoding({'gzip' => :encode_gzip}, 'identity, gzip') expect(subject.metadata['Content-Encoding']).to eq('gzip') expect(response.headers['Content-Encoding']).to eq('gzip') end - it "should choose the first acceptable encoding" \ - ", even when no white space after comma" do - subject.choose_encoding({"gzip" => :encode_gzip}, "identity,gzip") + it 'should choose the first acceptable encoding' \ + ', even when no white space after comma' do + subject.choose_encoding({'gzip' => :encode_gzip}, 'identity,gzip') expect(subject.metadata['Content-Encoding']).to eq('gzip') expect(response.headers['Content-Encoding']).to eq('gzip') end - it "should choose the preferred encoding over less-preferred encodings" do - subject.choose_encoding({"gzip" => :encode_gzip, "identity" => :encode_identity}, "gzip, identity;q=0.7") + it 'should choose the preferred encoding over less-preferred encodings' do + subject.choose_encoding({'gzip' => :encode_gzip, 'identity' => :encode_identity}, 'gzip, identity;q=0.7') expect(subject.metadata['Content-Encoding']).to eq('gzip') expect(response.headers['Content-Encoding']).to eq('gzip') end - it "should not set the encoding if none are acceptable" do - subject.choose_encoding({"gzip" => :encode_gzip}, "identity") + it 'should not set the encoding if none are acceptable' do + subject.choose_encoding({'gzip' => :encode_gzip}, 'identity') expect(subject.metadata['Content-Encoding']).to be_nil expect(response.headers['Content-Encoding']).to be_nil end end - context "choosing a charset" do - it "should not set the charset when none are provided" do - subject.choose_charset([], "ISO-8859-1") + context 'choosing a charset' do + it 'should not set the charset when none are provided' do + subject.choose_charset([], 'ISO-8859-1') expect(subject.metadata['Charset']).to be_nil end - it "should choose the first acceptable charset" do - subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "US-ASCII, UTF-8") - expect(subject.metadata['Charset']).to eq("US-ASCII") + it 'should choose the first acceptable charset' do + subject.choose_charset([['UTF-8', :to_utf8], ['US-ASCII', :to_ascii]], 'US-ASCII, UTF-8') + expect(subject.metadata['Charset']).to eq('US-ASCII') end - it "should choose the preferred charset over less-preferred charsets" do - subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "US-ASCII;q=0.7, UTF-8") - expect(subject.metadata['Charset']).to eq("UTF-8") + it 'should choose the preferred charset over less-preferred charsets' do + subject.choose_charset([['UTF-8', :to_utf8], ['US-ASCII', :to_ascii]], 'US-ASCII;q=0.7, UTF-8') + expect(subject.metadata['Charset']).to eq('UTF-8') end - it "should not set the charset if none are acceptable" do - subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "ISO-8859-1") + it 'should not set the charset if none are acceptable' do + subject.choose_charset([['UTF-8', :to_utf8], ['US-ASCII', :to_ascii]], 'ISO-8859-1') expect(subject.metadata['Charset']).to be_nil end - it "should choose a charset case-insensitively" do - subject.choose_charset([["UtF-8", :to_utf8],["US-ASCII", :to_ascii]], "iso-8859-1, utf-8") - expect(subject.metadata['Charset']).to eq("utf-8") + it 'should choose a charset case-insensitively' do + subject.choose_charset([['UtF-8', :to_utf8], ['US-ASCII', :to_ascii]], 'iso-8859-1, utf-8') + expect(subject.metadata['Charset']).to eq('utf-8') end end - context "choosing a language" do - it "should not set the language when none are provided" do - subject.choose_language([], "en") + context 'choosing a language' do + it 'should not set the language when none are provided' do + subject.choose_language([], 'en') expect(subject.metadata['Language']).to be_nil end - it "should choose the first acceptable language" do - subject.choose_language(['en', 'en-US', 'es'], "en-US, es") - expect(subject.metadata['Language']).to eq("en-US") - expect(response.headers['Content-Language']).to eq("en-US") + it 'should choose the first acceptable language' do + subject.choose_language(['en', 'en-US', 'es'], 'en-US, es') + expect(subject.metadata['Language']).to eq('en-US') + expect(response.headers['Content-Language']).to eq('en-US') end - it "should choose the preferred language over less-preferred languages" do - subject.choose_language(['en', 'en-US', 'es'], "en-US;q=0.6, es") - expect(subject.metadata['Language']).to eq("es") - expect(response.headers['Content-Language']).to eq("es") + it 'should choose the preferred language over less-preferred languages' do + subject.choose_language(['en', 'en-US', 'es'], 'en-US;q=0.6, es') + expect(subject.metadata['Language']).to eq('es') + expect(response.headers['Content-Language']).to eq('es') end - it "should select the first language if all are acceptable" do - subject.choose_language(['en', 'fr', 'es'], "*") - expect(subject.metadata['Language']).to eq("en") - expect(response.headers['Content-Language']).to eq("en") + it 'should select the first language if all are acceptable' do + subject.choose_language(['en', 'fr', 'es'], '*') + expect(subject.metadata['Language']).to eq('en') + expect(response.headers['Content-Language']).to eq('en') end - it "should select the closest acceptable language when an exact match is not available" do - subject.choose_language(['en-US', 'es'], "en, fr") + it 'should select the closest acceptable language when an exact match is not available' do + subject.choose_language(['en-US', 'es'], 'en, fr') expect(subject.metadata['Language']).to eq('en-US') expect(response.headers['Content-Language']).to eq('en-US') end - it "should not set the language if none are acceptable" do + it 'should not set the language if none are acceptable' do subject.choose_language(['en'], 'es') expect(subject.metadata['Language']).to be_nil expect(response.headers).not_to include('Content-Language') end - it "should choose a language case-insensitively" do + it 'should choose a language case-insensitively' do subject.choose_language(['en-US', 'ZH'], 'zh-ch, EN') expect(subject.metadata['Language']).to eq('en-US') expect(response.headers['Content-Language']).to eq('en-US') diff --git a/spec/webmachine/decision/falsey_spec.rb b/spec/webmachine/decision/falsey_spec.rb index 2a8bd210..52a4e337 100644 --- a/spec/webmachine/decision/falsey_spec.rb +++ b/spec/webmachine/decision/falsey_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe Webmachine::Decision::Falsey do - specify { expect(described_class.=== false).to be(true) } - specify { expect(described_class.=== nil).to be(true) } - specify { expect(described_class.=== true).to be(false) } - specify { expect(described_class.=== []).to be(false) } + specify { expect(described_class === false).to be(true) } + specify { expect(described_class === nil).to be(true) } + specify { expect(described_class === true).to be(false) } + specify { expect(described_class === []).to be(false) } end diff --git a/spec/webmachine/decision/flow_spec.rb b/spec/webmachine/decision/flow_spec.rb index 60bdb4e0..d1e696f6 100644 --- a/spec/webmachine/decision/flow_spec.rb +++ b/spec/webmachine/decision/flow_spec.rb @@ -3,9 +3,9 @@ describe Webmachine::Decision::Flow do subject { Webmachine::Decision::FSM.new(resource, request, response) } let(:method) { 'GET' } - let(:uri) { URI.parse("http://localhost/") } + let(:uri) { URI.parse('http://localhost/') } let(:headers) { Webmachine::Headers.new } - let(:body) { "" } + let(:body) { '' } let(:request) { Webmachine::Request.new(method, uri, headers, body) } let(:response) { Webmachine::Response.new } let(:default_resource) { resource_with } @@ -22,207 +22,246 @@ def resource_with(&block) klass = Class.new(Webmachine::Resource) do - def to_html; "test resource"; end + def to_html + 'test resource' + end end - klass.module_eval(&block) if block_given? + klass.module_eval(&block) if block klass.new(request, response) end def missing_resource_with(&block) resource_with do - def resource_exists?; false; end - self.module_eval(&block) if block + def resource_exists? + false + end + module_eval(&block) if block end end - describe "#b13 (Service Available?)" do + describe '#b13 (Service Available?)' do let(:resource) do resource_with do attr_accessor :available - def service_available?; @available; end + def service_available? + @available + end end end - it "should respond with 503 when the service is unavailable" do + it 'should respond with 503 when the service is unavailable' do resource.available = false subject.run expect(response.code).to eq 503 end end - describe "#b12 (Known method?)" do + describe '#b12 (Known method?)' do let(:resource) do resource_with do - def known_methods; ['HEAD']; end + def known_methods + ['HEAD'] + end end end - it "should respond with 501 when the method is unknown" do + it 'should respond with 501 when the method is unknown' do subject.run expect(response.code).to eq 501 end end - describe "#b11 (URI too long?)" do + describe '#b11 (URI too long?)' do let(:resource) do resource_with do - def uri_too_long?(uri); true; end + def uri_too_long?(uri) + true + end end end - it "should respond with 414 when the URI is too long" do + it 'should respond with 414 when the URI is too long' do subject.run expect(response.code).to eq 414 end end - describe "#b10 (Method allowed?)" do + describe '#b10 (Method allowed?)' do let(:resource) do resource_with do - def allowed_methods; ['POST']; end + def allowed_methods + ['POST'] + end end end - it "should respond with 405 when the method is not allowed" do + it 'should respond with 405 when the method is not allowed' do subject.run expect(response.code).to eq 405 - expect(response.headers['Allow']).to eq "POST" + expect(response.headers['Allow']).to eq 'POST' end end - describe "#b9 (Malformed request?)" do - let(:resource) { resource_with { def malformed_request?; true; end } } + describe '#b9 (Malformed request?)' do + let(:resource) { + resource_with { + def malformed_request? + true + end + } + } - it "should respond with 400 when the request is malformed" do + it 'should respond with 400 when the request is malformed' do subject.run expect(response.code).to eq 400 end - context "when the Content-MD5 header is present" do + context 'when the Content-MD5 header is present' do let(:resource) do resource_with do - def allowed_methods; ['POST']; end; - def process_post; true; end; + def allowed_methods + ['POST'] + end + + def process_post + true + end attr_accessor :validation - def validate_content_checksum; @validation; end + def validate_content_checksum + @validation + end end end - let(:method) { "POST" } - let(:body) { "This is the body." } - let(:headers) { Webmachine::Headers["Content-Type" => "text/plain"] } + let(:method) { 'POST' } + let(:body) { 'This is the body.' } + let(:headers) { Webmachine::Headers['Content-Type' => 'text/plain'] } - it "should respond with 204 when the request body does match the header" do + it 'should respond with 204 when the request body does match the header' do headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest(body) subject.run expect(response.code).to eq 204 end - it "should bypass validation when the header has a nil value" do + it 'should bypass validation when the header has a nil value' do headers['Content-MD5'] = nil subject.run expect(response.code).to eq 204 end - it "should respond with 400 when the header has a empty string value" do - headers['Content-MD5'] = "" + it 'should respond with 400 when the header has a empty string value' do + headers['Content-MD5'] = '' subject.run expect(response.code).to eq 400 end - it "should respond with 400 when the header has a non-hashed, non-encoded value" do - headers["Content-MD5"] = body + it 'should respond with 400 when the header has a non-hashed, non-encoded value' do + headers['Content-MD5'] = body subject.run expect(response.code).to eq 400 end - it "should respond with 400 when the header is not encoded as Base64 but digest matches the body" do + it 'should respond with 400 when the header is not encoded as Base64 but digest matches the body' do headers['Content-MD5'] = Digest::MD5.hexdigest(body) subject.run expect(response.code).to eq 400 end - it "should respond with 400 when the request body does not match the header" do - headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest("thiswillnotmatchthehash") + it 'should respond with 400 when the request body does not match the header' do + headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest('thiswillnotmatchthehash') subject.run expect(response.code).to eq 400 end - it "should respond with 400 when the resource invalidates the checksum" do + it 'should respond with 400 when the resource invalidates the checksum' do resource.validation = false - headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest("thiswillnotmatchthehash") + headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest('thiswillnotmatchthehash') subject.run expect(response.code).to eq 400 end - it "should not respond with 400 when the resource validates the checksum" do + it 'should not respond with 400 when the resource validates the checksum' do resource.validation = true - headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest("thiswillnotmatchthehash") + headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest('thiswillnotmatchthehash') subject.run expect(response.code).to_not eq 400 end - it "should respond with the given code when the resource returns a code while validating" do + it 'should respond with the given code when the resource returns a code while validating' do resource.validation = 500 - headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest("thiswillnotmatchthehash") + headers['Content-MD5'] = Base64.encode64 Digest::MD5.hexdigest('thiswillnotmatchthehash') subject.run expect(response.code).to eq 500 end end end - describe "#b8 (Authorized?)" do - let(:resource) { resource_with { attr_accessor :auth; def is_authorized?(header); @auth; end } } + describe '#b8 (Authorized?)' do + let(:resource) { + resource_with { + attr_accessor :auth + def is_authorized?(header) + @auth + end + } + } - it "should reply with 401 when the client is unauthorized" do + it 'should reply with 401 when the client is unauthorized' do resource.auth = false subject.run expect(response.code).to eq 401 end - it "should reply with 401 when the resource gives a challenge" do - resource.auth = "Basic realm=Webmachine" + it 'should reply with 401 when the resource gives a challenge' do + resource.auth = 'Basic realm=Webmachine' subject.run expect(response.code).to eq 401 - expect(response.headers['WWW-Authenticate']).to eq "Basic realm=Webmachine" + expect(response.headers['WWW-Authenticate']).to eq 'Basic realm=Webmachine' end - it "should halt with the given code when the resource returns a status code" do + it 'should halt with the given code when the resource returns a status code' do resource.auth = 400 subject.run expect(response.code).to eq 400 end - it "should not reply with 401 when the client is authorized" do + it 'should not reply with 401 when the client is authorized' do resource.auth = true subject.run expect(response.code).to_not eq 401 end end - describe "#b7 (Forbidden?)" do - let(:resource) { resource_with { attr_accessor :forbid; def forbidden?; @forbid; end } } + describe '#b7 (Forbidden?)' do + let(:resource) { + resource_with { + attr_accessor :forbid + def forbidden? + @forbid + end + } + } - it "should reply with 403 when the request is forbidden" do + it 'should reply with 403 when the request is forbidden' do resource.forbid = true subject.run expect(response.code).to eq 403 end - it "should not reply with 403 when the request is permitted" do + it 'should not reply with 403 when the request is permitted' do resource.forbid = false subject.run expect(response.code).to_not eq 403 end - it "should halt with the given code when the resource returns a status code" do + it 'should halt with the given code when the resource returns a status code' do resource.forbid = 400 subject.run expect(response.code).to eq 400 end end - describe "#b6 (Unsupported Content-* header?)" do + describe '#b6 (Unsupported Content-* header?)' do let(:resource) do resource_with do def valid_content_headers?(contents) @@ -231,101 +270,126 @@ def valid_content_headers?(contents) end end - it "should reply with 501 when an invalid Content-* header is present" do - headers['Content-Fail'] = "yup" + it 'should reply with 501 when an invalid Content-* header is present' do + headers['Content-Fail'] = 'yup' subject.run expect(response.code).to eq 501 end - it "should not reply with 501 when all Content-* headers are valid" do + it 'should not reply with 501 when all Content-* headers are valid' do subject.run expect(response.code).to_not eq 501 end end - describe "#b5 (Known Content-Type?)" do - let(:method) { "POST" } - let(:body) { "This is the body." } + describe '#b5 (Known Content-Type?)' do + let(:method) { 'POST' } + let(:body) { 'This is the body.' } let(:resource) do resource_with do - def known_content_type?(type) type !~ /unknown/; end; - def process_post; true; end - def allowed_methods; %w{POST}; end + def known_content_type?(type) + type !~ /unknown/ + end + + def process_post + true + end + + def allowed_methods + %w[POST] + end end end before { headers['Content-Length'] = body.length.to_s } - it "should reply with 415 when the Content-Type is unknown" do - headers['Content-Type'] = "application/x-unknown-type" + it 'should reply with 415 when the Content-Type is unknown' do + headers['Content-Type'] = 'application/x-unknown-type' subject.run expect(response.code).to eq 415 end - it "should not reply with 415 when the Content-Type is known" do - headers['Content-Type'] = "text/plain" + it 'should not reply with 415 when the Content-Type is known' do + headers['Content-Type'] = 'text/plain' subject.run expect(response.code).to_not eq 415 end end - describe "#b4 (Request entity too large?)" do + describe '#b4 (Request entity too large?)' do let(:resource) do resource_with do - def allowed_methods; %w{POST}; end - def process_post; true; end - def valid_entity_length?(length); length.to_i < 100; end + def allowed_methods + %w[POST] + end + + def process_post + true + end + + def valid_entity_length?(length) + length.to_i < 100 + end end end - let(:method) { "POST" } - before { headers['Content-Type'] = "text/plain"; headers['Content-Length'] = body.size.to_s } + let(:method) { 'POST' } + before { + headers['Content-Type'] = 'text/plain' + headers['Content-Length'] = body.size.to_s + } - context "when the request body is too large" do - let(:body) { "Big" * 100 } - it "should reply with 413" do + context 'when the request body is too large' do + let(:body) { 'Big' * 100 } + it 'should reply with 413' do subject.run expect(response.code).to eq 413 end end - context "when the request body is not too large" do - let(:body) { "small" } + context 'when the request body is not too large' do + let(:body) { 'small' } - it "should not reply with 413" do + it 'should not reply with 413' do subject.run expect(response.code).to_not eq 413 end end end - describe "#b3 (OPTIONS?)" do - let(:method){ "OPTIONS" } - let(:resource){ resource_with { def allowed_methods; %w[GET HEAD OPTIONS]; end } } - it "should reply with 200 when the request method is OPTIONS" do + describe '#b3 (OPTIONS?)' do + let(:method) { 'OPTIONS' } + let(:resource) { + resource_with { + def allowed_methods + %w[GET HEAD OPTIONS] + end + } + } + it 'should reply with 200 when the request method is OPTIONS' do subject.run expect(response.code).to eq 200 end end - describe "#c3, #c4 (Acceptable media types)" do + describe '#c3, #c4 (Acceptable media types)' do let(:resource) { default_resource } - context "when the Accept header exists" do - it "should reply with 406 when the type is unacceptable" do - headers['Accept'] = "text/plain" + context 'when the Accept header exists' do + it 'should reply with 406 when the type is unacceptable' do + headers['Accept'] = 'text/plain' subject.run expect(response.code).to eq 406 end - it "should not reply with 406 when the type is acceptable" do - headers['Accept'] = "text/*" + it 'should not reply with 406 when the type is acceptable' do + headers['Accept'] = 'text/*' subject.run expect(response.code).to_not eq 406 - expect(response.headers['Content-Type']).to eq "text/html" + expect(response.headers['Content-Type']).to eq 'text/html' end end - context "when the Accept header does not exist" do - it "should not negotiate a media type" do + context 'when the Accept header does not exist' do + it 'should not negotiate a media type' do expect(headers['Accept']).to be_nil expect(subject).to_not receive(:c4) subject.run @@ -334,26 +398,32 @@ def valid_entity_length?(length); length.to_i < 100; end end end - describe "#d4, #d5 (Acceptable languages)" do - let(:resource) { resource_with { def languages_provided; %w{en-US fr}; end } } - context "when the Accept-Language header exists" do - it "should reply with 406 when the language is unacceptable" do - headers['Accept-Language'] = "es, de" + describe '#d4, #d5 (Acceptable languages)' do + let(:resource) { + resource_with { + def languages_provided + %w[en-US fr] + end + } + } + context 'when the Accept-Language header exists' do + it 'should reply with 406 when the language is unacceptable' do + headers['Accept-Language'] = 'es, de' subject.run expect(response.code).to eq 406 end - it "should not reply with 406 when the language is acceptable" do - headers['Accept-Language'] = "en-GB, en;q=0.7" + it 'should not reply with 406 when the language is acceptable' do + headers['Accept-Language'] = 'en-GB, en;q=0.7' subject.run expect(response.code).to_not eq 406 - expect(response.headers['Content-Language']).to eq "en-US" + expect(response.headers['Content-Language']).to eq 'en-US' expect(resource.instance_variable_get(:@language)).to eq 'en-US' end end - context "when the Accept-Language header is absent" do - it "should not negotiate the language" do + context 'when the Accept-Language header is absent' do + it 'should not negotiate the language' do expect(headers['Accept-Language']).to be_nil expect(subject).to_not receive(:d5) subject.run @@ -363,34 +433,40 @@ def valid_entity_length?(length); length.to_i < 100; end end end - describe "#e5, #e6 (Acceptable charsets)" do + describe '#e5, #e6 (Acceptable charsets)' do let(:resource) do resource_with do def charsets_provided - [["iso8859-1", :to_iso],["utf-8", :to_utf]]; + [['iso8859-1', :to_iso], ['utf-8', :to_utf]] + end + + def to_iso(chunk) + chunk + end + + def to_utf(chunk) + chunk end - def to_iso(chunk); chunk; end - def to_utf(chunk); chunk; end end end - context "when the Accept-Charset header exists" do - it "should reply with 406 when the charset is unacceptable" do - headers['Accept-Charset'] = "utf-16" + context 'when the Accept-Charset header exists' do + it 'should reply with 406 when the charset is unacceptable' do + headers['Accept-Charset'] = 'utf-16' subject.run expect(response.code).to eq 406 end - it "should not reply with 406 when the charset is acceptable" do - headers['Accept-Charset'] = "iso8859-1" + it 'should not reply with 406 when the charset is acceptable' do + headers['Accept-Charset'] = 'iso8859-1' subject.run expect(response.code).to_not eq 406 - expect(response.headers['Content-Type']).to eq "text/html;charset=iso8859-1" + expect(response.headers['Content-Type']).to eq 'text/html;charset=iso8859-1' end end - context "when the Accept-Charset header is absent" do - it "should not negotiate the language" do + context 'when the Accept-Charset header is absent' do + it 'should not negotiate the language' do expect(headers['Accept-Charset']).to be_nil expect(subject).to_not receive(:e6) subject.run @@ -399,23 +475,23 @@ def to_utf(chunk); chunk; end end end - describe "#f6, #f7 (Acceptable encodings)" do + describe '#f6, #f7 (Acceptable encodings)' do let(:resource) do resource_with do def encodings_provided - super.merge("gzip" => :encode_gzip) + super.merge('gzip' => :encode_gzip) end end end - context "when the Accept-Encoding header is present" do - it "should reply with 406 if the encoding is unacceptable" do + context 'when the Accept-Encoding header is present' do + it 'should reply with 406 if the encoding is unacceptable' do headers['Accept-Encoding'] = 'deflate, identity;q=0.0' subject.run expect(response.code).to eq 406 end - it "should not reply with 406 if the encoding is acceptable" do + it 'should not reply with 406 if the encoding is acceptable' do headers['Accept-Encoding'] = 'gzip, deflate' subject.run expect(response.code).to_not eq 406 @@ -425,8 +501,8 @@ def encodings_provided end end - context "when the Accept-Encoding header is not present" do - it "should not negotiate an encoding" do + context 'when the Accept-Encoding header is not present' do + it 'should not negotiate an encoding' do expect(headers['Accept-Encoding']).to be_nil expect(subject).to_not receive(:f7) subject.run @@ -437,30 +513,37 @@ def encodings_provided end end - describe "#g7 (Resource exists?)" do - let(:resource) { resource_with { attr_accessor :exist; def resource_exists?; @exist; end } } + describe '#g7 (Resource exists?)' do + let(:resource) { + resource_with { + attr_accessor :exist + def resource_exists? + @exist + end + } + } - it "should not enter conditional requests if missing (and eventually reply with 404)" do + it 'should not enter conditional requests if missing (and eventually reply with 404)' do resource.exist = false expect(subject).to_not receive(:g8) subject.run expect(response.code).to eq 404 end - it "should not reply with 404 if it does exist" do + it 'should not reply with 404 if it does exist' do resource.exist = true expect(subject).to_not receive(:h7) subject.run expect(response.code).to_not eq 404 end - it "should not reply with 404 for truthy non-booleans" do + it 'should not reply with 404 for truthy non-booleans' do resource.exist = [] subject.run expect(response.code).to_not eq 404 end - it "should reply with 404 for nil" do + it 'should reply with 404 for nil' do resource.exist = nil subject.run expect(response.code).to eq 404 @@ -468,37 +551,50 @@ def encodings_provided end # Conditional requests/preconditions - describe "#g8, #g9, #g10 (ETag match)" do - let(:resource) { resource_with { def generate_etag; "etag"; end } } - it "should skip ETag matching when If-Match is missing" do + describe '#g8, #g9, #g10 (ETag match)' do + let(:resource) { + resource_with { + def generate_etag + 'etag' + end + } + } + it 'should skip ETag matching when If-Match is missing' do expect(headers['If-Match']).to be_nil expect(subject).to_not receive(:g9) expect(subject).to_not receive(:g11) subject.run expect(response.code).to_not eq 412 end - it "should not reply with 304 when If-Match is *" do - headers['If-Match'] = "*" + it 'should not reply with 304 when If-Match is *' do + headers['If-Match'] = '*' subject.run expect(response.code).to_not eq 412 end - it "should reply with 412 if the ETag is not in If-Match" do + it 'should reply with 412 if the ETag is not in If-Match' do headers['If-Match'] = '"notetag"' subject.run expect(response.code).to eq 412 end - it "should not reply with 412 if the ETag is in If-Match" do + it 'should not reply with 412 if the ETag is in If-Match' do headers['If-Match'] = '"etag"' subject.run expect(response.code).to_not eq 412 end end - describe "#h10, #h11, #h12 (If-Unmodified-Since match [IUMS])" do - let(:resource) { resource_with { attr_accessor :now; def last_modified; @now; end } } + describe '#h10, #h11, #h12 (If-Unmodified-Since match [IUMS])' do + let(:resource) { + resource_with { + attr_accessor :now + def last_modified + @now + end + } + } before { @now = resource.now = Time.now } - it "should skip LM matching if IUMS is missing" do + it 'should skip LM matching if IUMS is missing' do expect(headers['If-Unmodified-Since']).to be_nil expect(subject).to_not receive(:h11) expect(subject).to_not receive(:h12) @@ -506,90 +602,106 @@ def encodings_provided expect(response.code).to_not eq 412 end - it "should skip LM matching if IUMS is an invalid date" do - headers['If-Unmodified-Since'] = "garbage" + it 'should skip LM matching if IUMS is an invalid date' do + headers['If-Unmodified-Since'] = 'garbage' expect(subject).to_not receive(:h12) subject.run expect(response.code).to_not eq 412 end - it "should not reply with 412 if LM is <= IUMS" do + it 'should not reply with 412 if LM is <= IUMS' do headers['If-Unmodified-Since'] = (@now + 100).httpdate subject.run expect(response.code).to_not eq 412 end - it "should reply with 412 if LM is > IUMS" do + it 'should reply with 412 if LM is > IUMS' do headers['If-Unmodified-Since'] = (@now - 100).httpdate subject.run expect(response.code).to eq 412 end end - describe "#i12, #i13, #k13, #j18 (If-None-Match match)" do + describe '#i12, #i13, #k13, #j18 (If-None-Match match)' do let(:resource) do resource_with do - def generate_etag; "etag"; end; - def process_post; true; end - def allowed_methods; %w{GET HEAD POST}; end + def generate_etag + 'etag' + end + + def process_post + true + end + + def allowed_methods + %w[GET HEAD POST] + end end end - it "should skip ETag matching if If-None-Match is missing" do + it 'should skip ETag matching if If-None-Match is missing' do expect(headers['If-None-Match']).to be_nil - %w{i13 k13 j18}.each do |m| + %w[i13 k13 j18].each do |m| expect(subject).to_not receive(m.to_sym) end subject.run expect([304, 412]).to_not include(response.code) end - it "should not reply with 412 or 304 if the ETag is not in If-None-Match" do + it 'should not reply with 412 or 304 if the ETag is not in If-None-Match' do headers['If-None-Match'] = '"notetag"' subject.run expect([304, 412]).to_not include(response.code) end - context "when the method is GET or HEAD" do - let(:method){ %w{GET HEAD}[rand(1)] } - it "should reply with 304 when If-None-Match is *" do + context 'when the method is GET or HEAD' do + let(:method) { %w[GET HEAD][rand(2)] } + it 'should reply with 304 when If-None-Match is *' do headers['If-None-Match'] = '*' end - it "should reply with 304 when the ETag is in If-None-Match" do + it 'should reply with 304 when the ETag is in If-None-Match' do headers['If-None-Match'] = '"etag", "foobar"' end - after { subject.run; expect(response.code).to eq 304 } + after { + subject.run + expect(response.code).to eq 304 + } end - context "when the method is not GET or HEAD" do - let(:method){ "POST" } - let(:body) { "This is the body." } - let(:headers){ Webmachine::Headers["Content-Type" => "text/plain"] } + context 'when the method is not GET or HEAD' do + let(:method) { 'POST' } + let(:body) { 'This is the body.' } + let(:headers) { Webmachine::Headers['Content-Type' => 'text/plain'] } - it "should reply with 412 when If-None-Match is *" do + it 'should reply with 412 when If-None-Match is *' do headers['If-None-Match'] = '*' end - it "should reply with 412 when the ETag is in If-None-Match" do + it 'should reply with 412 when the ETag is in If-None-Match' do headers['If-None-Match'] = '"etag"' end - after { subject.run; expect(response.code).to eq 412 } + after { + subject.run + expect(response.code).to eq 412 + } end - context "when the resource does not define an ETag" do + context 'when the resource does not define an ETag' do let(:resource) do resource_with do - def generate_etag; nil; end + def generate_etag + nil + end end end - it "should reply with 200 when If-None-Match is missing" do + it 'should reply with 200 when If-None-Match is missing' do headers.delete 'If-None-Match' subject.run expect(response.code).to eq 200 end - it "should reply with 200 when If-None-Match is present" do + it 'should reply with 200 when If-None-Match is present' do headers['If-None-Match'] = '"etag"' subject.run expect(response.code).to eq 200 @@ -597,42 +709,49 @@ def generate_etag; nil; end end end - describe "#l13, #l14, #l15, #l17 (If-Modified-Since match)" do - let(:resource) { resource_with { attr_accessor :now; def last_modified; @now; end } } + describe '#l13, #l14, #l15, #l17 (If-Modified-Since match)' do + let(:resource) { + resource_with { + attr_accessor :now + def last_modified + @now + end + } + } before { @now = resource.now = Time.now } - it "should skip LM matching if IMS is missing" do + it 'should skip LM matching if IMS is missing' do expect(headers['If-Modified-Since']).to be_nil - %w{l14 l15 l17}.each do |m| + %w[l14 l15 l17].each do |m| expect(subject).to_not receive(m.to_sym) end subject.run expect(response.code).to_not eq 304 end - it "should skip LM matching if IMS is an invalid date" do - headers['If-Modified-Since'] = "garbage" - %w{l15 l17}.each do |m| + it 'should skip LM matching if IMS is an invalid date' do + headers['If-Modified-Since'] = 'garbage' + %w[l15 l17].each do |m| expect(subject).to_not receive(m.to_sym) end subject.run expect(response.code).to_not eq 304 end - it "should skip LM matching if IMS is later than current time" do + it 'should skip LM matching if IMS is later than current time' do headers['If-Modified-Since'] = (@now + 1000).httpdate expect(subject).to_not receive(:l17) subject.run expect(response.code).to_not eq 304 end - it "should reply with 304 if LM is <= IMS" do + it 'should reply with 304 if LM is <= IMS' do headers['If-Modified-Since'] = (@now - 1).httpdate resource.now = @now - 1000 subject.run expect(response.code).to eq 304 end - it "should not reply with 304 if LM is > IMS" do + it 'should not reply with 304 if LM is > IMS' do headers['If-Modified-Since'] = (@now - 1000).httpdate subject.run expect(response.code).to_not eq 304 @@ -640,35 +759,40 @@ def generate_etag; nil; end end # Resource missing branch (upper right) - describe "#h7 (If-Match: * exists?)" do + describe '#h7 (If-Match: * exists?)' do let(:resource) { missing_resource } - it "should reply with 412 when the If-Match header is *" do + it 'should reply with 412 when the If-Match header is *' do headers['If-Match'] = '"*"' subject.run expect(response.code).to eq 412 end - it "should not reply with 412 when the If-Match header is missing or not *" do - headers['If-Match'] = ['"etag"', nil][rand(1)] + it 'should not reply with 412 when the If-Match header is missing or not *' do + headers['If-Match'] = ['"etag"', nil][rand(2)] subject.run expect(response.code).to_not eq 412 end end - describe "#i7 (PUT?)" do + describe '#i7 (PUT?)' do let(:resource) do missing_resource_with do - def allowed_methods; %w{GET HEAD PUT POST}; end - def process_post; true; end + def allowed_methods + %w[GET HEAD PUT POST] + end + + def process_post + true + end end end - let(:body) { %W{GET HEAD DELETE}.include?(method) ? nil : "This is the body." } + let(:body) { %W[GET HEAD DELETE].include?(method) ? nil : 'This is the body.' } before { headers['Content-Type'] = 'text/plain' } - context "when the method is PUT" do - let(:method){ "PUT" } + context 'when the method is PUT' do + let(:method) { 'PUT' } - it "should not reach state k7" do + it 'should not reach state k7' do expect(subject).to_not receive(:k7) subject.run end @@ -676,10 +800,10 @@ def process_post; true; end after { expect([404, 410, 303]).to_not include(response.code) } end - context "when the method is not PUT" do - let(:method){ %W{GET HEAD POST DELETE}[rand(4)] } + context 'when the method is not PUT' do + let(:method) { %W[GET HEAD POST DELETE][rand(4)] } - it "should not reach state i4" do + it 'should not reach state i4' do expect(subject).to_not receive(:i4) subject.run end @@ -688,90 +812,112 @@ def process_post; true; end end end - describe "#i4 (Apply to a different URI?)" do + describe '#i4 (Apply to a different URI?)' do let(:resource) do missing_resource_with do attr_accessor :location - def moved_permanently?; @location; end - def allowed_methods; %w[PUT]; end + def moved_permanently? + @location + end + + def allowed_methods + %w[PUT] + end end end - let(:method){ "PUT" } - let(:body){ "This is the body." } - let(:headers) { Webmachine::Headers["Content-Type" => "text/plain", "Content-Length" => body.size.to_s] } + let(:method) { 'PUT' } + let(:body) { 'This is the body.' } + let(:headers) { Webmachine::Headers['Content-Type' => 'text/plain', 'Content-Length' => body.size.to_s] } - it "should reply with 301 when the resource has moved" do - resource.location = URI.parse("http://localhost:8098/newuri") + it 'should reply with 301 when the resource has moved' do + resource.location = URI.parse('http://localhost:8098/newuri') subject.run expect(response.code).to eq 301 expect(response.headers['Location']).to eq resource.location.to_s end - it "should not reply with 301 when resource has not moved" do + it 'should not reply with 301 when resource has not moved' do resource.location = false subject.run expect(response.code).to_not eq 301 end end - describe "Redirection (Resource previously existed)" do + describe 'Redirection (Resource previously existed)' do let(:resource) do missing_resource_with do attr_writer :moved_perm, :moved_temp, :allow_missing - def previously_existed?; true; end - def moved_permanently?; @moved_perm; end - def moved_temporarily?; @moved_temp; end - def allow_missing_post?; @allow_missing; end - def allowed_methods; %W{GET POST}; end - def process_post; true; end + def previously_existed? + true + end + + def moved_permanently? + @moved_perm + end + + def moved_temporarily? + @moved_temp + end + + def allow_missing_post? + @allow_missing + end + + def allowed_methods + %W[GET POST] + end + + def process_post + true + end end end - let(:method){ @method || "GET" } + let(:method) { @method || 'GET' } - describe "#k5 (Moved permanently?)" do - it "should reply with 301 when the resource has moved permanently" do - uri = resource.moved_perm = URI.parse("http://www.google.com/") + describe '#k5 (Moved permanently?)' do + it 'should reply with 301 when the resource has moved permanently' do + uri = resource.moved_perm = URI.parse('http://www.google.com/') subject.run expect(response.code).to eq 301 expect(response.headers['Location']).to eq uri.to_s end - it "should not reply with 301 when the resource has not moved permanently" do + it 'should not reply with 301 when the resource has not moved permanently' do resource.moved_perm = false subject.run expect(response.code).to_not eq 301 end end - describe "#l5 (Moved temporarily?)" do + describe '#l5 (Moved temporarily?)' do before { resource.moved_perm = false } - it "should reply with 307 when the resource has moved temporarily" do - uri = resource.moved_temp = URI.parse("http://www.basho.com/") + it 'should reply with 307 when the resource has moved temporarily' do + uri = resource.moved_temp = URI.parse('http://www.basho.com/') subject.run expect(response.code).to eq 307 expect(response.headers['Location']).to eq uri.to_s end - it "should not reply with 307 when the resource has not moved temporarily" do + it 'should not reply with 307 when the resource has not moved temporarily' do resource.moved_temp = false subject.run expect(response.code).to_not eq 307 end end - describe "#m5 (POST?), #n5 (POST to missing resource?)" do + describe '#m5 (POST?), #n5 (POST to missing resource?)' do before { resource.moved_perm = resource.moved_temp = false } - it "should reply with 410 when the method is not POST" do - expect(method).to_not eq "POST" + it 'should reply with 410 when the method is not POST' do + expect(method).to_not eq 'POST' subject.run expect(response.code).to eq 410 end - it "should reply with 410 when the resource disallows missing POSTs" do - @method = "POST" + it 'should reply with 410 when the resource disallows missing POSTs' do + @method = 'POST' resource.allow_missing = false subject.run expect(response.code).to eq 410 end - it "should not reply with 410 when the resource allows missing POSTs" do - @method = "POST" + it 'should not reply with 410 when the resource allows missing POSTs' do + @method = 'POST' resource.allow_missing = true subject.run expect(response.code).to eq 410 @@ -779,51 +925,67 @@ def process_post; true; end end end - describe "#l7 (POST?), #m7 (POST to missing resource?)" do + describe '#l7 (POST?), #m7 (POST to missing resource?)' do let(:resource) do missing_resource_with do attr_accessor :allow_missing - def allowed_methods; %W{GET POST}; end - def previously_existed?; false; end - def allow_missing_post?; @allow_missing; end - def process_post; true; end + def allowed_methods + %W[GET POST] + end + + def previously_existed? + false + end + + def allow_missing_post? + @allow_missing + end + + def process_post + true + end end end - let(:method){ @method || "GET" } - it "should reply with 404 when the method is not POST" do - expect(method).to_not eq "POST" + let(:method) { @method || 'GET' } + it 'should reply with 404 when the method is not POST' do + expect(method).to_not eq 'POST' subject.run expect(response.code).to eq 404 end - it "should reply with 404 when the resource disallows missing POSTs" do - @method = "POST" + it 'should reply with 404 when the resource disallows missing POSTs' do + @method = 'POST' resource.allow_missing = false subject.run expect(response.code).to eq 404 end - it "should not reply with 404 when the resource allows missing POSTs" do - @method = "POST" + it 'should not reply with 404 when the resource allows missing POSTs' do + @method = 'POST' resource.allow_missing = true subject.run expect(response.code).to_not eq 404 end end - describe "#p3 (Conflict?)" do + describe '#p3 (Conflict?)' do let(:resource) do missing_resource_with do attr_writer :conflict - def allowed_methods; %W{PUT}; end - def is_conflict?; @conflict; end + def allowed_methods + %W[PUT] + end + + def is_conflict? + @conflict + end end end - let(:method){ "PUT" } - it "should reply with 409 if the resource is in conflict" do + let(:method) { 'PUT' } + it 'should reply with 409 if the resource is in conflict' do resource.conflict = true subject.run expect(response.code).to eq 409 end - it "should not reply with 409 if the resource is in conflict" do + it 'should not reply with 409 if the resource is in conflict' do resource.conflict = false subject.run expect(response.code).to_not eq 409 @@ -831,13 +993,19 @@ def is_conflict?; @conflict; end end # Bottom right - describe "#n11 (Redirect?)" do - let(:method) { "POST" } + describe '#n11 (Redirect?)' do + let(:method) { 'POST' } let(:resource) do resource_with do attr_writer :new_loc, :exist - def allowed_methods; %w{POST}; end - def allow_missing_post?; true; end + def allowed_methods + %w[POST] + end + + def allow_missing_post? + true + end + def process_post response.redirect_to(@new_loc) if @new_loc true @@ -845,17 +1013,17 @@ def process_post end end [true, false].each do |e| - context "and the resource #{ e ? "does not exist" : 'exists'}" do + context "and the resource #{e ? "does not exist" : "exists"}" do before { resource.exist = e } - it "should reply with 303 if the resource redirected" do - resource.new_loc = URI.parse("/foo/bar") + it 'should reply with 303 if the resource redirected' do + resource.new_loc = URI.parse('/foo/bar') subject.run expect(response.code).to eq 303 - expect(response.headers['Location']).to eq "/foo/bar" + expect(response.headers['Location']).to eq '/foo/bar' end - it "should not reply with 303 if the resource did not redirect" do + it 'should not reply with 303 if the resource did not redirect' do resource.new_loc = nil subject.run expect(response.code).to_not eq 303 @@ -864,40 +1032,61 @@ def process_post end end - describe "#p11 (New resource?)" do + describe '#p11 (New resource?)' do let(:resource) do resource_with do attr_writer :exist, :new_loc, :create - def allowed_methods; %W{PUT POST}; end - def resource_exists?; @exist; end - def process_post; true; end - def allow_missing_post?; true; end - def post_is_create?; @create; end - def create_path; @new_loc; end - def content_types_accepted; [["text/plain", :accept_text]]; end + def allowed_methods + %W[PUT POST] + end + + def resource_exists? + @exist + end + + def process_post + true + end + + def allow_missing_post? + true + end + + def post_is_create? + @create + end + + def create_path + @new_loc + end + + def content_types_accepted + [['text/plain', :accept_text]] + end + def accept_text response.headers['Location'] = @new_loc.to_s if @new_loc true end end end - let(:body) { "new content" } - let(:headers){ Webmachine::Headers['content-type' => 'text/plain'] } + let(:body) { 'new content' } + let(:headers) { Webmachine::Headers['content-type' => 'text/plain'] } - context "when the method is PUT" do - let(:method){ "PUT" } + context 'when the method is PUT' do + let(:method) { 'PUT' } [true, false].each do |e| - context "and the resource #{ e ? "does not exist" : 'exists'}" do + context "and the resource #{e ? "does not exist" : "exists"}" do before { resource.exist = e } - it "should reply with 201 when the Location header has been set" do + it 'should reply with 201 when the Location header has been set' do resource.exist = e - resource.new_loc = "http://ruby-doc.org/" + resource.new_loc = 'http://ruby-doc.org/' subject.run expect(response.code).to eq 201 end - it "should not reply with 201 when the Location header has been set" do + it 'should not reply with 201 when the Location header has been set' do resource.exist = e subject.run expect(response.headers['Location']).to be_nil @@ -907,25 +1096,25 @@ def accept_text end end - context "when the method is POST" do - let(:method){ "POST" } + context 'when the method is POST' do + let(:method) { 'POST' } [true, false].each do |e| - context "and the resource #{ e ? 'exists' : "does not exist"}" do + context "and the resource #{e ? "exists" : "does not exist"}" do before { resource.exist = e } - it "should reply with 201 when post_is_create is true and create_path returns a URI" do - resource.new_loc = created = "/foo/bar/baz" + it 'should reply with 201 when post_is_create is true and create_path returns a URI' do + resource.new_loc = created = '/foo/bar/baz' resource.create = true subject.run expect(response.code).to eq 201 expect(response.headers['Location']).to eq created end - it "should reply with 500 when post_is_create is true and create_path returns nil" do + it 'should reply with 500 when post_is_create is true and create_path returns nil' do resource.create = true subject.run expect(response.code).to eq 500 expect(response.error).to_not be_nil end - it "should not reply with 201 when post_is_create is false" do + it 'should not reply with 201 when post_is_create is false' do resource.create = false subject.run expect(response.code).to_not eq 201 @@ -935,54 +1124,67 @@ def accept_text end end - describe "#o14 (Conflict?)" do + describe '#o14 (Conflict?)' do let(:resource) do resource_with do attr_writer :conflict - def allowed_methods; %W{PUT}; end - def is_conflict?; @conflict; end + def allowed_methods + %W[PUT] + end + + def is_conflict? + @conflict + end end end - let(:method){ "PUT" } - it "should reply with 409 if the resource is in conflict" do + let(:method) { 'PUT' } + it 'should reply with 409 if the resource is in conflict' do resource.conflict = true subject.run expect(response.code).to eq 409 end - it "should not reply with 409 if the resource is in conflict" do + it 'should not reply with 409 if the resource is in conflict' do resource.conflict = false subject.run expect(response.code).to_not eq 409 end end - describe "#m16 (DELETE?), #m20 (Delete enacted?)" do - let(:method){ @method || "DELETE" } + describe '#m16 (DELETE?), #m20 (Delete enacted?)' do + let(:method) { @method || 'DELETE' } let(:resource) do resource_with do attr_writer :deleted, :completed - def allowed_methods; %w{GET DELETE}; end - def delete_resource; @deleted; end - def delete_completed?; @completed; end + def allowed_methods + %w[GET DELETE] + end + + def delete_resource + @deleted + end + + def delete_completed? + @completed + end end end - it "should not reply with 202 if the method is not DELETE" do - @method = "GET" + it 'should not reply with 202 if the method is not DELETE' do + @method = 'GET' subject.run expect(response.code).to_not eq 202 end - it "should reply with 500 if the DELETE fails" do + it 'should reply with 500 if the DELETE fails' do resource.deleted = false subject.run expect(response.code).to eq 500 end - it "should reply with 202 if the DELETE succeeds but is not complete" do + it 'should reply with 202 if the DELETE succeeds but is not complete' do resource.deleted = true resource.completed = false subject.run expect(response.code).to eq 202 end - it "should not reply with 202 if the DELETE succeeds and completes" do + it 'should not reply with 202 if the DELETE succeeds and completes' do resource.completed = resource.deleted = true subject.run expect(response.code).to_not eq 202 @@ -994,45 +1196,65 @@ def delete_completed?; @completed; end # describe "#n16 (POST?)" do it; end # describe "#o16 (PUT?)" do it; end - describe "#o18 (Multiple representations?)" do + describe '#o18 (Multiple representations?)' do let(:resource) do resource_with do attr_writer :exist, :multiple def delete_resource - response.body = "Response content." + response.body = 'Response content.' + true + end + + def delete_completed? true end - def delete_completed?; true; end - def allowed_methods; %W{GET HEAD PUT POST DELETE}; end - def resource_exists?; @exist; end - def allow_missing_post?; true; end - def content_types_accepted; [[request.content_type, :accept_all]]; end - def multiple_choices?; @multiple; end + + def allowed_methods + %W[GET HEAD PUT POST DELETE] + end + + def resource_exists? + @exist + end + + def allow_missing_post? + true + end + + def content_types_accepted + [[request.content_type, :accept_all]] + end + + def multiple_choices? + @multiple + end + def process_post - response.body = "Response content." + response.body = 'Response content.' true end + def accept_all - response.body = "Response content." + response.body = 'Response content.' true end end end - [["GET", true],["HEAD", true],["PUT", true],["PUT", false],["POST",true],["POST",false], - ["DELETE", true]].each do |m, e| - context "when the method is #{m} and the resource #{e ? 'exists' : 'does not exist' }" do - let(:method){ m } - let(:body) { %W{PUT POST}.include?(m) ? "request body" : "" } - let(:headers) { %W{PUT POST}.include?(m) ? Webmachine::Headers['content-type' => 'text/plain'] : Webmachine::Headers.new } + [['GET', true], ['HEAD', true], ['PUT', true], ['PUT', false], ['POST', true], ['POST', false], + ['DELETE', true]].each do |m, e| + context "when the method is #{m} and the resource #{e ? "exists" : "does not exist"}" do + let(:method) { m } + let(:body) { %W[PUT POST].include?(m) ? 'request body' : '' } + let(:headers) { %W[PUT POST].include?(m) ? Webmachine::Headers['content-type' => 'text/plain'] : Webmachine::Headers.new } before { resource.exist = e } - it "should reply with 200 if there are not multiple representations" do + it 'should reply with 200 if there are not multiple representations' do resource.multiple = false subject.run puts response.error if response.code == 500 expect(response.code).to eq 200 end - it "should reply with 300 if there are multiple representations" do + it 'should reply with 300 if there are multiple representations' do resource.multiple = true subject.run puts response.error if response.code == 500 @@ -1042,37 +1264,56 @@ def accept_all end end - describe "#o20 (Response has entity?)" do + describe '#o20 (Response has entity?)' do let(:resource) do resource_with do attr_writer :exist, :body - def delete_resource; true; end - def delete_completed?; true; end - def allowed_methods; %{GET PUT POST DELETE}; end - def resource_exists?; @exist; end - def allow_missing_post?; true; end - def content_types_accepted; [[request.content_type, :accept_all]]; end + def delete_resource + true + end + + def delete_completed? + true + end + + def allowed_methods + %(GET PUT POST DELETE) + end + + def resource_exists? + @exist + end + + def allow_missing_post? + true + end + + def content_types_accepted + [[request.content_type, :accept_all]] + end + def process_post response.body = @body if @body true end + def accept_all response.body = @body if @body true end end end - let(:method) { @method || "GET" } - let(:headers) { %{PUT POST}.include?(method) ? Webmachine::Headers["content-type" => "text/plain"] : Webmachine::Headers.new } - let(:body) { %{PUT POST}.include?(method) ? "This is the body." : nil } - context "when a response body is present" do - before { resource.body = "Hello, world!" } + let(:method) { @method || 'GET' } + let(:headers) { %(PUT POST).include?(method) ? Webmachine::Headers['content-type' => 'text/plain'] : Webmachine::Headers.new } + let(:body) { %(PUT POST).include?(method) ? 'This is the body.' : nil } + context 'when a response body is present' do + before { resource.body = 'Hello, world!' } [ - ["PUT", false], - ["POST", false], - ["DELETE", true], - ["POST", true], - ["PUT", true] + ['PUT', false], + ['POST', false], + ['DELETE', true], + ['POST', true], + ['PUT', true] ].each do |m, e| it "should not reply with 204 (via exists:#{e}, #{m})" do @method = m @@ -1082,13 +1323,13 @@ def accept_all end end end - context "when a response body is not present" do + context 'when a response body is not present' do [ - ["PUT", false], - ["POST", false], - ["DELETE", true], - ["POST", true], - ["PUT", true] + ['PUT', false], + ['POST', false], + ['DELETE', true], + ['POST', true], + ['PUT', true] ].each do |m, e| it "should reply with 204 (via exists:#{e}, #{m})" do @method = m @@ -1100,8 +1341,8 @@ def accept_all end end - describe "On exception" do - context "handle_exception is inherited." do + describe 'On error' do + context 'handle_exception is inherited.' do let :resource do resource_with do def to_html @@ -1110,22 +1351,22 @@ def to_html end end - it "calls handle_exception" do + it 'calls handle_exception' do expect(resource).to receive(:handle_exception).with instance_of(RuntimeError) subject.run end - it "sets the response code to 500" do + it 'sets the response code to 500' do subject.run expect(response.code).to eq 500 end end - context "handle_exception is defined" do + context 'handle_exception is defined' do let :resource do resource_with do def handle_exception(e) - response.body = "error" + response.body = 'error' end def to_html @@ -1134,12 +1375,12 @@ def to_html end end - it "can define a response body" do + it 'can define a response body' do subject.run - expect(response.body).to eq "error" + expect(response.body).to eq 'error' end - it "sets the response code to 500" do + it 'sets the response code to 500' do subject.run expect(response.code).to eq 500 end diff --git a/spec/webmachine/decision/fsm_spec.rb b/spec/webmachine/decision/fsm_spec.rb index 8d20ea48..e1f8e800 100644 --- a/spec/webmachine/decision/fsm_spec.rb +++ b/spec/webmachine/decision/fsm_spec.rb @@ -5,15 +5,56 @@ subject { described_class.new(resource, request, response) } + let(:run_with_exception) do + subject.run + rescue Exception + end + describe 'handling of exceptions from decision methods' do - let(:exception) { Exception.new } + let(:UNRESCUABLE_exceptions) do + Webmachine::RescuableException::UNRESCUABLE + end + + describe 'rescueable exceptions' do + it 'does rescue Exception' do + allow(subject).to receive(Webmachine::Decision::Flow::START) { raise(Exception) } + expect(resource).to receive(:handle_exception).with instance_of(Exception) + expect { subject.run }.to_not raise_error + end + + it 'does rescue a failed require' do + allow(subject).to receive(Webmachine::Decision::Flow::START) { require 'laterequire' } + expect(resource).to receive(:handle_exception).with instance_of(LoadError) + expect { subject.run }.to_not raise_error + end + end + + describe 'UNRESCUABLE exceptions' do + shared_examples 'UNRESCUABLE' do |e| + specify "#{e} is not rescued" do + allow(subject).to receive(Webmachine::Decision::Flow::START) { raise(e) } + expect(resource).to_not receive(:handle_exception).with instance_of(e) + expect { subject.run }.to raise_error(e) + end + end + eary = Webmachine::RescuableException::UNRESCUABLE_DEFAULTS - [ + Webmachine::MalformedRequest, # Webmachine rescues by default, so it won't re-raise. + SignalException # Requires raise in form 'raise SignalException, "SIGSOMESIGNAL"'. + # Haven't found a good no-op signal to use here. + ] + eary.each { |e| include_examples 'UNRESCUABLE', e } + end + end + + describe 'handling of errors from decision methods' do + let(:error) { RuntimeError.new } before do - allow(subject).to receive(Webmachine::Decision::Flow::START) { raise exception } + allow(subject).to receive(Webmachine::Decision::Flow::START) { raise error } end it 'calls resource.handle_exception' do - expect(resource).to receive(:handle_exception).with(exception) + expect(resource).to receive(:handle_exception).with(error) subject.run end @@ -23,12 +64,12 @@ end end - describe 'handling of exceptions from resource.handle_exception' do - let(:exception) { Exception.new('an error message') } + describe 'handling of errors from resource.handle_exception' do + let(:error) { RuntimeError.new('an error message') } before do allow(subject).to receive(Webmachine::Decision::Flow::START) { raise } - allow(resource).to receive(:handle_exception) { raise exception } + allow(resource).to receive(:handle_exception) { raise error } end it 'does not call resource.handle_exception again' do @@ -42,22 +83,41 @@ end it 'renders an error' do - expect(Webmachine). - to receive(:render_error). - with(500, request, response, { :message => exception.message }) + expect(Webmachine) + .to receive(:render_error) + .with(500, request, response, {message: error.message}) subject.run end end describe 'handling of exceptions from resource.finish_request' do - let(:exception) { Exception.new } + let(:exception) { Class.new(RuntimeError).new } before do + Webmachine::RescuableException.remove(exception) allow(resource).to receive(:finish_request) { raise exception } end + it 'does not call resource.handle_exception' do + expect(resource).to_not receive(:handle_exception) + run_with_exception + end + + it 'does not call resource.finish_request again' do + expect(resource).to receive(:finish_request).once + run_with_exception + end + end + + describe 'handling of errors from resource.finish_request' do + let(:error) { RuntimeError.new } + + before do + allow(resource).to receive(:finish_request) { raise error } + end + it 'calls resource.handle_exception' do - expect(resource).to receive(:handle_exception).with(exception) + expect(resource).to receive(:handle_exception).with(error) subject.run end @@ -67,7 +127,7 @@ end end - it "sets the response code before calling finish_request" do + it 'sets the response code before calling finish_request' do resource_class.class_eval do class << self attr_accessor :current_response_code diff --git a/spec/webmachine/decision/helpers_spec.rb b/spec/webmachine/decision/helpers_spec.rb index cd1b09d5..4caab4bb 100644 --- a/spec/webmachine/decision/helpers_spec.rb +++ b/spec/webmachine/decision/helpers_spec.rb @@ -1,20 +1,22 @@ require 'spec_helper' describe Webmachine::Decision::Helpers do - include_context "default resource" + include_context 'default resource' subject { Webmachine::Decision::FSM.new(resource, request, response) } def resource_with(&block) klass = Class.new(Webmachine::Resource) do - def to_html; "test resource"; end + def to_html + 'test resource' + end end - klass.module_eval(&block) if block_given? + klass.module_eval(&block) if block klass.new(request, response) end let(:resource) { resource_with } - describe "accepting request bodies" do + describe 'accepting request bodies' do let(:resource) do resource_with do def initialize @@ -22,31 +24,34 @@ def initialize end attr_accessor :accepted, :result def content_types_accepted - (accepted || []).map {|t| Array === t ? t : [t, :accept_doc] } + (accepted || []).map { |t| (Array === t) ? t : [t, :accept_doc] } + end + + def accept_doc + result end - def accept_doc; result; end end end - it "should return 415 when no types are accepted" do + it 'should return 415 when no types are accepted' do expect(subject.accept_helper).to eq 415 end - it "should return 415 when the posted type is not acceptable" do - resource.accepted = %W{application/json} - headers['Content-Type'] = "text/xml" + it 'should return 415 when the posted type is not acceptable' do + resource.accepted = %W[application/json] + headers['Content-Type'] = 'text/xml' expect(subject.accept_helper).to eq 415 end - it "should call the method for the first acceptable type, taking into account params" do - resource.accepted = ["application/json;v=3", ["application/json", :other]] + it 'should call the method for the first acceptable type, taking into account params' do + resource.accepted = ['application/json;v=3', ['application/json', :other]] expect(resource).to receive(:other).and_return(true) headers['Content-Type'] = 'application/json;v=2' expect(subject.accept_helper).to be(true) end end - context "setting the Content-Length header when responding" do + context 'setting the Content-Length header when responding' do [204, 205, 304].each do |code| it "removes the header for entity-less response code #{code}" do response.headers['Content-Length'] = '0' @@ -81,82 +86,82 @@ def accept_doc; result; end end end - describe "#encode_body" do + describe '#encode_body' do before { subject.run } - context "with a String body" do + context 'with a String body' do before { response.body = '' } - it "does not modify the response body" do + it 'does not modify the response body' do subject.encode_body expect(response.body).to be_instance_of(String) end - it "sets the Content-Length header in the response" do + it 'sets the Content-Length header in the response' do subject.encode_body expect(response.headers['Content-Length']).to eq response.body.bytesize.to_s end end - shared_examples_for "a non-String body" do - it "does not set the Content-Length header in the response" do + shared_examples_for 'a non-String body' do + it 'does not set the Content-Length header in the response' do subject.encode_body expect(response.headers).to_not have_key('Content-Length') end - it "sets the Transfer-Encoding response header to chunked" do + it 'sets the Transfer-Encoding response header to chunked' do subject.encode_body expect(response.headers['Transfer-Encoding']).to eq 'chunked' end end - context "with an Enumerable body" do + context 'with an Enumerable body' do before { response.body = ['one', 'two'] } - it "wraps the response body in an EnumerableEncoder" do + it 'wraps the response body in an EnumerableEncoder' do subject.encode_body expect(response.body).to be_instance_of(Webmachine::Streaming::EnumerableEncoder) end - it_should_behave_like "a non-String body" + it_should_behave_like 'a non-String body' end - context "with a callable body" do - before { response.body = Proc.new { 'proc' } } + context 'with a callable body' do + before { response.body = proc { 'proc' } } - it "wraps the response body in a CallableEncoder" do + it 'wraps the response body in a CallableEncoder' do subject.encode_body expect(response.body).to be_instance_of(Webmachine::Streaming::CallableEncoder) end - - it_should_behave_like "a non-String body" + + it_should_behave_like 'a non-String body' end - context "with a Fiber body" do - before { response.body = Fiber.new { Fiber.yield "foo" } } + context 'with a Fiber body' do + before { response.body = Fiber.new { Fiber.yield 'foo' } } - it "wraps the response body in a FiberEncoder" do + it 'wraps the response body in a FiberEncoder' do subject.encode_body expect(response.body).to be_instance_of(Webmachine::Streaming::FiberEncoder) end - it_should_behave_like "a non-String body" + it_should_behave_like 'a non-String body' end - context "with a File body" do - before { response.body = File.open("spec/spec_helper.rb", "r") } + context 'with a File body' do + before { response.body = File.open('spec/spec_helper.rb', 'r') } - it "wraps the response body in an IOEncoder" do + it 'wraps the response body in an IOEncoder' do subject.encode_body expect(response.body).to be_instance_of(Webmachine::Streaming::IOEncoder) end - it "sets the Content-Length header to the size of the file" do + it 'sets the Content-Length header to the size of the file' do subject.encode_body expect(response.headers['Content-Length']).to eq File.stat('spec/spec_helper.rb').size.to_s end - it "progressively yields file contents for each enumeration" do + it 'progressively yields file contents for each enumeration' do subject.encode_body body_size = 0 response.body.each do |chunk| @@ -166,50 +171,50 @@ def accept_doc; result; end expect(body_size).to eq File.stat('spec/spec_helper.rb').size end - context "when the resource provides a non-identity encoding that the client accepts" do + context 'when the resource provides a non-identity encoding that the client accepts' do let(:resource) do resource_with do def encodings_provided - { "deflate" => :encode_deflate, "identity" => :encode_identity } + {'deflate' => :encode_deflate, 'identity' => :encode_identity} end end end let(:headers) do - Webmachine::Headers.new({"Accept-Encoding" => "deflate, identity"}) + Webmachine::Headers.new({'Accept-Encoding' => 'deflate, identity'}) end - it_should_behave_like "a non-String body" + it_should_behave_like 'a non-String body' end end - context "with a StringIO body" do - before { response.body = StringIO.new("A VERY LONG STRING, NOT") } + context 'with a StringIO body' do + before { response.body = StringIO.new('A VERY LONG STRING, NOT') } - it "wraps the response body in an IOEncoder" do + it 'wraps the response body in an IOEncoder' do subject.encode_body expect(response.body).to be_instance_of(Webmachine::Streaming::IOEncoder) end - it "sets the Content-Length header to the size of the string" do + it 'sets the Content-Length header to the size of the string' do subject.encode_body expect(response.headers['Content-Length']).to eq response.body.size.to_s end - context "when the resource provides a non-identity encoding that the client accepts" do + context 'when the resource provides a non-identity encoding that the client accepts' do let(:resource) do resource_with do def encodings_provided - { "deflate" => :encode_deflate, "identity" => :encode_identity } + {'deflate' => :encode_deflate, 'identity' => :encode_identity} end end end let(:headers) do - Webmachine::Headers.new({"Accept-Encoding" => "deflate, identity"}) + Webmachine::Headers.new({'Accept-Encoding' => 'deflate, identity'}) end - it_should_behave_like "a non-String body" + it_should_behave_like 'a non-String body' end end end diff --git a/spec/webmachine/dispatcher/rfc3986_percent_decode_spec.rb b/spec/webmachine/dispatcher/rfc3986_percent_decode_spec.rb new file mode 100644 index 00000000..b2a0236e --- /dev/null +++ b/spec/webmachine/dispatcher/rfc3986_percent_decode_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Webmachine::Dispatcher::Route do + describe '#rfc3986_percent_decode' do + def call_subject(value) + Webmachine::Dispatcher::Route.rfc3986_percent_decode(value) + end + + it 'does not change un-encoded strings' do + expect(call_subject('this is a normal string, I think')).to eq 'this is a normal string, I think' + expect(call_subject('')).to eq '' + end + + it 'decodes percent encoded sequences' do + expect(call_subject('/tenants/esckimo+test%20%65')).to eq '/tenants/esckimo+test e' + end + + it 'leaves incorrectly encoded sequences as is' do + expect(call_subject('/tenants/esckimo+test%2%65')).to eq '/tenants/esckimo+test%2e' + end + end +end diff --git a/spec/webmachine/dispatcher/route_spec.rb b/spec/webmachine/dispatcher/route_spec.rb index c5cdbb77..27272ad0 100644 --- a/spec/webmachine/dispatcher/route_spec.rb +++ b/spec/webmachine/dispatcher/route_spec.rb @@ -1,14 +1,17 @@ require 'spec_helper' Webmachine::Dispatcher::Route.class_eval do - def warn(*msgs); end # silence warnings for tests + # silence warnings for tests + def warn(*msgs) + end end describe Webmachine::Dispatcher::Route do - let(:method) { "GET" } - let(:uri) { URI.parse("http://localhost:8080/") } - let(:request){ Webmachine::Request.new(method, uri, Webmachine::Headers.new, "") } - let(:resource){ Class.new(Webmachine::Resource) } + let(:method) { 'GET' } + let(:uri) { URI.parse('http://localhost:8080/') } + let(:routing_tokens) { nil } + let(:request) { Webmachine::Request.new(method, uri, Webmachine::Headers.new, '', routing_tokens) } + let(:resource) { Class.new(Webmachine::Resource) } describe '#apply' do let(:route) { @@ -16,13 +19,11 @@ def warn(*msgs); end # silence warnings for tests } describe 'a path_info fragment' do - before do - uri.path = '/hello/planet%20earth%20++' - end + let(:uri) { URI.parse('http://localhost:8080/hello/planet%20earth%20++') } it 'should decode the value' do route.apply(request) - expect(request.path_info).to eq({:string => 'planet earth ++'}) + expect(request.path_info).to eq({string: 'planet earth ++'}) end end end @@ -30,8 +31,10 @@ def warn(*msgs); end # silence warnings for tests matcher :match_route do |*expected| route = Webmachine::Dispatcher::Route.new(expected[0], Class.new(Webmachine::Resource), expected[1] || {}) match do |actual| - request.uri.path = actual if String === actual - route.match?(request) + uri = URI.parse('http://localhost:8080') + uri.path = actual + req = Webmachine::Request.new('GET', uri, Webmachine::Headers.new, '', routing_tokens) + route.match?(req) end failure_message do |_| @@ -42,61 +45,61 @@ def warn(*msgs); end # silence warnings for tests end end - it "warns about the deprecated string splat when initializing" do - [["*"],["foo", "*"],["foo", :bar, "*"]].each do |path| + it 'warns about the deprecated string splat when initializing' do + [['*'], ['foo', '*'], ['foo', :bar, '*']].each do |path| route = described_class.allocate expect(route).to receive(:warn) route.send :initialize, path, resource, {} end end - context "matching a request" do - context "on the root path" do - subject { "/" } + context 'matching a request' do + context 'on the root path' do + subject { '/' } it { is_expected.to match_route([]) } it { is_expected.to match_route ['*'] } it { is_expected.to match_route [:*] } - it { is_expected.not_to match_route %w{foo} } + it { is_expected.not_to match_route %w[foo] } it { is_expected.not_to match_route [:id] } end - context "on a deep path" do - subject { "/foo/bar/baz" } - it { is_expected.to match_route %w{foo bar baz} } - it { is_expected.to match_route ['foo', :id, "baz"] } + context 'on a deep path' do + subject { '/foo/bar/baz' } + it { is_expected.to match_route %w[foo bar baz] } + it { is_expected.to match_route ['foo', :id, 'baz'] } it { is_expected.to match_route ['foo', :*] } it { is_expected.to match_route [:id, :*] } it { is_expected.not_to match_route [] } it { is_expected.not_to match_route ['bar', :*] } end - context "with a guard on the request method" do - let(:uri){ URI.parse("http://localhost:8080/notes") } + context 'with a guard on the request method' do + let(:uri) { URI.parse('http://localhost:8080/notes') } let(:route) do described_class.new( - ["notes"], - lambda { |request| request.method == "POST" }, - resource - ) + ['notes'], + lambda { |request| request.method == 'POST' }, + resource + ) end subject { route } - context "when guard passes" do - let(:method){ "POST" } + context 'when guard passes' do + let(:method) { 'POST' } it { is_expected.to be_match(request) } - context "but the path match fails" do - let(:uri){ URI.parse("http://localhost:8080/other") } + context 'but the path match fails' do + let(:uri) { URI.parse('http://localhost:8080/other') } it { is_expected.not_to be_match(request) } end end - context "when guard fails" do - let(:method) { "GET" } + context 'when guard fails' do + let(:method) { 'GET' } it { is_expected.not_to be_match(request) } end - context "when the guard responds to #call" do + context 'when the guard responds to #call' do let(:guard_class) do Class.new do def initialize(method) @@ -110,101 +113,136 @@ def call(request) end let(:route) do - described_class.new(["notes"], guard_class.new("POST"), resource) + described_class.new(['notes'], guard_class.new('POST'), resource) end - context "when the guard passes" do - let(:method){ "POST" } + context 'when the guard passes' do + let(:method) { 'POST' } it { is_expected.to be_match(request) } end - context "when the guard fails" do + context 'when the guard fails' do # let(:method){ "GET" } it { is_expected.not_to be_match(request) } end end end + + context 'with a request with explicitly specified routing tokens' do + subject { '/some/route/foo/bar' } + let(:routing_tokens) { ['foo', 'bar'] } + it { is_expected.to match_route(['foo', 'bar']) } + it { is_expected.to match_route(['foo', :id]) } + it { is_expected.to match_route ['*'] } + it { is_expected.to match_route [:*] } + it { is_expected.not_to match_route(['some', 'route', 'foo', 'bar']) } + it { is_expected.not_to match_route %w[foo] } + it { is_expected.not_to match_route [:id] } + end end - context "applying bindings" do - context "on the root path" do + context 'applying bindings' do + context 'on the root path' do subject { described_class.new([], resource) } before { subject.apply(request) } - it "should assign the dispatched path to the empty string" do - expect(request.disp_path).to eq("") + it 'should assign the dispatched path to the empty string' do + expect(request.disp_path).to eq('') end - it "should assign empty bindings" do + it 'should assign empty bindings' do expect(request.path_info).to eq({}) end - it "should assign empty path tokens" do + it 'should assign empty path tokens' do expect(request.path_tokens).to eq([]) end - context "with extra user-defined bindings" do - subject { described_class.new([], resource, "bar" => "baz") } + context 'with extra user-defined bindings' do + subject { described_class.new([], resource, 'bar' => 'baz') } - it "should assign the user-defined bindings" do - expect(request.path_info).to eq({"bar" => "baz"}) + it 'should assign the user-defined bindings' do + expect(request.path_info).to eq({'bar' => 'baz'}) end end - context "with a splat" do + context 'with a splat' do subject { described_class.new([:*], resource) } - it "should assign empty path tokens" do + it 'should assign empty path tokens' do expect(request.path_tokens).to eq([]) end end - context "with a deprecated splat string" do + context 'with a deprecated splat string' do subject { described_class.new(['*'], resource) } - it "should assign empty path tokens" do + it 'should assign empty path tokens' do expect(request.path_tokens).to eq([]) end end end + context 'on a deep path' do + subject { described_class.new(%w[foo bar baz], resource) } + let(:uri) { URI.parse('http://localhost:8080/foo/bar/baz') } + before { subject.apply(request) } - context "on a deep path" do - subject { described_class.new(%w{foo bar baz}, resource) } - before { request.uri.path = "/foo/bar/baz"; subject.apply(request) } - - it "should assign the dispatched path as the path past the initial slash" do - expect(request.disp_path).to eq("foo/bar/baz") + it 'should assign the dispatched path as the path past the initial slash' do + expect(request.disp_path).to eq('foo/bar/baz') end - it "should assign empty bindings" do + it 'should assign empty bindings' do expect(request.path_info).to eq({}) end - it "should assign empty path tokens" do + it 'should assign empty path tokens' do expect(request.path_tokens).to eq([]) end - context "with path variables" do + context 'with path variables' do subject { described_class.new(['foo', :id, 'baz'], resource) } - it "should assign the path variables in the bindings" do - expect(request.path_info).to eq({:id => "bar"}) + it 'should assign the path variables in the bindings' do + expect(request.path_info).to eq({id: 'bar'}) + end + end + context 'with regex' do + subject { described_class.new([/foo/, /(.*)/, 'baz'], resource) } + + it 'should assign the captures path variables' do + expect(request.path_info).to eq({captures: ['bar']}) + end + end + context 'with multi-capture regex' do + subject { described_class.new([/foo/, /(.*)/, /baz\.(.*)/], resource) } + let(:uri) { URI.parse('http://localhost:8080/foo/bar/baz.json') } + + it 'should assign the captures path variables' do + expect(request.path_info).to eq({captures: ['bar', 'json']}) + end + end + context 'with named capture regex' do + subject { described_class.new(['foo', :bar, /(?[^.]+)\.(?.*)/], resource) } + let(:uri) { URI.parse('http://localhost:8080/foo/bar/baz.json') } + + it 'should assign the captures path variables' do + expect(request.path_info).to eq({bar: 'bar', baz: 'baz', format: 'json'}) end end - context "with a splat" do + context 'with a splat' do subject { described_class.new(['foo', :*], resource) } - it "should capture the path tokens matched by the splat" do - expect(request.path_tokens).to eq(%w{ bar baz }) + it 'should capture the path tokens matched by the splat' do + expect(request.path_tokens).to eq(%w[bar baz]) end end - context "with a deprecated splat string" do - subject { described_class.new(%w{foo *}, resource) } + context 'with a deprecated splat string' do + subject { described_class.new(%w[foo *], resource) } - it "should capture the path tokens matched by the splat" do - expect(request.path_tokens).to eq(%w{ bar baz }) + it 'should capture the path tokens matched by the splat' do + expect(request.path_tokens).to eq(%w[bar baz]) end end end diff --git a/spec/webmachine/dispatcher_spec.rb b/spec/webmachine/dispatcher_spec.rb index faeede65..795ff0ab 100644 --- a/spec/webmachine/dispatcher_spec.rb +++ b/spec/webmachine/dispatcher_spec.rb @@ -2,23 +2,36 @@ describe Webmachine::Dispatcher do let(:dispatcher) { Webmachine.application.dispatcher } - let(:request) { Webmachine::Request.new("GET", URI.parse("http://localhost:8080/"), Webmachine::Headers["accept" => "*/*"], "") } + let(:request) { Webmachine::Request.new('GET', URI.parse('http://localhost:8080/'), Webmachine::Headers['accept' => '*/*'], '') } + let(:request2) { Webmachine::Request.new('GET', URI.parse('http://localhost:8080/hello/bob.html'), Webmachine::Headers['accept' => '*/*'], '') } let(:response) { Webmachine::Response.new } let(:resource) do Class.new(Webmachine::Resource) do - def to_html; "hello world!"; end + def to_html + 'hello world!' + end end end let(:resource2) do Class.new(Webmachine::Resource) do - def to_html; "goodbye, cruel world"; end + def to_html + 'goodbye, cruel world' + end end end - let(:fsm){ double } + let(:resource3) do + Class.new(Webmachine::Resource) do + def to_html + name, format = request.path_info[:captures] + "Hello #{name} with #{format}" + end + end + end + let(:fsm) { double } before { dispatcher.reset } - it "should add routes from a block" do + it 'should add routes from a block' do _resource = resource expect(Webmachine.routes do add [:*], _resource @@ -26,27 +39,33 @@ def to_html; "goodbye, cruel world"; end expect(dispatcher.routes.size).to eq(1) end - it "should add routes" do + it 'should add routes' do expect { dispatcher.add_route [:*], resource }.to_not raise_error end - it "should have add_route return the newly created route" do + it 'should have add_route return the newly created route' do route = dispatcher.add_route [:*], resource expect(route).to be_instance_of Webmachine::Dispatcher::Route end - it "should route to the proper resource" do - dispatcher.add_route ["goodbye"], resource2 + it 'should route to the proper resource' do + dispatcher.add_route ['goodbye'], resource2 dispatcher.add_route [:*], resource expect(Webmachine::Decision::FSM).to receive(:new).with(instance_of(resource), request, response).and_return(fsm) expect(fsm).to receive(:run) dispatcher.dispatch(request, response) end + it 'should handle regex path segments in route definition' do + dispatcher.add_route ['hello', /(.*)\.(.*)/], resource3 + expect(Webmachine::Decision::FSM).to receive(:new).with(instance_of(resource3), request2, response).and_return(fsm) + expect(fsm).to receive(:run) + dispatcher.dispatch(request2, response) + end - it "should apply route to request before creating the resource" do - route = dispatcher.add_route [:*], resource + it 'should apply route to request before creating the resource' do + route = dispatcher.add_route [:*], resource applied = false expect(route).to receive(:apply) { applied = true } @@ -58,32 +77,32 @@ def to_html; "goodbye, cruel world"; end dispatcher.dispatch(request, response) end - it "should add routes with guards" do - dispatcher.add [], lambda {|req| req.method == "POST" }, resource + it 'should add routes with guards' do + dispatcher.add [], lambda { |req| req.method == 'POST' }, resource dispatcher.add [:*], resource2 do |req| !req.query.empty? end - request.uri.query = "?foo=bar" + request.uri.query = '?foo=bar' expect(dispatcher.routes.size).to eq(2) expect(Webmachine::Decision::FSM).to receive(:new).with(instance_of(resource2), request, response).and_return(fsm) expect(fsm).to receive(:run) dispatcher.dispatch(request, response) end - it "should respond with a valid resource for a 404" do + it 'should respond with a valid resource for a 404' do dispatcher.dispatch(request, response) - expect(response.code).to eq(404) + expect(response.code).to eq(404) expect(response.body).to_not be_empty - expect(response.headers).to have_key('Content-Length') - expect(response.headers).to have_key('Date') + expect(response.headers).to have_key('Content-Length') + expect(response.headers).to have_key('Date') end - it "should respond with a valid resource for a 404 with a custom Accept header" do - request.headers['Accept'] = "application/json" + it 'should respond with a valid resource for a 404 with a custom Accept header' do + request.headers['Accept'] = 'application/json' dispatcher.dispatch(request, response) - expect(response.code).to eq(404) + expect(response.code).to eq(404) expect(response.body).to_not be_empty - expect(response.headers).to have_key('Content-Length') - expect(response.headers).to have_key('Date') + expect(response.headers).to have_key('Content-Length') + expect(response.headers).to have_key('Date') end end diff --git a/spec/webmachine/errors_spec.rb b/spec/webmachine/errors_spec.rb index 84b9c493..2b10ae59 100644 --- a/spec/webmachine/errors_spec.rb +++ b/spec/webmachine/errors_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe "Webmachine errors" do - describe ".render_error" do - it "sets the given response code on the response object" do - req = double('request', :method => 'GET').as_null_object +describe 'Webmachine errors' do + describe '.render_error' do + it 'sets the given response code on the response object' do + req = double('request', method: 'GET').as_null_object res = Webmachine::Response.new Webmachine.render_error(404, req, res) diff --git a/spec/webmachine/etags_spec.rb b/spec/webmachine/etags_spec.rb index c3266c03..e8e65629 100644 --- a/spec/webmachine/etags_spec.rb +++ b/spec/webmachine/etags_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Webmachine::ETag do - let(:etag_str){ '"deadbeef12345678"' } + let(:etag_str) { '"deadbeef12345678"' } let(:etag) { described_class.new etag_str } subject { etag } @@ -12,7 +12,7 @@ its(:etag) { should == '"deadbeef12345678"' } it { is_expected.to eq(described_class.new(etag_str.dup)) } - context "when the original etag is unquoted" do + context 'when the original etag is unquoted' do let(:etag_str) { 'deadbeef12345678' } it { is_expected.to eq(etag_str) } @@ -21,7 +21,7 @@ it { is_expected.to eq(described_class.new(etag_str.dup)) } end - context "when the original etag contains unbalanced quotes" do + context 'when the original etag contains unbalanced quotes' do let(:etag_str) { 'deadbeef"12345678' } it { is_expected.to eq(etag_str) } @@ -32,7 +32,7 @@ end describe Webmachine::WeakETag do - let(:strong_etag){ '"deadbeef12345678"' } + let(:strong_etag) { '"deadbeef12345678"' } let(:weak_etag) { described_class.new strong_etag } subject { weak_etag } @@ -43,7 +43,7 @@ its(:etag) { should == '"deadbeef12345678"' } it { is_expected.to eq(described_class.new(strong_etag.dup)) } - context "when the original etag is unquoted" do + context 'when the original etag is unquoted' do let(:strong_etag) { 'deadbeef12345678' } it { is_expected.to eq(strong_etag) } @@ -53,7 +53,7 @@ it { is_expected.to eq(described_class.new(strong_etag.dup)) } end - context "when the original etag contains unbalanced quotes" do + context 'when the original etag contains unbalanced quotes' do let(:strong_etag) { 'deadbeef"12345678' } it { is_expected.to eq(strong_etag) } @@ -63,7 +63,7 @@ it { is_expected.to eq(described_class.new(strong_etag.dup)) } end - context "when the original etag is already a weak tag" do + context 'when the original etag is already a weak tag' do let(:strong_etag) { 'W/"deadbeef12345678"' } it { is_expected.to eq(strong_etag) } diff --git a/spec/webmachine/events_spec.rb b/spec/webmachine/events_spec.rb index aa703476..889802be 100644 --- a/spec/webmachine/events_spec.rb +++ b/spec/webmachine/events_spec.rb @@ -1,54 +1,54 @@ require 'spec_helper' describe Webmachine::Events do - describe ".backend" do - it "defaults to AS::Notifications" do + describe '.backend' do + it 'defaults to AS::Notifications' do expect(described_class.backend).to be(AS::Notifications) end end - describe ".publish" do - it "calls the backend" do + describe '.publish' do + it 'calls the backend' do expect(described_class.backend).to receive(:publish).with('test.event', 1, 'two') described_class.publish('test.event', 1, 'two') end end - describe ".instrument" do - it "calls the backend" do + describe '.instrument' do + it 'calls the backend' do expect(described_class.backend).to receive(:instrument).with( 'test.event', {} ).and_yield - described_class.instrument('test.event') { } + described_class.instrument('test.event') {} end end - describe ".subscribe" do - it "calls the backend" do + describe '.subscribe' do + it 'calls the backend' do expect(described_class.backend).to receive(:subscribe).with( 'test.event' ).and_yield - described_class.subscribe('test.event') { } + described_class.subscribe('test.event') {} end end - describe ".subscribed" do - it "calls the backend" do - callback = Proc.new { } + describe '.subscribed' do + it 'calls the backend' do + callback = proc {} expect(described_class.backend).to receive(:subscribed).with( callback, 'test.event' ).and_yield - described_class.subscribed(callback, 'test.event') { } + described_class.subscribed(callback, 'test.event') {} end end - describe ".unsubscribe" do - it "calls the backend" do - subscriber = described_class.subscribe('test.event') { } + describe '.unsubscribe' do + it 'calls the backend' do + subscriber = described_class.subscribe('test.event') {} expect(described_class.backend).to receive(:unsubscribe).with(subscriber) diff --git a/spec/webmachine/headers_spec.rb b/spec/webmachine/headers_spec.rb index bc2c497c..fe1107ae 100644 --- a/spec/webmachine/headers_spec.rb +++ b/spec/webmachine/headers_spec.rb @@ -1,22 +1,22 @@ require 'spec_helper' describe Webmachine::Headers do - it "should set and access values insensitive to case" do - subject['Content-TYPE'] = "text/plain" + it 'should set and access values insensitive to case' do + subject['Content-TYPE'] = 'text/plain' expect(subject['CONTENT-TYPE']).to eq('text/plain') expect(subject.delete('CoNtEnT-tYpE')).to eq('text/plain') end - describe "#from_cgi" do - it "should understand the Content-Length header" do - headers = described_class.from_cgi("CONTENT_LENGTH" => 14) - expect(headers["content-length"]).to eq(14) + describe '#from_cgi' do + it 'should understand the Content-Length header' do + headers = described_class.from_cgi('CONTENT_LENGTH' => 14) + expect(headers['content-length']).to eq(14) end end - describe ".[]" do + describe '.[]' do context "Webmachine::Headers['Content-Type', 'application/json']" do - it "creates a hash with lowercase keys" do + it 'creates a hash with lowercase keys' do headers = described_class[ 'Content-Type', 'application/json', 'Accept', 'application/json' @@ -30,7 +30,7 @@ end context "Webmachine::Headers[[['Content-Type', 'application/json']]]" do - it "creates a hash with lowercase keys" do + it 'creates a hash with lowercase keys' do headers = described_class[ [ ['Content-Type', 'application/json'], @@ -46,7 +46,7 @@ end context "Webmachine::Headers['Content-Type' => 'application/json']" do - it "creates a hash with lowercase keys" do + it 'creates a hash with lowercase keys' do headers = described_class[ 'Content-Type' => 'application/json', 'Accept' => 'application/json' @@ -60,39 +60,39 @@ end end - describe "#fetch" do + describe '#fetch' do subject { described_class['Content-Type' => 'application/json'] } - it "returns the value for the given key" do + it 'returns the value for the given key' do expect(subject.fetch('conTent-tYpe')).to eq('application/json') end - context "acessing a missing key" do - it "raises an IndexError" do + context 'acessing a missing key' do + it 'raises an IndexError' do expect { subject.fetch('accept') }.to raise_error(IndexError) end - context "and a default value given" do - it "returns the default value if the key does not exist" do + context 'and a default value given' do + it 'returns the default value if the key does not exist' do expect(subject.fetch('accept', 'text/html')).to eq('text/html') end end - context "and a block given" do + context 'and a block given' do it "passes the value to the block and returns the block's result" do - expect(subject.fetch('access') {|k| "#{k} not found"}).to eq('access not found') + expect(subject.fetch('access') { |k| "#{k} not found" }).to eq('access not found') end end end end - context "filtering with #grep" do - subject { described_class["content-type" => "text/plain", "etag" => '"abcdef1234567890"'] } - it "should filter keys by the given pattern" do - expect(subject.grep(/content/i)).to include("content-type") + context 'filtering with #grep' do + subject { described_class['content-type' => 'text/plain', 'etag' => '"abcdef1234567890"'] } + it 'should filter keys by the given pattern' do + expect(subject.grep(/content/i)).to include('content-type') end - it "should return a Headers instance" do + it 'should return a Headers instance' do expect(subject.grep(/etag/i)).to be_instance_of(described_class) end end diff --git a/spec/webmachine/media_type_spec.rb b/spec/webmachine/media_type_spec.rb index 9d7b7598..e9cc4144 100644 --- a/spec/webmachine/media_type_spec.rb +++ b/spec/webmachine/media_type_spec.rb @@ -1,85 +1,85 @@ require 'spec_helper' describe Webmachine::MediaType do - let(:raw_type){ "application/xml;charset=UTF-8" } - subject { described_class.new("application/xml", {"charset" => "UTF-8"}) } + let(:raw_type) { 'application/xml;charset=UTF-8' } + subject { described_class.new('application/xml', {'charset' => 'UTF-8'}) } - context "equivalence" do + context 'equivalence' do it { is_expected.to eq(raw_type) } it { is_expected.to eq(described_class.parse(raw_type)) } end - context "when it is the wildcard type" do - subject { described_class.new("*/*") } + context 'when it is the wildcard type' do + subject { described_class.new('*/*') } it { is_expected.to be_matches_all } end - context "parsing a type" do - it "should return MediaTypes untouched" do + context 'parsing a type' do + it 'should return MediaTypes untouched' do expect(described_class.parse(subject)).to equal(subject) end - it "should parse a String" do + it 'should parse a String' do type = described_class.parse(raw_type) expect(type).to be_kind_of(described_class) - expect(type.type).to eq("application/xml") - expect(type.params).to eq({"charset" => "UTF-8"}) + expect(type.type).to eq('application/xml') + expect(type.params).to eq({'charset' => 'UTF-8'}) end - it "should parse a type/params pair" do - type = described_class.parse(["application/xml", {"charset" => "UTF-8"}]) + it 'should parse a type/params pair' do + type = described_class.parse(['application/xml', {'charset' => 'UTF-8'}]) expect(type).to be_kind_of(described_class) - expect(type.type).to eq("application/xml") - expect(type.params).to eq({"charset" => "UTF-8"}) + expect(type.type).to eq('application/xml') + expect(type.params).to eq({'charset' => 'UTF-8'}) end - it "should parse a type/params pair where the type has some params in the string" do - type = described_class.parse(["application/xml;version=1", {"charset" => "UTF-8"}]) + it 'should parse a type/params pair where the type has some params in the string' do + type = described_class.parse(['application/xml;version=1', {'charset' => 'UTF-8'}]) expect(type).to be_kind_of(described_class) - expect(type.type).to eq("application/xml") - expect(type.params).to eq({"charset" => "UTF-8", "version" => "1"}) + expect(type.type).to eq('application/xml') + expect(type.params).to eq({'charset' => 'UTF-8', 'version' => '1'}) end - it "should parse a type/params pair with params and whitespace in the string" do - type = described_class.parse(["multipart/form-data; boundary=----------------------------2c46a7bec2b9", {"charset" => "UTF-8"}]) + it 'should parse a type/params pair with params and whitespace in the string' do + type = described_class.parse(['multipart/form-data; boundary=----------------------------2c46a7bec2b9', {'charset' => 'UTF-8'}]) expect(type).to be_kind_of(described_class) - expect(type.type).to eq("multipart/form-data") - expect(type.params).to eq({"boundary" => "----------------------------2c46a7bec2b9", "charset" => "UTF-8"}) + expect(type.type).to eq('multipart/form-data') + expect(type.params).to eq({'boundary' => '----------------------------2c46a7bec2b9', 'charset' => 'UTF-8'}) end - it "should parse a type/params pair where type has single-token params" do - type = described_class.parse(["text/html;q=1;rdfa", {"charset" => "UTF-8"}]) + it 'should parse a type/params pair where type has single-token params' do + type = described_class.parse(['text/html;q=1;rdfa', {'charset' => 'UTF-8'}]) expect(type).to be_kind_of(described_class) - expect(type.type).to eq("text/html") - expect(type.params).to eq({"q" => "1", "rdfa" => "", "charset" => "UTF-8"}) + expect(type.type).to eq('text/html') + expect(type.params).to eq({'q' => '1', 'rdfa' => '', 'charset' => 'UTF-8'}) end - it "should raise an error when given an invalid type/params pair" do + it 'should raise an error when given an invalid type/params pair' do expect { - described_class.parse([false, "blah"]) + described_class.parse([false, 'blah']) }.to raise_error(ArgumentError) end end - describe "matching a requested type" do - it { is_expected.to be_exact_match("application/xml;charset=UTF-8") } - it { is_expected.to be_exact_match("application/*;charset=UTF-8") } - it { is_expected.to be_exact_match("*/*;charset=UTF-8") } - it { is_expected.to be_exact_match("*;charset=UTF-8") } - it { is_expected.not_to be_exact_match("text/xml") } - it { is_expected.not_to be_exact_match("application/xml") } - it { is_expected.not_to be_exact_match("application/xml;version=1") } + describe 'matching a requested type' do + it { is_expected.to be_exact_match('application/xml;charset=UTF-8') } + it { is_expected.to be_exact_match('application/*;charset=UTF-8') } + it { is_expected.to be_exact_match('*/*;charset=UTF-8') } + it { is_expected.to be_exact_match('*;charset=UTF-8') } + it { is_expected.not_to be_exact_match('text/xml') } + it { is_expected.not_to be_exact_match('application/xml') } + it { is_expected.not_to be_exact_match('application/xml;version=1') } - it { is_expected.to be_type_matches("application/xml") } - it { is_expected.to be_type_matches("application/*") } - it { is_expected.to be_type_matches("*/*") } - it { is_expected.to be_type_matches("*") } - it { is_expected.not_to be_type_matches("text/xml") } - it { is_expected.not_to be_type_matches("text/*") } + it { is_expected.to be_type_matches('application/xml') } + it { is_expected.to be_type_matches('application/*') } + it { is_expected.to be_type_matches('*/*') } + it { is_expected.to be_type_matches('*') } + it { is_expected.not_to be_type_matches('text/xml') } + it { is_expected.not_to be_type_matches('text/*') } - it { is_expected.to be_params_match({}) } - it { is_expected.to be_params_match({"charset" => "UTF-8"}) } - it { is_expected.not_to be_params_match({"charset" => "Windows-1252"}) } - it { is_expected.not_to be_params_match({"version" => "3"}) } + it { is_expected.to be_params_match({}) } + it { is_expected.to be_params_match({'charset' => 'UTF-8'}) } + it { is_expected.not_to be_params_match({'charset' => 'Windows-1252'}) } + it { is_expected.not_to be_params_match({'version' => '3'}) } end end diff --git a/spec/webmachine/request_spec.rb b/spec/webmachine/request_spec.rb index ff08b344..28c93596 100644 --- a/spec/webmachine/request_spec.rb +++ b/spec/webmachine/request_spec.rb @@ -3,53 +3,64 @@ describe Webmachine::Request do subject { request } - let(:uri) { URI.parse("http://localhost:8080/some/resource") } - let(:http_method) { "GET" } - let(:headers) { Webmachine::Headers.new } - let(:body) { "" } - let(:request) { Webmachine::Request.new(http_method, uri, headers, body) } - - it "should provide access to the headers via brackets" do - subject.headers['Accept'] = "*/*" - expect(subject["accept"]).to eq("*/*") + let(:uri) { URI.parse('http://localhost:8080/some/resource') } + let(:http_method) { 'GET' } + let(:headers) { Webmachine::Headers.new } + let(:body) { '' } + let(:routing_tokens) { nil } + let(:base_uri) { nil } + let(:request) { Webmachine::Request.new(http_method, uri, headers, body, routing_tokens, base_uri) } + + it 'should provide access to the headers via brackets' do + subject.headers['Accept'] = '*/*' + expect(subject['accept']).to eq('*/*') end - it "should provide access to the cookies" do - subject.headers['Cookie'] = 'name=value;name2=value2'; - expect(subject.cookies).to eq({ 'name' => 'value', 'name2' => 'value2' }) + it 'should provide access to the cookies' do + subject.headers['Cookie'] = 'name=value;name2=value2' + expect(subject.cookies).to eq({'name' => 'value', 'name2' => 'value2'}) end - it "should handle cookies with extra whitespace" do - subject.headers['Cookie'] = 'name = value; name2 = value2'; - expect(subject.cookies).to eq({ 'name' => 'value', 'name2' => 'value2' }) + it 'should handle cookies with extra whitespace' do + subject.headers['Cookie'] = 'name = value; name2 = value2' + expect(subject.cookies).to eq({'name' => 'value', 'name2' => 'value2'}) end - it "should provide access to the headers via underscored methods" do - subject.headers["Accept-Encoding"] = "identity" - expect(subject.accept_encoding).to eq("identity") + it 'should provide access to the headers via underscored methods' do + subject.headers['Accept-Encoding'] = 'identity' + expect(subject.accept_encoding).to eq('identity') expect(subject.content_md5).to be_nil end - it "should calculate a base URI" do - expect(subject.base_uri).to eq(URI.parse("http://localhost:8080/")) + context 'base_uri' do + it 'should calculate a base URI' do + expect(subject.base_uri).to eq(URI.parse('http://localhost:8080/')) + end + + context 'when base_uri has been explicitly set' do + let(:base_uri) { URI.parse('http://localhost:8080/some_base_uri/here') } + it 'should use the provided base_uri' do + expect(subject.base_uri).to eq(URI.parse('http://localhost:8080/some_base_uri/here')) + end + end end - it "should provide a hash of query parameters" do - subject.uri.query = "foo=bar&baz=bam" - expect(subject.query).to eq({"foo" => "bar", "baz" => "bam"}) + it 'should provide a hash of query parameters' do + subject.uri.query = 'foo=bar&baz=bam' + expect(subject.query).to eq({'foo' => 'bar', 'baz' => 'bam'}) end - it "should handle = being encoded as a query value." do - subject.uri.query = "foo=bar%3D%3D" - expect(subject.query).to eq({ "foo" => "bar=="}) + it 'should handle = being encoded as a query value.' do + subject.uri.query = 'foo=bar%3D%3D' + expect(subject.query).to eq({'foo' => 'bar=='}) end it "should treat '+' characters in query parameters as spaces" do - subject.uri.query = "a%20b=foo+bar&c+d=baz%20quux" - expect(subject.query).to eq({"a b" => "foo bar", "c d" => "baz quux"}) + subject.uri.query = 'a%20b=foo+bar&c+d=baz%20quux' + expect(subject.query).to eq({'a b' => 'foo bar', 'c d' => 'baz quux'}) end - it "should handle a query parameter value of nil" do + it 'should handle a query parameter value of nil' do subject.uri.query = nil expect(subject.query).to eq({}) end @@ -57,38 +68,43 @@ describe '#has_body?' do let(:wreq) do Class.new { - def initialize(body); @body = body; end - def body; block_given? ? yield(@body) : @body; end + def initialize(body) + @body = body + end + + def body + block_given? ? yield(@body) : @body + end } end subject { request.has_body? } - context "when body is nil" do + context 'when body is nil' do let(:body) { nil } it { is_expected.to be(false) } end - context "when body is an empty string" do + context 'when body is an empty string' do let(:body) { '' } it { is_expected.to be(false) } end - context "when body is not empty" do + context 'when body is not empty' do let(:body) { 'foo' } it { is_expected.to be(true) } end - context "when body is an empty LazyRequestBody" do + context 'when body is an empty LazyRequestBody' do let(:body) { Webmachine::Adapters::LazyRequestBody.new(wreq.new('')) } it { is_expected.to be(false) } end - context "when body is a LazyRequestBody" do + context 'when body is a LazyRequestBody' do let(:body) { Webmachine::Adapters::LazyRequestBody.new(wreq.new('foo')) } it { is_expected.to be(true) } @@ -98,14 +114,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#https?' do subject { request.https? } - context "when the request was issued via HTTPS" do - let(:uri) { URI.parse("https://localhost.com:8080/some/resource") } + context 'when the request was issued via HTTPS' do + let(:uri) { URI.parse('https://localhost.com:8080/some/resource') } it { is_expected.to be(true) } end - context "when the request was not issued via HTTPS" do - let(:uri) { URI.parse("http://localhost.com:8080/some/resource") } + context 'when the request was not issued via HTTPS' do + let(:uri) { URI.parse('http://localhost.com:8080/some/resource') } it { is_expected.to be(false) } end @@ -114,14 +130,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#get?' do subject { request.get? } - context "when the request method is GET" do - let(:http_method) { "GET" } + context 'when the request method is GET' do + let(:http_method) { 'GET' } it { is_expected.to be(true) } end - context "when the request method is not GET" do - let(:http_method) { "POST" } + context 'when the request method is not GET' do + let(:http_method) { 'POST' } it { is_expected.to be(false) } end @@ -130,14 +146,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#head?' do subject { request.head? } - context "when the request method is HEAD" do - let(:http_method) { "HEAD" } + context 'when the request method is HEAD' do + let(:http_method) { 'HEAD' } it { is_expected.to be(true) } end - context "when the request method is not HEAD" do - let(:http_method) { "GET" } + context 'when the request method is not HEAD' do + let(:http_method) { 'GET' } it { is_expected.to be(false) } end @@ -146,14 +162,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#post?' do subject { request.post? } - context "when the request method is POST" do - let(:http_method) { "POST" } + context 'when the request method is POST' do + let(:http_method) { 'POST' } it { is_expected.to be(true) } end - context "when the request method is not POST" do - let(:http_method) { "GET" } + context 'when the request method is not POST' do + let(:http_method) { 'GET' } it { is_expected.to be(false) } end @@ -162,14 +178,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#put?' do subject { request.put? } - context "when the request method is PUT" do - let(:http_method) { "PUT" } + context 'when the request method is PUT' do + let(:http_method) { 'PUT' } it { is_expected.to be(true) } end - context "when the request method is not PUT" do - let(:http_method) { "GET" } + context 'when the request method is not PUT' do + let(:http_method) { 'GET' } it { is_expected.to be(false) } end @@ -178,14 +194,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#delete?' do subject { request.delete? } - context "when the request method is DELETE" do - let(:http_method) { "DELETE" } + context 'when the request method is DELETE' do + let(:http_method) { 'DELETE' } it { is_expected.to be(true) } end - context "when the request method is not DELETE" do - let(:http_method) { "GET" } + context 'when the request method is not DELETE' do + let(:http_method) { 'GET' } it { is_expected.to be(false) } end @@ -194,14 +210,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#trace?' do subject { request.trace? } - context "when the request method is TRACE" do - let(:http_method) { "TRACE" } + context 'when the request method is TRACE' do + let(:http_method) { 'TRACE' } it { is_expected.to be(true) } end - context "when the request method is not TRACE" do - let(:http_method) { "GET" } + context 'when the request method is not TRACE' do + let(:http_method) { 'GET' } it { is_expected.to be(false) } end @@ -210,14 +226,14 @@ def body; block_given? ? yield(@body) : @body; end describe '#connect?' do subject { request.connect? } - context "when the request method is CONNECT" do - let(:http_method) { "CONNECT" } + context 'when the request method is CONNECT' do + let(:http_method) { 'CONNECT' } it { is_expected.to be(true) } end - context "when the request method is not CONNECT" do - let(:http_method) { "GET" } + context 'when the request method is not CONNECT' do + let(:http_method) { 'GET' } it { is_expected.to be(false) } end @@ -226,17 +242,35 @@ def body; block_given? ? yield(@body) : @body; end describe '#options?' do subject { request.options? } - context "when the request method is OPTIONS" do - let(:http_method) { "OPTIONS" } + context 'when the request method is OPTIONS' do + let(:http_method) { 'OPTIONS' } it { is_expected.to be(true) } end - context "when the request method is not OPTIONS" do - let(:http_method) { "GET" } + context 'when the request method is not OPTIONS' do + let(:http_method) { 'GET' } it { is_expected.to be(false) } end end + describe '#routing_tokens' do + subject { request.routing_tokens } + + context "haven't been explicitly set" do + let(:routing_tokens) { nil } + it 'extracts the routing tokens from the path portion of the uri' do + expect(subject).to eq(['some', 'resource']) + end + end + + context 'have been explicitly set' do + let(:routing_tokens) { ['foo', 'bar'] } + + it 'uses the specified routing_tokens' do + expect(subject).to eq(['foo', 'bar']) + end + end + end end diff --git a/spec/webmachine/rescueable_exception_spec.rb b/spec/webmachine/rescueable_exception_spec.rb new file mode 100644 index 00000000..0147d2ed --- /dev/null +++ b/spec/webmachine/rescueable_exception_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' +RSpec.describe Webmachine::RescuableException do + before { described_class.default! } + + describe '.UNRESCUABLEs' do + specify 'returns an array of UNRESCUABLE exceptions' do + expect(described_class.UNRESCUABLEs).to eq(described_class::UNRESCUABLE_DEFAULTS) + end + + specify 'returns an array of UNRESCUABLE exceptions, with custom exceptions added' do + described_class.remove(Exception) + expect(described_class.UNRESCUABLEs).to eq(described_class::UNRESCUABLE_DEFAULTS.dup.concat([Exception])) + end + end +end diff --git a/spec/webmachine/resource/authentication_spec.rb b/spec/webmachine/resource/authentication_spec.rb index 8dad61bc..d7f89d24 100644 --- a/spec/webmachine/resource/authentication_spec.rb +++ b/spec/webmachine/resource/authentication_spec.rb @@ -3,63 +3,66 @@ describe Webmachine::Resource::Authentication do subject { Webmachine::Decision::FSM.new(resource, request, response) } let(:method) { 'GET' } - let(:uri) { URI.parse("http://localhost/") } + let(:uri) { URI.parse('http://localhost/') } let(:headers) { Webmachine::Headers.new } - let(:body) { "" } + let(:body) { '' } let(:request) { Webmachine::Request.new(method, uri, headers, body) } let(:response) { Webmachine::Response.new } def resource_with(&block) klass = Class.new(Webmachine::Resource) do - def to_html; "test resource"; end + def to_html + 'test resource' + end end - klass.module_eval(&block) if block_given? + klass.module_eval(&block) if block klass.new(request, response) end - describe "Basic authentication" do + describe 'Basic authentication' do let(:resource) do resource_with do include Webmachine::Resource::Authentication + attr_accessor :realm def is_authorized?(auth) - basic_auth(auth, @realm || "Webmachine") {|u,p| u == "webmachine" && p == "http" } + basic_auth(auth, @realm || 'Webmachine') { |u, p| u == 'webmachine' && p == 'http' } end end end - context "when no authorization is sent by the client" do - it "should reply with a 401 Unauthorized and a WWW-Authenticate header using Basic" do + context 'when no authorization is sent by the client' do + it 'should reply with a 401 Unauthorized and a WWW-Authenticate header using Basic' do subject.run expect(response.code).to eq(401) expect(response.headers['WWW-Authenticate']).to eq('Basic realm="Webmachine"') end - it "should use the specified realm in the WWW-Authenticate header" do - resource.realm = "My App" + it 'should use the specified realm in the WWW-Authenticate header' do + resource.realm = 'My App' subject.run expect(response.headers['WWW-Authenticate']).to eq('Basic realm="My App"') end end - context "when the client sends invalid authorization" do + context 'when the client sends invalid authorization' do before do - headers['Authorization'] = "Basic " + ["invalid:auth"].pack('m*').chomp + headers['Authorization'] = 'Basic ' + ['invalid:auth'].pack('m*').chomp end - it "should reply with a 401 Unauthorized and a WWW-Authenticate header using Basic" do + it 'should reply with a 401 Unauthorized and a WWW-Authenticate header using Basic' do subject.run expect(response.code).to eq(401) expect(response.headers['WWW-Authenticate']).to eq('Basic realm="Webmachine"') end end - context "when the client sends valid authorization" do + context 'when the client sends valid authorization' do before do - headers['Authorization'] = "Basic " + ["webmachine:http"].pack('m*').chomp + headers['Authorization'] = 'Basic ' + ['webmachine:http'].pack('m*').chomp end - it "should not reply with 401 Unauthorized" do + it 'should not reply with 401 Unauthorized' do subject.run expect(response.code).not_to eq(401) end diff --git a/spec/webmachine/response_spec.rb b/spec/webmachine/response_spec.rb index f82ddf5b..4a81e060 100644 --- a/spec/webmachine/response_spec.rb +++ b/spec/webmachine/response_spec.rb @@ -1,44 +1,49 @@ require 'spec_helper' describe Webmachine::Response do - - it "should have sane default values" do + it 'should have sane default values' do expect(subject.code).to eq(200) expect(subject.is_redirect?).to be(false) expect(subject.headers).to be_empty end - describe "a redirected response" do - let(:redirect_url) { "/" } + describe 'a redirected response' do + let(:redirect_url) { '/' } before(:each) { subject.redirect_to redirect_url } its(:is_redirect?) { should be(true) } - it "should have a proper Location header" do - expect(subject.headers["Location"]).to eq(redirect_url) + it 'should have a proper Location header' do + expect(subject.headers['Location']).to eq(redirect_url) end end - describe "setting a cookie" do - let(:cookie) { "monster" } - let(:cookie_value) { "mash" } + describe 'setting a cookie' do + let(:cookie) { 'monster' } + let(:cookie_value) { 'mash' } before(:each) { subject.set_cookie(cookie, cookie_value) } - it "should have a proper Set-Cookie header" do - expect(subject.headers["Set-Cookie"]).to include "monster=mash" + it 'should have a proper Set-Cookie header' do + expect(subject.headers['Set-Cookie']).to include 'monster=mash' end - describe "setting multiple cookies" do - let(:cookie2) { "rodeo" } - let(:cookie2_value) { "clown" } - before(:each) { subject.set_cookie(cookie2, cookie2_value) } + describe 'setting multiple cookies' do + let(:cookie2) { 'rodeo' } + let(:cookie2_value) { 'clown' } + let(:cookie3) { 'color' } + let(:cookie3_value) { 'blue' } + before(:each) do + subject.set_cookie(cookie2, cookie2_value) + subject.set_cookie(cookie3, cookie3_value) + end - it "should have a proper Set-Cookie header" do - expect(subject.headers["Set-Cookie"]).to be_a Array - expect(subject.headers["Set-Cookie"]).to include "rodeo=clown" - expect(subject.headers["Set-Cookie"]).to include "monster=mash" + it 'should have a proper Set-Cookie header' do + expect(subject.headers['Set-Cookie']).to be_a Array + expect(subject.headers['Set-Cookie']).to include 'rodeo=clown' + expect(subject.headers['Set-Cookie']).to include 'monster=mash' + expect(subject.headers['Set-Cookie']).to include 'color=blue' end end end diff --git a/spec/webmachine/trace/fsm_spec.rb b/spec/webmachine/trace/fsm_spec.rb index 0ecc8a29..6e7a6a57 100644 --- a/spec/webmachine/trace/fsm_spec.rb +++ b/spec/webmachine/trace/fsm_spec.rb @@ -1,34 +1,34 @@ require 'spec_helper' describe Webmachine::Trace::FSM do - include_context "default resource" + include_context 'default resource' subject { Webmachine::Decision::FSM.new(resource, request, response) } before { Webmachine::Trace.trace_store = :memory } - context "when tracing is enabled" do + context 'when tracing is enabled' do before { allow(Webmachine::Trace).to receive(:trace?).and_return(true) } - it "proxies the resource" do + it 'proxies the resource' do expect(subject.resource).to be_kind_of(Webmachine::Trace::ResourceProxy) end - it "records a trace" do + it 'records a trace' do subject.run expect(response.trace).to_not be_empty expect(Webmachine::Trace.traces.size).to eq(1) end - it "commits the trace to separate storage when the request has finished processing" do + it 'commits the trace to separate storage when the request has finished processing' do expect(Webmachine::Trace).to receive(:record).with(subject.resource.object_id.to_s, response.trace).and_return(true) subject.run end end - context "when tracing is disabled" do + context 'when tracing is disabled' do before { allow(Webmachine::Trace).to receive(:trace?).and_return(false) } - it "leaves no trace" do + it 'leaves no trace' do subject.run expect(response.trace).to be_empty expect(Webmachine::Trace.traces).to be_empty diff --git a/spec/webmachine/trace/resource_proxy_spec.rb b/spec/webmachine/trace/resource_proxy_spec.rb index 4d87bf32..86e9cc3f 100644 --- a/spec/webmachine/trace/resource_proxy_spec.rb +++ b/spec/webmachine/trace/resource_proxy_spec.rb @@ -2,33 +2,32 @@ require 'webmachine/trace/resource_proxy' describe Webmachine::Trace::ResourceProxy do - include_context "default resource" + include_context 'default resource' subject { described_class.new(resource) } - it "duck-types all callback methods" do + it 'duck-types all callback methods' do Webmachine::Resource::Callbacks.instance_methods(false).each do |m| expect(subject).to respond_to(m) end end - it "logs invocations of callbacks" do + it 'logs invocations of callbacks' do subject.generate_etag - expect(response.trace).to eq([{:type => :attempt, :name => "(default)#generate_etag"}, - {:type => :result, :value => nil}]) - + expect(response.trace).to eq([{type: :attempt, name: '(default)#generate_etag'}, + {type: :result, value: nil}]) end - it "logs invocations of body-producing methods" do - expect(subject.content_types_provided).to eq([["text/html", :to_html]]) + it 'logs invocations of body-producing methods' do + expect(subject.content_types_provided).to eq([['text/html', :to_html]]) subject.to_html expect(response.trace[-2][:type]).to eq(:attempt) expect(response.trace[-2][:name]).to match(/to_html$/) - expect(response.trace[-2][:source]).to include("spec_helper.rb") if response.trace[-2][:source] - expect(response.trace[-1]).to eq({:type => :result, :value => "Hello, world!"}) + expect(response.trace[-2][:source]).to include('spec_helper.rb') if response.trace[-2][:source] + expect(response.trace[-1]).to eq({type: :result, value: 'Hello, world!'}) end - it "sets the trace id header when the request has finished processing" do + it 'sets the trace id header when the request has finished processing' do subject.finish_request - expect(response.headers["X-Webmachine-Trace-Id"]).to eq(subject.object_id.to_s) + expect(response.headers['X-Webmachine-Trace-Id']).to eq(subject.object_id.to_s) end end diff --git a/spec/webmachine/trace/trace_store_spec.rb b/spec/webmachine/trace/trace_store_spec.rb index 324d80fa..9b4692e3 100644 --- a/spec/webmachine/trace/trace_store_spec.rb +++ b/spec/webmachine/trace/trace_store_spec.rb @@ -1,29 +1,29 @@ require 'spec_helper' require 'fileutils' -shared_examples_for "trace storage" do +shared_examples_for 'trace storage' do it { is_expected.to respond_to(:[]=) } it { is_expected.to respond_to(:keys) } it { is_expected.to respond_to(:fetch) } - it "stores a trace" do - subject["foo"] = [:bar] - expect(subject.fetch("foo")).to eq([:bar]) + it 'stores a trace' do + subject['foo'] = [:bar] + expect(subject.fetch('foo')).to eq([:bar]) end - it "lists a stored trace in the keys" do - subject["foo"] = [:bar] - expect(subject.keys).to eq(["foo"]) + it 'lists a stored trace in the keys' do + subject['foo'] = [:bar] + expect(subject.keys).to eq(['foo']) end end describe Webmachine::Trace::PStoreTraceStore do - subject { described_class.new("./wmtrace") } - after { FileUtils.rm_rf("./wmtrace") } - it_behaves_like "trace storage" + subject { described_class.new('./wmtrace') } + after { FileUtils.rm_rf('./wmtrace') } + it_behaves_like 'trace storage' end -describe "Webmachine::Trace :memory Trace Store (Hash)" do - subject { Hash.new } - it_behaves_like "trace storage" +describe 'Webmachine::Trace :memory Trace Store (Hash)' do + subject { {} } + it_behaves_like 'trace storage' end diff --git a/spec/webmachine/trace_spec.rb b/spec/webmachine/trace_spec.rb index 346e8d27..d855a34d 100644 --- a/spec/webmachine/trace_spec.rb +++ b/spec/webmachine/trace_spec.rb @@ -3,13 +3,13 @@ describe Webmachine::Trace do subject { described_class } - context "determining whether the resource should be traced" do - include_context "default resource" - it "does not trace by default" do + context 'determining whether the resource should be traced' do + include_context 'default resource' + it 'does not trace by default' do expect(subject.trace?(resource)).to be(false) end - it "traces when the resource enables tracing" do + it 'traces when the resource enables tracing' do expect(resource).to receive(:trace?).and_return(true) expect(subject.trace?(resource)).to be(true) end diff --git a/webmachine.gemspec b/webmachine.gemspec index 07b87e7f..cbc61ac0 100644 --- a/webmachine.gemspec +++ b/webmachine.gemspec @@ -1,25 +1,36 @@ -$:.push File.expand_path("../lib", __FILE__) -require 'webmachine/version' +require_relative 'lib/webmachine/version' Gem::Specification.new do |gem| - gem.name = "webmachine" + gem.name = 'webmachine' gem.version = Webmachine::VERSION - gem.summary = %Q{webmachine is a toolkit for building HTTP applications,} + gem.summary = %(webmachine is a toolkit for building HTTP applications,) gem.description = <<-DESC.gsub(/\s+/, ' ') webmachine is a toolkit for building HTTP applications in a declarative fashion, that avoids the confusion of going through a CGI-style interface like Rack. It is strongly influenced by the original Erlang project of the same name and shares its opinionated nature about HTTP. DESC - gem.homepage = "http://github.com/seancribbs/webmachine-ruby" - gem.authors = ["Sean Cribbs"] - gem.email = ["sean@basho.com"] - gem.license = "Apache 2.0" + gem.homepage = 'https://github.com/webmachine/webmachine-ruby' + gem.authors = ['Sean Cribbs'] + gem.email = ['sean@basho.com'] + gem.license = 'Apache-2.0' - gem.add_runtime_dependency(%q, [">= 0.4.0"]) - gem.add_runtime_dependency(%q) - gem.add_runtime_dependency(%q, ["~> 1.0"]) + gem.metadata['allowed_push_host'] = 'https://rubygems.org' + gem.metadata['bug_tracker_uri'] = "#{gem.homepage}/issues" + gem.metadata['changelog_uri'] = "#{gem.homepage}/blob/HEAD/CHANGELOG.md" + gem.metadata['documentation_uri'] = "https://www.rubydoc.info/gems/webmachine/#{gem.version}" + gem.metadata['homepage_uri'] = gem.homepage + gem.metadata['source_code_uri'] = gem.homepage + gem.metadata['wiki_uri'] = "#{gem.homepage}/wiki" - ignores = File.read(".gitignore").split(/\r?\n/).reject{ |f| f =~ /^(#.+|\s*)$/ }.map {|f| Dir[f] }.flatten - gem.files = (Dir['**/*','.gitignore'] - ignores).reject {|f| !File.file?(f) } - gem.test_files = (Dir['spec/**/*','features/**/*','.gitignore'] - ignores).reject {|f| !File.file?(f) } + gem.required_ruby_version = '>= 2.6.0' + + gem.add_runtime_dependency('as-notifications', ['>= 1.0.2', '< 2.0']) + gem.add_runtime_dependency('base64') + gem.add_runtime_dependency('i18n', ['>= 0.4.0']) + gem.add_runtime_dependency('multi_json') + + ignores = File.read('.gitignore').split(/\r?\n/).reject { |f| f =~ /^(#.+|\s*)$/ }.map { |f| Dir[f] }.flatten + gem.files = (Dir['**/*', '.gitignore'] - ignores).reject do |f| + !File.file?(f) || f.start_with?(*%w[. Gemfile RELEASING Rakefile doc/ memory_test pkg/ spec/ vendor/ webmachine.gemspec]) + end end