kiss 1.1 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/LICENSE +1 -1
  2. data/Rakefile +2 -1
  3. data/VERSION +1 -1
  4. data/bin/kiss +151 -34
  5. data/data/scaffold.tgz +0 -0
  6. data/lib/kiss.rb +389 -742
  7. data/lib/kiss/accessors/controller.rb +47 -0
  8. data/lib/kiss/accessors/request.rb +106 -0
  9. data/lib/kiss/accessors/template.rb +23 -0
  10. data/lib/kiss/action.rb +502 -132
  11. data/lib/kiss/bench.rb +14 -5
  12. data/lib/kiss/debug.rb +14 -6
  13. data/lib/kiss/exception_report.rb +22 -299
  14. data/lib/kiss/ext/core.rb +700 -0
  15. data/lib/kiss/ext/rack.rb +33 -0
  16. data/lib/kiss/ext/sequel_database.rb +47 -0
  17. data/lib/kiss/ext/sequel_mysql_dataset.rb +23 -0
  18. data/lib/kiss/form.rb +404 -179
  19. data/lib/kiss/form/field.rb +183 -307
  20. data/lib/kiss/form/field_types.rb +239 -0
  21. data/lib/kiss/format.rb +88 -70
  22. data/lib/kiss/html/exception_report.css +222 -0
  23. data/lib/kiss/html/exception_report.html +210 -0
  24. data/lib/kiss/iterator.rb +14 -12
  25. data/lib/kiss/login.rb +8 -8
  26. data/lib/kiss/mailer.rb +68 -66
  27. data/lib/kiss/model.rb +323 -36
  28. data/lib/kiss/rack/bench.rb +16 -8
  29. data/lib/kiss/rack/email_errors.rb +25 -15
  30. data/lib/kiss/rack/errors_ok.rb +2 -2
  31. data/lib/kiss/rack/facebook.rb +6 -6
  32. data/lib/kiss/rack/file_not_found.rb +10 -8
  33. data/lib/kiss/rack/log_exceptions.rb +3 -3
  34. data/lib/kiss/rack/recorder.rb +2 -2
  35. data/lib/kiss/rack/show_debug.rb +2 -2
  36. data/lib/kiss/rack/show_exceptions.rb +2 -2
  37. data/lib/kiss/request.rb +435 -0
  38. data/lib/kiss/sequel_session.rb +15 -14
  39. data/lib/kiss/static_file.rb +20 -13
  40. data/lib/kiss/template.rb +327 -0
  41. metadata +60 -25
  42. data/lib/kiss/controller_accessors.rb +0 -81
  43. data/lib/kiss/hacks.rb +0 -188
  44. data/lib/kiss/sequel_mysql.rb +0 -25
  45. data/lib/kiss/template_methods.rb +0 -167
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2005-2008 MultiWidget LLC.
1
+ Copyright (c) 2005-2010 MultiWidget LLC.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/Rakefile CHANGED
@@ -23,8 +23,9 @@ spec = Gem::Specification.new do |s|
23
23
  )
24
24
 
25
25
  s.add_dependency('rack','=0.4.0')
26
- s.add_dependency('sequel','>=2.5.0')
26
+ s.add_dependency('sequel','>=3.0.0')
27
27
  s.add_dependency('erubis')
28
+ s.add_dependency('tzinfo')
28
29
  end
29
30
 
30
31
  Rake::GemPackageTask.new(spec) do |pkg|
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1
1
+ 1.7
data/bin/kiss CHANGED
@@ -1,45 +1,172 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'rubygems'
4
+ require 'kiss'
5
+ require 'optparse'
6
+ require 'irb'
7
+
8
+ def main
9
+ parse_options
10
+ Kiss::default_project_dir = $project_dir
11
+
12
+ command = ARGV.shift
13
+
14
+ case command
15
+ when 'create'
16
+ create
17
+ when 'run'
18
+ run
19
+ when 'irb'
20
+ kiss_irb
21
+ when 'set'
22
+ set
23
+ when 'evolve'
24
+ evolve
25
+ when 'help'
26
+ display_usage(ARGV[0])
27
+ else
28
+ display_usage
29
+ end
30
+ Kernel::exit
31
+ end
32
+
33
+ def parse_options
34
+ $project_dir = Dir.pwd
35
+ $rack_builder = []
36
+
37
+ opts = OptionParser.new do |opt|
38
+ opt.program_name = File.basename $0
39
+ opt.version = '1'
40
+ opt.release = nil
41
+ opt.summary_indent = ' ' * 4
42
+ # opt.banner = <<-EOT
43
+ #
44
+ # EOT
45
+
46
+ opt.separator nil
47
+ opt.separator "Options:"
48
+ opt.separator nil
49
+
50
+ opt.on("--evolution=VERSION", "-e",
51
+ "Specifies the desired evolution number.") do |value|
52
+ $evolution_number = value.sub(/\A\=/,'')
53
+ end
54
+
55
+ opt.on("--project=DIRECTORY", "-p",
56
+ "Specifies the desired application/project directory.") do |value|
57
+ $project_dir = value.sub(/\A\=/,'')
58
+ end
59
+
60
+ opt.on("--builder=RACK_BUILDER", "-b",
61
+ "Specifies a desired Rack builder module name for kiss run.") do |value|
62
+ $rack_builder.push( *( value.sub(/\A\=/,'').split(/,/).map{|b| b.to_sym} ))
63
+ end
64
+
65
+ opt.separator nil
66
+
67
+ end
68
+
69
+ result = opts.parse! ARGV
70
+ rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
71
+ puts opts
72
+ puts
73
+ puts e
74
+ Kernel::exit 1
75
+ end
3
76
 
4
77
  def create
5
- project_name = ARGV[0]
6
- display_usage('create') unless project_name =~ /\S/
78
+ archive_path = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))),'data','scaffold.tgz')
7
79
 
8
- path = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))),'data','scaffold.tgz')
80
+ Dir.mkdir($project_dir) unless File.directory?($project_dir)
9
81
 
10
- `mkdir #{project_name}`
82
+ puts "Expanding '#{archive_path}' to '#{$project_dir}'..."
83
+ `cd '#{$project_dir}'; tar xvfz '#{archive_path}'`
84
+
85
+ puts 'Kiss application scaffold created.'
86
+ end
11
87
 
12
- print "Expanding '#{path}' to '#{project_name}'..."
88
+ def load_kiss_project
89
+ Kiss.load
90
+ end
13
91
 
14
- `cd '#{ARGV[0]}'; tar xvfz '#{path}'`
92
+ def set
93
+ app = load_kiss_project
94
+ db = app.database
95
+
96
+ if $evolution_number
97
+ db[:evolution_number].update(:version => $evolution_number)
98
+ puts "Database evolution number set to #{$evolution_number}."
99
+ end
100
+ end
15
101
 
