module Padrino
class ApplicationSetupError < RuntimeError #:nodoc:
end
##
# Subclasses of this become independent Padrino applications (stemming from Sinatra::Application)
# These subclassed applications can be easily mounted into other Padrino applications as well.
#
class Application < Sinatra::Application
class << self
def inherited(subclass)
CALLERS_TO_IGNORE.concat(PADRINO_IGNORE_CALLERS)
subclass.default_configuration!
Padrino.set_load_paths File.join(subclass.root, "/models")
Padrino.require_dependencies File.join(subclass.root, "/models.rb")
Padrino.require_dependencies File.join(subclass.root, "/models/**/*.rb")
super # Loading the subclass
subclass.default_filters!
subclass.default_routes!
subclass.default_errors!
end
##
# Hooks into when a new instance of the application is created
# This is used because putting the configuration into inherited doesn't
# take into account overwritten app settings inside subclassed definitions
# Only performs the setup first time application is initialized.
#
def new(*args, &bk)
setup_application!
super
end
##
# Method for organize in a better way our routes like:
#
# controller :admin do
# get :index do; ...; end
# get :show, :with => :id do; ...; end
# end
#
# Now you can call your actions with:
#
# url(:admin_index) # => "/admin"
# url(:admin_show, :id => 1) # "/admin/show/1"
#
# You can instead using named routes follow the sinatra way like:
#
# controller "/admin" do
# get "/index" do; ...; end
# get "/show/:id" do; ...; end
# end
#
# And you can call directly these urls:
#
# # => "/admin"
# # => "/admin/show/1"
#
def controller(*extensions, &block)
if block_given?
@_controller, original = extensions, @_controller
instance_eval(&block)
@_controller = original
else
include(*extensions) if extensions.any?
end
end
alias :controllers :controller
##
# Usher router, for fatures and configurations see: http://github.com/joshbuddy/usher
#
# Examples:
#
# router.add_route('/greedy/{!:greed,.*}')
# router.recognize_path('/simple')
#
def router
@router ||= Usher.new(:request_methods => [:request_method, :host, :port, :scheme],
:ignore_trailing_delimiters => true,
:generator => Usher::Util::Generators::URL.new)
block_given? ? yield(@router) : @router
end
alias :urls :router
##
# Instance method for url generation like:
#
# url(:show, :id => 1)
# url(:show, :name => :test)
# url("/show/:id/:name", :id => 1, :name => foo)
#
def url(name, *params)
params.map! do |param|
if param.is_a?(Hash)
param[:format] = param[:format].to_s if param.has_key?(:format)
param.each { |k,v| param[k] = v.to_param if v.respond_to?(:to_param) }
end
end
url = router.generator.generate(name, *params)
uri_root != "/" ? uri_root + url : url
end
alias :url_for :url
##
# With this method we can use layout like rails do or if a block given like sinatra
# By default we look in your/app/views/layouts/application.(haml|erb|etc)
#
# If you define:
#
# layout :custom
#
# Padrino look for your/app/views/layouts/custom.(haml|erb|etc)
#
def layout(name=:layout, &block)
return super if block_given?
@_layout = name
end
##
# Reloads the application files from all defined load paths
#
def reload!
reset_routes! # remove all existing user-defined application routes
Padrino.load_dependency(self.app_file) # reload the app file
load_paths.each { |path| Padrino.load_dependencies(File.join(self.root, path)) } # reload dependencies
end
##
# Resets application routes to only routes not defined by the user
#
def reset_routes!
router.reset!
default_routes!
end
##
# Setup the application by registering initializers, load paths and logger
# Invoked automatically when an application is first instantiated
#
def setup_application!
return if @_configured
self.register_framework_extensions
self.calculate_paths
self.register_initializers
self.require_load_paths
self.disable :logging # We need do that as default because Sinatra use commonlogger.
I18n.locale = self.locale
I18n.load_path += self.translations
@_configured = true
end
protected
##
# Defines default settings for Padrino application
#
def default_configuration!
# Overwriting Sinatra defaults
set :app_file, caller_files.first || $0 # Assume app file is first caller
set :environment, PADRINO_ENV.to_sym
set :raise_errors, true if development?
set :logging, false # !test?
set :sessions, true
set :public, Proc.new { Padrino.root('public', self.uri_root) }
# Padrino specific
set :uri_root, "/"
set :reload, development?
set :app_name, self.to_s.underscore.to_sym
set :default_builder, 'StandardFormBuilder'
set :flash, defined?(Rack::Flash)
set :authentication, false
# Padrino locale
set :locale, :en
set :translations, Proc.new { Dir[File.join(self.root, "/locale/**/*.{rb,yml}")] }
set :auto_locale, false
# Plugin specific
set :padrino_mailer, defined?(Padrino::Mailer)
set :padrino_helpers, defined?(Padrino::Helpers)
end
##
# We need to add almost __sinatra__ images.
#
def default_routes!
# images resources
get "/__sinatra__/:image.png" do
filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png"
send_file filename
end
end
##
# This filter it's used for know the format of the request, and automatically set the content type.
#
def default_filters!
before do
request.path_info =~ /\.([^\.\/]+)$/
@_content_type = ($1 || :html).to_sym
content_type(@_content_type, :charset => 'utf-8') rescue content_type('application/octet-stream')
end
end
##
# This log errors for production environments
#
def default_errors!
configure :production do
error ::Exception do
boom = env['sinatra.error']
logger.error ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n ")
response.status = 500
content_type 'text/html'
'
Internal Server Error
'
end
end
end
##
# Calculates any required paths after app_file and root have been properly configured
# Executes as part of the setup_application! method
#
def calculate_paths
raise ApplicationSetupError.new("Please define 'app_file' option for #{self.name} app!") unless self.app_file
set :views, find_view_path if find_view_path
set :images_path, File.join(self.public, "/images") unless self.respond_to?(:images_path)
end
##
# Requires the middleware and initializer modules to configure components
#
def register_initializers
use Padrino::RackLogger
use Padrino::Reloader if reload?
use Rack::Flash if flash?
@initializer_path ||= Padrino.root + '/config/initializers/*.rb'
Dir[@initializer_path].each { |file| register_initializer(file) }
end
##
# Registers all desired padrino extension helpers
#
def register_framework_extensions
register Padrino::Mailer if padrino_mailer?
register Padrino::Helpers if padrino_helpers?
register Padrino::AccessControl if authentication?
end
##
# Returns the load_paths for the application (relative to the application root)
#
def load_paths
@load_paths ||= ["urls.rb", "config/urls.rb", "mailers/*.rb", "controllers/**/*.rb", "controllers.rb", "helpers/*.rb"]
end
##
# Requires all files within the application load paths
#
def require_load_paths
load_paths.each { |path| Padrino.require_dependencies(File.join(self.root, path)) }
end
##
# Returns the path to the views directory from root by returning the first that is found
#
def find_view_path
@view_paths = ["views"].collect { |path| File.join(self.root, path) }
@view_paths.find { |path| Dir[File.join(path, '/**/*')].any? }
end
##
# Registers an initializer with the application
# register_initializer('/path/to/initializer')
#
def register_initializer(file_path)
Padrino.require_dependencies(file_path)
file_class = File.basename(file_path, '.rb').camelize
register "#{file_class}Initializer".constantize
rescue NameError => e
logger.error "The module '#{file_class}Initializer' (#{file_path}) didn't loaded properly!"
logger.error " Initializer error was '#{e.message}'"
end
private
##
# Rewrite default because now routes can be:
#
# get :index # => "/"
# get :index, :map => "/" # => "/"
# get :show, :map => "/show-me" # => "/show-me"
# get "/foo/bar" # => "/show"
# get :show, :with => :id # => "/show/:id"
# get :show, :with => [:id, :name] # => "/show/:id/:name"
# get :list, :respond_to => :js # => "/list.{:format,js)"
# get :list, :respond_to => :any # => "/list(.:format)"
# get :list, :respond_to => [:js, :json] # => "/list.{!format,js|json}"
# gen :list, :respond_to => [:html, :js, :json] # => "/list(.{!format,js|json})"
#
def route(verb, path, options={}, &block)
# We dup options so we can build HEAD request correctly
options = options.dup
# We need check if path is a symbol, if that it's a named route
map = options.delete(:map)
if path.kind_of?(Symbol)
name = path # The route name
path = map || "/#{path}" # The route path
end
if path.kind_of?(String)
# Little reformats
path.sub!(/\/index$/, "") # If the route end with /index we remove them
path = (uri_root == "/" ? "/" : "(/)") if path.blank? # Add a trailing delimiter if empty
# Now we need to parse our with params
if params = options.delete(:with)
path += "/" unless path =~ /\/$/
path += Array(params).collect(&:inspect).join("/")
end
# Now we need to parse our respond_to
if format = options.delete(:respond_to)
path += case format
when :any then "(.:format)"
when Array then
formats = format.dup # Prevent changes to HEAD verb
container = formats.delete(:html) ? "(%s)" : "%s"
match = ".{:format," + formats.collect { |f| "#{f}$" }.join("|") + "}"
container % match
else ".{:format,#{format}}"
end
end
# Build our controller
controller = Array(@_controller).collect(&:to_s)
unless controller.empty?
# Now we need to add our controller path only if not mapped directly
if map.blank?
controller_path = controller.join("/")
path = controller_path + path
end
# Here we build the correct name route
if name
controller_name = controller.join("_")
name = "#{controller_name}_#{name}".to_sym unless controller_name.blank?
end
end
# We need to have a path that start with / in some circumstances and that don't end with /
if path != "(/)" && path != "/"
path = "/" + path if path !~ /^\//
path.sub!(/\/$/, '')
end
end
# Standard Sinatra requirements
options[:conditions] ||= {}
options[:conditions][:request_method] = verb
options[:conditions][:host] = options.delete(:host) if options.key?(:host)
# Because of self.options.host
host_name(options.delete(:host)) if options.key?(:host)
# Sinatra defaults
define_method "#{verb} #{path}", &block
unbound_method = instance_method("#{verb} #{path}")
block =
if block.arity != 0
lambda { unbound_method.bind(self).call(*@block_params) }
else
lambda { unbound_method.bind(self).call }
end
invoke_hook(:route_added, verb, path, block)
route = router.add_route(path, options).to(block)
route.name(name) if name
route
end
end # self
##
# Return the request format, this is useful when we need to respond to a given content_type like:
#
# get :index, :respond_to => :any do
# case content_type
# when :js then ...
# when :json then ...
# when :html then ...
# end
# end
#
def content_type(type=nil, params={})
type.nil? ? @_content_type : super(type, params)
end
##
# Instance method for url generation like:
#
# url(:show, :id => 1)
# url(:show, :name => :test)
# url("/show/:id/:name", :id => 1, :name => foo)
#
def url(name, *params)
self.class.url(name, *params)
end
alias :url_for :url
##
# This is mostly just a helper so request.path_info isn't changed when
# serving files from the public directory
#
def static_file?(path_info)
return if (public_dir = options.public).nil?
public_dir = File.expand_path(public_dir)
path = File.expand_path(public_dir + unescape(path_info))
return if path[0, public_dir.length] != public_dir
return unless File.file?(path)
return path
end
private
##
# Method for deliver static files, Sinatra 0.10.x or 1.0.x have this method
# but for now we use this (because we need a compatibility with 0.9.x) and also
# because we just have +static_file?+ method.
#
def static!
if path = static_file?(request.path_info)
send_file(path, :disposition => nil)
end
end
##
# Compatibility with usher
#
def route!(base=self.class, pass_block=nil)
# TODO: remove this when sinatra 1.0 will be released
if Sinatra::VERSION =~ /^0\.9/
# enable nested params in Rack < 1.0; allow indifferent access
@params = if Rack::Utils.respond_to?(:parse_nested_query)
indifferent_params(@request.params)
else
nested_params(@request.params)
end
# deliver static files
static! if options.static? && (request.get? || request.head?)
# perform before filters
self.class.filters.each { |block| instance_eval(&block) }
end
# Usher
if self.class.router and match = self.class.router.recognize(@request, @request.path_info)
@block_params = match.params.map{|p| p.last}
@params = @params ? @params.merge(match.params_as_hash) : match.params_as_hash
pass_block = catch(:pass) do
route_eval(&match.destination)
end
elsif base.superclass.respond_to?(:routes)
route! base.superclass
else
route_missing
end
end
##
# When we set :auto_locale, true then if we use param locale like:
#
# get "/:locale/some/foo" do; ...; end
#
# we automatically set the I18n locale to params[:locale]
#
def route_eval(&block)
if options.auto_locale
if params[:locale]
I18n.locale = params[:locale].to_sym rescue options.locale
end
end
super
end
##
# Hijacking the sinatra render for do three thing:
#
# * Use layout like rails do
# * Use render 'path/to/my/template' (without symbols)
# * Use render 'path/to/my/template' (with auto enegine lookup)
#
def render(engine, data=nil, options={}, locals={}, &block)
# TODO: remove these @template_cache.respond_to?(:clear) when sinatra 1.0 will be released
@template_cache.clear if Padrino.env != :production && @template_cache && @template_cache.respond_to?(:clear)
# If engine is an hash we convert to json
return engine.to_json if engine.is_a?(Hash)
# If an engine is a string probably is a path so we try to resolve them
if data.nil?
data = engine.to_sym
engine = resolve_template_engine(engine)
end
# Use layout as rails do
if (options[:layout].nil? || options[:layout] == true) && !self.class.templates.has_key?(:layout)
layout = self.class.instance_variable_defined?(:@_layout) ? self.class.instance_variable_get(:@_layout) : :application
if layout
# We look first for views/layout_name.ext then then for views/layouts/layout_name.ext
options[:layout] = Dir["#{self.options.views}/#{layout}.*"].present? ? layout.to_sym : File.join('layouts', layout.to_s).to_sym
logger.debug "Rendering layout #{options[:layout]}"
end
end
super
end
##
# Returns the template engine (i.e haml) to use for a given template_path
# resolve_template_engine('users/new') => :haml
#
def resolve_template_engine(template_path)
resolved_template_path = File.join(self.options.views, template_path.to_s + ".*")
template_file = Dir[resolved_template_path].first
raise "Template path '#{template_path}' could not be located in views!" unless template_file
template_engine = File.extname(template_file)[1..-1].to_sym
end
end # Application
end # Padrino