require 'logger'
require 'set'
require 'pathname'
module Bowline
class << self
# The Configuration instance used to configure the Bowline environment
def configuration
@@configuration
end
def configuration=(configuration)
@@configuration = configuration
end
def initialized?
@initialized || false
end
def initialized=(initialized)
@initialized ||= initialized
end
def logger
if defined?(BOWLINE_LOGGER)
BOWLINE_LOGGER
else
nil
end
end
def root
Pathname.new(APP_ROOT) if defined?(APP_ROOT)
end
end
class Initializer
# The Configuration instance used by this Initializer instance.
attr_reader :configuration
# Runs the initializer. By default, this will invoke the #process method,
# which simply executes all of the initialization routines. Alternately,
# you can specify explicitly which initialization routine you want:
#
# Bowline::Initializer.run(:set_load_path)
#
# This is useful if you only want the load path initialized, without
# incurring the overhead of completely loading the entire environment.
def self.run(command = :process, configuration = Configuration.new)
yield configuration if block_given?
initializer = new configuration
initializer.send(command)
initializer
end
# Create a new Initializer instance that references the given Configuration
# instance.
def initialize(configuration)
@configuration = configuration
@loaded_plugins = []
end
def require_frameworks
configuration.frameworks.each { |framework| require(framework.to_s) }
end
def set_load_path
load_paths = configuration.load_paths
load_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) }
$LOAD_PATH.uniq!
end
# Set the paths from which Bowline will automatically load source files, and
# the load_once paths.
def set_autoload_paths
ActiveSupport::Dependencies.load_paths = configuration.load_paths.uniq
ActiveSupport::Dependencies.load_once_paths = configuration.load_once_paths.uniq
extra = ActiveSupport::Dependencies.load_once_paths - ActiveSupport::Dependencies.load_paths
unless extra.empty?
abort <<-end_error
load_once_paths must be a subset of the load_paths.
Extra items in load_once_paths: #{extra * ','}
end_error
end
# Freeze the arrays so future modifications will fail rather than do nothing mysteriously
configuration.load_once_paths.freeze
end
def add_plugin_load_paths
Dir.glob(File.join(configuration.plugin_glob, 'lib')).sort.each do |path|
$LOAD_PATH << path
ActiveSupport::Dependencies.load_paths << path
unless configuration.reload_plugins?
ActiveSupport::Dependencies.load_once_paths << path
end
end
$LOAD_PATH.uniq!
end
def initialize_database
if defined?(ActiveRecord)
ActiveRecord::Base.establish_connection(configuration.database_configuration)
end
end
def initialize_logger
# if the environment has explicitly defined a logger, use it
return if Bowline.logger
unless logger = configuration.logger
begin
logger = ActiveSupport::BufferedLogger.new(configuration.log_path)
logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase)
rescue StandardError => e
logger = ActiveSupport::BufferedLogger.new(STDERR)
logger.level = ActiveSupport::BufferedLogger::WARN
logger.warn(
"Bowline Error: Unable to access log file. Please ensure that #{configuration.log_path} exists and is chmod 0666. " +
"The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
)
end
end
silence_warnings { Object.const_set "BOWLINE_LOGGER", logger }
end
def initialize_framework_logging
ActiveRecord::Base.logger ||= Bowline.logger if defined?(ActiveRecord)
ActiveSupport::Dependencies.logger ||= Bowline.logger
end
# Loads support for "whiny nil" (noisy warnings when methods are invoked
# on +nil+ values) if Configuration#whiny_nils is true.
def initialize_whiny_nils
require('active_support/whiny_nil') if configuration.whiny_nils
end
# Sets the default value for Time.zone, and turns on ActiveRecord::Base#time_zone_aware_attributes.
# If assigned value cannot be matched to a TimeZone, an exception will be raised.
def initialize_time_zone
if configuration.time_zone
zone_default = Time.__send__(:get_zone, configuration.time_zone)
unless zone_default
raise \
'Value assigned to config.time_zone not recognized.' +
'Run "rake -D time" for a list of tasks for finding appropriate time zone names.'
end
Time.zone_default = zone_default
if defined?(ActiveRecord)
ActiveRecord::Base.time_zone_aware_attributes = true
ActiveRecord::Base.default_timezone = :utc
end
end
end
def initialize_framework_settings
(configuration.frameworks - [:active_support]).each do |framework|
base_class = framework.to_s.camelize.constantize.const_get("Base")
settings = configuration.send(framework)
next if !settings
settings.each do |setting, value|
base_class.send("#{setting}=", value)
end
end
configuration.active_support.each do |setting, value|
ActiveSupport.send("#{setting}=", value)
end
end
def initialize_gems
require 'rubygems'
Gem.clear_paths
Gem.path.unshift(configuration.gem_path)
end
def load_gems
configuration.gems.each do |dep|
options = {
:lib => dep.name
}.merge(dep.options)
next unless options[:lib]
begin
gem(dep.name, *dep.versions)
require(options[:lib])
rescue LoadError => e
puts "was unable to require #{dep.name} as '#{options[:lib]}'
Reason: #{e.class.name} error raised with message: #{e.message}"
end
end
end
def load_plugins
Dir.glob(File.join(configuration.plugin_glob, 'init.rb')).sort.each do |path|
config = configuration # Need local config variable
eval(IO.read(path), binding, path)
end
end
def load_application_initializers
Dir.glob(configuration.initializer_glob).sort.each do |initializer|
load(initializer)
end
end
def after_initialize
configuration.after_initialize_blocks.each do |block|
block.call
end
end
def load_application_helpers
helpers = configuration.helpers
helpers = helpers.map(&:constantize)
helpers.each {|h| Helpers.module_eval { extend h } }
Helpers.init
end
def load_application_classes
if configuration.cache_classes
configuration.eager_load_paths.each do |load_path|
matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
end
end
end
# For Ruby 1.8, this initialization sets $KCODE to 'u' to enable the
# multibyte safe operations. Plugin authors supporting other encodings
# should override this behaviour and set the relevant +default_charset+
# on ActionController::Base.
#
# For Ruby 1.9, this does nothing. Specify the default encoding in the Ruby
# shebang line if you don't want UTF-8.
def initialize_encoding
$KCODE='u' if RUBY_VERSION < '1.9'
end
def initialize_name
unless configuration.name
raise "You must provide an application name in environment.rb"
end
silence_warnings { Object.const_set "APP_NAME", configuration.name }
end
def load_app_config
app_config = configuration.app_config
return unless app_config
Object.const_set("AppConfig", Class.new {
app_config.keys.each do |key|
cattr_accessor key
send("#{key}=", app_config[key])
end
})
end
def initialize_js
Bowline.js.bowline_loaded
end
def process
Bowline.configuration = configuration
set_load_path
initialize_gems
load_gems
require_frameworks
set_autoload_paths
add_plugin_load_paths
initialize_encoding
initialize_database
initialize_logger
initialize_framework_logging
initialize_whiny_nils
initialize_time_zone
initialize_framework_settings
initialize_name
load_app_config
load_plugins
load_application_initializers
after_initialize
load_application_classes
load_application_helpers
initialize_js
Bowline.initialized = true
end
end
# The Configuration class holds all the parameters for the Initializer and
# ships with defaults that suites most Bowline applications. But it's possible
# to overwrite everything. Usually, you'll create an Configuration file
# implicitly through the block running on the Initializer, but it's also
# possible to create the Configuration instance in advance and pass it in
# like this:
#
# config = Bowline::Configuration.new
# Bowline::Initializer.run(:process, config)
class Configuration
# The application's base directory.
attr_reader :root_path
# A stub for setting options on ActiveRecord::Base.
attr_accessor :active_record
# A stub for setting options on ActiveResource::Base.
attr_accessor :active_resource
# A stub for setting options on ActiveSupport.
attr_accessor :active_support
attr_accessor :bowline
attr_accessor :frameworks
# Whether or not classes should be cached (set to false if you want
# application classes to be reloaded on each request)
attr_accessor :cache_classes
attr_accessor :binder_paths
# The path to the database configuration file to use. (Defaults to
# config/database.yml.)
attr_accessor :database_configuration_file
attr_accessor :app_config_file
# An array of additional paths to prepend to the load path. By default,
# all +app+, +lib+, +vendor+ and mock paths are included in this list.
attr_accessor :load_paths
# An array of paths from which Bowline will automatically load from only once.
# All elements of this array must also be in +load_paths+.
attr_accessor :load_once_paths
# An array of paths from which Bowline will eager load on boot if cache
# classes is enabled. All elements of this array must also be in
# +load_paths+.
attr_accessor :eager_load_paths
# The log level to use for the default Bowline logger.
attr_accessor :log_level
# The path to the log file to use. Defaults to log/#{environment}.log
# (e.g. log/development.log or log/production.log).
attr_accessor :log_path
# The specific logger to use. By default, a logger will be created and
# initialized using #log_path and #log_level, but a programmer may
# specifically set the logger to use via this accessor and it will be
# used directly.
attr_accessor :logger
# Set to +true+ if you want to be warned (noisily) when you try to invoke
# any method of +nil+. Set to +false+ for the standard Ruby behavior.
attr_accessor :whiny_nils
attr_accessor :reload_plugins
# Returns true if plugin reloading is enabled.
def reload_plugins?
!!@reload_plugins
end
# An array of gems that this Bowline application depends on. Bowline will automatically load
# these gems during installation, and allow you to install any missing gems with:
#
# rake gems:sync
#
# You can add gems with the #gem method.
attr_accessor :gems
# Adds a single Gem dependency to the Bowline application. By default, it will require
# the library with the same name as the gem. Use :lib to specify a different name.
#
# # gem 'aws-s3', '>= 0.4.0'
# # require 'aws/s3'
# config.gem 'aws-s3', :lib => 'aws/s3', :version => '>= 0.4.0', \
# :source => "http://code.whytheluckystiff.net"
#
# To require a library be installed, but not attempt to load it, pass :lib => false
#
# config.gem 'qrp', :version => '0.4.1', :lib => false
def gem(*args)
@gems << Dependencies::Dependency.new(*args)
end
attr_accessor :gem_path
# Sets the default +time_zone+. Setting this will enable +time_zone+
# awareness for Active Record models and set the Active Record default
# timezone to :utc.
attr_accessor :time_zone
attr_accessor :plugin_glob
attr_accessor :helper_glob
attr_accessor :initializer_glob
attr_accessor :name
attr_accessor :id
attr_accessor :version
attr_accessor :description
attr_accessor :publisher
attr_accessor :url
attr_accessor :icon
attr_accessor :sdk
attr_accessor :copyright
attr_accessor :titanium_version
# Create a new Configuration instance, initialized with the default values.
def initialize
set_root_path!
self.frameworks = default_frameworks
self.load_paths = default_load_paths
self.load_once_paths = default_load_once_paths
self.eager_load_paths = default_eager_load_paths
self.log_path = default_log_path
self.log_level = default_log_level
self.binder_paths = default_binder_paths
self.cache_classes = default_cache_classes
self.whiny_nils = default_whiny_nils
self.database_configuration_file = default_database_configuration_file
self.app_config_file = default_app_config_file
self.gems = default_gems
self.gem_path = default_gem_path
self.plugin_glob = default_plugin_glob
self.helper_glob = default_helper_glob
self.initializer_glob = default_initalizer_glob
self.publisher = default_publisher
self.copyright = default_copyright
self.titanium_version = default_titanium_version
for framework in default_frameworks
self.send("#{framework}=", Bowline::OrderedOptions.new)
end
end
# Set the root_path to APP_ROOT and canonicalize it.
def set_root_path!
raise 'APP_ROOT is not set' unless defined?(::APP_ROOT)
raise 'APP_ROOT is not a directory' unless File.directory?(::APP_ROOT)
@root_path =
# Pathname is incompatible with Windows, but Windows doesn't have
# real symlinks so File.expand_path is safe.
if RUBY_PLATFORM =~ /(:?mswin|mingw)/
File.expand_path(::APP_ROOT)
# Otherwise use Pathname#realpath which respects symlinks.
else
Pathname.new(::APP_ROOT).realpath.to_s
end
Object.const_set(:RELATIVE_APP_ROOT, ::APP_ROOT.dup) unless defined?(::RELATIVE_APP_ROOT)
::APP_ROOT.replace @root_path
end
def app_config
require 'erb'
YAML::load(ERB.new(IO.read(app_config_file)).result) if File.exists?(app_config_file)
end
# Loads and returns the contents of the #database_configuration_file. The
# contents of the file are processed via ERB before being sent through
# YAML::load.
def database_configuration
require 'erb'
YAML::load(ERB.new(IO.read(database_configuration_file)).result) if File.exists?(database_configuration_file)
end
def helpers
Dir[helper_glob].map {|f| File.basename(f, '.rb').classify }
end
# Adds a block which will be executed after bowline has been fully initialized.
# Useful for per-environment configuration which depends on the framework being
# fully initialized.
def after_initialize(&after_initialize_block)
after_initialize_blocks << after_initialize_block if after_initialize_block
end
# Returns the blocks added with Configuration#after_initialize
def after_initialize_blocks
@after_initialize_blocks ||= []
end
private
def default_frameworks
[:active_support, :bowline]
end
def default_load_paths
paths = []
# Followed by the standard includes.
paths.concat %w(
app
app/binders
app/models
app/remote
app/helpers
lib
vendor
).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) }
paths
end
# Doesn't matter since plugins aren't in load_paths yet.
def default_load_once_paths
[]
end
def default_eager_load_paths
%w(
app/binders
app/models
app/remote
app/helpers
).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) }
end
def default_log_path
File.join(root_path, 'log', "application.log")
end
def default_log_level
:info
end
def default_database_configuration_file
File.join(root_path, 'config', 'database.yml')
end
def default_app_config_file
File.join(root_path, 'config', 'application.yml')
end
def default_binder_paths
File.join(root_path, 'app', 'binders')
end
def default_cache_classes
true
end
def default_whiny_nils
false
end
def default_gems
gems = []
gems << Dependencies::Dependency.new(
"bowline", Bowline::Version::STRING, :lib => false
)
gems << Dependencies::Dependency.new("activesupport")
gems
end
def default_gem_path
File.join(root_path, *%w{ vendor gems })
end
def default_plugin_glob
File.join(root_path, *%w{ vendor plugins * })
end
def default_helper_glob
File.join(root_path, *%w{ app helpers *.rb })
end
def default_initalizer_glob
File.join(root_path, *%w{ config initializers **/*.rb })
end
def default_titanium_version
"0.4.4"
end
def default_publisher
"Bowline"
end
def default_copyright
"Copyright © #{Time.now.year}"
end
end
end
# Needs to be duplicated from Active Support since its needed before Active
# Support is available. Here both Options and Hash are namespaced to prevent
# conflicts with other implementations AND with the classes residing in Active Support.
class Bowline::OrderedOptions < Array #:nodoc:
def []=(key, value)
key = key.to_sym
if pair = find_pair(key)
pair.pop
pair << value
else
self << [key, value]
end
end
def [](key)
pair = find_pair(key.to_sym)
pair ? pair.last : nil
end
def method_missing(name, *args)
if name.to_s =~ /(.*)=$/
self[$1.to_sym] = args.first
else
self[name]
end
end
private
def find_pair(key)
self.each { |i| return i if i.first == key }
return false
end
end