Class: TentSteak

Inherits:
Object
  • Object
show all
Defined in:
lib/tent_steak.rb

Overview

Application-level support for TentSteak.

Constant Summary collapse

VERSION =
"0.3.0"
MEM_LOG =
"<mem_log>"
BASE_FEATURES =

Default TentSteak features.

[ :form, :html, :table ]
ALL_FEATURES =

Array of all available TentSteak features.

BASE_FEATURES + [ :auth, :debug ]
@@logger =
nil
@@injected =
[]
@@features =
[]

Class Method Summary collapse

Class Method Details

.add_local_paths(base_dir, lib_dirs) ⇒ Object

Load local dependencies by prepending to $LOAD_PATH. Assumes each library has a subdirectory of ‘lib’ to add to the path, as is the case for most gems. Thus, calling

add_local_paths("extdir", %w{lib1 lib2})

would add the following to $LOAD_PATH:

"extdir/lib1/lib"
"extdir/lib2/lib"

Useful for using gem packages that haven’t been installed yet. For example:

> mkdir extdir
> cd extdir
> gem unpack mygem

TentSteak.add_local_paths("extdir", %w{mygem})


409
410
411
412
413
414
415
# File 'lib/tent_steak.rb', line 409

def self.add_local_paths(base_dir, lib_dirs)
  # Reverse order so libs appear (prepended) in $LOAD_PATH in the same order
  # that they appear in lib_dirs.
  lib_dirs.reverse.each do |library|
    $:.unshift File.expand_path(File.join(base_dir, library, "lib"))
  end
end

.bootstrap(app_module, *features) ⇒ Object

Initialize TentSteak for a Camping application. app_module is the root module for your Camping application, but like <tt>Camping.goes<tt/> it can also be expressed in symbol form. Thus, these invocations are functionally equivalent:

TentSteak.bootstrap MyApp
TentSteak.bootstrap :MyApp

For convenience, you can list any TentSteak features you want after the app module. If no features are given, #bootstrap implicitly loads the :base feature set.

# Implicitly load :base feature set (i.e. :form, :html, :table).
TentSteak.bootstrap :MyApp

# Explicitly load :base feature set.
TentSteak.bootstrap :MyApp, :base

# Explicitly load :html and :form features (i.e. omit :table from :base).
TentSteak.bootstrap :MyApp, :html, :form

To skip default features, pass in :none for the feature.

TentSteak.bootstrap :MyApp, :none

See #view_inject for details on TentSteak features.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/tent_steak.rb', line 177

def self.bootstrap(app_module, *features)
  @@rootmod = _to_mod(app_module)
  @@features = []

  # Add :base if no features given.  Ignore :none.
  features = BASE_FEATURES if features.empty?
  # features = [] if features.uniq == [:none]

  inject_features(*features)

  # Must log after feature injection because TSLogger.mem_log checks for
  # the presence of the :debug feature.
  logger.progname = app_module
  logger << "#{"="*75}\n"
  logger.info "Bootstrapping application #{app_module} with features: #{features.join(', ')}"
end

.controllersObject

Retrieves controller helper modules for all loaded features. Useful for injecting feature support into controllers:

class MyController < R '/my_controller'
  include *TentSteak.controllers

  def get
    ...
  end
end


134
135
136
137
138
# File 'lib/tent_steak.rb', line 134

def self.controllers
  loaded_features.map do |feature|
    TentSteakFeatures.const_get("#{feature.to_s.titleize}Controller")
  end
end

.init_db(config_file, log_file = nil, model_hash = {}) ⇒ Object

Set up the database environment. Loads ActiveRecord-style YAML database config info and optionally assigns an ActiveRecord log file. Pass in a block to globally tweak the configuration; pass config fragments inside model_hash to tweak the configuration per model base class.

The database info should reside in the “dbconfig” YAML block:

dbconfig:
  adapter: mysql
  database: my_app_db
  host: localhost
  username: myuser
  password: mypass

By default, init_db will call establish_connection on ActiveRecord::Base with the settings in config_file:

TentSteak.init_db("dbconfig.yml")

Optionally assigns an ActiveRecord logging file if log_file is given. Passing a log_file of true will hook AR logging into the TentSteak logger; passing in a String filename will log to that file. Pass nil to disable AR logging.

TentSteak.init_db("dbconfig.yml", true)
TentSteak.init_db("dbconfig.yml", "my_app.log")

You can use model_hash to pass in additional custom config settings per model object. After initializing ActiveRecord::Base, init_db will call establish_connection on each model class in the hash. This is typically useful if you have more than one database to connect models to. The example below will connect SomeTable and AnotherTable to my_app_db via ActiveRecord::Base, and WayOverThereTable to other_db via OtherModelBase.

