Make our Rack pipeline a simple class abstracted from the App.

This commit is contained in:
Thomas Reynolds 2014-07-05 12:14:58 -07:00
parent c94470d33f
commit 6752a86b83
12 changed files with 243 additions and 395 deletions

View file

@ -53,6 +53,7 @@ module Middleman::Cli
require 'middleman-core'
require 'middleman-core/logger'
require 'middleman-core/rack'
require 'rack'
require 'rack/mock'
@ -66,7 +67,7 @@ module Middleman::Cli
verbose = options['verbose'] ? 0 : 1
instrument = options['instrument']
app = ::Middleman::Application.server.inst do
app = ::Middleman::Application.new do
config[:mode] = :build
config[:environment] = env
::Middleman::Logger.singleton(verbose, instrument)
@ -118,7 +119,8 @@ module Middleman::Cli
@to_clean = Set.new
@logger = @app.logger
@rack = ::Rack::MockRequest.new(@app.class.to_rack_app)
rack_app = ::Middleman::Rack.new(@app).to_app
@rack = ::Rack::MockRequest.new(rack_app)
super(base, @build_dir, config)
end

View file

@ -26,21 +26,38 @@ require 'middleman-core/config_context'
require 'middleman-core/file_renderer'
require 'middleman-core/template_renderer'
# Rack Request
require 'middleman-core/core_extensions/request'
# Core Middleman Class
module Middleman
class Application
extend Forwardable
# Global configuration
include Configuration::Global
class << self
# Global configuration for the whole Middleman project.
# @return [ConfigurationManager]
def config
@config ||= ::Middleman::Configuration::ConfigurationManager.new
end
# Root project directory (overwritten in middleman build/server)
# @return [String]
def root
ENV['MM_ROOT'] || Dir.pwd
end
# Pathname-addressed root
def root_path
Pathname(root)
end
end
# Uses callbacks
include Hooks
include Hooks::InstanceHooks
define_hook :initialized
define_hook :after_configuration
define_hook :before_configuration
# Before request hook
define_hook :before
@ -56,19 +73,6 @@ module Middleman
define_hook :before_render
define_hook :after_render
# Root project directory (overwritten in middleman build/server)
# @return [String]
def self.root
ENV['MM_ROOT'] || Dir.pwd
end
def_delegator :"self.class", :root
# Pathname-addressed root
def self.root_path
Pathname(root)
end
def_delegator :"self.class", :root_path
# Name of the source directory
# @return [String]
config.define_setting :source, 'source', 'Name of the source directory'
@ -158,44 +162,33 @@ module Middleman
}
}, 'Callbacks that can exclude paths from the sitemap'
define_hook :initialized
define_hook :instance_available
define_hook :after_configuration
define_hook :before_configuration
config.define_setting :autoload_sprockets, true, 'Automatically load sprockets at startup?'
config[:autoload_sprockets] = (ENV['AUTOLOAD_SPROCKETS'] == 'true') if ENV['AUTOLOAD_SPROCKETS']
# Basic Rack Request Handling
include Middleman::CoreExtensions::Request
attr_reader :config_context
attr_reader :sitemap
attr_reader :cache
attr_reader :template_context_class
attr_reader :config
attr_reader :generic_template_context
attr_reader :extensions
attr_reader :middleware
attr_reader :mappings
# Reference to Logger singleton
def_delegator :"::Middleman::Logger", :singleton, :logger
# New container for config.rb commands
attr_reader :config_context
# Reference to Sitemap
attr_reader :sitemap
# Template cache
attr_reader :cache
attr_reader :template_context_class
# Hack to get a sandboxed copy of these helpers for overriding similar methods inside Markdown renderers.
attr_reader :generic_template_context
def_delegator :"::Middleman::Util", :instrument
def_delegators :"self.class", :root, :root_path
def_delegators :@generic_template_context, :link_to, :image_tag, :asset_path
attr_reader :extensions
# Initialize the Middleman project
def initialize(&block)
self.class.inst = self
# Search the root of the project for required files
$LOAD_PATH.unshift(root) unless $LOAD_PATH.include?(root)
@middleware = []
@mappings = []
@template_context_class = Class.new(Middleman::TemplateContext)
@generic_template_context = @template_context_class.new(self)
@config_context = ConfigContext.new(self, @template_context_class)
@ -204,7 +197,8 @@ module Middleman
::Middleman::TemplateRenderer.cache.clear
# Setup the default values from calls to set before initialization
self.class.config.load_settings(self.class.superclass.config.all_settings)
@config = ::Middleman::Configuration::ConfigurationManager.new
@config.load_settings(self.class.config.all_settings)
@extensions = ::Middleman::ExtensionManager.new(self)
@extensions.auto_activate(:before_sitemap)
@ -238,8 +232,6 @@ module Middleman
evaluate_configuration(&block)
run_hook :instance_available
# This is for making the tests work - since the tests
# don't completely reload middleman, I18n.load_path can get
# polluted with paths from other test app directories that don't
@ -262,6 +254,9 @@ module Middleman
config_context.execute_after_configuration_callbacks
@extensions.activate_all
run_hook :ready
@config_context.execute_ready_callbacks
end
def evaluate_configuration(&block)
@ -317,7 +312,21 @@ module Middleman
File.join(root, config[:source])
end
def_delegator ::Middleman::Util, :instrument
# Use Rack middleware
#
# @param [Class] middleware Middleware module
# @return [void]
def use(middleware, *args, &block)
@middleware << [middleware, args, block]
end
# Add Rack App mapped to specific path
#
# @param [String] map Path to map
# @return [void]
def map(map, &block)
@mappings << [map, block]
end
# Work around this bug: http://bugs.ruby-lang.org/issues/4521
# where Ruby will call to_s/inspect while printing exception
@ -328,9 +337,5 @@ module Middleman
end
alias_method :inspect, :to_s # Ruby 2.0 calls inspect for NoMethodError instead of to_s
# Hooks clones _hooks from the class to the instance.
# https://github.com/apotonick/hooks/blob/master/lib/hooks/instance_hooks.rb#L10
# Middleman expects the same list of hooks for class and instance hooks:
def_delegator :"self.class", :_hooks
end
end