16
- puts 'done.'
102
+ def evolve
103
+ app = load_kiss_project
104
+ db = app.database
105
+
106
+ db_version = db.evolution_number
107
+ target_version = app.last_evolution_file_number
108
+
109
+ target_version = $evolution_number if $evolution_number &&
110
+ ($evolution_number < target_version)
111
+
112
+ while db_version < target_version
113
+ db_version += 1
114
+
115
+ filename = app.evolution_file(db_version)
116
+ print "Applying evolution file number #{db_version}: #{filename}...\n\n"
117
+
118
+ db.transaction do
119
+ File.read(filename).gsub(/\#[^\n]*/,'').split(';').each do |query|
120
+ next if query.blank?
121
+ query.sub!(/\A\s*/,'')
122
+ print (query + ";\n\n")
123
+ db << query
124
+ end
125
+ end
126
+ db[:evolution_number].update(:version => db_version)
127
+ puts "Database at evolution number #{db_version}."
128
+ end
17
129
  end
18
130
 
19
131
  def run
20
- project_name = ARGV[0]
21
- project_name = '.' unless project_name =~ /\S/
22
-
23
- require 'rubygems'
24
- require 'kiss'
132
+ Kiss.run( :rack_builder => $rack_builder )
133
+ end
25
134
 
26
- Kiss.run( :project_dir => project_name )
135
+ def kiss_irb
136
+ catch (:IRB_EXIT) do
137
+ IRB.start
138
+ end
27
139
  end
28
140
 
29
141
  def display_usage(command = nil)
30
142
  case command
31
143
  when 'create'
32
144
  puts <<-EOT
33
- usage: kiss create <app_directory>
34
- Creates a skeleton for a new Kiss application.
145
+ usage: kiss create
146
+ Creates a Kiss application scaffold.
35
147
  EOT
36
148
  when 'run'
37
149
  puts <<-EOT
38
- usage: kiss run [app_directory]
39
- Runs a Kiss application located within app_directory.
150
+ usage: kiss run
151
+ Runs a Kiss application.
152
+ EOT
153
+ when 'irb'
154
+ puts <<-EOT
155
+ usage: kiss irb
156
+ Begin an interactive Ruby (IRB) session in the context of a
157
+ Kiss application.
40
158
 
41
- The app_directory path may be omitted when running this command from
42
- within the top-level directory of a Kiss application.
159
+ Type 'app = Kiss.new' at the irb prompt to load the application.
160
+ EOT
161
+ when 'set'
162
+ puts <<-EOT
163
+ usage: kiss set [-options]
164
+ Set application variables as specified in the command options.
165
+ EOT
166
+ when 'evolve'
167
+ puts <<-EOT
168
+ usage: kiss evolve
169
+ Applies evolution files to database of Kiss application.
43
170
  EOT
44
171
  else
45
172
  puts <<-EOT
@@ -48,6 +175,10 @@ Utility to support and facilitate the Kiss web framework.
48
175
 
49
176
  Commands:
50
177
  create - Create a new Kiss application project.
178
+ evolve - Run database evolutions from Kiss project.
179
+ set - Set application variables such as current database
180
+ evolution number.
181
+ irb - Start an interactive Ruby session with Kiss loaded.
51
182
  run - Run a Kiss application.
52
183
  help - Display usage info.
53
184
 
@@ -56,20 +187,6 @@ For help on any command above, type:
56
187
  EOT
57
188
  end
58
189
  puts
59
- exit
60
190
  end
61
191
 
62
-
63
- # main
64
-
65
- case (command = ARGV.shift)
66
- when 'create'
67
- create
68
- when 'run'
69
- run
70
- when 'help'
71
- display_usage(ARGV[0])
72
- else
73
- display_usage
74
- end
75
- exit
192
+ main
Binary file
@@ -1,9 +1,9 @@
1
1
  # Kiss - A web application framework for Ruby
2
- # Copyright (C) 2005-2008 MultiWidget LLC.
2
+ # Copyright (C) 2005-2010 MultiWidget LLC.
3
3
  # See LICENSE for details.
4
4
 
5
5
  # Author:: Shawn Van Ittersum, MultiWidget LLC
6
- # Copyright:: Copyright (c) 2005-2008 MultiWidget LLC.
6
+ # Copyright:: Copyright (c) 2005-2010 MultiWidget LLC.
7
7
  # License:: MIT X11 License
8
8
 
9
9
  require 'rubygems'
@@ -12,688 +12,365 @@ require 'rack'
12
12
  require 'rack/request'
13
13
  require 'erubis'
14
14
 
15
- require 'kiss/hacks'
15
+ # Sequel may not be required anymore for string inflectors (singular, plural, titlecase, etc.),
16
+ # if database functionality is not used, since inflectors are now in sequel/extensions/inflector.
17
+ require 'sequel'
18
+ require 'sequel/extensions/inflector'
16
19
 
17
- require 'kiss/controller_accessors'
18
- require 'kiss/template_methods'
19
- require 'kiss/action'
20
-
21
- autoload :Sequel, 'sequel'
22
-
23
- module Rack
24
- autoload :Bench, 'kiss/rack/bench'
25
- autoload :ErrorsOK, 'kiss/rack/errors_ok'
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 :Recorder, 'kiss/rack/recorder'
31
- autoload :ShowDebug, 'kiss/rack/show_debug'
32
- autoload :ShowExceptions, 'kiss/rack/show_exceptions'
33
- end
20
+ require 'tzinfo'
34
21
 
35
- module Digest; end
36
-
37
- class Class
38
- # adapted from Rails, re-written for speed (only one class_eval call)
39
- def cattr_reader(*syms)
40
- class_eval(
41
- syms.flatten.map do |sym|
42
- sym.is_a?(Hash) ? '' : %Q(
43
- unless defined? @@#{sym}
44
- @@#{sym} = nil
45
- end
22
+ require 'kiss/ext/core'
23
+ require 'kiss/ext/rack'
46
24
 
47
- def self.#{sym}
48
- @@#{sym}
49
- end
50
-
51
- def #{sym}
52
- @@#{sym}
53
- end
54
-
55
- )
56
- end.join, __FILE__, __LINE__
57
- )
58
- end
59
- end
25
+ require 'kiss/accessors/controller'
26
+ require 'kiss/accessors/request'
27
+ require 'kiss/accessors/template'
28
+ require 'kiss/action'
60
29
 
61
30
  # Kiss - An MVC web application framework for Ruby, built on:
62
31
  # * Erubis template engine
63
32
  # * Sequel database ORM library
64
33
  # * Rack web server abstraction
65
34
  class Kiss
35
+ autoload :Bench, 'kiss/bench'
36
+ autoload :Debug, 'kiss/debug'
66
37
  autoload :ExceptionReport, 'kiss/exception_report'
67
- autoload :Mailer, 'kiss/mailer'
68
- autoload :Iterator, 'kiss/iterator'
69
- autoload :Login, 'kiss/login'
70
38
  autoload :Form, 'kiss/form'
71
39
  autoload :Format, 'kiss/format'
40
+ autoload :Iterator, 'kiss/iterator'
41
+ autoload :Login, 'kiss/login'
42
+ autoload :Mailer, 'kiss/mailer'
43
+ autoload :Request, 'kiss/request'
44
+ autoload :SequelDatabase, 'kiss/ext/sequel_database'
45
+ autoload :SequelMySQLDataset, 'kiss/ext/sequel_mysql_dataset'
72
46
  autoload :SequelSession, 'kiss/sequel_session'
73
47
  autoload :StaticFile, 'kiss/static_file'
74
- autoload :Bench, 'kiss/bench'
75
- autoload :Debug, 'kiss/debug'
76
-
77
- @@digest = {
78
- :MD5 => "digest/md5",
79
- :RMD160 => "digest/rmd160",
80
- :SHA1 => "digest/sha1",
81
- :SHA256 => "digest/sha2",
82
- :SHA384 => "digest/sha2",
83
- :SHA512 => "digest/sha2"
84
- }
85
- @@digest.each_pair do |type,path|
86
- Digest.autoload type, path
48
+ autoload :Template, 'kiss/template'
49
+
50
+ # Exceptions classes.
51
+ class FileNotFoundError < RuntimeError
52
+ class InvalidAction < self; end
53
+ class Action < self; end
54
+ class Template < self; end
55
+ class Page < self; end
56
+ class Object < self; end
87
57
  end
88
58
 
89
- # attributes below are application-wide
90
- cattr_reader :action_dir, :template_dir, :email_template_dir, :model_dir, :upload_dir,
91
- :evolution_dir, :asset_dir, :public_dir, :environment, :options, :layout, :rack_file
92
-
93
- # attributes below are request-specific
94
- attr_reader :protocol, :host, :request, :exception_cache, :env
59
+ # These supplement the MIME types defined by Rack.
60
+ MIME_TYPES = {
61
+ 'rhtml' => 'text/html'
62
+ }
95
63
 
96
64
  @@default_action = 'index'
97
65
  @@default_cookie_name = 'Kiss'
66
+ @@default_project_dir = '.'
98
67
 
99
- # these supplement the mime types from Rack::File
100
- @@mime_types = {
101
- 'rhtml' => 'text/html'
102
- }
68
+ cattr_accessor :default_project_dir
103
69
 
104
- ### Class Methods
70
+ # application-wide attributes
71
+ _attr_reader :action_dir, :template_dir, :email_template_dir, :model_dir, :upload_dir,
72
+ :evolution_dir, :asset_host, :asset_uri, :asset_dir, :public_dir, :environment,
73
+ :rack_file, :default_action, :exception_log_file, :session_class, :cookie_name,
74
+ :authenticate_all, :authenticate_exclude, :exception_handlers, :project_dir,
75
+ :config, :mailer_config, :mailer_override
76
+
77
+ _attr_accessor :session_setup
105
78
 
79
+ # Registry of classes by file path.
80
+ @_classes = {}
81
+
82
+ ### Class Methods
106
83
  class << self
107
- # Creates new controller instance to handle Rack request.
108
- def call(env)
109
- new.call(env)
110
- end
84
+ alias_method :load, :new
111
85
 
112
- # Runs Kiss application found at project_dir (default: '..'), with options
113
- # read from config files plus additional options if passed in.
114
- def run(options = nil)
115
- begin
116
- if @@options
117
- merge_options(options) if options
118
- else
119
- load(options)
120
- end
121
-
122
- app = self
123
- builder_options = @@options[:rack_builder] || []
124
- rack = Rack::Builder.new do
125
- builder_options.each do |builder_option|
126
- if builder_option.is_a?(Array)
127
- builder_args = builder_option
128
- builder_option = builder_args.shift
129
- else
130
- builder_args = []
131
- end
132
-
133
- unless builder_option.is_a?(Class)
134
- builder_option = Rack.const_get(builder_option.to_s)
135
- end
136
-
137
- use(builder_option,*builder_args)
138
- end
139
-
140
- run app
141
- end.to_app
142
-
143
- handler = @@options[:rack_handler] || Rack::Handler::WEBrick
144
- if !handler.is_a?(Class)
145
- handler = Rack::Handler.const_get(handler.to_s)
146
- end
147
- handler.run(rack,@@options[:rack_handler_options] || {:Port => 4000})
148
- rescue StandardError, LoadError, SyntaxError => e
149
- if @@options[:rack_handler] == :CGI
150
- print "Content-type: text/html\n\n"
151
- print $debug_messages.to_s + Kiss::ExceptionReport.generate(e)
152
- else
153
- print "Content-type: text/plain\n\n"
154
- puts "exception:\n" + e.message
155
- puts "\ntraceback:\n" + e.backtrace.join("\n")
156
- end
157
- end
86
+ def rack(config = {})
87
+ self.new(config).rack
158
88
  end
159
89
 
160
- # Load and set up Kiss application from config file options and
161
- # any passed-in options.
162
- def load(loader_options = nil)
163
- # store cached files
164
- @@file_cache = {}
165
- @@directory_cache = {}
166
- @@file_cache_time = {}
167
-
168
- loader_options ||= {}
169
- # if loader_options is string, then it specifies environment
170
- # else it should be a hash of config options
171
- if loader_options.is_a?(String)
172
- loader_options = { :environment => loader_options }
173
- end
174
-
175
- # environment
176
- @@environment = loader_options[:environment]
177
-
178
- # directories
179
- script_dir = $0.sub(/[^\/]+\Z/,'')
180
- script_dir = '' if script_dir == './'
181
- @@project_dir = script_dir + (loader_options[:project_dir] || loader_options[:root_dir] || '..')
182
- Dir.chdir(@@project_dir)
183
-
184
- @@config_dir = loader_options[:config_dir] || 'config'
185
-
186
- # get environment name from config/environment
187
- if (@@environment.nil?) && File.file?(env_file = @@config_dir+'/environment')
188
- @@environment = File.read(env_file).sub(/\s+\Z/,'')
189
- end
190
-
191
- # init options
192
- @@options = {
193
- :layout => '/_layout'
194
- }
195
- @@lib_dirs = ['lib']
196
- @@gem_dirs = ['gems']
197
- @@require = []
198
- @@authenticate_exclude = ['/login','/logout']
199
-
200
- # common (shared) config
201
- if (File.file?(config_file = @@config_dir+'/common.yml'))
202
- merge_options( YAML::load(File.read(config_file)) )
203
- end
204
- # environment config
205
- if (File.file?(config_file = "#{@@config_dir}/environments/#{@@environment}.yml"))
206
- merge_options( YAML::load(File.read(config_file)) )
207
- end
208
-
209
- merge_options( loader_options )
210
-
211
- # set class vars from options
212
- @@action_dir = @@options[:action_dir] || 'actions'
213
- @@template_dir = @@options[:template_dir] ? @@options[:template_dir] : @@action_dir
214
-
215
- @@asset_dir = @@public_dir = @@options[:asset_dir] || @@options[:public_dir] || 'public_html'
216
-
217
- @@model_dir = @@options[:model_dir] || 'models'
218
-
219
- @@evolution_dir = @@options[:evolution_dir] || 'evolutions'
220
-
221
- @@email_template_dir = @@options[:email_template_dir] || 'email_templates'
222
- @@upload_dir = @@options[:upload_dir] || 'uploads'
223
-
224
- @@cookie_name = @@options[:cookie_name] || @@default_cookie_name
225
- @@default_action = @@options[:default_action] || @@default_action
226
- @@action_root_class = @@options[:action_class] || Class.new(Kiss::Action)
227
-
228
- # exception log
229
- @@exception_log_file = @@options[:exception_log] ? ::File.open(@@options[:exception_log],'a') : nil
230
-
231
- # default layout
232
- @@layout = @@options[:layout]
233
-
234
- # app_url: URL of the app actions root
235
- @@protocol = ENV['HTTPS'] == 'on' ? 'https' : 'http'
236
- @@app_host = @@options[:app_host] ? (@@protocol + '://' + @@options[:app_host]) : ''
237
- @@app_uri = @@options[:app_uri] || ''
238
- @@app_url = @@app_host + @@app_uri
239
-
240
- # asset host: hostname of static assets
241
- # (remove http:// prefix if present)
242
- @@asset_host = @@options[:asset_host]
243
- @@asset_host.sub!(/^http:\/\//,'') if @@asset_host
244
-
245
- # public_uri: uri of requests to serve from public_dir
246
- @@asset_uri = @@options[:asset_uri] || @@options[:public_uri] || ''
247
- @@rack_file = Rack::File.new(@@asset_dir) if @@asset_uri
248
-
249
- # include lib dirs
250
- $LOAD_PATH.unshift(*( @@lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
251
-
252
- # add gem dir to rubygems search path
253
- Gem.path.unshift(*( @@gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
254
-
255
- # require libs
256
- @@require.flatten.each {|lib| require lib }
257
-
258
- # session class
259
- @@session_class = @@options[:session_class] ?
260
- (@@session_class = (@@options[:session_class].class == Class) ?
261
- @@options[:session_class] : @@options[:session_class].to_const
262
- ) : nil
263
-
264
- # load extensions to action class
265
- action_extension_path = @@action_dir + '/_action.rb'
266
- if File.file?(action_extension_path)
267
- @@action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
268
- end
269
-
270
- @@action_classes = {}
271
-
272
- # database
273
- @@database_config = @@options[:database]
274
- @@database_pool = []
275
-
276
- self
277
- end
278
-
279
- # Returns true if specified path is a directory.
280
- # Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
281
- def directory_exists?(dir)
282
- @@options[:file_cache_no_reload] ? (
283
- @@directory_cache.has_key?(path) ?
284
- @@directory_cache[dir] :
285
- @@directory_cache[dir] = File.directory?(dir)
286
- ) : File.directory?(dir)
287
- end
288
-
289
- # Merges specified options into previously defined/merged Kiss options.
290
- def merge_options(config_options)
291
- if config_options
292
- if env_vars = config_options.delete(:ENV)
293
- env_vars.each_pair {|k,v| ENV[k] = v }
294
- end
295
- if lib_dirs = config_options.delete(:lib_dirs)
296
- @@lib_dirs.push( lib_dirs )
297
- end
298
- if gem_dirs = config_options.delete(:gem_dirs)
299
- @@gem_dirs.push( gem_dirs )
300
- end
301
- if require_libs = config_options.delete(:require)
302
- @@require.push( require_libs )
303
- end
304
- if auth_exclude = config_options.delete(:authenticate_exclude)
305
- @@authenticate_exclude.push( *(auth_exclude.map { |action| action =~ /\A\// ? action : '/'+action }) )
306
- end
307
-
308
- @@options.merge!( config_options )
309
- end
90
+ def run(config = {})
91
+ self.new(config).run
310
92
  end
311
93
 
312
94
  # Converts passed-in filename to absolute path if it does not start with '/'.
313
95
  def absolute_path(filename)
314
- filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
315
- end
316
-
317
- # prepend addition just inside the specified tag of document
318
- def html_prepend(addition,document,tag)
319
- document = document.body unless document.is_a?(String)
320
- document.sub(/(\<#{tag}[^\>]*\>)/i, '\1'+addition)
321
- end
322
-
323
- # Returns string representation of object with HTML entities escaped.
324
- def html_escape(obj)
325
- Rack::Utils.escape_html(
326
- obj.is_a?(String) ? obj : obj.inspect
327
- ).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
328
- end
329
-
330
- # Escapes string for use in URLs.
331
- def url_escape(string)
332
- # encode space to '+'; don't encode letters, numbers, periods
333
- string.gsub(/([^A-Za-z0-9\.])/) { sprintf("%%%02X", $&.unpack("C")[0]) }
96
+ ( filename[0,1] == '/' ) ? filename : "#{Dir.pwd}/#{filename}"
334
97
  end
335
98
 
336
99
  # Returns MIME type corresponding to passed-in extension.
337
100
  def mime_type(extension)
338
- Rack::File::MIME_TYPES[extension] || @@mime_types[extension]
101
+ extension = extension.to_s
102
+ rack_mime_types = Rack::Mime::MIME_TYPES rescue Rack::File::MIME_TYPES
103
+ rack_mime_types[extension] || rack_mime_types['.' + extension] || Kiss::MIME_TYPES[extension]
339
104
  end
340
105
 
341
- # Returns Digest class used to generate digest of specified type.
342
- def digest_class(type)
343
- type = type.to_sym
344
- @@digest[type] ? Digest.const_get(type) : nil
106
+ # Register a class by its file path, to enable context_class to work.
107
+ def register_class_path(klass, path)
108
+ @_classes[path] = klass
345
109
  end
346
110
 
347
- # Converts date strings from m/d/y to y/m/d. Useful for MySQL.
348
- def mdy_to_ymd(date)
349
- return '0000-00-00' unless date && date =~ /\S/
350
- date.sub!(/\A\D+/,'')
351
- date.sub!(/\D+\Z/,'')
352
-
353
- month, day, year = date.split(/\D+/,3).each {|s| s.to_i}
354
-
355
- return 0 unless month && day
356
-
357
- current_year = Time.now.year
358
- if !year || year.length == 0
359
- # use current year if year is missing.
360
- year = current_year
361
- else
362
- # convert two-digit years to four-digit years
363
- year = year.to_i
364
- if year < 100
365
- year += 1900
366
- year += 100 if year < current_year - 95
367
- end
368
- end
369
-
370
- return sprintf("%04d-%02d-%02d",year,month.to_i,day.to_i)
371
- end
372
-
373
- # Validates value against specified format.
374
- # If required is true, value must contain a non-whitespace character.
375
- # If required is false, value need not match format if and only if value contains only whitespace.
376
- def validate_value(value, format, required = false, label = nil)
377
- if required && (value !~ /\S/)
378
- # value required
379
- raise "#{label || 'value'} required"
380
- elsif format && (value =~ /\S/)
381
- format = Kiss::Format.lookup(format)
382
-
383
- begin
384
- format.validate(value)
385
- rescue Kiss::Format::ValidateError => e
386
- raise e.class, "#{label} validation error: #{e.message}"
111
+ # Finds the class defined by the file path of the execution context.
112
+ def context_class
113
+ caller.each do |frame|
114
+ if klass = @_classes[frame.sub(/\:.*/, '')]
115
+ return klass
387
116
  end
388
117
  end
389
118
  end
390
-
391
- # Generates string of random text of the specified length.
392
- def random_text(length)
393
- chars = ('A'..'Z').to_a + ('0'..'9').to_a # array
394
-
395
- text = ''
396
- size = chars.size
397
- length.times { text += chars[rand(size)] }
398
-
399
- text
400
- end
401
-
402
- # Given a file path, caches or returns the file's contents or the return value of
403
- # the passed block applied to the file's contents.
404
- # If file is not found, the file's contents are nil.
405
- def file_cache(path, report_change = false)
406
- raise 'nil file path cannot be cached' unless path
407
-
408
- if @@file_cache.has_key?(path) && @@options[:file_cache_no_reload]
409
- # already loaded this path, and no_reload option is on; don't re-cache
410
- change = false
411
- elsif !@@file_cache.has_key?(path)
412
- # haven't loaded this path yet
413
- change = true
414
- if !File.file?(path)
415
- # nil path, of file doesn't exist
416
- @@file_cache_time[path] = nil
417
- contents = nil
418
- else
419
- # file exists; mark cache time and read file
420
- @@file_cache_time[path] = Time.now
421
- contents = File.read(path)
422
- end
423
- else
424
- # we've read this path before; may need to re-cache
425
- if !File.file?(path)
426
- if @@file_cache_time[path]
427
- # file cached as existing but has been removed; update cache to show no file
428
- change = true
429
- @@file_cache_time[path] = nil
430
- contents = nil
431
- else
432
- change = false
433
- end
434
- elsif !@@file_cache_time[path] ||
435
- @@file_cache_time[path] < File.mtime(path) ||
436
- ( File.symlink?(path) && (@@file_cache_time[path] < File.lstat(path).mtime) )
437
- # cache shows file missing, or file has been modified since cached
438
- change = true
439
- @@file_cache_time[path] = Time.now
440
- contents = File.read(path)
441
- else
442
- # cached file is still current; don't re-cache
443
- change = false
444
- end
445
- end
446
-
447
- if change
448
- @@file_cache[path] ||= block_given? ? yield(contents) : contents
449
- end
450
-
451
- report_change ? [@@file_cache[path], change] : @@file_cache[path]
452
- end
453
- end # end class methods
454
-
455
- ### Instance Methods
456
-
457
- # Creates a new controller instance, and also configures the application with the
458
- # specified options.
459
- def initialize(options = nil)
460
- if @@options
461
- self.class.merge_options(options) if options
462
- else
463
- self.class.load(options)
119
+ end # class methods
120
+
121
+
122
+ # Creates a new application controller instance, and also configures the
123
+ # application from config file options and any passed-in options.
124
+ def initialize(options = {})
125
+ # init config
126
+ @_config = {
127
+ :layout => '/_layout',
128
+ :file_cache_reload => true
129
+ }
130
+ @_lib_dirs = ['lib']
131
+ @_gem_dirs = ['gems']
132
+ @_require = []
133
+ @_authenticate_exclude = ['/login', '/logout']
134
+ @_exception_handlers = {}
135
+ @_mailer_config = {}
136
+ @_mailer_override = {}
137
+
138
+ # store for cached files and directories
139
+ @_file_cache = {}
140
+ @_directory_cache = {}
141
+ @_file_cache_time = {}
142
+
143
+
144
+ # If options is string, then it specifies an environment
145
+ # (else it should be a hash of config options)
146
+ options = { :environment => options } if options.is_a?(String)
147
+
148
+ # project dir
149
+ # all other files and directories are relative to the project dir
150
+ Dir.chdir(options[:project_dir] || options[:root_dir] || @@default_project_dir)
151
+ # save current path to force return there in case an action changes directory
152
+ @_project_dir = Dir.pwd
153
+
154
+ # directory containing the config files
155
+ @_config_dir = options[:config_dir] || 'config'
156
+
157
+ # get environment name from options or config/environment
158
+ @_environment = options[:environment] || if File.file?(env_file = @_config_dir + '/environment')
159
+ File.read(env_file).sub(/\s+\Z/, '')
464
160
  end
465
161
 
466
- @exception_cache = {}
467
- @debug_messages = []
468
- @benchmarks = []
469
- @files_cached_this_request = {}
470
- end
471
-
472
- def invoke_action(path,params,render_options = {})
473
- action, action_handler = get_action_handler(path,params)
474
- catch :kiss_action_done do
475
- action_handler.expand_login
476
-
477
- if @@options[:authenticate_all]
478
- if (!@@authenticate_exclude.is_a?(Array) ||
479
- @@authenticate_exclude.select {|a| a == action}.size == 0)
480
- action_handler.authenticate
481
- end
482
- end
162
+ # read common (shared) config
163
+ merge_config_file(@_config_dir + '/common.yml')
164
+ # read environment config
165
+ merge_config_file(@_config_dir + "/environments/#{@_environment}.yml") if @_environment
483
166
 
484
- action_handler.call
485
- action_handler.render(render_options)
486
- end
487
- action_handler
488
- end
167
+ # merge options passed in to override config files
168
+ merge_config( options )
489
169
 
490
- # Processes and responds to a request received via Rack. Should only be called once
491
- # for each controller instance (i.e. each controller instance should handle at most
492
- # one request, then be discarded). Returns array of response code, headers, and body.
493
- def call(env)
494
- @env = env
170
+ # set app instance variables from config data and defaults
171
+ @_action_dir = @_config[:action_dir] || 'actions'
172
+ @_template_dir = (@_config[:template_dir] ? @_config[:template_dir] : @_action_dir)
173
+ @_model_dir = @_config[:model_dir] || 'models'
174
+ @_evolution_dir = @_config[:evolution_dir] || 'evolutions'
495
175
 
496
- # if @@rack_file && (
497
- # (env["PATH_INFO"] == '/favicon.ico') ||
498
- # (env["PATH_INFO"].sub!(Regexp.new("\\A#{@@asset_uri}"),''))
499
- # )
500
- # return @@rack_file.call(env)
501
- # end
176
+ @_asset_dir = @_public_dir = @_config[:asset_dir] || @_config[:public_dir] || 'public_html'
177
+ @_email_template_dir = @_config[:email_template_dir] || 'email_templates'
178
+ @_upload_dir = @_config[:upload_dir] || 'uploads'
502
179
 
503
- # catch and report exceptions in this block
180
+ @_cookie_name = @_config[:cookie_name] || @@default_cookie_name
181
+ @_default_action = @_config[:default_action] || @@default_action
504
182
 
505
- code, headers, body = begin
506
- @request = Rack::Request.new(env)
507
- @response = Rack::Response.new
183
+ # exception log
184
+ @_exception_log_file = @_config[:exception_log] ? ::File.open(@_config[:exception_log], 'a') : nil
508
185
 
509
- @protocol, @app_host = (@request.server rescue '').split(/\:\/\//, 2)
510
- @app_host = @@options[:app_host] if @@options[:app_host]
511
- @app_uri = @@options[:app_uri] || @request.script_name || ''
186
+ # authenticate all actions?
187
+ @_authenticate_all = @_config[:authenticate_all]
512
188
 
513
- @host ||= @request.host rescue ''
189
+ # don't require authentication on exception actions
190
+ @_authenticate_exclude << @_config.exception_action if @_config.exception_action
191
+ @_authenticate_exclude << @_config.file_not_found_action if @_config.file_not_found_action
514
192
 
515
- path = @request.path_info || '/'
516
- params = @request.params
517
-
518
- catch :kiss_request_done do
519
- action_handler = invoke_action(path,params)
520
- extension = action_handler.extension
521
-
522
- if content_type = options[:content_type] || (extension ? Kiss.mime_type(extension) : nil)
523
- @response['Content-Type'] = "#{content_type}; #{options[:document_encoding] || 'utf-8'}"
524
- end
525
-
526
- send_response(action_handler.output, action_handler.output_options)
527
- end
528
- finalize_session if @session
529
-
530
- @response.finish
531
- rescue StandardError, LoadError, SyntaxError => e
532
- body = Kiss::ExceptionReport.generate(e, env, @exception_cache, @db ? @db.last_query : nil)
533
- if @@exception_log_file
534
- @@exception_log_file.print(body + "\n--- End of exception report --- \n\n")
535
- end
536
- [500, {
537
- "Content-Type" => "text/html",
538
- "Content-Length" => body.length.to_s,
539
- "X-Kiss-Error-Type" => e.class.name,
540
- "X-Kiss-Error-Message" => e.message.sub(/\n.*/m,'')
541
- }, body]
542
- end
193
+ # app host: default hostname of application
194
+ @_app_host = @_config[:app_host]
195
+ # app uri: default URI of application
196
+ @_app_uri = @_config[:app_uri]
543
197
 
544
- if @debug_messages.size > 0
545
- extend Kiss::Debug
546
- body = prepend_debug(body)
547
- headers['Content-Length'] = body.length.to_s
548
- end
198
+ # asset host: hostname of static assets
199
+ @_asset_host = @_config[:asset_host]
200
+
201
+ # public_uri: URI of requests to serve from public_dir
202
+ @_asset_uri = @_config[:asset_uri] || @_config[:public_uri] || nil
203
+ @_rack_file = Rack::File.new(@_asset_dir) if @_asset_uri
549
204
 
550
- if @benchmarks.size > 0
551
- stop_benchmark
552
- extend Kiss::Bench
553
- body = prepend_benchmarks(body)
554
- headers['Content-Length'] = body.length.to_s
555
- end
205
+ # add lib dirs to load path
206
+ $LOAD_PATH.unshift(*( @_lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
207
+
208
+ # add gem dirs to rubygems search path
209
+ Gem.path.unshift(*( @_gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
210
+
211
+ # require specified libs
212
+ @_require.flatten.each {|lib| require lib }
556
213
 
557
- if @db
558
- @db.kiss_controller = nil
559
- @@database_pool.push(@db)
560
- end
214
+ # session class
215
+ @_session_class = @_config[:session_class]
216
+ @_session_class = @_session_class.to_const if @_session_class && !@_session_class.is_a?(Class)
217
+
218
+ # database
219
+ @_database_config = @_config[:database]
220
+ @_database_pool = []
561
221
 
562
- [code,headers,body]
222
+ self
563
223
  end
564
224
 
225
+ private
226
+
227
+ # TODO: Turn exception handlers into a new class;
228
+ # move exception handling methods from Request to the new class
229
+ def prepare_exception_handler(value)
230
+ # start with defaults
231
+ {
232
+ :send_email => true
233
+ }.merge(
234
+ # add data from the handler config value
235
+ if value.is_a?(Hash)
236
+ value
237
+ elsif value.is_a?(Array)
238
+ value.hash_with_keys(:action, :send_email)
239
+ elsif value.is_a?(String)
240
+ { :action => value }
241
+ else
242
+ raise 'Invalid exception handler config setting.'
243
+ end
244
+ )
245
+ end
565
246
 
566
- ##### ACTION METHODS #####
247
+ def merge_config_file(filename)
248
+ if File.file?(filename)
249
+ merge_config( YAML::load(File.read(filename)) )
250
+ end
251
+ end
567
252
 
568
- # Parses request URI to determine action path and arguments, then
569
- # instantiates action class to create action handler.
570
- def get_action_handler(path,params)
571
- action_subdir = '/'
572
- action_uri = ''
573
- action = nil
574
- args = []
575
-
576
- redirect_url(app_url + '/') if path == ''
577
-
578
- path += @@default_action if path =~ /\/\Z/
579
-
580
- parts = path.sub(/^\/*/,'').split('/')
581
-
582
- action_class = Kiss::Action.get_subclass('/', @@action_dir + '/_action.rb', self) ||
583
- Kiss::Action
584
-
585
- while part = parts.shift
586
- action_uri += '/' + part
587
-
588
- if part =~ /\A(\d+)\Z/ || part =~ /\A\=([\w\.\-]+)\Z/
589
- args << $1
590
- next
253
+ # Merges specified config options into previously defined/merged Kiss config.
254
+ def merge_config(new_config)
255
+ if new_config
256
+ if env_vars = new_config.delete(:ENV)
257
+ env_vars.each_pair {|k, v| ENV[k] = v }
258
+ end
259
+ if lib_dirs = new_config.delete(:lib_dirs)
260
+ @_lib_dirs += lib_dirs
261
+ end
262
+ if gem_dirs = new_config.delete(:gem_dirs)
263
+ @_gem_dirs += gem_dirs
264
+ end
265
+ if require_libs = new_config.delete(:require)
266
+ @_require += require_libs
591
267
  end
592
268
 
593
- raise 'bad action path' if part !~ /\A[a-z][\w\-\.]*\Z/i
594
-
595
- test_path = @@action_dir + action_subdir + part
596
- if Kiss.directory_exists?(test_path)
597
- action_subdir += part + '/'
598
- action_class = action_class.get_subclass(
599
- part + '/',
600
- @@action_dir + action_subdir + '_action.rb',
601
- self
602
- )
603
- next
269
+ if auth_exclude = new_config.delete(:authenticate_exclude)
270
+ auth_exclude = auth_exclude.map {|action| (action[0,1] == '/') ? action : '/' + action }
271
+ @_authenticate_exclude += auth_exclude
604
272
  end
605
273
 
606
- if part =~ /\A(.+)\.(\w+)\Z/
607
- extension = $2
608
- part = $1
609
- else
610
- extension = 'rhtml'
274
+ if exception_handler = new_config.delete(:exception_handler)
275
+ exception_handlers[Exception] = prepare_exception_handler(value)
276
+ end
277
+ if exception_handlers = new_config.delete(:exception_handlers)
278
+ exception_handlers.each_pair do |key, value|
279
+ @_exception_handlers[key == :generic ? Exception : key.to_s.to_const] = prepare_exception_handler(value)
280
+ end
611
281
  end
612
282
 
613
- action = action_subdir + part
283
+ if mailer_config = new_config.delete(:mailer)
284
+ @_mailer_override.merge!(mailer_config.delete(:override) || {})
285
+ @_mailer_config.merge!(mailer_config)
286
+ end
287
+ if mailer_override = new_config.delete(:mailer_override)
288
+ @_mailer_override.merge!(mailer_override)
289
+ end
614
290
 
615
- action_class = action_class.get_subclass(part, @@action_dir + action + '.rb', self)
616
- break
291
+ @_config.merge!( new_config )
617
292
  end
618
-
619
- # if no action, must have traversed all parts to a directory
620
- # add a trailing slash and try again
621
- redirect_url(app_url + action_subdir) unless action
622
-
623
- # keep rest of path_info in args
624
- args.push(*parts)
625
-
626
- # return action path and action handler (instance of action class)
627
- [action, action_class.new(self,action,action_uri,action_subdir,extension,args,params)]
628
293
  end
629
294
 
295
+ public
630
296
 
631
- ##### FILE METHODS #####
632
-
633
- # If file has already been cached in handling the current request, retrieve from cache
634
- # and do not check filesystem for updates. Else cache file via Kiss.file_cache.
635
- def file_cache(path, *args, &block)
636
- return @@file_cache[path] if @files_cached_this_request[path]
637
- @files_cached_this_request[path] = true
297
+ def rack(config = nil)
298
+ merge_config(config)
299
+
300
+ app = self
301
+ builder_options = @_config[:rack_builder] || []
302
+ rack = Rack::Builder.new do
303
+ builder_options.each do |builder_option|
304
+ if builder_option.is_a?(Array)
305
+ builder_args = builder_option
306
+ builder_option = builder_args.shift
307
+ else
308
+ builder_args = []
309
+ end
638
310
 
639
- self.class.file_cache(path, *args, &block)
311
+ unless builder_option.is_a?(Class)
312
+ builder_option = Rack.const_get(builder_option.to_s)
313
+ end
314
+
315
+ use(builder_option, *builder_args)
316
+ end
317
+
318
+ run app
319
+ end.to_app
640
320
  end
641
321
 
322
+ # Runs Kiss application found at project_dir (default: '..'), with config
323
+ # read from config files plus additional options if passed in.
324
+ def run(options = nil)
325
+ merge_config(options)
326
+
327
+ handler = @_config[:rack_handler] || Rack::Handler::WEBrick
328
+ handler = Rack::Handler.const_get(handler.to_s) unless handler.is_a?(Class)
329
+
330
+ handler.run(rack, @_config[:rack_handler_options] || {:Port => 4000})
331
+ end
642
332
 
643
- ##### DATABASE METHODS #####
333
+ # Creates new controller instance to handle Rack request.
334
+ def call(env)
335
+ Kiss::Request.new(env, self, @_config).call(env)
336
+ end
644
337
 
645
338
  # Acquires and returns a database connection object from the connection pool,
646
339
  # opening a new connection if the pool is empty.
647
- #
648
- # Tip: `db' is a shorthand alias for `database'.
649
340
  def database
650
- @db ||= @@database_pool.shift || begin
651
- raise 'database config missing' unless @@database_config
341
+ @_database_pool.shift || begin
342
+ raise 'database config missing' unless @_database_config
343
+
344
+ # open database connection
345
+ db = Sequel.connect @_database_config
346
+ load_db_class_extensions(db.class)
652
347
 
653
- # open database connection (if not already open)
654
- @db = @@database_config.is_a?(String) ? (Sequel.open @@database_config) :
655
- @@database_config.is_a?(Hash) ? (Sequel.open @@database_config) : @@database_config
348
+ # create model cache for this database connection
349
+ db.kiss_controller = self
350
+ db.kiss_model_cache = Kiss::ModelCache.new(db, @_model_dir)
656
351
 
657
- @@db_extras_loaded ||= begin
658
- @db.class.class_eval do
659
- attr_accessor :kiss_controller, :kiss_model_cache
660
-
661
- @last_query = nil
662
- def last_query #:nodoc:
663
- @last_query
664
- end
352
+ db
353
+ end
354
+ end
355
+ alias_method :db, :database
665
356
 
666
- alias_method :execute_old, :execute
667
- def execute(sql, *args, &block) #:nodoc:
668
- @last_query = sql
669
- execute_old(sql, *args, &block)
670
- end
671
- end
672
-
673
- require 'kiss/model'
674
- Kiss::ModelCache.model_dir = @@model_dir
675
-
676
- if @db.class.name == 'Sequel::MySQL::Database'
677
- # add fetch_arrays, all_arrays methods
678
- require 'kiss/sequel_mysql'
679
- # turn off convert_tinyint_to_bool, unless options say otherwise
680
- Sequel.convert_tinyint_to_bool = false unless @@options[:convert_tinyint_to_bool]
681
- end
357
+ def load_db_class_extensions(db_class)
358
+ @_db_class_extensions_loaded ||= {}
359
+ @_db_class_extensions_loaded[db_class] ||= begin
360
+ db_class.class_eval { include Kiss::SequelDatabase }
361
+
362
+ if db_class.name == 'Sequel::MySQL::Database'
363
+ # add fetch_arrays, all_arrays methods
364
+ Sequel::MySQL::Dataset.class_eval { include Kiss::SequelMySQLDataset }
682
365
 
683
- true
366
+ # turn off convert_tinyint_to_bool, unless app config says otherwise
367
+ Sequel::MySQL.convert_tinyint_to_bool = false unless @_config[:convert_tinyint_to_bool]
684
368
  end
685
369
 
686
- # TODO: rewrite evolution file exists check for speed
687
- check_evolution_number
688
-
689
- # create model cache for this database connection
690
- @db.kiss_model_cache = Kiss::ModelCache.new(@db)
691
- @db.kiss_controller = self
692
-
693
- @db
370
+ require 'kiss/model'
371
+ true
694
372
  end
695
373
  end
696
- alias_method :db, :database
697
374
 
698
375
  # Kiss Model cache, used to invoke and store Kiss database models.
699
376
  #
@@ -708,173 +385,143 @@ class Kiss
708
385
  end
709
386
  alias_method :dbm, :models
710
387
 
711
- # Returns Sequel dataset to evolution_number table, which specifies app's current evolution number.
712
- # Creates evolution_number table if it does not exist.
713
- def evolution_number_table
714
- unless db.table_exists?(:evolution_number)
715
- db.create_table :evolution_number do
716
- column :version, :integer, :null=> false
717
- end
718
- db[:evolution_number].insert(:version => 0)
719
- end
720
- db[:evolution_number]
721
- end
722
-
723
- # Returns app's current evolution number.
724
- def evolution_number
725
- evolution_number_table.first.version
388
+ def return_database(db)
389
+ @_database_pool.push(db)
726
390
  end
727
-
728
- # Sets app's current evolution number.
729
- def evolution_number=(version)
730
- load unless @@options
731
- evolution_number_table.update(:version => version)
732
- end
733
-
734
- # Check whether there exists a file in evolution_dir whose number is greater than app's
735
- # current evolution number. If so, raise an error to indicate need to apply new evolutions.
736
- def check_evolution_number
737
- version = evolution_number
738
- if Kiss.directory_exists?(@@evolution_dir) &&
739
- Dir.entries(@@evolution_dir).select { |f| f =~ /\A0*#{version+1}_/ }.size > 0
740
- raise "current evolution #{version} is outdated; apply evolutions or update evolution number"
741
- end
742
- end
743
-
744
391
 
745
- ##### SESSION METHODS #####
746
-
747
- # Retrieves or generates session data object, based on session ID from cookie value.
748
- def session
749
- @session ||= begin
750
- @@session_class ? begin
751
- @@session_setup ||= begin
752
- # setup session storage
753
- @@session_class.setup_storage(self)
754
- true
755
- end
756
-
757
- session = @@session_class.persist(self,@request.cookies[@@cookie_name])
758
- @session_fingerprint = Marshal.dump(session.data).hash
759
-
760
- cookie_vars = {
761
- :value => session.values[:session_id],
762
- :path => @@options[:cookie_path] || @app_uri,
763
- :domain => @@options[:cookie_domain] || @request.host
764
- }
765
- cookie_vars[:expires] = Time.now + @@options[:cookie_lifespan] if @@options[:cookie_lifespan]
766
-
767
- # set_cookie here or at render time
768
- @response.set_cookie @@cookie_name, cookie_vars
769
-
770
- session
771
- end : {}
392
+ # Gets the number of the last file in the evolution dir, or 0 if the directory
393
+ # does not exist.
394
+ def last_evolution_file_number
395
+ version = 0
396
+
397
+ if directory_exists?(@_evolution_dir)
398
+ digits = 1
399
+ while ( entries = Dir.glob("#{@_evolution_dir}/#{'[0-9]' * digits}*") ).size > 0
400
+ version = entries.sort.last.sub(/.*\//, '').sub(/\D.*/, '').to_i
401
+ digits += 1
402
+ end
772
403
  end
773
- end
774
-
775
- # Saves session to session store, if session data has changed since load.
776
- def finalize_session
777
- @session.save if @session_fingerprint != Marshal.dump(@session.data).hash
778
- end
779
-
780
- # Returns a Kiss::Login object containing data from session.login.
781
- def login
782
- @login ||= Kiss::Login.new(session)
783
- end
784
-
785
-
786
- ##### OUTPUT METHODS #####
787
-
788
- # Outputs a Kiss::StaticFile object as response to Rack.
789
- # Used to return static files efficiently.
790
- def send_file(path, mime_type = nil)
791
- @response = Kiss::StaticFile.new(path,mime_type)
792
404
 
793
- throw :kiss_request_done
405
+ version
794
406
  end
795
407
 
796
- # Prepares Rack::Response object to return application response to Rack.
797
- # Raises Kiss::ActionDone exception to bypass caller stack and return directly
798
- # to Kiss#call.
799
- def send_response(output = '',options = {})
800
- @response['Content-Length'] = output.length.to_s
801
- @response.body = output
802
-
803
- throw :kiss_request_done
408
+ # Returns an array of evolution filenames (relative to project dir) matching
409
+ # evolution number specified by index.
410
+ def evolution_file(index)
411
+ # find files matching ev_dir/.*next_version_number
412
+ files = Dir.glob("#{@_evolution_dir}/*#{index}[^0-9]*").
413
+ # make sure we have a match for ev_dir/0*next_version_number
414
+ select {|f| f =~ /\/0*#{index}[_\.][^\/]*\Z/ }
804
415
 
805
- # back to Kiss#call, which finalizes session and returns @response
806
- # (throws exception if no @response set)
807
- end
808
-
809
- # Sends HTTP 302 response to redirect client browser agent to specified URL.
810
- def redirect_url(url)
811
- @response.status = 302
812
- @response['Location'] = url
813
- @response.body = ''
416
+ raise "multiple evolution files for evolution number #{index}" if files.size > 1
814
417
 
815
- throw :kiss_request_done
418
+ files[0]
816
419
  end
817
420
 
818
421
 
819
- ##### DEBUG/BENCH OUTPUT #####
820
-
821
- # Adds debug message to inspect object. Debug messages will be shown at top of
822
- # application response body.
823
- def debug(object, context = Kernel.caller[0])
824
- @debug_messages.push( [object.inspect, context] )
825
- object
826
- end
827
- alias_method :trace, :debug
828
-
829
- # Starts a new benchmark timer, with optional label. Benchmark results will be shown
830
- # at top of application response body.
831
- def bench(label = nil, context = Kernel.caller[0])
832
- stop_benchmark(context)
833
- @benchmarks.push(
834
- :label => label,
835
- :start_time => Time.now,
836
- :start_context => context
837
- )
422
+ # Returns true if specified path is a directory.
423
+ # Always check filesystem if file_cache_reload option is set; otherwise, cache result.
424
+ def directory_exists?(dir)
425
+ @_config[:file_cache_reload] ? File.directory?(dir) : (
426
+ @_directory_cache.has_key?(dir) ?
427
+ @_directory_cache[dir] :
428
+ @_directory_cache[dir] = File.directory?(dir)
429
+ )
838
430
  end
839
431
 
840
- # Stops last benchmark timer, if still running.
841
- def stop_benchmark(end_context = nil)
842
- if @benchmarks[-1] && !@benchmarks[-1][:end_time]
843
- @benchmarks[-1][:end_time] = Time.now
844
- @benchmarks[-1][:end_context] = end_context
432
+ # TODO: Move file and directory caching to new class(es)
433
+ # TODO: File cache should store hashes of file info, keyed by path,
434
+ # instead of using separate hashes (cache time, contents, etc)
435
+ #
436
+ # TODO: FIX BUG: The file cache keeps old classes after they are removed from
437
+ # the action/model class hierarchies. Will the class hierarchies force reload
438
+ # these paths, or get the old cached versions? Answer: They reload the classes
439
+ # because they only cache the source text in the file cache. They cache the
440
+ # classes in the hierarchy.
441
+ #
442
+ # TODO: Need a generic class for Action/Template/Model class hierarchy caches.
443
+ #
444
+ # Given a file path, caches or returns the file's contents or the return value of
445
+ # the passed block applied to the file's contents.
446
+ # If file is not found, the file's contents are nil.
447
+ def file_cache(path = nil, return_changed_state = false)
448
+ return @_file_cache unless path
449
+
450
+ cache_changed = false
451
+
452
+ if @_file_cache.has_key?(path)
453
+ # we've loaded this path before
454
+ if @_config[:file_cache_reload]
455
+ # check to see if there's been a change that needs to be reloaded
456
+ if !File.file?(path)
457
+ if @_file_cache_time[path]
458
+ # file cached as existing but has been removed; update cache to show no file
459
+ cache_changed = true
460
+ contents = nil
461
+ end
462
+ elsif !@_file_cache_time[path] ||
463
+ @_file_cache_time[path] < File.mtime(path) ||
464
+ ( File.symlink?(path) && (@_file_cache_time[path] < File.lstat(path).mtime) )
465
+ # cache shows file missing, or file has been modified since cached
466
+ cache_changed = true
467
+ contents = File.read(path)
468
+ end
469
+ end
470
+ else
471
+ # haven't loaded this path yet
472
+ cache_changed = true
473
+ if !File.file?(path)
474
+ # nil path, of file doesn't exist
475
+ contents = nil
476
+ else
477
+ # file exists; mark cache time and read file
478
+ contents = File.read(path)
479
+ end
480
+ end
481
+
482
+ if cache_changed
483
+ @_file_cache_time[path] = contents ? Time.now : nil
484
+ @_file_cache[path] = block_given? ? yield(contents) : contents
845
485
  end
486
+
487
+ return_changed_state ? [@_file_cache[path], cache_changed] : @_file_cache[path]
846
488
  end
847
489
 
490
+ # Returns new Kiss::Mailer object using specified options.
491
+ def new_email(options = {})
492
+ Kiss::Mailer.new({
493
+ :controller => self,
494
+ :request => self
495
+ }.merge(options))
496
+ end
848
497
 
849
- ##### OTHER METHODS #####
498
+ def send_email(options = {})
499
+ new_email(options).send
500
+ end
850
501
 
851
502
  # Returns URL/URI of app root (corresponding to top level of action_dir).
503
+ # Part of Kiss class to be available to kiss irb.
852
504
  def app_url(options = {})
853
- options.empty? ? (@app_url ||= @protocol + '://' + @app_host + @app_uri) :
854
- (options[:protocol] || @protocol) + '://' + (options[:host] || @app_host) +
855
- (options[:uri] || @app_uri)
856
- end
857
-
858
- # Returns URL/URI of app's static assets (asset_host or public_uri).
859
- def pub_url(options = {})
860
- options.empty? ?
861
- (@pub ||= (@@asset_host ? @protocol + '://' + @@asset_host : '') + @@asset_uri) :
862
- (options[:protocol] || @protocol) + '://' + (options[:host] || @@asset_host) +
863
- (options[:uri] || @@asset_uri)
505
+ # cache return values by unique options input
506
+ @_app_url_cache ||= {}
507
+ @_app_url_cache[options.inspect] ||= begin
508
+ url_settings = {
509
+ :protocol => @_protocol || 'http',
510
+ :host => @_app_host,
511
+ :uri => @_app_uri
512
+ }.merge(options)
513
+
514
+ raise 'host missing' unless url_settings[:host]
515
+
516
+ "#{url_settings[:protocol]}://#{url_settings[:host]}#{url_settings[:uri]}"
517
+ end
864
518
  end
865
- alias_method :asset_url, :pub_url
866
519
 
867
- # Adds data to be displayed in "Cache" section of Kiss exception reports.
868
- def exception_cache(data = nil)
869
- @exception_cache.merge!(data) if data
870
- @exception_cache
520
+ def login
521
+ {}
871
522
  end
872
- alias_method :set_exception_cache, :exception_cache
873
523
 
874
- # Returns new Kiss::Mailer object using specified options.
875
- def new_email(options = {})
876
- mailer = Kiss::Mailer.new(options)
877
- mailer.controller = self
878
- mailer
524
+ def debug(obj, *args)
525
+ gdebug obj
879
526
  end
880
527
  end