Class: Kiss

Inherits:
Object show all
Defined in:
lib/kiss.rb,
lib/kiss/form.rb,
lib/kiss/bench.rb,
lib/kiss/debug.rb,
lib/kiss/login.rb,
lib/kiss/model.rb,
lib/kiss/action.rb,
lib/kiss/format.rb,
lib/kiss/mailer.rb,
lib/kiss/request.rb,
lib/kiss/iterator.rb,
lib/kiss/template.rb,
lib/kiss/form/field.rb,
lib/kiss/static_file.rb,
lib/kiss/sequel_session.rb,
lib/kiss/exception_report.rb,
lib/kiss/accessors/request.rb,
lib/kiss/accessors/template.rb,
lib/kiss/ext/sequel_database.rb,
lib/kiss/accessors/controller.rb,
lib/kiss/ext/sequel_mysql_dataset.rb

Overview

Kiss - An MVC web application framework for Ruby, built on:

  • Erubis template engine

  • Sequel database ORM library

  • Rack web server abstraction

Defined Under Namespace

Modules: Bench, ControllerAccessors, DatabaseAccessors, Debug, KissAccessors, RequestAccessors, SequelDatabase, SequelMySQLDataset, TemplateMethods Classes: Action, ExceptionReport, FileNotFoundError, Form, Format, Iterator, Login, Mailer, Model, ModelCache, Request, SequelSession, StaticFile, Template

Constant Summary collapse

MIME_TYPES =

These supplement the MIME types defined by Rack.

{
  'rhtml' => 'text/html'
}
@@default_action =
'index'
'Kiss'
@@default_project_dir =
'.'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Kiss

Creates a new application controller instance, and also configures the application from config file options and any passed-in options.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/kiss.rb', line 124

def initialize(options = {})
  # init config
  @_config = {
    :layout => '/_layout',
    :file_cache_reload => true
  }
  @_lib_dirs = ['lib']
  @_gem_dirs = ['gems']
  @_require = []
  @_authenticate_exclude = ['/login', '/logout']
  @_mailer_config = {}
  @_mailer_override = {}
  @_exception_handlers = {}
  @_exception_mailer_config = {}
  
  # store for cached files and directories
  @_file_cache = {}
  @_directory_cache = {}
  @_file_cache_time = {}
  
  
  # If options is string, then it specifies an environment
  # (else it should be a hash of config options)
  options = { :environment => options } if options.is_a?(String)

  # project dir
  # all other files and directories are relative to the project dir
  Dir.chdir(options[:project_dir] || options[:root_dir] || @@default_project_dir)
  # save current path to force return there in case an action changes directory
  @_project_dir = Dir.pwd
  
  # directory containing the config files
  @_config_dir = options[:config_dir] || 'config'

  # get environment name from options or config/environment
  @_environment = options[:environment] || if File.file?(env_file = @_config_dir + '/environment')
    File.read(env_file).sub(/\s+\Z/, '')
  end
  
  # read common (shared) config
  merge_config_file(@_config_dir + '/common.yml')
  # read environment config
  merge_config_file(@_config_dir + "/environments/#{@_environment}.yml") if @_environment
  
  # merge options passed in to override config files
  merge_config( options )

  # set app instance variables from config data and defaults
  @_action_dir = @_config[:action_dir] || 'actions'
  @_template_dir = (@_config[:template_dir] ? @_config[:template_dir] : @_action_dir)
  @_model_dir = @_config[:model_dir] || 'models'
  @_evolution_dir = @_config[:evolution_dir] || 'evolutions'
  
  @_asset_dir = @_public_dir = @_config[:asset_dir] || @_config[:public_dir] || 'public_html'
  @_email_template_dir = @_config[:email_template_dir] || 'email_templates'
  @_upload_dir = @_config[:upload_dir] || 'uploads'
  
  @_cookie_name = @_config[:cookie_name] || @@default_cookie_name
  @_default_action = @_config[:default_action] || @@default_action
  
  # exception log
  @_exception_log_file = @_config[:exception_log] ? ::File.open(@_config[:exception_log], 'a') : nil
  
  # authenticate all actions?
  @_authenticate_all = @_config[:authenticate_all]
  
  # don't require authentication on exception actions
  @_authenticate_exclude << @_config.exception_action if @_config.exception_action
  @_authenticate_exclude << @_config.file_not_found_action if @_config.file_not_found_action
  
  # app host: default hostname of application
  @_app_host = @_config[:app_host]
  # app uri: default URI of application
  @_app_uri = @_config[:app_uri]
  
  # asset host: hostname of static assets
  @_asset_host = @_config[:asset_host]

  # public_uri: URI of requests to serve from public_dir
  @_asset_uri = @_config[:asset_uri] || @_config[:public_uri] || nil
  @_rack_file = Rack::File.new(@_asset_dir) if @_asset_uri
  
  # add lib dirs to load path
  $LOAD_PATH.unshift(*( @_lib_dirs.flatten.select {|dir| File.directory?(dir) } ))

  # add gem dirs to rubygems search path
  Gem.path.unshift(*( @_gem_dirs.flatten.select {|dir| File.directory?(dir) } ))

  # require specified libs
  @_require.flatten.each {|lib| require lib }
  
  # session class
  @_session_class = @_config[:session_class]
  @_session_class = @_session_class.to_const if @_session_class && !@_session_class.is_a?(Class)

  # database
  @_database_config = @_config[:database]
  @_database_pool = []
  
  self