View file

@ -1,3 +1,5 @@
require 'rack/mime'
module Middleman
class ConfigContext
extend Forwardable
@ -87,5 +89,15 @@ module Middleman
config.define_setting(key, default) unless config.defines_setting?(key)
@app.config[key] = block_given? ? block : default
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)
type = ".#{type}" unless type.to_s[0] == '.'
::Rack::Mime::MIME_TYPES[type] = value
end
end
end

View file

@ -1,21 +1,5 @@
module Middleman
module Configuration
# Access to a global configuration manager for the whole Middleman project,
# plus backwards compatibility mechanisms for older Middleman projects.
module Global
def self.included(app)
app.send :extend, ClassMethods
app.send :def_delegator, :"self.class", :config
end
module ClassMethods
# Global configuration for the whole Middleman project.
# @return [ConfigurationManager]
def config
@_config ||= ConfigurationManager.new
end
end
end
# A class that manages a collection of documented settings.
# Can be used by extensions as well as the main Middleman

View file

@ -1,263 +0,0 @@
# Built on Rack
require 'rack'
require 'rack/file'
require 'rack/lint'
require 'rack/head'
require 'middleman-core/util'
require 'middleman-core/template_renderer'
module Middleman
module CoreExtensions
# Base helper to manipulate asset paths
module Request
# Extension registered
class << self
# @private
def included(app)
# CSSPIE HTC File
::Rack::Mime::MIME_TYPES['.htc'] = 'text/x-component'
# Let's serve all HTML as UTF-8
::Rack::Mime::MIME_TYPES['.html'] = 'text/html; charset=utf-8'
::Rack::Mime::MIME_TYPES['.htm'] = 'text/html; charset=utf-8'
app.extend ClassMethods
app.extend ServerMethods
Middleman.extend CompatibleClassMethods
# Include instance methods
app.send :include, InstanceMethods
end
end
module ClassMethods
# Reset Rack setup
#
# @private
def reset!
@rack_app = nil
end
# Get the static instance
#
# @private
# @return [Middleman::Application]
def inst(&block)
@inst ||= begin
mm = new(&block)
mm.run_hook :ready
mm.config_context.execute_ready_callbacks
mm
end
end
# Set the shared instance
#
# @private
attr_writer :inst
# Return built Rack app
#
# @private
# @return [Rack::Builder]
def to_rack_app(&block)
@rack_app ||= begin
app = ::Rack::Builder.new
app.use Rack::Lint
app.use Rack::Head
Array(@middleware).each do |klass, options, middleware_block|
app.use(klass, *options, &middleware_block)
end
inner_app = inst(&block)
app.map('/') { run inner_app }
Array(@mappings).each do |path, map_block|
app.map(path, &map_block)
end
app
end
end
# Prototype app. Used in config.ru
#
# @private
# @return [Rack::Builder]
def prototype
reset!
to_rack_app
end
# Call prototype, use in config.ru
#
# @private
def call(env)
prototype.call(env)
end
# Use Rack middleware
#
# @param [Class] middleware Middleware module
# @return [void]
def use(middleware, *args, &block)
@middleware ||= []
@middleware << [middleware, args, block]
end
# Add Rack App mapped to specific path
#
# @param [String] map 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, &block))
end
end
module CompatibleClassMethods
# 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)
::Middleman::Application.server(&block)
end
end
# Methods to be mixed-in to Middleman::Application
module InstanceMethods
def self.included(app)
app.send :def_delegators, :"self.class", :use, :map
end
def call(env)
dup.call!(env)
end
# Rack Interface
#
# @param env Rack environment
def call!(env)
# Store environment, request and response for later
req = ::Rack::Request.new(env)
res = ::Rack::Response.new
logger.debug "== Request: #{env['PATH_INFO']}"
# 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
#
# @param [String] response 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.
#
# @param env
# @param [Rack::Response] res
def process_request(env, _, res)
start_time = Time.now
request_path = URI.decode(env['PATH_INFO'].dup)
if request_path.respond_to? :force_encoding
request_path.force_encoding('UTF-8')
end
request_path = ::Middleman::Util.full_path(request_path, self)
# Run before callbacks
run_hook :before
# Get the resource object for this path
resource = sitemap.find_resource_by_destination_path(request_path.gsub(' ', '%20'))
# Return 404 if not in sitemap
return not_found(res, request_path) unless resource && !resource.ignored?
# If this path is a binary file, send it immediately
return send_file(resource, env) if resource.binary?
res['Content-Type'] = resource.content_type || 'text/plain'
begin
# Write out the contents of the page
res.write resource.render
# Valid content is a 200 status
res.status = 200
rescue Middleman::TemplateRenderer::TemplateNotFound => e
res.write "Error: #{e.message}"
res.status = 500
end
# End the request
logger.debug "== Finishing Request: #{resource.destination_path} (#{(Time.now - start_time).round(2)}s)"
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)
type = ".#{type}" unless type.to_s[0] == '.'
::Rack::Mime::MIME_TYPES[type] = value
end
# Halt request and return 404
def not_found(res, path)
res.status = 404
res.write "<html><head></head><body><h1>File Not Found</h1><p>#{path}</p></body></html>"
res.finish
end
# Immediately send static file
def send_file(resource, env)
file = ::Rack::File.new nil
file.path = resource.source_file
response = file.serving(env)
status = response[0]
response[1]['Content-Encoding'] = 'gzip' if %w(.svgz .gz).include?(resource.ext)
# Do not set Content-Type if status is 1xx, 204, 205 or 304, otherwise
# Rack will throw an error (500)
if !(100..199).include?(status) && ![204, 205, 304].include?(status)
response[1]['Content-Type'] = resource.content_type || 'application/octet-stream'
end
halt response
end
end
end
end
end