module MyApp::Models
  # Alternate anchor point for other_db.
  class OtherModelBase < Base; end

  class SomeTable < Base; end
  class AnotherTable < Base; end
  class WayOverThereTable < OtherModelBase; end
end

if __FILE__ == $0
  model_hash = { MyApp::Models::OtherModelBase => { "database" => "other_db" } }
  TentSteak.init_db("dbconfig.yml", "my_app.log", model_hash)
  TentSteak.run_app(MyApp)
end

To post-process the config file before connecting with it – for example adding or decrypting the password – pass in a block to init_db. The config block parameter will contain the hash loaded from the YAML config file.

model_hash = { MyApp::Models::OtherModelBase => { "database" => "other_db" } }
TentSteak.init_db("dbconfig.yml", "my_app.log", model_hash) do |config|
  config[:my_prop] = "my_custom_value"
end

This example will add :my_prop to the config for ActiveRecord::Base and OtherModelBase, and set the database to “other_db” for OtherModelBase. Note that block config is applied first; any config values passed into model_hash will override those set in the config block.



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/tent_steak.rb', line 372

def self.init_db(config_file, log_file = nil, model_hash = {})
  logger.info "Loading database configuration from config file '#{config_file}'"
  config = YAML.load_file(config_file)["dbconfig"]

  if block_given?
    yield config
  end

  ActiveRecord::Base.logger = (log_file == true) ? TentSteak.logger : Logger.new(log_file)

  # Always assign to Base; also assign configs in model_hash, if any.
  ActiveRecord::Base.establish_connection config unless model_hash.has_key? ActiveRecord::Base
  if model_hash.any?
    model_hash.each do |model_class, model_config|
      model_class.establish_connection(config.merge(model_config))
    end
  end
end

.inject_features(*features) ⇒ Object

Inject named TentSteak features into your Camping application. Can be invoked indirectly by passing features into #bootstrap. Cannot be called before bootstrap.

Each feature is represented as a symbol, e.g. :html, :table. Injection consists of calling require on the feature source file, then calling view_inject on the feature’s view module.

The following features are distributed with TentSteak:

  • :auth - User authentication helpers

  • :debug - Debug log helpers

  • :form - HTML form helpers

  • :html - HTML generation helpers

  • :table - HTML table helpers

  • :base - Feature set of :form, :html, and :table

  • :all - Feature set of all built-in features

Bundled feature source files reside in the TentSteak distribution, in tent_steak-x.x.x/lib/tent_steak/[feature].rb. Applications can install their own feature files in ./tent_steak/[feature].rb, relative to $LOAD_PATH. Thus the source file for the :html built-in feature is at tent_steak-x.x.x/lib/tent_steak/html.rb, and an application’s :wowza feature would be at ./tent_steak/wowza.rb. NOTE: Custom features with identical names to built-in features (e.g. :html) are completely ignored.

The feature code should all reside in the ::TentSteakFeatures module, with view methods in a [Feature]View module and controller methods in a [Feature]Controller module. For example:

module TentSteakFeatures
  module WowzaView
    # view methods...
  end

  module WowzaController
    # controller methods...
  end
end

In the example above, calling TentSteak.inject_features :wowza would require the ./tent_steak/wowza.rb file, then call TentSteak.view_inject "TentSteakFeatures::WowzaView". The same happens for TentSteak.bootstrap :MyApp, :wowza. Any code outside those special modules will be loaded by the require, but will not be automatically injected into your Camping application.

The feature’s controller modules can be included explicitly, or in bulk with <tt>TentSteak.controllers<tt/>, as below, respectively:

class MyController < R '/my_controller'
  include TentSteakFeatures::WowzaController   # explicitly
  include *TentSteak.controllers              # all injected feature controllers
end

Features are only injected once; subsequent injections of loaded features are safely ignored. Thus, if your custom feature is dependent on other TentSteak features, you can add a (recursive) call to inject_features at the top of your feature source file:

TentSteak.inject_features :html, :form
module TentSteakFeatures
  ...
end


259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/tent_steak.rb', line 259

