diff --git a/.gitignore b/.gitignore index 1eabcf7a..c3301e7b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ doc pkg *.log .DS_Store -Manifest \ No newline at end of file +Manifest +x.rb \ No newline at end of file diff --git a/lib/sinatra.rb b/lib/sinatra.rb index fccaa562..39a47dc8 100644 --- a/lib/sinatra.rb +++ b/lib/sinatra.rb @@ -96,19 +96,15 @@ module Sinatra def options application.options end - + def application - unless @app - @app = Application.new - Sinatra::Environment.setup! - end - @app + @app ||= Application.new end - + def application=(app) @app = app end - + def port application.options.port end @@ -116,14 +112,10 @@ module Sinatra def env application.options.env end - - def build_application - app = application - app = Rack::Session::Cookie.new(app) if Sinatra.options.sessions == true - app = Rack::CommonLogger.new(app) if Sinatra.options.logging == true - app - end - + + # Deprecated: use application instead of build_application. + alias :build_application :application + def server @server ||= case options.server when "mongrel" @@ -145,11 +137,10 @@ module Sinatra end def run - begin puts "== Sinatra has taken the stage on port #{port} for #{env} with backup by #{server.name}" require 'pp' - server.run(build_application, :Port => port) do |server| + server.run(application, :Port => port) do |server| trap(:INT) do server.stop puts "\n== Sinatra has ended his set (crowd applauds)" @@ -158,9 +149,8 @@ module Sinatra rescue Errno::EADDRINUSE => e puts "== Someone is already performing on port #{port}!" end - end - + class Event URI_CHAR = '[^/?:,\.]'.freeze unless defined?(URI_CHAR) @@ -830,17 +820,83 @@ module Sinatra end end - + + + # The Application class represents the top-level working area of a + # Sinatra app. It provides the DSL for defining various aspects of the + # application and implements a Rack compatible interface for dispatching + # requests. + # + # Many of the instance methods defined in this class (#get, #post, + # #put, #delete, #layout, #before, #error, #not_found, etc.) are + # available at top-level scope. When invoked from top-level, the + # messages are forwarded to the "default application" (accessible + # at Sinatra::application). class Application - - attr_reader :events, :errors, :templates, :filters - attr_reader :clearables, :reloading - - attr_writer :options - + + # Hash of event handlers with request method keys and + # arrays of potential handlers as values. + attr_reader :events + + # Hash of error handlers with error status codes as keys and + # handlers as values. + attr_reader :errors + + # Hash of template name mappings. + attr_reader :templates + + # Hash of filters with event name keys (:before) and arrays of + # handlers as values. + attr_reader :filters + + # Array of objects to clear during reload. The objects in this array + # must respond to :clear. + attr_reader :clearables + + # Object including open attribute methods for modifying Application + # configuration. + attr_reader :options + + # List of methods available from top-level scope. When invoked from + # top-level the method is forwarded to the default application + # (Sinatra::application). + FORWARD_METHODS = %w[ + get put post delete head template layout before error not_found + configures configure set_options set_option enable disable use + ] + + # Create a new Application with a default configuration taken + # from the default_options Hash. + # + # NOTE: A default Application is automatically created the first + # time any of Sinatra's DSL related methods is invoked so there + # is typically no need to create an instance explicitly. See + # Sinatra::application for more information. + def initialize + @reloading = false + @clearables = [ + @events = Hash.new { |hash, key| hash[key] = [] }, + @errors = Hash.new, + @filters = Hash.new { |hash, key| hash[key] = [] }, + @templates = Hash.new, + @middleware = [] + ] + @options = OpenStruct.new(self.class.default_options) + load_default_configuration! + end + + # Hash of default application configuration options. When a new + # Application is created, the #options object takes its initial values + # from here. + # + # Changes to the default_options Hash effect only Application objects + # created after the changes are made. For this reason, modifications to + # the default_options Hash typically occur at the very beginning of a + # file, before any DSL related functions are invoked. def self.default_options + return @default_options unless @default_options.nil? root = File.expand_path(File.dirname($0)) - @@default_options ||= { + @default_options = { :run => true, :port => 4567, :env => :development, @@ -848,19 +904,18 @@ module Sinatra :views => root + '/views', :public => root + '/public', :sessions => false, - :logging => true + :logging => true, + :raise_errors => false } - end - - def default_options - self.class.default_options + load_default_options_from_command_line! + @default_options end - - ## - # Load all options given on the command line + # Search ARGV for command line arguments and update the + # Sinatra::default_options Hash accordingly. This method is + # invoked the first time the default_options Hash is accessed. # NOTE: Ignores --name so unit/spec tests can run individually - def load_options! + def self.load_default_options_from_command_line! #:nodoc: require 'optparse' OptionParser.new do |op| op.on('-p port') { |port| default_options[:port] = port } @@ -870,39 +925,104 @@ module Sinatra end.parse!(ARGV.dup.select { |o| o !~ /--name/ }) end - # Called immediately after the application is initialized or reloaded to - # register default events. Events added here have dibs on requests since - # they appear first in the list. - def load_default_events! - events[:get] << Static.new + # Determine whether the application is in the process of being + # reloaded. + def reloading? + @reloading == true end - def initialize - @clearables = [ - @events = Hash.new { |hash, key| hash[key] = [] }, - @errors = Hash.new, - @filters = Hash.new { |hash, key| hash[key] = [] }, - @templates = Hash.new - ] - load_options! - load_default_events! + # Yield to the block for configuration if the current environment + # matches any included in the +envs+ list. Always yield to the block + # when no environment is specified. + # + # NOTE: configuration blocks are not executed during reloads. + def configures(*envs, &b) + return if reloading? + yield self if envs.empty? || envs.include?(options.env) end - def define_event(method, path, options = {}, &b) - events[method] << event = Event.new(path, options, &b) - event + alias :configure :configures + + # When both +option+ and +value+ arguments are provided, set the option + # specified. With a single Hash argument, set all options specified in + # Hash. Options are available via the Application#options object. + # + # Setting individual options: + # set :port, 80 + # set :env, :production + # set :views, '/path/to/views' + # + # Setting multiple options: + # set :port => 80, + # :env => :production, + # :views => '/path/to/views' + # + def set(option, value=self) + if value == self && option.kind_of?(Hash) + option.each { |key,val| set(key, val) } + else + options.send("#{option}=", value) + end end - - def define_template(name=:layout, &b) - templates[name] = b + + alias :set_option :set + alias :set_options :set + + # Enable the options specified by setting their values to true. For + # example, to enable sessions and logging: + # enable :sessions, :logging + def enable(*opts) + opts.each { |key| set(key, true) } end - - def define_error(code, options = {}, &b) - errors[code] = Error.new(code, &b) + + # Disable the options specified by setting their values to false. For + # example, to disable logging and automatic run: + # disable :logging, :run + def disable(*opts) + opts.each { |key| set(key, false) } end - - def define_filter(type, &b) - filters[:before] << b + + # Define an event handler for the given request method and path + # spec. The block is executed when a request matches the method + # and spec. + # + # NOTE: The #get, #post, #put, and #delete helper methods should + # be used to define events when possible. + def event(method, path, options = {}, &b) + events[method].push(Event.new(path, options, &b)).last + end + + # Define an event handler for GET requests. + def get(path, options={}, &b) + event(:get, path, options, &b) + end + + # Define an event handler for POST requests. + def post(path, options={}, &b) + event(:post, path, options, &b) + end + + # Define an event handler for HEAD requests. + def head(path, options={}, &b) + event(:head, path, options, &b) + end + + # Define an event handler for PUT requests. + # + # NOTE: PUT events are triggered when the HTTP request method is + # PUT and also when the request method is POST and the body includes a + # "_method" parameter set to "PUT". + def put(path, options={}, &b) + event(:put, path, options, &b) + end + + # Define an event handler for DELETE requests. + # + # NOTE: DELETE events are triggered when the HTTP request method is + # DELETE and also when the request method is POST and the body includes a + # "_method" parameter set to "DELETE". + def delete(path, options={}, &b) + event(:delete, path, options, &b) end # Visits and invokes each handler registered for the +request_method+ in @@ -920,27 +1040,96 @@ module Sinatra errors[NotFound].invoke(request) end - def options - @options ||= OpenStruct.new(default_options) + + # Define a named template. The template may be referenced from + # event handlers by passing the name as a Symbol to rendering + # methods. The block is executed each time the template is rendered + # and the resulting object is passed to the template handler. + # + # The following example defines a HAML template named hello and + # invokes it from an event handler: + # + # template :hello do + # "h1 Hello World!" + # end + # + # get '/' do + # haml :hello + # end + # + def template(name, &b) + templates[name] = b end - + + # Define a layout template. + def layout(name=:layout, &b) + template(name, &b) + end + + # Define a custom error handler for the exception class +type+. The block + # is invoked when the specified exception type is raised from an error + # handler and can manipulate the response as needed: + # + # error MyCustomError do + # status 500 + # 'So what happened was...' + request.env['sinatra.error'].message + # end + # + # The Sinatra::ServerError handler is used by default when an exception + # occurs and no matching error handler is found. + def error(type=ServerError, options = {}, &b) + errors[type] = Error.new(type, &b) + end + + # Define a custom error handler for '404 Not Found' responses. This is a + # shorthand for: + # error NotFound do + # .. + # end + def not_found(options={}, &b) + error NotFound, options, &b + end + + # Define a request filter. When +type+ is +:before+, execute the block + # in the context of each request before matching event handlers. + def filter(type, &b) + filters[type] << b + end + + # Invoke the block in the context of each request before invoking + # matching event handlers. + def before(&b) + filter :before, &b + end + def development? options.env == :development end + # Clear all events, templates, filters, and error handlers + # and then reload the application source file. This occurs + # automatically before each request is processed in development. def reload! clearables.each(&:clear) - Environment.setup! + load_default_configuration! + @pipeline = nil @reloading = true - load_default_events! Kernel.load $0 @reloading = false end - + + # Determine whether the application is in the process of being + # reloaded. + def reloading? + @reloading == true + end + + # Mutex instance used for thread synchronization. def mutex @@mutex ||= Mutex.new end - + + # Yield to the block with thread synchronization def run_safely if options.mutex mutex.synchronize { yield } @@ -948,16 +1137,74 @@ module Sinatra yield end end - + + # Add a piece of Rack middleware to the pipeline leading to the + # application. + def use(klass, *args, &block) + fail "#{klass} must respond to 'new'" unless klass.respond_to?(:new) + @pipeline = nil + @middleware.push([ klass, args, block ]).last + end + + private + + # Rack middleware derived from current state of application options. + # These components are plumbed in at the very beginning of the + # pipeline. + def optional_middleware + [ + ([ Rack::CommonLogger, [], nil ] if options.logging), + ([ Rack::Session::Cookie, [], nil ] if options.sessions) + ].compact + end + + # Rack middleware explicitly added to the application with #use. These + # components are plumbed into the pipeline downstream from + # #optional_middle. + def explicit_middleware + @middleware + end + + # All Rack middleware used to construct the pipeline. + def middleware + optional_middleware + explicit_middleware + end + + public + + # An assembled pipeline of Rack middleware that leads eventually to + # the Application#invoke method. The pipeline is built upon first + # access. Defining new middleware with Application#use or manipulating + # application options may cause the pipeline to be rebuilt. + def pipeline + @pipeline ||= + middleware.inject(method(:dispatch)) do |app,(klass,args,block)| + klass.new(app, *args, &block) + end + end + + # Rack compatible request invocation interface. def call(env) reload! if development? + pipeline.call(env) + end + + # Request invocation handler - called at the end of the Rack pipeline + # for each request. + # + # 1. Create Rack::Request, Rack::Response helper objects. + # 2. Lookup event handler based on request method and path. + # 3. Create new EventContext to house event handler evaluation. + # 4. Invoke each #before filter in context of EventContext object. + # 5. Invoke event handler in context of EventContext object. + # 6. Return response to Rack. + # + # See the Rack specification for detailed information on the + # +env+ argument and return value. + def dispatch(env) request = Rack::Request.new(env) result = lookup(request) - context = EventContext.new( - request, - Rack::Response.new, - result.params - ) + context = EventContext.new(request, Rack::Response.new, result.params) context.status(result.status) begin returned = run_safely do @@ -983,14 +1230,15 @@ module Sinatra context.body = body.kind_of?(String) ? [*body] : body context.finish end - - end - - - module Environment - extend self - - def setup! + + # Called immediately after the application is initialized or reloaded to + # register default events, templates, and error handlers. + def load_default_configuration! + + # The static event is always executed first. + events[:get] << Static.new + + # Default configuration for all environments. configure do error do raise request.env['sinatra.error'] if Sinatra.options.raise_errors @@ -1087,50 +1335,27 @@ end
end end end + + private :load_default_configuration! + end - + end -def get(path, options ={}, &b) - Sinatra.application.define_event(:get, path, options, &b) -end - -def post(path, options ={}, &b) - Sinatra.application.define_event(:post, path, options, &b) -end - -def put(path, options ={}, &b) - Sinatra.application.define_event(:put, path, options, &b) -end - -def delete(path, options ={}, &b) - Sinatra.application.define_event(:delete, path, options, &b) -end - -def before(&b) - Sinatra.application.define_filter(:before, &b) +# Delegate DSLish methods to the currently active Sinatra::Application +# instance. +Sinatra::Application::FORWARD_METHODS.each do |method| + eval(<<-EOS, binding, '(__DSL__)', 1) + def #{method}(*args, &b) + Sinatra.application.#{method}(*args, &b) + end + EOS end def helpers(&b) Sinatra::EventContext.class_eval(&b) end -def error(type = Sinatra::ServerError, options = {}, &b) - Sinatra.application.define_error(type, options, &b) -end - -def not_found(options = {}, &b) - Sinatra.application.define_error(Sinatra::NotFound, options, &b) -end - -def layout(name = :layout, &b) - Sinatra.application.define_template(name, &b) -end - -def template(name, &b) - Sinatra.application.define_template(name, &b) -end - def use_in_file_templates! require 'stringio' templates = IO.read(caller.first.split(':').first).split('__FILE__').last @@ -1146,22 +1371,6 @@ def use_in_file_templates! end end -def configures(*envs, &b) - yield if !Sinatra.application.reloading && - (envs.include?(Sinatra.application.options.env) || - envs.empty?) -end -alias :configure :configures - -def set_options(opts) - Sinatra::Application.default_options.merge!(opts) - Sinatra.application.options = nil -end - -def set_option(key, value) - set_options(key => value) -end - def mime(ext, type) Rack::File::MIME_TYPES[ext.to_s] = type end diff --git a/lib/sinatra/test/unit.rb b/lib/sinatra/test/unit.rb index 92cc9bd3..7b25cbc1 100644 --- a/lib/sinatra/test/unit.rb +++ b/lib/sinatra/test/unit.rb @@ -10,4 +10,4 @@ Sinatra::Application.default_options.merge!( :logging => false ) -Sinatra.application.options = nil +Sinatra.application = nil diff --git a/test/app_test.rb b/test/app_test.rb index c7d2e51b..cc94839a 100644 --- a/test/app_test.rb +++ b/test/app_test.rb @@ -5,7 +5,14 @@ context "Sinatra" do setup do Sinatra.application = nil end - + + specify "should put all DSL methods on (main)" do + object = Object.new + Sinatra::Application::FORWARD_METHODS.each do |method| + object.private_methods.should.include(method) + end + end + specify "should handle result of nil" do get '/' do nil diff --git a/test/application_test.rb b/test/application_test.rb index 20a195eb..57a67bbf 100644 --- a/test/application_test.rb +++ b/test/application_test.rb @@ -97,7 +97,53 @@ context "An app returns" do end end - + +context "Application#configure blocks" do + + setup do + Sinatra.application = nil + end + + specify "run when no environment specified" do + ref = false + configure { ref = true } + ref.should.equal true + end + + specify "run when matching environment specified" do + ref = false + configure(:test) { ref = true } + ref.should.equal true + end + + specify "do not run when no matching environment specified" do + configure(:foo) { flunk "block should not have been executed" } + configure(:development, :production, :foo) { flunk "block should not have been executed" } + end + + specify "accept multiple environments" do + ref = false + configure(:foo, :test, :bar) { ref = true } + ref.should.equal true + end + +end + +context "Default Application Configuration" do + + specify "includes 404 and 500 error handlers" do + Sinatra.application.errors.should.include(Sinatra::ServerError) + Sinatra.application.errors[Sinatra::ServerError].should.not.be.nil + Sinatra.application.errors.should.include(Sinatra::NotFound) + Sinatra.application.errors[Sinatra::NotFound].should.not.be.nil + end + + specify "includes Static event" do + assert Sinatra.application.events[:get].any? { |e| Sinatra::Static === e } + end + +end + context "Events in an app" do setup do @@ -173,3 +219,69 @@ context "Events in an app" do end +context "Options in an app" do + + setup do + Sinatra.application = nil + @app = Sinatra::application + end + + specify "can be set singly on app" do + @app.set :foo, 1234 + @app.options.foo.should.equal 1234 + end + + specify "can be set singly from top-level" do + set_option :foo, 1234 + @app.options.foo.should.equal 1234 + end + + specify "can be set multiply on app" do + @app.options.foo.should.be.nil + @app.set :foo => 1234, + :bar => 'hello, world' + @app.options.foo.should.equal 1234 + @app.options.bar.should.equal 'hello, world' + end + + specify "can be set multiply from top-level" do + @app.options.foo.should.be.nil + set_options :foo => 1234, + :bar => 'hello, world' + @app.options.foo.should.equal 1234 + @app.options.bar.should.equal 'hello, world' + end + + specify "can be enabled on app" do + @app.options.foo.should.be.nil + @app.enable :sessions, :foo, :bar + @app.options.sessions.should.equal true + @app.options.foo.should.equal true + @app.options.bar.should.equal true + end + + specify "can be enabled from top-level" do + @app.options.foo.should.be.nil + enable :sessions, :foo, :bar + @app.options.sessions.should.equal true + @app.options.foo.should.equal true + @app.options.bar.should.equal true + end + + specify "can be disabled on app" do + @app.options.foo.should.be.nil + @app.disable :sessions, :foo, :bar + @app.options.sessions.should.equal false + @app.options.foo.should.equal false + @app.options.bar.should.equal false + end + + specify "can be enabled from top-level" do + @app.options.foo.should.be.nil + disable :sessions, :foo, :bar + @app.options.sessions.should.equal false + @app.options.foo.should.equal false + @app.options.bar.should.equal false + end + +end diff --git a/test/pipeline_test.rb b/test/pipeline_test.rb new file mode 100644 index 00000000..67f08501 --- /dev/null +++ b/test/pipeline_test.rb @@ -0,0 +1,66 @@ +require File.dirname(__FILE__) + '/helper' + +class UpcaseMiddleware + def initialize(app, *args, &block) + @app = app + @args = args + @block = block + end + def call(env) + env['PATH_INFO'] = env['PATH_INFO'].to_s.upcase + @app.call(env) + end +end + +context "Middleware Pipelines" do + + setup do + Sinatra.application = nil + @app = Sinatra.application + end + + teardown do + Sinatra.application = nil + end + + specify "includes default middleware with options set" do + @app.set_options :sessions => true, :logging => true + @app.send(:optional_middleware).should.include([Rack::Session::Cookie, [], nil]) + @app.send(:optional_middleware).should.include([Rack::CommonLogger, [], nil]) + end + + specify "does not include default middleware with options unset" do + @app.set_options :sessions => false, :logging => false + @app.send(:optional_middleware).should.not.include([Rack::Session::Cookie, [], nil]) + @app.send(:optional_middleware).should.not.include([Rack::CommonLogger, [], nil]) + end + + specify "includes only optional middleware when no explicit middleware added" do + @app.set_options :sessions => true, :logging => true + @app.send(:middleware).should.equal @app.send(:optional_middleware) + end + + specify "should clear middleware before reload" do + @app.clearables.should.include(@app.send(:explicit_middleware)) + end + + specify "should add middleware with use" do + block = Proc.new { |env| } + @app.use UpcaseMiddleware + @app.use UpcaseMiddleware, "foo", "bar" + @app.use UpcaseMiddleware, "foo", "bar", &block + @app.send(:middleware).should.include([UpcaseMiddleware, [], nil]) + @app.send(:middleware).should.include([UpcaseMiddleware, ["foo", "bar"], nil]) + @app.send(:middleware).should.include([UpcaseMiddleware, ["foo", "bar"], block]) + end + + specify "should run middleware added with use" do + get('/foo') { "FAIL!" } + get('/FOO') { "PASS!" } + use UpcaseMiddleware + get_it '/foo' + should.be.ok + body.should.equal "PASS!" + end + +end diff --git a/test/sessions_test.rb b/test/sessions_test.rb index bda96e99..37912304 100644 --- a/test/sessions_test.rb +++ b/test/sessions_test.rb @@ -1,10 +1,10 @@ require File.dirname(__FILE__) + '/helper' context "Sessions" do - - specify "should be off by default" do - Sinatra.application = nil + setup { Sinatra.application = nil } + + specify "should be off by default" do get '/asdf' do session[:test] = true "asdf" @@ -18,10 +18,9 @@ context "Sessions" do assert ok? assert !include?('Set-Cookie') end - + specify "should be able to store data accross requests" do - set_options(:sessions => true) - Sinatra.application = nil + set_option :sessions, true get '/foo' do session[:test] = true