View file

@ -53,7 +53,6 @@ module Middleman
# * {#after_configuration}
# * {#after_build}
# * {#before_build}
# * {#instance_available}
#
# There are also some less common hooks that can be listened to from within an extension's `initialize` method:
#
@ -181,16 +180,15 @@ module Middleman
def_delegator :"@app.extensions[:file_watcher]", :api, :file_watcher
# Extensions are instantiated when they are activated.
# @param [Class] klass The Middleman::Application class
# @param [Middleman::Application] app The Middleman::Application instance
# @param [Hash] options_hash The raw options hash. Subclasses should not manipulate this directly - it will be turned into {#options}.
# @yield An optional block that can be used to customize options before the extension is activated.
# @yieldparam [Middleman::Configuration::ConfigurationManager] options Extension options
def initialize(klass, options_hash={}, &block)
def initialize(app, options_hash={}, &block)
@_helpers = []
@klass = klass
@app = app
setup_options(options_hash, &block)
setup_app_reference_when_available
# Bind app hooks to local methods
bind_before_configuration
@ -216,10 +214,6 @@ module Middleman
# Respond to the `after_build` event.
# If an `after_build` method is implemented, that method will be run after the builder runs.
# @!method instance_available
# Respond to the `instance_available` event.
# If an `instance_available` method is implemented, that method will be run after `config.rb` is run and after environment-specific config blocks have been run, but before any `after_configuration` callbacks.
# @!method manipulate_resource_list(resources)
# Manipulate the resource list by transforming or adding {Sitemap::Resource}s.
# Sitemap manipulation is a powerful way of interacting with a project, since it can modify each {Sitemap::Resource} or generate new {Sitemap::Resources}. This method is used in a pipeline where each sitemap manipulator is run in turn, with each one being fed the output of the previous manipulator. See the source of built-in Middleman extensions like {Middleman::Extensions::DirectoryIndexes} and {Middleman::Extensions::AssetHash} for examples of how to use this.
@ -230,20 +224,6 @@ module Middleman
# @param [Array<Sitemap::Resource>] resources A list of all the resources known to the sitemap.
# @return [Array<Sitemap::Resource>] The transformed list of resources.
# Assign the app instance. Used internally.
# @api private
def app=(app)
@app = app
ext = self
return unless ext.respond_to?(:instance_available)
@klass.instance_available do
ext.instance_available
end
end
private
# @yield An optional block that can be used to customize options before the extension is activated.
@ -259,30 +239,14 @@ module Middleman
yield @options if block_given?
end
def setup_app_reference_when_available
ext = self
@klass.initialized do
ext.app = self
end
@klass.instance_available do
ext.app ||= self
end
end
def bind_before_configuration
ext = self
return unless ext.respond_to?(:before_configuration)
@klass.before_configuration do
ext.before_configuration
end
return unless respond_to?(:before_configuration)
@app.before_configuration(&method(:before_configuration))
end
def bind_after_configuration
ext = self
@klass.after_configuration do
@app.after_configuration do
ext.after_configuration if ext.respond_to?(:after_configuration)
if ext.respond_to?(:manipulate_resource_list)
@ -295,7 +259,7 @@ module Middleman
ext = self
return unless ext.respond_to?(:before_build)
@klass.before_build do |builder|
@app.before_build do |builder|
if ext.method(:before_build).arity == 1
ext.before_build(builder)
else
@ -308,7 +272,7 @@ module Middleman
ext = self
return unless ext.respond_to?(:after_build)
@klass.after_build do |builder|
@app.after_build do |builder|
if ext.method(:after_build).arity == 1
ext.after_build(builder)
else

