Split out Request handling into an Extension. Dup the Application (like Sinatra) when responding

This commit is contained in:
Thomas Reynolds 2012-05-01 13:11:42 -07:00
parent 8d6739b15b
commit 342044a01d
3 changed files with 381 additions and 303 deletions

View file

@ -1,7 +1,3 @@
# Built on Rack
require "rack"
require "rack/file"
# Using Tilt for templating
require "tilt"
@ -13,10 +9,6 @@ require "middleman-core/vendor/hooks-0.2.0/lib/hooks"
require "middleman-core/sitemap"
# Let's serve all HTML as UTF-8
::Rack::Mime::MIME_TYPES['.html'] = 'text/html;charset=utf8'
::Rack::Mime::MIME_TYPES['.htm'] = 'text/html;charset=utf8'
# Core Middleman Class
module Middleman
class Application
@ -30,97 +22,6 @@ module Middleman
define_hook :ready
class << self
# Reset Rack setup
#
# @private
def reset!
@app = nil
@prototype = nil
end
# The shared Rack instance being build
#
# @private
# @return [Rack::Builder]
def app
@app ||= Rack::Builder.new
end
# Get the static instance
#
# @private
# @return [Middleman::Application]
def inst(&block)
@inst ||= begin
mm = new(&block)
mm.run_hook :ready
mm
end
end
# Set the shared instance
#
# @private
# @param [Middleman::Application] inst
# @return [void]
def inst=(inst)
@inst = inst
end
# Return built Rack app
#
# @private
# @return [Rack::Builder]
def to_rack_app(&block)
inner_app = inst(&block)
(@middleware || []).each do |m|
app.use(m[0], *m[1], &m[2])
end
app.map("/") { run inner_app }
(@mappings || []).each do |m|
app.map(m[0], &m[1])
end
app
end
# Prototype app. Used in config.ru
#
# @private
# @return [Rack::Builder]
def prototype
@prototype ||= to_rack_app
end
# Call prototype, use in config.ru
#
# @private
def call(env)
prototype.call(env)
end
# Use Rack middleware
#
# @param [Class] Middleware
# @return [void]
def use(middleware, *args, &block)
@middleware ||= []
@middleware << [middleware, args, block]
end
# Add Rack App mapped to specific path
#
# @param [String] Path to map
# @return [void]
def map(map, &block)
@mappings ||= []
@mappings << [map, block]
end
# Mix-in helper methods. Accepts either a list of Modules
# and/or a block to be evaluated
# @return [void]
@ -150,6 +51,8 @@ module Middleman
end
end
delegate :helpers, :to => :"self.class"
# Set attributes (global variables)
#
# @param [Symbol] Name of the attribue
@ -217,6 +120,9 @@ module Middleman
# Activate custom features and extensions
include Middleman::CoreExtensions::Extensions
# Basic Rack Request Handling
register Middleman::CoreExtensions::Request
# Handle exceptions
register Middleman::CoreExtensions::ShowExceptions
@ -263,27 +169,6 @@ module Middleman
Middleman::Extensions.register(:asset_host) {
Middleman::Extensions::AssetHost }
# Backwards-compatibility with old request.path signature
attr :request
# Accessor for current path
# @return [String]
def current_path
Thread.current[:current_path]
end
# Set the current path
#
# @param [String] path The new current path
# @return [void]
def current_path=(path)
Thread.current[:current_path] = path
@request = ::Thor::CoreExt::HashWithIndifferentAccess.new({
:path => path,
:params => req ? ::Thor::CoreExt::HashWithIndifferentAccess.new(req.params) : {}
})
end
# Initialize the Middleman project
def initialize(&block)
# Current path defaults to nil, used in views.
@ -316,74 +201,6 @@ module Middleman
end
delegate :cache, :to => :"self.class"
# Rack env
def env
Thread.current[:env]
end
def env=(value)
Thread.current[:env] = value
end
# Rack request
# @return [Rack::Request]
def req
Thread.current[:req]
end
def req=(value)
Thread.current[:req] = value
end
# Rack response
# @return [Rack::Response]
def res
Thread.current[:res]
end
def res=(value)
Thread.current[:res] = value
end
# Rack Interface
#
# @private
# @param Rack environment
def call(env)
self.env = env
# Store environment, request and response for later
self.req = req = Rack::Request.new(env)
self.res = res = Rack::Response.new
if env["PATH_INFO"] == "/__middleman__"
if env["REQUEST_METHOD"] == "POST"
if req.params.has_key?("change")
self.files.did_change(req.params["change"])
elsif req.params.has_key?("delete")
self.files.did_delete(req.params["delete"])
end
end
res.status = 200
return res.finish
end
puts "== Request: #{env["PATH_INFO"]}" if logging?
# Catch :halt exceptions and use that response if given
catch(:halt) do
process_request(env, req, res)
res.status = 404
res.finish
end
end
# Halt the current request and return a response
#
# @private
# @param [String] Response value
def halt(response)
throw :halt, response
end
# Whether we're in development mode
# @return [Boolean] If we're in dev mode
def development?; environment == :development; end
@ -392,59 +209,6 @@ module Middleman
# @return [Boolean] If we're in build mode
def build?; environment == :build; end
# Core response method. We process the request, check with the sitemap,
# and return the correct file, response or status message.
#
# @private
def process_request(env, req, res)
start_time = Time.now
# Normalize the path and add index if we're looking at a directory
original_path = URI.decode(env["PATH_INFO"].dup)
if original_path.respond_to? :force_encoding
original_path.force_encoding('UTF-8')
end
request_path = full_path(original_path)
# Run before callbacks
run_hook :before
if original_path != request_path
# Get the resource object for this path
resource = sitemap.find_resource_by_destination_path(original_path)
end
# Get the resource object for this full path
resource ||= sitemap.find_resource_by_destination_path(request_path)
# Return 404 if not in sitemap
return not_found(res) unless resource && !resource.ignored?
# If this path is a static file, send it immediately
return send_file(resource.source_file, env, res) unless resource.template?
# Set the current path for use in helpers
self.current_path = request_path.dup
# Set a HTTP content type based on the request's extensions
content_type(res, resource.mime_type)
begin
# Write out the contents of the page
res.write resource.render
# Valid content is a 200 status
res.status = 200
rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e
res.write "Error: #{e.message}"
res.status = 500
end
# End the request
puts "== Finishing Request: #{self.current_path} (#{(Time.now - start_time).round(2)}s)" if logging?
halt res.finish
end
# Backwards compatibilty with old Sinatra template interface
#
# @return [Middleman::Application]
@ -474,66 +238,6 @@ module Middleman
end
end
# Add a new mime-type for a specific extension
#
# @param [Symbol] type File extension
# @param [String] value Mime type
# @return [void]
def mime_type(type, value=nil)
return type if type.nil? || type.to_s.include?('/')
type = ".#{type}" unless type.to_s[0] == ?.
return ::Rack::Mime.mime_type(type, nil) unless value
::Rack::Mime::MIME_TYPES[type] = value
end
protected
# Halt request and return 404
def not_found(res)
res.status == 404
res.write "<html><body><h1>File Not Found</h1><p>#{@request_path}</p></body>"
res.finish
end
delegate :helpers, :use, :map, :to => :"self.class"
# Immediately send static file
#
# @param [String] path File to send
def send_file(path, env, res)
extension = File.extname(path)
matched_mime = mime_type(extension)
matched_mime = "application/octet-stream" if matched_mime.nil?
content_type res, matched_mime
file = ::Rack::File.new nil
file.path = path
response = file.serving(env)
response[1]['Content-Encoding'] = 'gzip' if %w(.svgz).include?(extension)
halt response
end
# Set the content type for the current request
#
# @param [String] type Content type
# @param [Hash] params
# @return [void]
def content_type(res, type, params={})
return res['Content-Type'] unless type
default = params.delete :default
mime_type = mime_type(type) || default
throw "Unknown media type: %p" % type if mime_type.nil?
mime_type = mime_type.dup
unless params.include? :charset
params[:charset] = params.delete('charset') || "utf-8"
end
params.delete :charset if mime_type.include? 'charset'
unless params.empty?
mime_type << (mime_type.include?(';') ? ', ' : ';')
mime_type << params.map { |kv| kv.join('=') }.join(', ')
end
res['Content-Type'] = mime_type
end
end
class << self