end

Class Method Details

.absolute_path(filename) ⇒ Object

Converts passed-in filename to absolute path if it does not start with ‘/’.



95
96
97
# File 'lib/kiss.rb', line 95

def absolute_path(filename)
  ( filename[0,1] == '/' ) ? filename : "#{Dir.pwd}/#{filename}"
end

.context_classObject

Finds the class defined by the file path of the execution context.



112
113
114
115
116
117
118
# File 'lib/kiss.rb', line 112

def context_class
  caller.each do |frame|
    if klass = @_classes[frame.sub(/\:.*/, '')]
      return klass
    end
  end
end

.mime_type(extension) ⇒ Object

Returns MIME type corresponding to passed-in extension.



100
101
102
103
104
# File 'lib/kiss.rb', line 100

def mime_type(extension)
  extension = extension.to_s
  rack_mime_types = Rack::Mime::MIME_TYPES rescue Rack::File::MIME_TYPES
  rack_mime_types[extension] || rack_mime_types['.' + extension] || Kiss::MIME_TYPES[extension]
end

.rack(config = {}) ⇒ Object



86
87
88
# File 'lib/kiss.rb', line 86

def rack(config = {})
  self.new(config).rack
end

.register_class_path(klass, path) ⇒ Object

Register a class by its file path, to enable context_class to work.



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

def register_class_path(klass, path)
  @_classes[path] = klass
end

.run(config = {}) ⇒ Object



90
91
92
# File 'lib/kiss.rb', line 90

def run(config = {})
  self.new(config).run
end

Instance Method Details

#app_url(options = {}) ⇒ Object

Returns URL/URI of app root (corresponding to top level of action_dir). Part of Kiss class to be available to kiss irb.



517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/kiss.rb', line 517

def app_url(options = {})
  # cache return values by unique options input
  @_app_url_cache ||= {}
  @_app_url_cache[options.inspect] ||= begin
    url_settings = {
      :protocol => @_protocol || 'http',
      :host => @_app_host,
      :uri => @_app_uri
    }.merge(options)
    
    raise 'host missing' unless url_settings[:host]
    
    "#{url_settings[:protocol]}://#{url_settings[:host]}#{url_settings[:uri]}"
  end
end

#call(env) ⇒ Object

Creates new controller instance to handle Rack request.