View file

@ -1,6 +1,7 @@
module Middleman
class ExtensionManager
extend Forwardable
def_delegator :@app, :logger
def_delegators :@activated, :[]
@ -44,11 +45,11 @@ module Middleman
if extension.supports_multiple_instances?
@activated[ext_name] ||= {}
key = "instance_#{@activated[ext_name].keys.length}"
@activated[ext_name][key] = extension.new(@app.class, options, &block)
@activated[ext_name][key] = extension.new(@app, options, &block)
elsif @activated.key?(ext_name)
raise "#{ext_name} has already been activated and cannot be re-activated."
else
@activated[ext_name] = extension.new(@app.class, options, &block)
@activated[ext_name] = extension.new(@app, options, &block)
end
end

View file

@ -45,7 +45,10 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
# Update the main sitemap resource list
# @return [void]
def manipulate_resource_list(resources)
@rack_client = ::Rack::MockRequest.new(app.class.to_rack_app)
@rack_client ||= begin
rack_app = ::Middleman::Rack.new(app).to_app
::Rack::MockRequest.new(rack_app)
end
# Process resources in order: binary images and fonts, then SVG, then JS/CSS.
# This is so by the time we get around to the text files (which may reference

View file

@ -17,9 +17,9 @@ module Middleman
@middleman = middleman
meta_pages = self
@rack_app = Rack::Builder.new do
@rack_app = ::Rack::Builder.new do
# Serve assets from metadata/assets
use Rack::Static, urls: ['/assets'], root: File.join(File.dirname(__FILE__), 'meta_pages')
use ::Rack::Static, urls: ['/assets'], root: File.join(File.dirname(__FILE__), 'meta_pages')
map '/' do
run meta_pages.method(:index)
@ -46,7 +46,7 @@ module Middleman
# Inspect the sitemap
def sitemap(_)
resources = @middleman.inst.sitemap.resources(true)
resources = @middleman.sitemap.resources(true)
sitemap_tree = SitemapTree.new
@ -59,10 +59,10 @@ module Middleman
# Inspect configuration
def config(_)
global_config = @middleman.inst.config.all_settings.map { |c| ConfigSetting.new(c) }
global_config = @middleman.config.all_settings.map { |c| ConfigSetting.new(c) }
extension_config = {}
@middleman.inst.extensions.each do |ext_name, extension|
@middleman.extensions.each do |ext_name, extension|
next if ::Middleman::Extension.auto_activated.include? ext_name
if extension.is_a?(Hash)