View file

@ -0,0 +1,371 @@
# Base helper to manipulate asset paths
module Middleman
module CoreExtensions
module Request
# Extension registered
class << self
# @private
def registered(app)
# Built on Rack
require "rack"
require "rack/file"
# Let's serve all HTML as UTF-8
::Rack::Mime::MIME_TYPES['.html'] = 'text/html;charset=utf8'
::Rack::Mime::MIME_TYPES['.htm'] = 'text/html;charset=utf8'
app.extend ClassMethods
app.extend ServerMethods
# Include instance methods
app.send :include, InstanceMethods
end
alias :included :registered
end
module ClassMethods
# Reset Rack setup
#
# @private
def reset!
@app = nil
@prototype = nil
end
# The shared Rack instance being build
#
# @private
# @return [Rack::Builder]
def app
@app ||= ::Rack::Builder.new
end
# Get the static instance
#
# @private
# @return [Middleman::Application]
def inst(&block)
@inst ||= begin
mm = new(&block)
mm.run_hook :ready
mm
end
end
# Set the shared instance
#
# @private
# @param [Middleman::Application] inst
# @return [void]
def inst=(inst)
@inst = inst
end
# Return built Rack app
#
# @private
# @return [Rack::Builder]
def to_rack_app(&block)
inner_app = inst(&block)
(@middleware || []).each do |m|
app.use(m[0], *m[1], &m[2])
end
app.map("/") { run inner_app }
(@mappings || []).each do |m|
app.map(m[0], &m[1])
end
app
end
# Prototype app. Used in config.ru
#
# @private
# @return [Rack::Builder]
def prototype
@prototype ||= to_rack_app
end
# Call prototype, use in config.ru
#
# @private
def call(env)
prototype.call(env)
end
# Use Rack middleware
#
# @param [Class] Middleware
# @return [void]
def use(middleware, *args, &block)
@middleware ||= []
@middleware << [middleware, args, block]
end
# Add Rack App mapped to specific path
#
# @param [String] Path to map
# @return [void]
def map(map, &block)
@mappings ||= []
@mappings << [map, block]
end
end
module ServerMethods
# Create a new Class which is based on Middleman::Application
# Used to create a safe sandbox into which extensions and
# configuration can be included later without impacting
# other classes and instances.
#
# @return [Class]
def server(&block)
@@servercounter ||= 0
@@servercounter += 1
const_set("MiddlemanApplication#{@@servercounter}", Class.new(Middleman::Application))
end
# Creates a new Rack::Server
#
# @param [Hash] options to pass to Rack::Server.new
# @return [Rack::Server]
def start_server(options={})
opts = {
:Port => options[:port] || 4567,
:Host => options[:host] || "0.0.0.0",
:AccessLog => []
}
app_class = options[:app] ||= ::Middleman.server.inst
opts[:app] = app_class
require "webrick"
opts[:Logger] = WEBrick::Log::new("/dev/null", 7) if !options[:logging]
opts[:server] = 'webrick'
server = ::Rack::Server.new(opts)
server.start
server
end
end
# Methods to be mixed-in to Middleman::Application
module InstanceMethods
# Backwards-compatibility with old request.path signature
def request
Thread.current[:request]
end
# Accessor for current path
# @return [String]
def current_path
Thread.current[:current_path]
end
# Set the current path
#
# @param [String] path The new current path
# @return [void]
def current_path=(path)
Thread.current[:current_path] = path
Thread.current[:request] = ::Thor::CoreExt::HashWithIndifferentAccess.new({
:path => path,
:params => req ? ::Thor::CoreExt::HashWithIndifferentAccess.new(req.params) : {}
})
end
def use(*args, &block); self.class.use(*args); end
def map(*args, &block); self.class.map(*args, &block); end
# Rack env
def env
Thread.current[:env]
end
def env=(value)
Thread.current[:env] = value
end
# Rack request
# @return [Rack::Request]
def req
Thread.current[:req]
end
def req=(value)
Thread.current[:req] = value
end
# Rack response
# @return [Rack::Response]
def res
Thread.current[:res]
end
def res=(value)
Thread.current[:res] = value
end
def call(env)
# Keep `__middleman__` messaging to this thread
if env["PATH_INFO"] == "/__middleman__"
if env["REQUEST_METHOD"] == "POST"
req = ::Rack::Request.new(env)
if req.params.has_key?("change")
self.files.did_change(req.params["change"])
elsif req.params.has_key?("delete")
self.files.did_delete(req.params["delete"])
end
end
res = ::Rack::Response.new
res.status = 200
return res.finish
end
dup.call!(env)
end
# Rack Interface
#
# @private
# @param Rack environment
def call!(env)
self.env = env
# Store environment, request and response for later
self.req = req = ::Rack::Request.new(env)
self.res = res = ::Rack::Response.new
puts "== Request: #{env["PATH_INFO"]}" if logging?
# Catch :halt exceptions and use that response if given
catch(:halt) do
process_request(env, req, res)
res.status = 404
res.finish
end
end
# Halt the current request and return a response
#
# @private
# @param [String] Response value
def halt(response)
throw :halt, response
end
# Core response method. We process the request, check with the sitemap,
# and return the correct file, response or status message.
#
# @private
def process_request(env, req, res)
start_time = Time.now
# Normalize the path and add index if we're looking at a directory
original_path = URI.decode(env["PATH_INFO"].dup)
if original_path.respond_to? :force_encoding
original_path.force_encoding('UTF-8')
end
request_path = full_path(original_path)
# Run before callbacks
run_hook :before
if original_path != request_path
# Get the resource object for this path
resource = sitemap.find_resource_by_destination_path(original_path)
end
# Get the resource object for this full path
resource ||= sitemap.find_resource_by_destination_path(request_path)
# Return 404 if not in sitemap
return not_found(res) unless resource && !resource.ignored?
# If this path is a static file, send it immediately
return send_file(resource.source_file, env, res) unless resource.template?
# Set the current path for use in helpers
self.current_path = request_path.dup
# Set a HTTP content type based on the request's extensions
content_type(res, resource.mime_type)
begin
# Write out the contents of the page
res.write resource.render
# Valid content is a 200 status
res.status = 200
rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e
res.write "Error: #{e.message}"
res.status = 500
end
# End the request
puts "== Finishing Request: #{self.current_path} (#{(Time.now - start_time).round(2)}s)" if logging?
halt res.finish
end
# Add a new mime-type for a specific extension
#
# @param [Symbol] type File extension
# @param [String] value Mime type
# @return [void]
def mime_type(type, value=nil)
return type if type.nil? || type.to_s.include?('/')
type = ".#{type}" unless type.to_s[0] == ?.
return ::Rack::Mime.mime_type(type, nil) unless value
::Rack::Mime::MIME_TYPES[type] = value
end
# Halt request and return 404
def not_found(res)
res.status == 404
res.write "<html><body><h1>File Not Found</h1><p>#{@request_path}</p></body>"
res.finish
end
# Immediately send static file
#
# @param [String] path File to send
def send_file(path, env, res)
extension = File.extname(path)
matched_mime = mime_type(extension)
matched_mime = "application/octet-stream" if matched_mime.nil?
content_type res, matched_mime
file = ::Rack::File.new nil
file.path = path
response = file.serving(env)
response[1]['Content-Encoding'] = 'gzip' if %w(.svgz).include?(extension)
halt response
end
# Set the content type for the current request
#
# @param [String] type Content type
# @param [Hash] params
# @return [void]
def content_type(res, type, params={})
return res['Content-Type'] unless type
default = params.delete :default
mime_type = mime_type(type) || default
throw "Unknown media type: %p" % type if mime_type.nil?
mime_type = mime_type.dup
unless params.include? :charset
params[:charset] = params.delete('charset') || "utf-8"
end
params.delete :charset if mime_type.include? 'charset'
unless params.empty?
mime_type << (mime_type.include?(';') ? ', ' : ';')
mime_type << params.map { |kv| kv.join('=') }.join(', ')
end
res['Content-Type'] = mime_type
end
end
end
end
end

View file

@ -5,6 +5,9 @@ module Middleman
end
module CoreExtensions
# Rack Request
autoload :Request, "middleman-core/core_extensions/request"
# File Change Notifier
autoload :FileWatcher, "middleman-core/core_extensions/file_watcher"