342
343
344
# File 'lib/kiss.rb', line 342

def call(env)
  Kiss::Request.new(env, self, @_config).call(env)
end

#databaseObject Also known as: db

Acquires and returns a database connection object from the connection pool, opening a new connection if the pool is empty.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/kiss.rb', line 354

def database
  @_database_pool.shift || begin
    raise 'database config missing' unless @_database_config
  
    # open database connection
    db = new_database_connection(@_database_config)
    
    # create model cache for this database connection
    db.kiss_controller = self
    db.kiss_model_cache = Kiss::ModelCache.new(db, @_model_dir)
    
    db
  end
end

#debug(obj, *args) ⇒ Object



537
538
539
# File 'lib/kiss.rb', line 537

def debug(obj, *args)
  gdebug obj
end

#directory_exists?(dir) ⇒ Boolean

Returns true if specified path is a directory. Always check filesystem if file_cache_reload option is set; otherwise, cache result.

Returns:

  • (Boolean)


437
438
439
440
441
442
443
# File 'lib/kiss.rb', line 437

def directory_exists?(dir)
  @_config[:file_cache_reload] ? File.directory?(dir) : (
    @_directory_cache.has_key?(dir) ?
      @_directory_cache[dir] :
      @_directory_cache[dir] = File.directory?(dir)
    )
end

#evolution_file(index) ⇒ Object

Returns an array of evolution filenames (relative to project dir) matching evolution number specified by index.



423
424
425
426
427
428
429
430
431
432
# File 'lib/kiss.rb', line 423