View file

@ -1,6 +1,7 @@
require 'webrick'
require 'middleman-core/meta_pages'
require 'middleman-core/logger'
require 'middleman-core/rack'
# rubocop:disable GlobalVars
module Middleman
@ -100,17 +101,17 @@ module Middleman
opts[:instrumenting] || false
)
server = ::Middleman::Application.server
app = ::Middleman::Application.new do
config[:environment] = opts[:environment].to_sym if opts[:environment]
end
# Add in the meta pages application
meta_app = Middleman::MetaPages::Application.new(server)
server.map '/__middleman' do
meta_app = Middleman::MetaPages::Application.new(app)
app.map '/__middleman' do
run meta_app
end
@app = server.inst do
config[:environment] = opts[:environment].to_sym if opts[:environment]
end
app
end
def start_file_watcher
@ -197,7 +198,7 @@ module Middleman
start_file_watcher
rack_app = app.class.to_rack_app
rack_app = ::Middleman::Rack.new(@app).to_app
@webrick.mount '/', ::Rack::Handler::WEBrick, rack_app
end

View file

@ -0,0 +1,138 @@
require 'rack'
require 'rack/file'
require 'rack/lint'
require 'rack/head'
require 'middleman-core/util'
require 'middleman-core/template_renderer'
# CSSPIE HTC File
::Rack::Mime::MIME_TYPES['.htc'] = 'text/x-component'
# Let's serve all HTML as UTF-8
::Rack::Mime::MIME_TYPES['.html'] = 'text/html; charset=utf-8'
::Rack::Mime::MIME_TYPES['.htm'] = 'text/html; charset=utf-8'
module Middleman
class Rack
extend Forwardable
def to_app
app = ::Rack::Builder.new
app.use ::Rack::Lint
app.use ::Rack::Head
@middleman.middleware.each do |klass, options, middleware_block|
app.use(klass, *options, &middleware_block)
end
inner_app = self
app.map('/') { run inner_app }
@middleman.mappings.each do |path, map_block|
app.map(path, &map_block)
end
app
end
def_delegator :"::Middleman::Logger", :singleton, :logger
def initialize(middleman)
@middleman = middleman
end
# Rack Interface
#
# @param env Rack environment
def call(env)
# Store environment, request and response for later
req = ::Rack::Request.new(env)
res = ::Rack::Response.new
logger.debug "== Request: #{env['PATH_INFO']}"
# 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
#
# @param [String] response 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.
#
# @param env
# @param [Rack::Response] res
def process_request(env, _, res)
start_time = Time.now
request_path = URI.decode(env['PATH_INFO'].dup)
if request_path.respond_to? :force_encoding
request_path.force_encoding('UTF-8')
end
request_path = ::Middleman::Util.full_path(request_path, @middleman)
# Run before callbacks
@middleman.run_hook :before
# Get the resource object for this path
resource = @middleman.sitemap.find_resource_by_destination_path(request_path.gsub(' ', '%20'))
# Return 404 if not in sitemap
return not_found(res, request_path) unless resource && !resource.ignored?
# If this path is a binary file, send it immediately
return send_file(resource, env) if resource.binary?
res['Content-Type'] = resource.content_type || 'text/plain'
begin
# Write out the contents of the page
res.write resource.render
# Valid content is a 200 status
res.status = 200
rescue Middleman::TemplateRenderer::TemplateNotFound => e
res.write "Error: #{e.message}"
res.status = 500
end
# End the request
logger.debug "== Finishing Request: #{resource.destination_path} (#{(Time.now - start_time).round(2)}s)"
halt res.finish
end
# Halt request and return 404
def not_found(res, path)
res.status = 404
res.write "<html><head></head><body><h1>File Not Found</h1><p>#{path}</p></body></html>"
res.finish
end
# Immediately send static file
def send_file(resource, env)
file = ::Rack::File.new nil
file.path = resource.source_file
response = file.serving(env)
status = response[0]
response[1]['Content-Encoding'] = 'gzip' if %w(.svgz .gz).include?(resource.ext)
# Do not set Content-Type if status is 1xx, 204, 205 or 304, otherwise
# Rack will throw an error (500)
if !(100..199).include?(status) && ![204, 205, 304].include?(status)
response[1]['Content-Type'] = resource.content_type || 'application/octet-stream'
end
halt response
end
end
end

View file

@ -1,6 +1,7 @@
# encoding: UTF-8
require 'rack/mock'
require 'middleman-core/rack'
Given /^a clean server$/ do
@initialize_commands = []
@ -43,14 +44,14 @@ Given /^the Server is running$/ do
initialize_commands = @initialize_commands || []
initialize_commands.unshift lambda { config[:show_exceptions] = false }
@server_inst = Middleman::Application.server.inst do
@server_inst = ::Middleman::Application.new do
initialize_commands.each do |p|
instance_exec(&p)
end
end
app_rack = @server_inst.class.to_rack_app
@browser = ::Rack::MockRequest.new(app_rack)
rack = ::Middleman::Rack.new(@server_inst)
@browser = ::Rack::MockRequest.new(rack.to_app)
end
Given /^the Server is running at "([^\"]*)"$/ do |app_path|