kiss 0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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