def evolution_file(index)
  # find files matching ev_dir/.*next_version_number
  files = Dir.glob("#{@_evolution_dir}/*#{index}[^0-9]*").
    # make sure we have a match for ev_dir/0*next_version_number
    select {|f| f =~ /\/0*#{index}[_\.][^\/]*\Z/ }
  
  raise "multiple evolution files for evolution number #{index}" if files.size > 1
  
  files[0]
end

#file_cache(path = nil, return_changed_state = false) ⇒ Object

TODO: Move file and directory caching to new class(es) TODO: File cache should store hashes of file info, keyed by path, instead of using separate hashes (cache time, contents, etc)

TODO: FIX BUG: The file cache keeps old classes after they are removed from the action/model class hierarchies. Will the class hierarchies force reload these paths, or get the old cached versions? Answer: They reload the classes because they only cache the source text in the file cache. They cache the classes in the hierarchy.

TODO: Need a generic class for Action/Template/Model class hierarchy caches.

Given a file path, caches or returns the file’s contents or the return value of the passed block applied to the file’s contents. If file is not found, the file’s contents are nil.



460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/kiss.rb', line 460

def file_cache(path = nil, return_changed_state = false)
  return @_file_cache unless path
  
  cache_changed = false
  
  if @_file_cache.has_key?(path)
    # we've loaded this path before
    if @_config[:file_cache_reload]
      # check to see if there's been a change that needs to be reloaded
      if !File.file?(path)
        if @_file_cache_time[path]
          # file cached as existing but has been removed; update cache to show no file
          cache_changed = true
          contents = nil
        end
      elsif !@_file_cache_time[path] ||
            @_file_cache_time[path] < File.mtime(path) ||
            ( File.symlink?(path) && (@_file_cache_time[path] < File.lstat(path).mtime) )
        # cache shows file missing, or file has been modified since cached
        cache_changed = true
        contents = File.read(path)
      end
    end
  else
    # haven't loaded this path yet
    cache_changed = true
    if !File.file?(path)
      # nil path, of file doesn't exist
      contents = nil
    else
      # file exists; mark cache time and read file
      contents = File.read(path)
    end
  end
  
  if cache_changed
    @_file_cache_time[path] = contents ? Time.now : nil
    @_file_cache[path] = block_given? ? yield(contents) : contents
  end
  
  return_changed_state ? [@_file_cache[path], cache_changed] : @_file_cache[path]
end

#last_evolution_file_numberObject

Gets the number of the last file in the evolution dir, or 0 if the directory does not exist.



407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/kiss.rb', line 407

def last_evolution_file_number
  version = 0
  
  if directory_exists?(@_evolution_dir)
    digits = 1
    while ( entries = Dir.glob("#{@_evolution_dir}/#{'[0-9]' * digits}*") ).size > 0
      version = entries.sort.last.sub(/.*\//, '').sub(/\D.*/, '').to_i
      digits += 1
    end
  end
  
  version
end

#load_db_class_extensions(db_class) ⇒ Object



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/kiss.rb', line 370

def load_db_class_extensions(db_class)
  @_db_class_extensions_loaded ||= {}
  @_db_class_extensions_loaded[db_class] ||= begin
    db_class.class_eval { include Kiss::SequelDatabase }
    
    if db_class.name == 'Sequel::MySQL::Database'
      # add fetch_arrays, all_arrays methods
      Sequel::MySQL::Dataset.class_eval { include Kiss::SequelMySQLDataset }
      
      # turn off convert_tinyint_to_bool, unless app config says otherwise
      Sequel::MySQL.convert_tinyint_to_bool = false unless @_config[:convert_tinyint_to_bool]
    end
    
    require 'kiss/model'
    true
  end
end

#loginObject



533
534
535
# File 'lib/kiss.rb', line 533

def 
  {}
end

#modelsObject Also known as: dbm

Kiss Model cache, used to invoke and store Kiss database models.

Example: models == database model for ‘users’ table

Tip: ‘dbm’ (stands for ‘database models’) is a shorthand alias for ‘models’.



394
395
396
397
398
# File 'lib/kiss.rb', line 394

def models
  # make sure we have a database connection
  # create new model cache unless exists already
  db.kiss_model_cache
end

#new_database_connection(database_config) ⇒ Object



346
347
348
349
350
# File 'lib/kiss.rb', line 346

def new_database_connection(database_config)
  db = Sequel.connect database_config
  load_db_class_extensions(db.class)
  db
end

#new_email(options = {}) ⇒ Object

Returns new Kiss::Mailer object using specified options.



504
505
506
507
508
509
# File 'lib/kiss.rb', line 504

def new_email(options = {})
  Kiss::Mailer.new({
    :controller => self,
    :request => self
  }.merge(options))
end

#rack(config = nil) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/kiss.rb', line 305

def rack(config = nil)
  merge_config(config)
  
  app = self
  builder_options = @_config[:rack_builder] || []
  rack = Rack::Builder.new do
    builder_options.each do |builder_option|
      if builder_option.is_a?(Array)
        builder_args = builder_option
        builder_option = builder_args.shift
      else
        builder_args = []
      end
  
      unless builder_option.is_a?(Class)
        builder_option = Rack.const_get(builder_option.to_s)
      end
      
      use(builder_option, *builder_args)
    end
    
    run app
  end.to_app
end

#return_database(db) ⇒ Object



401
402
403
# File 'lib/kiss.rb', line 401

def return_database(db)
  @_database_pool.push(db)
end

#run(options = nil) ⇒ Object

Runs Kiss application found at project_dir (default: ‘..’), with config read from config files plus additional options if passed in.



332
333
334
335
336
337
338
339
# File 'lib/kiss.rb', line 332

def run(options = nil)
  merge_config(options)
  
  handler = @_config[:rack_handler] || Rack::Handler::WEBrick
  handler = Rack::Handler.const_get(handler.to_s) unless handler.is_a?(Class)
  
  handler.run(rack, @_config[:rack_handler_options] || {:Port => 4000})
end

#send_email(options = {}) ⇒ Object



511
512
513
# File 'lib/kiss.rb', line 511

def send_email(options = {})
  new_email(options).send
end