diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87d4be --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..16f9cdb --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--format documentation diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c80ee36 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "http://rubygems.org" + +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3437cdb --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 Nick Sieger + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9b4c89 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Rack::Chain + +TODO: Write a gem description + +## Installation + +Add this line to your application's Gemfile: + + gem 'rack-chain' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install rack-chain + +## Usage + +TODO: Write usage instructions here + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f2c6857 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require 'bundler/gem_tasks' + +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new + +task :default => :spec diff --git a/lib/rack-chain.rb b/lib/rack-chain.rb new file mode 100644 index 0000000..7f67923 --- /dev/null +++ b/lib/rack-chain.rb @@ -0,0 +1,2 @@ +require 'rack-chain/version' +require 'rack/chain' diff --git a/lib/rack-chain/version.rb b/lib/rack-chain/version.rb new file mode 100644 index 0000000..ff66412 --- /dev/null +++ b/lib/rack-chain/version.rb @@ -0,0 +1,5 @@ +module Rack + class Chain + VERSION = "0.0.1" + end +end diff --git a/lib/rack/chain.rb b/lib/rack/chain.rb new file mode 100644 index 0000000..f845fd5 --- /dev/null +++ b/lib/rack/chain.rb @@ -0,0 +1,56 @@ +require 'rack' +require 'rack/builder' +require 'fiber' + +class Rack::Chain + attr_reader :endpoint + attr_accessor :filters + + class Link + def initialize(to) + @to = to + end + + def call(env) + Fiber.new do + Fiber.yield @to.call(env) + end.resume + end + end + + def initialize(endpoint, filters = []) + @endpoint = endpoint + @filters = filters + end + + def call(env) + Link.new(filters.reverse.inject(endpoint) do |endpt,filter| + if filter.respond_to?(:[]) + filter[Link.new(endpt)] + else + filter.new(Link.new(endpt)) + end + end).call(env) + end + + # Include this module in Rack::Builder to make all apps use Rack::Chain. + # + # Alternatively, extend Rack::Builder in config.ru to use + # Rack::Chain for that particular config.ru. Example: + # + #
+ # require 'rack/chain' + # extend Rack::Chain::Linker + # use Middleware1 + # use Middleware2 + # run App + #+ module Linker + def to_app + app = @map ? generate_map(@run, @map) : @run + fail "missing run or map statement" unless app + Rack::Chain.new(app, @use) + end + end +end + diff --git a/rack-chain.gemspec b/rack-chain.gemspec new file mode 100644 index 0000000..194e73f --- /dev/null +++ b/rack-chain.gemspec @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/rack-chain/version', __FILE__) + +Gem::Specification.new do |gem| + gem.authors = ["Nick Sieger"] + gem.email = ["nick@nicksieger.com"] + gem.description = %q{TODO: Write a gem description} + gem.summary = %q{TODO: Write a gem summary} + gem.homepage = "" + + gem.files = `git ls-files`.split($\) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.name = "rack-chain" + gem.require_paths = ["lib"] + gem.version = Rack::Chain::VERSION + + gem.add_runtime_dependency "rack", [">= 1.4.0"] + gem.add_development_dependency "rspec", [">= 2.8.0"] +end diff --git a/spec/rack/chain_spec.rb b/spec/rack/chain_spec.rb new file mode 100644 index 0000000..56aece0 --- /dev/null +++ b/spec/rack/chain_spec.rb @@ -0,0 +1,76 @@ +require File.expand_path('../../spec_helper', __FILE__) + +describe Rack::Chain do + let(:env) { Hash.new } + + let(:app) { app_dummy.new } + + let(:chain) { Rack::Chain.new(app) } + + let(:filter_names) { %w(Foo Bar Baz) } + + let(:filters) { filter_names.map {|x| filter_dummy(x) } } + + let(:full_chain) { chain.tap {|c| c.filters += filters } } + + it "has an ordered list of filters" do + full_chain.filters.should == filters + end + + it "calls each filter in order" do + full_chain.call(env)[2].should == ["App", *filter_names.reverse] + end + + context Rack::Chain::Linker, "#to_app" do + let(:builder) do + Rack::Builder.new do + extend Rack::Chain::Linker + end.tap do |builder| + filters.each do |filter| + builder.use filter + end + builder.run app + end + end + + it "overrides Rack::Builder#to_app to create a Rack::Chain" do + builder.to_app.should be_instance_of(Rack::Chain) + end + + it "calls each filter in order" do + builder.to_app.call(env)[2].should == ["App", *filter_names.reverse] + end + end + + context "with a large set of middleware" do + let(:number_of_filters) { 100 } + + let(:filter_names) { (1..number_of_filters).map {|n| "Filter#{n}" } } + + let(:filters) { filter_names.map {|x| filter_dummy(x) { caller.size } } } + + # Skip the call stack size for the app + let(:app) { app_dummy { nil }.new } + + it "the call stack size stays constant" do + call_stack_sizes = full_chain.call(env)[2].compact + call_stack_sizes.max.should == call_stack_sizes.min + end + + context "but using normal Rack::Builder" do + let(:builder) do + Rack::Builder.new.tap {|builder| + filters.each do |filter| + builder.use filter + end + builder.run app + } + end + + it "the call stack size grows with each layer" do + call_stack_sizes = builder.call(env)[2].compact + call_stack_sizes.max.should > number_of_filters + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..c04dc3a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,60 @@ +require 'bundler/setup' +require 'rspec' +require 'rack/chain' + +module Rack::Chain::Fixtures + class FilterDummy + def initialize(app) + @app = app + end + + def call(env) + @app.call(env).tap do |result| + result[2] ||= [] + result[2] << body if body + end + end + + def body + self.class.name.split(/::/)[-1] + end + end + + class AppDummy + def call(env) + [200, {"Content-Type" => "text/plain"}, [body]] + end + + def body + self.class.name.split(/::/)[-1] + end + end + + module Dummies + end + + def reset_fixtures + Dummies.class_eval do + constants.each {|c| remove_const(c) } + end + end + + def filter_dummy(name = "Foo", &block) + Dummies.const_set(name, Class.new(FilterDummy) do + define_method(:body, &block) if block + end) + end + + def app_dummy(name = "App", &block) + Dummies.const_set(name, Class.new(AppDummy) do + define_method(:body, &block) if block + end) + end +end + +RSpec.configure do |config| + config.include Rack::Chain::Fixtures + config.after :each do + reset_fixtures + end +end