def self.inject_features(*features)
  raise "Must call TentSteak.bootstrap before inject_features" if @@rootmod.nil?

  # Expand feature sets into actual features.
  real_features = features.map do |feature|
    case feature
    when :none then []
    when :all then ALL_FEATURES
    when :base then BASE_FEATURES
    else feature
    end
  end.flatten.uniq

  # Load feature source files.
  # 
  # Check tent_steak library path, then local path for feature file.
  # This allows local per-app feature extensions.  Be nice and don't throw
  # exceptions if features or source files not found (but DO log an error).
  real_features.each do |feature|
    next if @@features.include? feature  # Only inject a feature once.
    paths = _feature_paths(feature)
    source_file = paths.find { |file| File.exist? file }
    logger.error "TentSteak Feature #{feature.inspect} not found at #{paths.inspect}!" unless source_file

    if source_file && File.exist?(source_file)
      require source_file
      @@features << feature
    else
      real_features.delete feature
    end
  end

  # Found all valid features, so inject them into the Markaby view context.
  view_inject real_features.map { |feature| "TentSteakFeatures::#{feature.to_s.titleize}View" }
end

.loaded_featuresObject

Array of all currently loaded TentSteak features.



107
108
109
# File 'lib/tent_steak.rb', line 107

def self.loaded_features
  @@features
end

.loggerObject

Retrieves TentSteak logger. Defaults to TSLogger.



102
103
104
# File 'lib/tent_steak.rb', line 102

def self.logger
  @@logger ||= TSLogger.new
end

.method_missing(sym, *args, &block) ⇒ Object

Check for feature query methods, e.g. html? returns true if the :html feature is activated.



113
114
115
116
117
118
119
120
121
122
# File 'lib/tent_steak.rb', line 113

def self.method_missing(sym, *args, &block)
  meth_name = sym.to_s[/(\w+)\?/, 1]
  # NOTE: loaded_features might contain user-contributed features, so
  # ALL_FEATURES might not list all possible valid features.
  if meth_name && (ALL_FEATURES + loaded_features).include?(meth_name.to_sym)
    loaded_features.include? meth_name.to_sym
  else
    super
  end
end

.run_app(app_module, app_base = "app") ⇒ Object

Runs the camping app. app_module is whatever you call Camping.goes with. app_base is the base directory to mount the application on, e.g. host.com/app_base (ignored in CGI mode).

Supports webrick, mongrel, and CGI modes. Pass in “webrick” as the first command-line parameter to run in a webrick instance, “mongrel” to run in Mongrel, and nothing to run in CGI mode (the default).



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/tent_steak.rb', line 425

def self.run_app(app_module, app_base = "app")
  case ARGV.first
  when "mongrel"
    puts "Running Mongrel in FCGI mode"
    require "mongrel/camping"

    app_module::Models::Base.threaded_connections = false
    server = Mongrel::Camping::start("0.0.0.0", 3301, "/#{app_base}/", app_module)
    puts "#{app_module} app is running at http://localhost:3301/#{app_base}/"
    server.run.join
  when "webrick"
    puts "Running in WEBrick mode"
    require 'webrick/httpserver'
    require 'camping/webrick'

    s = WEBrick::HTTPServer.new(:BindAddress => "0.0.0.0", :Port => 3301)
    s.mount("/#{app_base}", WEBrick::CampingHandler, app_module)
    trap(:INT) { s.shutdown }
    s.start
  when "fcgi"
    # FIXME: Add FCGI runner.
  when "dreamhost"
    # FIXME: Add Dreamhost FCGI runner.
  else
    # Default to CGI.
    puts app_module.run
  end
end

.set_logging(log_file, log_level = Logger::INFO) ⇒ Object

Assign a TentSteak library logging file. Turn off logging if log_file is nil.



92
93
94
95
96
97
98
99
# File 'lib/tent_steak.rb', line 92

def self.set_logging(log_file, log_level = Logger::INFO)
  if log_file.nil?
    @@logger = nil
  else
    @@logger = TSLogger.new(log_file)
    @@logger.level = log_level
  end
end

.view_inject(*helpers) ⇒ Object

Inject one or more external helper modules into the Application View.

TentSteak.view_inject MyViewHelper, MyOtherViewHelper


144
145
146
147
148
149
# File 'lib/tent_steak.rb', line 144

def self.view_inject(*helpers)
  raise "Must call TentSteak.bootstrap before view_inject" if @@rootmod.nil?

  logger.info "Injecting View helpers: #{helpers.join(', ')}"
  @@rootmod.module_eval "class Mab < Markaby::Builder; include #{helpers.join(', ')}; end"
end

.view_method_defined?(meth_sym) ⇒ Boolean

Determine if a method is defined in the Camping View context. Only meaningful after #bootstrap is called.

Returns:

  • (Boolean)


456
457
458
# File 'lib/tent_steak.rb', line 456

def self.view_method_defined?(meth_sym)
  @@rootmod ? @@rootmod::Mab.method_defined?(meth_sym) : false
end