require "pathname"
require "fileutils"
module Bowline
class << self
# The Configuration instance used to configure the Bowline environment
def configuration
@@configuration
end
alias_method :conf, :configuration
def configuration=(configuration) #:nodoc:
@@configuration = configuration
end
def initialized?
@initialized || false
end
def initialized=(initialized) #:nodoc:
@initialized ||= initialized
end
# The default Bowline logger.
# Also see the Bowline::Logging class.
def logger
if defined?(BOWLINE_LOGGER)
BOWLINE_LOGGER
else
nil
end
end
# The application's root
def root
Pathname.new(APP_ROOT) if defined?(APP_ROOT)
end
def public_path
root && root.join("public")
end
def assets_path
File.join(library_path, "assets")
end
def env
@env ||= ActiveSupport::StringInquirer.new(ENV["APP_ENV"] || "development")
end
def irb?
!!ENV["APP_IRB"]
end
end
class Initializer #:nodoc:
# 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 set_environment
production_path = Bowline.root.join("app_production")
ENV["APP_ENV"] ||= begin
production_path.exist? ? "production" : "development"
end
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
def load_gems
if defined?(Bundler)
# API changed between 0.8 and 0.9
if defined?(Bundler.require_env)
Bundler.require_env
else
Bundler.require
end
end
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 disable_dependency_loading
if configuration.cache_classes && !configuration.dependency_loading
ActiveSupport::Dependencies.unhook!
end
end
def initialize_dependency_mechanism
ActiveSupport::Dependencies.mechanism = configuration.cache_classes ? :require : :load
end
# Initializes ActiveRecord databases
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 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_environment
config = configuration
path = Bowline.root.join(*%w{config environments}, Bowline.env + ".rb").to_s
eval(IO.read(path), binding, path)
end
def load_application_initializers
Dir.glob(configuration.initializer_glob).sort.each do |initializer|
load(initializer)
end
end
def load_application_first_run
first_run = Bowline.root.join("app_first_run")
if first_run.exist?
first_run.unlink
Dir.glob(configuration.first_run_glob).sort.each do |initializer|
load(initializer)
end
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 } }
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
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
# Creates a class called AppConfig from configuration
# variables found in config/application.yml
def load_app_config
Object.const_set("AppConfig", AppConfig.load!(configuration.app_config_file))
end
def initialize_desktop
return unless Desktop.enabled?
Desktop::Runtime.setup!
Desktop::JS.setup!
end
def initialize_windows
return unless Desktop.enabled?
MainWindow.setup!
MainWindow.title = configuration.name
end
def initialize_trap
return unless Desktop.enabled?
trap("INT") {
Desktop::App.exit
}
end
def initialize_path
# Dir::tmpdir/Tempfile uses this
ENV["TMPDIR"] = Desktop::Path.temp if Desktop.enabled?
FileUtils.mkdir_p(Desktop::Path.user_data)
end
def initialize_marshal
return unless defined?(SuperModel)
path = configuration.marshal_path
path = path.call if path.is_a?(Proc)
SuperModel::Marshal.path = path
SuperModel::Marshal.load
at_exit {
SuperModel::Marshal.dump
}
end
def process
Bowline.configuration = configuration
set_environment
set_load_path
load_gems
load_application_environment
require_frameworks
set_autoload_paths
add_plugin_load_paths
initialize_dependency_mechanism
disable_dependency_loading
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_classes
load_application_helpers
initialize_desktop
initialize_marshal
initialize_windows
initialize_trap
initialize_path
load_application_initializers
load_application_first_run
after_initialize
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
attr_accessor :marshal_path
# 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
attr_accessor :dependency_loading
def threadsafe!
self.cache_classes = true
self.dependency_loading = false
end
# 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 :first_run_glob
attr_accessor :initializer_glob
# Set the application's name.
# This is required.
attr_accessor :name
# Set the application's globally unique id.
# This is required.
# Example:
# config.id = "com.maccman.bowline"
attr_accessor :id
# Set the application's version.
# Example:
# config.version = "0.1.2"
attr_accessor :version
attr_accessor :description
attr_accessor :url
# Set the application's icon.
# Point this variable to the icons path.
#
# If this isn't specified, Bowline's default one will be used.
#
# Supported icon files are PNGs and JPGs, preferably 512px x 512px.
attr_accessor :icon
attr_accessor :publisher
attr_accessor :copyright
# 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.dependency_loading = default_dependency_loading
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.marshal_path = default_marshal_path
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.plugin_glob = default_plugin_glob
self.helper_glob = default_helper_glob
self.initializer_glob = default_initalizer_glob
self.first_run_glob = default_first_run_glob
self.publisher = default_publisher
self.copyright = default_copyright
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
# 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
app/windows
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_dependency_loading
true
end
def default_eager_load_paths
%w(
app/models
app/remote
app/windows
app/binders
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_marshal_path
Proc.new { File.join(Desktop::Path.user_data, 'marshal.db') }
end
def default_cache_classes
true
end
def default_whiny_nils
false
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_first_run_glob
File.join(root_path, *%w{ config first_run **/*.rb })
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