kiss 1.1 → 1.7

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.
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