Merge commit 'bmizerany/master'

This commit is contained in:
Markus Prinz 2008-05-19 09:20:44 +02:00
commit b73450982a
7 changed files with 539 additions and 145 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ doc
pkg
*.log
.DS_Store
Manifest
Manifest
x.rb

View File

@ -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<pre>
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

View File

@ -10,4 +10,4 @@ Sinatra::Application.default_options.merge!(
:logging => false
)
Sinatra.application.options = nil
Sinatra.application = nil

View File

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

View File

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

66
test/pipeline_test.rb Normal file
View File

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

View File

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