kiss 0.9

Sign up to get free protection for your applications and to get access to all the features.
data/lib/kiss.rb ADDED
@@ -0,0 +1,725 @@
1
+ # Kiss - A web application framework for Ruby
2
+ # Copyright (C) 2005-2008 MultiWidget LLC.
3
+ # See LICENSE for details.
4
+
5
+ # Author:: Shawn Van Ittersum, MultiWidget LLC
6
+ # Copyright:: Copyright (c) 2005-2008 MultiWidget LLC.
7
+ # License:: MIT X11 License
8
+
9
+ require 'rubygems'
10
+ require 'yaml'
11
+ require 'rack'
12
+ require 'rack/request'
13
+ require 'sequel'
14
+ require 'erubis'
15
+
16
+ require 'kiss/hacks'
17
+
18
+ require 'kiss/controller_accessors'
19
+ require 'kiss/template_methods'
20
+ require 'kiss/action'
21
+
22
+ require 'kiss/model'
23
+
24
+ module Rack
25
+ autoload :Bench, 'kiss/rack/bench'
26
+ autoload :EmailErrors, 'kiss/rack/email_errors'
27
+ autoload :Facebook, 'kiss/rack/facebook'
28
+ autoload :FileNotFound, 'kiss/rack/file_not_found'
29
+ autoload :LogExceptions, 'kiss/rack/log_exceptions'
30
+ autoload :ShowDebug, 'kiss/rack/show_debug'
31
+ autoload :ShowExceptions, 'kiss/rack/show_exceptions'
32
+ end
33
+
34
+ # Kiss - An MVC web application framework for Ruby, built on:
35
+ # * Erubis template engine
36
+ # * Sequel database ORM library
37
+ # * Rack web server abstraction
38
+ class Kiss
39
+ autoload :ExceptionReport, 'kiss/exception_report'
40
+ autoload :Mailer, 'kiss/mailer'
41
+ autoload :Iterator, 'kiss/iterator'
42
+ autoload :Form, 'kiss/form'
43
+ autoload :SequelSession, 'kiss/sequel_session'
44
+
45
+ attr_reader :action, :action_subdir, :action_path, :extension, :host, :template_dir,
46
+ :email_template_dir, :upload_dir, :evolution_dir, :public_dir, :params, :args, :db,
47
+ :login, :request, :environment, :content_types
48
+
49
+ @@default_action = 'index'
50
+
51
+ @@cookie_name = 'Kiss'
52
+
53
+ @@content_types = {
54
+ :rhtml => 'text/html',
55
+ :html => 'text/html',
56
+ :txt => 'text/plain',
57
+ :xml => 'application/xml',
58
+ :doc => 'application/msword',
59
+ :xls => 'application/application/vnd.ms-excel'
60
+ }
61
+
62
+ # Purposely empty class.
63
+ # ActionDone exception is raised when render complete to abort execution.
64
+ # Could probably use throw...catch instead.
65
+ class ActionDone < Exception; end
66
+
67
+ ### Class Methods
68
+
69
+ # Creates a new Kiss instance and runs it.
70
+ def self.run(options = {})
71
+ new(options).run
72
+ end
73
+
74
+ # Converts date strings from m/d/y to y/m/d. Useful for MySQL.
75
+ def self.mdy_to_ymd(date)
76
+ return '0000-00-00' unless date && date =~ /\S/
77
+ date.sub!(/\A\D+/,'')
78
+ date.sub!(/\D+\Z/,'')
79
+
80
+ month, day, year = date.split(/\D+/,3).each {|s| s.to_i}
81
+
82
+ return 0 unless month && day
83
+
84
+ current_year = Time.now.year
85
+ if !year || year.length == 0
86
+ # use current year if year is missing.
87
+ year = current_year
88
+ else
89
+ # convert two-digit years to four-digit years
90
+ year = year.to_i
91
+ if year < 100
92
+ year += 1900
93
+ year += 100 if year < current_year - 95
94
+ end
95
+ end
96
+
97
+ return sprintf("%04d-%02d-%02d",year,month.to_i,day.to_i)
98
+ end
99
+
100
+ # Returns regexp and error message for validating specified format.
101
+ # Will likely be rewritten in a future version of Kiss.
102
+ def self.format_details(format)
103
+ if format.is_a?(Regexp)
104
+ return format, 'invalid value'
105
+ end
106
+
107
+ case format
108
+ when :integer
109
+ return /\A\-?\d+\Z/,
110
+ 'must be an integer'
111
+
112
+ when :integer_positive, :positive_integer, :id
113
+ return /\A\d*[1-9]\d*\Z/,
114
+ 'must be a positive integer'
115
+
116
+ when :integer_unsigned, :unsigned_integer, :id_or_zero, :id_zero
117
+ return /\A\d+\Z/,
118
+ 'must be a positive integer or zero'
119
+
120
+ when :integer_negative, :negative_integer
121
+ return /\A\-\d*[1-9]\d*\Z/,
122
+ 'must be a negative integer'
123
+
124
+ when :decimal
125
+ return /\A\-?(\d+(\.\d*)?|\.\d+)\Z/,
126
+ 'must be a decimal number'
127
+
128
+ when :alphanum
129
+ return /\A[a-z0-9]+\Z/,
130
+ 'only letters and numbers'
131
+
132
+ when :word
133
+ return /\A\w+\Z/,
134
+ 'only letters, numbers, and _'
135
+
136
+ when :email_address
137
+ return /\A[A-Z0-9._%+-]+\@([A-Z0-9-]+\.)+[A-Z]{2,4}\Z/i,
138
+ 'must be a valid email address'
139
+
140
+ when :date
141
+ return /\A\d+\D\d+(\D\d+)?\Z/,
142
+ 'must be a valid date'
143
+
144
+ when :time
145
+ return /\A\d+\:\d+\s*[ap]m\Z/i,
146
+ 'must be a valid time'
147
+
148
+ when :datetime
149
+ return /\A\d+\D\d+(\D\d+)?\s+\d{1,2}\:\d{2}\s*[ap]m\Z/i,
150
+ 'must be a valid date and time'
151
+
152
+ end
153
+ end
154
+
155
+ # Validates value against specified format.
156
+ # If required is true, value must contain a non-whitespace character.
157
+ # If required is false, value need not match format if and only if value contains only whitespace.
158
+ def self.validate_value(value,format,required = false)
159
+ if required && (value !~ /\S/)
160
+ # value required
161
+ return 'required'
162
+ elsif format && (value =~ /\S/)
163
+ regexp,error = format_details(format)
164
+ return error unless value =~ regexp
165
+ end
166
+
167
+ return nil
168
+ end
169
+
170
+ # Generates string of random text of the specified length.
171
+ def self.random_text(length)
172
+ chars = ('A'..'Z').to_a + ('0'..'9').to_a # array
173
+
174
+ text = ''
175
+ size = chars.size
176
+ length.times { text += chars[rand(size)] }
177
+
178
+ text
179
+ end
180
+
181
+ # Returns exception cache, for use in Kiss::ExceptionReport.
182
+ def self.exception_cache
183
+ @@exception_cache
184
+ end
185
+
186
+ ### Instance Methods
187
+
188
+ # Adds specified data to exception cache, to be included in reports generated
189
+ # by Kiss::ExceptionReport in case of an exception.
190
+ def set_exception_cache(data)
191
+ @@exception_cache.merge!(data)
192
+ end
193
+
194
+ # Clears exception cache.
195
+ def clear_exception_cache
196
+ @@exception_cache = {}
197
+ end
198
+
199
+ # Generates string of random text of the specified length.
200
+ def random_text(*args)
201
+ self.class.random_text(*args)
202
+ end
203
+
204
+ # Returns URL/URI of app root (corresponding to top level of action_dir).
205
+ def app(suffix = nil)
206
+ suffix ? @app_url + suffix : @app_url
207
+ end
208
+
209
+ # Returns path of current action, under action_dir.
210
+ def action
211
+ @action
212
+ end
213
+
214
+ # Kiss Model cache, used to invoke and store Kiss database models.
215
+ #
216
+ # Example:
217
+ # models[:users] : database model for `users' table
218
+ def models
219
+ @dbm
220
+ end
221
+ alias_method :dbm, :models
222
+
223
+ # Returns URL/URI of app's static assets (asset_host or public_uri).
224
+ def pub(suffix = nil)
225
+ @pub ||= @options[:asset_host] || (@request.server + @options[:public_uri].to_s)
226
+ suffix ? @pub + '/' + suffix : @pub
227
+ end
228
+
229
+ # Returns true if specified path is a directory.
230
+ # Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
231
+ def directory_exists?(dir)
232
+ @options[:file_cache_no_reload] ? (
233
+ @directory_cache.has_key?(path) ?
234
+ @directory_cache[dir] :
235
+ @directory_cache[dir] = File.directory?(dir)
236
+ ) : File.directory?(dir)
237
+ end
238
+
239
+ # Caches the specified file and return its contents.
240
+ # If block given, executes block on contents, then cache and return block result.
241
+ # If fnf_file_type given, raises exception (of type fnf_exception_class) if file is not found.
242
+ def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound)
243
+ if (@options[:file_cache_no_reload] && @file_cache.has_key?(path)) || @files_cached_this_request[path]
244
+ return @file_cache[path]
245
+ end
246
+
247
+ @files_cached_this_request[path] = true
248
+
249
+ if !File.file?(path)
250
+ raise fnf_exception_class, "#{fnf_file_type} file missing: '#{path}'" if fnf_file_type
251
+
252
+ @file_cache[path] = nil
253
+ contents = nil
254
+ else
255
+ # expire cache if file (or symlink) modified
256
+ # TODO: what about symlinks to symlinks?
257
+ if !@file_cache_time[path] ||
258
+ @file_cache_time[path] < File.mtime(path) ||
259
+ ( File.symlink?(path) && (@file_cache_time[path] < File.lstat(path).mtime) )
260
+
261
+ @file_cache[path] = nil
262
+ @file_cache_time[path] = Time.now
263
+ contents = File.read(path)
264
+ end
265
+ end
266
+
267
+ @file_cache[path] ||= begin
268
+ (block_given?) ? yield(contents) : contents
269
+ end
270
+ end
271
+
272
+ # Merges specified options into previously defined/merged Kiss options.
273
+ def merge_options(config_options)
274
+ if config_options
275
+ if env_vars = config_options.delete(:ENV)
276
+ env_vars.each_pair {|k,v| ENV[k] = v }
277
+ end
278
+ if lib_dirs = config_options.delete(:lib_dirs)
279
+ @lib_dirs.push( lib_dirs )
280
+ end
281
+ if gem_dirs = config_options.delete(:gem_dirs)
282
+ @gem_dirs.push( gem_dirs )
283
+ end
284
+ if require_libs = config_options.delete(:require)
285
+ @require.push( require_libs )
286
+ end
287
+
288
+ @options.merge!( config_options )
289
+ end
290
+ end
291
+
292
+ # Create a new Kiss application instance, based on specified options.
293
+ def initialize(loader_options = {})
294
+ # store cached files
295
+ @file_cache = {}
296
+ @directory_cache = {}
297
+ @file_cache_time = {}
298
+
299
+ # if loader_options is string, then it specifies environment
300
+ # else it should be a hash of config options
301
+ if loader_options.is_a?(String)
302
+ loader_options = { :environment => loader_options }
303
+ end
304
+
305
+ # environment
306
+ @environment = loader_options[:environment]
307
+
308
+ # directories
309
+ script_dir = $0.sub(/[^\/]+\Z/,'')
310
+ script_dir = '' if script_dir == './'
311
+ @project_dir = script_dir + (loader_options[:project_dir] || loader_options[:root_dir] || '..')
312
+ Dir.chdir(@project_dir)
313
+
314
+ @config_dir = loader_options[:config_dir] || 'config'
315
+
316
+ # get environment name from config/environment
317
+ if (@environment.nil?) && File.file?(env_file = @config_dir+'/environment')
318
+ @environment = File.read(env_file).sub(/\s+\Z/,'')
319
+ end
320
+
321
+ # init options
322
+ @options = {}
323
+ @lib_dirs = ['lib']
324
+ @gem_dirs = ['gems']
325
+ @require = []
326
+
327
+ # common (shared) config
328
+ if (File.file?(config_file = @config_dir+'/common.yml'))
329
+ merge_options( YAML::load(File.read(config_file)) )
330
+ end
331
+ # environment config
332
+ if (File.file?(config_file = "#{@config_dir}/environments/#{@environment}.yml"))
333
+ merge_options( YAML::load(File.read(config_file)) )
334
+ end
335
+
336
+ merge_options( loader_options )
337
+
338
+ # set class vars from options
339
+ @action_dir = @options[:action_dir] || 'actions'
340
+ @template_dir = @options[:template_dir] ? @options[:template_dir] : @action_dir
341
+
342
+ @public_dir = @options[:public_dir] || 'public_html'
343
+
344
+ @model_dir = @options[:model_dir] || 'models'
345
+ @evolution_dir = @options[:evolution_dir] || 'evolutions'
346
+
347
+ @email_template_dir = @options[:email_template_dir] || 'email_templates'
348
+ @upload_dir = @options[:upload_dir] || 'uploads'
349
+
350
+ @cookie_name = @options[:cookie_name] || 'Kiss'
351
+ @default_action = @options[:default_action] || @@default_action
352
+ @action_root_class = @options[:action_class] || Class.new(Kiss::Action)
353
+
354
+ # include lib dirs
355
+ $LOAD_PATH.unshift(*( @lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
356
+
357
+ # add gem dir to rubygems search path
358
+ Gem.path.unshift(*( @gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
359
+
360
+ # require libs
361
+ @require.flatten.each {|lib| require lib }
362
+
363
+ # session class
364
+ if (@options[:session_class])
365
+ @session_class = (@options[:session_class].class == Class) ? @options[:session_class] : @options[:session_class].to_const
366
+ end
367
+
368
+ # load extensions to action class
369
+ action_extension_path = @action_dir + '/_action.rb'
370
+ if File.file?(action_extension_path)
371
+ @action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
372
+ end
373
+
374
+ # set controller access variables
375
+ @action_root_class.set_controller(self)
376
+ Kiss::Model.set_controller(self)
377
+ Kiss::Mailer.set_controller(self)
378
+
379
+ # database
380
+ if sequel = @options[:database]
381
+ # open database connection (if not already open)
382
+ @db = sequel.is_a?(String) ? (Sequel.open sequel) : sequel.is_a?(Hash) ? (Sequel.open sequel) : sequel
383
+ # add query logging to database class
384
+ @db.class.class_eval do
385
+ @@query = nil
386
+ def self.last_query
387
+ @@query
388
+ end
389
+
390
+ alias_method :execute_old, :execute
391
+ def execute(sql, *args, &block)
392
+ @@query = sql
393
+ execute_old(sql, *args, &block)
394
+ end
395
+ end
396
+
397
+ if @db.class.name == 'Sequel::MySQL::Database'
398
+ # fix sequel mysql bugs; add all_rows
399
+ require 'kiss/sequel_mysql'
400
+ Sequel.convert_tinyint_to_bool = false unless @options[:convert_tinyint_to_bool]
401
+ end
402
+
403
+ # create models cache
404
+ @dbm = Kiss::ModelCache.new(self,@model_dir)
405
+ end
406
+
407
+ # setup session storage, if session class specified in config
408
+ @session_class.setup_storage(self) if @session_class
409
+
410
+ self
411
+ end
412
+
413
+ # Sets up Rack builder options and begins Rack execution.
414
+ def run(options = {})
415
+ merge_options(options)
416
+
417
+ app = self
418
+ builder_options = @options[:rack_builder] || []
419
+ rack = Rack::Builder.new do
420
+ builder_options.each do |builder_option|
421
+ if builder_option.is_a?(Array)
422
+ builder_args = builder_option
423
+ builder_option = builder_args.shift
424
+ else
425
+ builder_args = []
426
+ end
427
+
428
+ unless builder_option.is_a?(Class)
429
+ builder_option = Rack.const_get(builder_option.to_s)
430
+ end
431
+
432
+ use(builder_option,*builder_args)
433
+ end
434
+
435
+ run app
436
+ end
437
+
438
+ handler = @options[:rack_handler] || Rack::Handler::WEBrick
439
+ if !handler.is_a?(Class)
440
+ handler = Rack::Handler.const_get(handler.to_s)
441
+ end
442
+ handler.run(rack,@options[:rack_handler_options] || {:Port => 4000})
443
+ end
444
+
445
+ # Processes and responds to requests received via Rack.
446
+ # Returns Rack::Response object.
447
+ def call(env)
448
+ clear_exception_cache
449
+
450
+ check_evolution_number if @db
451
+
452
+ @files_cached_this_request = {}
453
+
454
+ get_request(env)
455
+
456
+ @response = Rack::Response.new
457
+
458
+ begin
459
+ parse_action_path(@path)
460
+
461
+ setup_session
462
+ if login_session_valid?
463
+ load_from_login_session
464
+ elsif @options[:authenticate_all]
465
+ if (!@options[:authenticate_exclude].is_a?(Array) ||
466
+ @options[:authenticate_exclude].select {|action| action == @action}.size == 0)
467
+ authenticate
468
+ end
469
+ end
470
+ process.render
471
+ rescue Kiss::ActionDone
472
+ end
473
+ finalize_session if @session
474
+
475
+ @response.finish
476
+ end
477
+
478
+ # Sets up request-specified variables based in request information received from Rack.
479
+ def get_request(env)
480
+ @request = Rack::Request.new(env)
481
+
482
+ @app_host = @options[:app_host] ? ('http://' + @options[:app_host]) : @request.server
483
+ @app_uri = @options[:app_uri] || @request.script_name || ''
484
+ @app_url = @app_host + @app_uri
485
+
486
+ @path = @request.path_info || '/'
487
+ @params = @request.params
488
+
489
+ @host ||= @request.host
490
+ @protocol = env['HTTPS'] == 'on' ? 'https' : 'http'
491
+ end
492
+
493
+ # Returns Sequel dataset to evolution_number table, which specifies app's current evolution number.
494
+ # Creates evolution_number table if it does not exist.
495
+ def evolution_number_table
496
+ unless db.table_exists?(:evolution_number)
497
+ db.create_table :evolution_number do
498
+ column :version, :integer, :null=> false
499
+ end
500
+ db[:evolution_number].insert(:version => 0)
501
+ end
502
+ db[:evolution_number]
503
+ end
504
+
505
+ # Returns app's current evolution number.
506
+ def evolution_number
507
+ evolution_number_table.first.version
508
+ end
509
+
510
+ # Sets app's current evolution number.
511
+ def evolution_number=(version)
512
+ evolution_number_table.update(:version => version)
513
+ end
514
+
515
+ # Check whether there exists a file in evolution_dir whose number is greater than app's
516
+ # current evolution number. If so, raise an error to indicate need to apply new evolutions.
517
+ def check_evolution_number
518
+ version = evolution_number
519
+ if directory_exists?(@evolution_dir) &&
520
+ Dir.entries(@evolution_dir).select { |f| f =~ /\A0*#{version+1}_/ }.size > 0
521
+ raise "current evolution #{version} is outdated; apply evolutions or update evolution number"
522
+ end
523
+ end
524
+
525
+ # Loads session from session store (specified by session_class).
526
+ def setup_session
527
+ @login = {}
528
+ @session = @session_class ? begin
529
+ session = @session_class.persist(@request.cookies[@cookie_name])
530
+ @_fingerprint = Marshal.dump(session.data).hash
531
+
532
+ cookie_vars = {
533
+ :value => session.values[:session_id],
534
+ :path => @options[:cookie_path] || @app_uri,
535
+ :domain => @options[:cookie_domain] || @request.host
536
+ }
537
+ cookie_vars[:expires] = Time.now + @options[:cookie_lifespan] if @options[:cookie_lifespan]
538
+
539
+ # set_cookie here or at render time
540
+ @response.set_cookie @cookie_name, cookie_vars
541
+ @login.merge!(session[:login]) if session[:login]
542
+
543
+ session
544
+ end : {}
545
+ end
546
+
547
+ # Saves session to session store, if session data has changed since load.
548
+ def finalize_session
549
+ @session.save if @_fingerprint != Marshal.dump(@session.data).hash
550
+ end
551
+
552
+ def session
553
+ @session
554
+ end
555
+
556
+ ##### LOGIN SESSION #####
557
+
558
+ # Empties request and session login hashes.
559
+ def reset_login_session
560
+ @session[:login] = @login = {}
561
+ end
562
+ alias_method :reset_login_data, :reset_login_session
563
+
564
+ # Merges data hash (key-value pairs) into request login hash.
565
+ def set_login_data(data)
566
+ @login = @login.merge(data)
567
+ end
568
+
569
+ # Merges data hash (key-value pairs) into session login hash.
570
+ def set_login_session(data)
571
+ set_login_data(data)
572
+ @session[:login] = (@session[:login] || {}).merge(data)
573
+ end
574
+
575
+ # Sets expire time of session login hash, after which time it will be reset (emptied).
576
+ # If given a FixNum, sets expire time to now plus number of seconds.
577
+ def set_login_expires(time)
578
+ time = Time.now + time if time.is_a?(Fixnum)
579
+ set_login_session(:expires_at => time)
580
+ end
581
+
582
+ # Returns true if login hash is defined and not expired.
583
+ def login_session_valid?
584
+ @login && !login_session_expired?
585
+ end
586
+
587
+ # Returns true if login hash is expired.
588
+ def login_session_expired?
589
+ @login && (!@login[:expires_at] || @login[:expires_at] < Time.now)
590
+ end
591
+
592
+ # Calls login action's load_from_session method to populate request login hash.
593
+ def load_from_login_session
594
+ klass = action_class('login')
595
+ raise 'load_from_login_session called, but no login action found' unless klass
596
+
597
+ action_handler = klass.new
598
+ action_handler.load_from_session
599
+ end
600
+
601
+ # Returns path to login action.
602
+ def login_path
603
+ @action_dir + '/login.rb'
604
+ end
605
+
606
+ # If valid login session exists, loads login action to populate request login hash data.
607
+ # Otherwise, loads and calls login action to authenticate user.
608
+ def authenticate
609
+ if login_session_valid?
610
+ load_from_login_session
611
+ else
612
+ klass = action_class('login')
613
+ raise 'authenticate called, but no login action found' unless klass
614
+ old_extension = @extension
615
+ @extension = 'rhtml'
616
+ process(klass,login_path)
617
+ @extension = old_extension
618
+
619
+ unless login_session_valid?
620
+ #raise 'login action completed without setting valid login session'
621
+ end
622
+ end
623
+ end
624
+
625
+ ##### ACTION METHODS #####
626
+
627
+ def action_dir
628
+ @action_dir
629
+ end
630
+
631
+ # Creates and caches anonymous class with which to invoke specified (or current) action.
632
+ def action_class(action = @action)
633
+ action_path = @action_dir + '/' + action.to_s + '.rb'
634
+ return nil unless action_path.is_a?(String) && action_path.length > 0
635
+
636
+ file_cache(action_path) do |src|
637
+ # create new action class, subclass of shared action parent class
638
+ klass = Class.new(@action_root_class)
639
+ klass.class_eval(src,action_path) if src
640
+ klass
641
+ end
642
+ end
643
+
644
+ # Parses request URI to determine action path and arguments.
645
+ def parse_action_path(path)
646
+ @action_subdir = ''
647
+ @action = nil
648
+
649
+ redirect_url(app + '/') if path == ''
650
+
651
+ path += @default_action if path =~ /\/\Z/
652
+
653
+ parts = path.sub(/^\/*/,'').split('/')
654
+
655
+ while part = parts.shift
656
+ raise 'bad action' if part !~ /\A[a-z0-9][\w\-\.]*\Z/i
657
+
658
+ test_path = @action_dir + @action_subdir + '/' + part
659
+ if directory_exists?(test_path)
660
+ @action_subdir += '/' + part
661
+ next
662
+ end
663
+
664
+ if part =~ /\A(.+)\.(\w+)\Z/
665
+ @extension = $2
666
+ part = $1
667
+ else
668
+ @extension = 'rhtml'
669
+ end
670
+
671
+ @action = @action_subdir + '/' + part
672
+ break
673
+ end
674
+
675
+ # if no action, must have traversed all parts to a directory
676
+ # add a trailing slash and try again
677
+ redirect_url(app + '/' + path + '/') unless @action
678
+
679
+ @action_path = @action_dir + '/' + @action + '.rb'
680
+
681
+ # keep rest of path_info in args
682
+ @args = parts
683
+ end
684
+
685
+ # Processes specified (or current) action, by instantiating its anonymous
686
+ # action class and invoking `call' method on the instance.
687
+ def process(klass = action_class, action_path = @action_path)
688
+ action_handler = klass.new
689
+ action_handler.call
690
+
691
+ # return handler to follow with render
692
+ action_handler
693
+ end
694
+
695
+ ##### RACK OUTPUT METHODS #####
696
+
697
+ # Prepares Rack::Response object to be returned to Rack.
698
+ # Raises Kiss::ActionDone exception to bypass caller stack and return directly
699
+ # to Kiss#call.
700
+ def send_response(output = '',options = {})
701
+ content_type = options[:content_type] || @content_type || extension ? @@content_types[extension.to_sym] : nil
702
+ document_encoding ||= 'utf-8'
703
+
704
+ @response['Content-Type'] = "#{content_type}; #{document_encoding}" if content_type
705
+ @response['Content-Length'] = output.length.to_s
706
+ @response.body = output
707
+
708
+ raise Kiss::ActionDone
709
+
710
+ # back to Kiss#call, which finalizes session and returns @response
711
+ # (throws exception if no @response set)
712
+ end
713
+
714
+ # Sends HTTP 302 response to redirect client browser agent to specified URL.
715
+ def redirect_url(url)
716
+ @response.status = 302
717
+ @response['Location'] = url
718
+ send_response
719
+ end
720
+
721
+ # Returns new Kiss::Mailer object using specified options.
722
+ def new_email(*options)
723
+ Kiss::Mailer.new(*options)
724
+ end
725
+ end