kiss 0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +19 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/lib/kiss/action.rb +204 -0
- data/lib/kiss/controller_accessors.rb +101 -0
- data/lib/kiss/exception_report.rb +359 -0
- data/lib/kiss/form/field.rb +296 -0
- data/lib/kiss/form.rb +414 -0
- data/lib/kiss/format.rb +80 -0
- data/lib/kiss/hacks.rb +140 -0
- data/lib/kiss/iterator.rb +56 -0
- data/lib/kiss/mailer.rb +92 -0
- data/lib/kiss/model.rb +114 -0
- data/lib/kiss/rack/bench.rb +131 -0
- data/lib/kiss/rack/email_errors.rb +64 -0
- data/lib/kiss/rack/facebook.rb +28 -0
- data/lib/kiss/rack/file_not_found.rb +42 -0
- data/lib/kiss/rack/log_exceptions.rb +23 -0
- data/lib/kiss/rack/show_debug.rb +82 -0
- data/lib/kiss/rack/show_exceptions.rb +27 -0
- data/lib/kiss/sequel_mysql.rb +23 -0
- data/lib/kiss/sequel_session.rb +166 -0
- data/lib/kiss/template_methods.rb +125 -0
- data/lib/kiss.rb +725 -0
- metadata +108 -0
data/lib/kiss/mailer.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
class Kiss
|
2
|
+
# This class creates, renders, and sends email messages.
|
3
|
+
class Mailer
|
4
|
+
include Kiss::TemplateMethods
|
5
|
+
|
6
|
+
# Class Methods
|
7
|
+
|
8
|
+
def self.set_controller(controller)
|
9
|
+
@@controller = controller
|
10
|
+
end
|
11
|
+
|
12
|
+
# Instance Methods
|
13
|
+
|
14
|
+
# Creates new email message object.
|
15
|
+
def initialize(options = {})
|
16
|
+
@options = {
|
17
|
+
:engine => :sendmail
|
18
|
+
}.merge(options)
|
19
|
+
|
20
|
+
@data = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Invokes controller's file_cache.
|
24
|
+
def file_cache(*args,&block)
|
25
|
+
controller.file_cache(*args,&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def controller
|
29
|
+
@@controller
|
30
|
+
end
|
31
|
+
|
32
|
+
# Renders email template to string, unless message option is
|
33
|
+
# already set to a string value.
|
34
|
+
def prepare_email_message(options = {})
|
35
|
+
@options.merge!(options)
|
36
|
+
|
37
|
+
unless @options[:message].is_a?(String)
|
38
|
+
if template_name = @options[:template]
|
39
|
+
@template_dir = @@controller.email_template_dir
|
40
|
+
raise 'email_template_dir path not set' unless @template_dir
|
41
|
+
|
42
|
+
data = vars = @options[:data] || @options[:vars]
|
43
|
+
|
44
|
+
path = "#{@template_dir}/#{template_name}"
|
45
|
+
@options[:message] = erubis(path,binding)
|
46
|
+
else
|
47
|
+
raise 'email message not defined'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
return @options[:message]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Sets data to be passed to email template.
|
55
|
+
def set(key,value)
|
56
|
+
@data[key] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Attempts to send message using SMTP, unless :engine option is set to
|
60
|
+
# :sendmail.
|
61
|
+
def send(options)
|
62
|
+
@options.merge!(options)
|
63
|
+
if options[:engine] == :sendmail
|
64
|
+
return sendmail
|
65
|
+
else
|
66
|
+
return send_smtp
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Attempts to send message using /usr/sbin/sendmail.
|
71
|
+
def sendmail(options = {})
|
72
|
+
prepare_email_message(options)
|
73
|
+
|
74
|
+
IO.popen(@options[:sendmail_path] || "/usr/sbin/sendmail -t","w") do |pipe|
|
75
|
+
pipe.puts(@options[:message])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Attempts to send message using Net::SMTP.
|
80
|
+
def send_smtp(options = {})
|
81
|
+
prepare_email_message(options)
|
82
|
+
|
83
|
+
require 'net/smtp' unless defined?(Net::SMTP)
|
84
|
+
# begin
|
85
|
+
Net::SMTP.start('localhost') do |smtp|
|
86
|
+
smtp.sendmail(@options[:message], options[:from], options[:to])
|
87
|
+
end
|
88
|
+
# rescue
|
89
|
+
# end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/kiss/model.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
class Kiss
|
2
|
+
# This class adds functionality to Sequel::Model and automatically loads
|
3
|
+
# model class definitions from model_dir. It also uses Kiss#file_cache
|
4
|
+
# to cache database model classes, unless no model_dir is specified.
|
5
|
+
class Model < Sequel::Model
|
6
|
+
class << self
|
7
|
+
def name
|
8
|
+
@table.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
# Name symbol for default foreign key
|
12
|
+
def default_remote_key
|
13
|
+
:"#{name.singularize.demodulize.underscore}_id"
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_controller(controller)
|
17
|
+
@@controller = controller
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_dataset(source)
|
21
|
+
super(source)
|
22
|
+
end
|
23
|
+
|
24
|
+
def table=(table)
|
25
|
+
@table = table
|
26
|
+
end
|
27
|
+
|
28
|
+
def controller
|
29
|
+
@@controller
|
30
|
+
end
|
31
|
+
|
32
|
+
def dbm
|
33
|
+
@@controller.dbm
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO: Fix has_many and many_to_many associations
|
37
|
+
def associate(type, name, opts = {}, &block)
|
38
|
+
opts = opts.clone
|
39
|
+
|
40
|
+
unless opts[:class] || opts[:class_name]
|
41
|
+
opts[:class_name] = name.to_s.pluralize
|
42
|
+
end
|
43
|
+
|
44
|
+
super(type, name, opts, &block)
|
45
|
+
|
46
|
+
association_reflections[name]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def method_missing(meth)
|
51
|
+
raise NoMethodError, "undefined method `#{meth}' for database model `#{self.class.name}'"
|
52
|
+
end
|
53
|
+
|
54
|
+
def controller
|
55
|
+
@@controller
|
56
|
+
end
|
57
|
+
|
58
|
+
include Kiss::ControllerAccessors
|
59
|
+
end
|
60
|
+
|
61
|
+
class ModelCache
|
62
|
+
def initialize(controller,model_dir = nil)
|
63
|
+
@controller = controller
|
64
|
+
@model_dir = model_dir && File.directory?(model_dir) ? model_dir : nil
|
65
|
+
@cache = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](source)
|
69
|
+
(@model_dir && source.is_a?(Symbol)) ? begin
|
70
|
+
# use controller's file_cache
|
71
|
+
model_path = "#{@model_dir}/#{source}.rb"
|
72
|
+
@controller.file_cache(model_path) do |src|
|
73
|
+
klass = Class.new(Kiss::Model)
|
74
|
+
klass.set_dataset(Model.db[source])
|
75
|
+
klass.table = source
|
76
|
+
klass.class_eval(src,model_path) if src
|
77
|
+
klass
|
78
|
+
end
|
79
|
+
end : begin
|
80
|
+
# no model_dir, or source is not a symbol
|
81
|
+
# no mapping from source to filesystem path
|
82
|
+
# use ModelCache's own cache
|
83
|
+
@cache[source] ||= begin
|
84
|
+
klass = Class.new(Kiss::Model)
|
85
|
+
klass.set_dataset(Model.db[source])
|
86
|
+
klass.table = source if source.is_a?(Symbol)
|
87
|
+
klass
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def db
|
93
|
+
Sequel::Model.db
|
94
|
+
end
|
95
|
+
|
96
|
+
def literal(*args)
|
97
|
+
Sequel::Model.dataset.literal(*args)
|
98
|
+
end
|
99
|
+
alias_method :quote, :literal
|
100
|
+
|
101
|
+
def mdy_to_ymd(*args)
|
102
|
+
Kiss.mdy_to_ymd(*args)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
Sequel::Model::Associations::AssociationReflection.class_eval do
|
108
|
+
def associated_class
|
109
|
+
self[:class] ||= Kiss::Model.controller.dbm[self[:class_name].to_s.pluralize.to_sym]
|
110
|
+
end
|
111
|
+
def default_left_key
|
112
|
+
:"#{self[:model].name.singularize.underscore}_id"
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
def bench(label = nil)
|
2
|
+
Rack::Bench.close_bench_item(Kernel.caller[0])
|
3
|
+
Rack::Bench.on
|
4
|
+
|
5
|
+
if label
|
6
|
+
Rack::Bench.push_bench_item(
|
7
|
+
:label => label,
|
8
|
+
:start_time => Time.now,
|
9
|
+
:start_context => Kernel.caller[0]
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Rack
|
15
|
+
# Rack::Bench adds benchmarking capabilities to Kiss applications.
|
16
|
+
#
|
17
|
+
# bench(label) starts a new timer, which ends upon the next call to
|
18
|
+
# bench, or when execution returns to Rack::Bench.
|
19
|
+
#
|
20
|
+
# bench can be called without a label to end the previous timer
|
21
|
+
# without starting a new one.
|
22
|
+
#
|
23
|
+
# Total request duration is also displayed for any request in which
|
24
|
+
# the bench function is called.
|
25
|
+
class Bench
|
26
|
+
def self.on
|
27
|
+
@@bench = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.close_bench_item(end_context = nil)
|
31
|
+
if @@bench_items[-1] && !@@bench_items[-1][:end_time]
|
32
|
+
@@bench_items[-1][:end_time] = Time.now
|
33
|
+
@@bench_items[-1][:end_context] = end_context
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.push_bench_item(item)
|
38
|
+
@@bench_items.push(item)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(app)
|
42
|
+
@app = app
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(env)
|
46
|
+
@@bench = false
|
47
|
+
@@bench_items = []
|
48
|
+
|
49
|
+
start_time = Time.now
|
50
|
+
code, headers, body = @app.call(env)
|
51
|
+
end_time = Time.now
|
52
|
+
|
53
|
+
if @@bench
|
54
|
+
Rack::Bench.close_bench_item
|
55
|
+
contents = <<-EOT
|
56
|
+
<style>
|
57
|
+
.kiss_bench {
|
58
|
+
text-align: left;
|
59
|
+
padding: 3px 7px;
|
60
|
+
border: 1px solid #ec4;
|
61
|
+
border-top: 1px solid #fff4bb;
|
62
|
+
border-bottom: 1px solid #d91;
|
63
|
+
background-color: #ffe590;
|
64
|
+
font-size: 12px;
|
65
|
+
color: #101;
|
66
|
+
}
|
67
|
+
.kiss_bench a {
|
68
|
+
color: #930;
|
69
|
+
text-decoration: none;
|
70
|
+
}
|
71
|
+
.kiss_bench a:hover {
|
72
|
+
color: #930;
|
73
|
+
text-decoration: underline;
|
74
|
+
}
|
75
|
+
</style>
|
76
|
+
EOT
|
77
|
+
|
78
|
+
contents += @@bench_items.map do |item|
|
79
|
+
start_link = context_link(item[:start_context])
|
80
|
+
end_link = context_link(item[:end_context])
|
81
|
+
|
82
|
+
<<-EOT
|
83
|
+
<div class="kiss_bench">
|
84
|
+
<tt><b>#{item[:label].gsub(/\</,'<')} duration: #{sprintf("%0.3f",item[:end_time].to_f - item[:start_time].to_f)} s</b></tt>
|
85
|
+
<small style="line-height: 105%; display: block; padding-bottom: 3px">kiss bench<br/>started at #{start_link}<br/>ended at #{end_link || 'return to kiss bench'}</small>
|
86
|
+
</div>
|
87
|
+
EOT
|
88
|
+
end.join
|
89
|
+
|
90
|
+
contents += <<-EOT
|
91
|
+
<div class="kiss_bench">
|
92
|
+
<tt><b>TOTAL request duration: #{sprintf("%0.3f",end_time.to_f - start_time.to_f)} s</b></tt>
|
93
|
+
<br><small>kiss bench request total</small>
|
94
|
+
</div>
|
95
|
+
EOT
|
96
|
+
else
|
97
|
+
contents = ''
|
98
|
+
end
|
99
|
+
|
100
|
+
body.each {|p| contents += p }
|
101
|
+
headers['Content-Length'] = contents.length.to_s
|
102
|
+
|
103
|
+
[ code, headers, contents ]
|
104
|
+
end
|
105
|
+
|
106
|
+
def absolute_path(filename)
|
107
|
+
filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
|
108
|
+
end
|
109
|
+
|
110
|
+
def context_link(context)
|
111
|
+
return nil unless context
|
112
|
+
|
113
|
+
filename, line, method = context.split(/:/)
|
114
|
+
textmate_url = "txmt://open?url=file://" + h(absolute_path(filename)) + '&line=' + line
|
115
|
+
%Q(<a href="#{textmate_url}">#{filename}:#{line}</a> #{method})
|
116
|
+
end
|
117
|
+
|
118
|
+
def textmate_href(frame)
|
119
|
+
"txmt://open?url=file://" + h(absolute_path(context)).sub(/:/,'&line=')
|
120
|
+
end
|
121
|
+
|
122
|
+
def h(obj) # :nodoc:
|
123
|
+
case obj
|
124
|
+
when String
|
125
|
+
Utils.escape_html(obj).gsub(/^(\s+)/) {' ' * $1.length}
|
126
|
+
else
|
127
|
+
Utils.escape_html(obj.inspect)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::EmailErrors sends error responses (code 5xx) to email addresses as
|
3
|
+
# specified in the Rack::Builder config.
|
4
|
+
class EmailErrors
|
5
|
+
def initialize(app,agent,app_name,from,*to)
|
6
|
+
@app = app
|
7
|
+
@agent = agent == :sendmail ? '/usr/sbin/sendmail -t' : agent
|
8
|
+
@app_name = app_name
|
9
|
+
@from = from
|
10
|
+
@to = to.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
code, headers, body = @app.call(env)
|
15
|
+
|
16
|
+
if code >= 500 && code < 600
|
17
|
+
begin # rescue any errors in message composition and sending
|
18
|
+
error_type = headers['X-Kiss-Error-Type'] || "#{code} Error"
|
19
|
+
error_message = headers['X-Kiss-Error-Message']
|
20
|
+
|
21
|
+
message = <<-EOT
|
22
|
+
Content-type: text/html
|
23
|
+
From: #{@from}
|
24
|
+
To: #{@to.join(', ')}
|
25
|
+
Subject: #{@app_name} - #{error_type}#{ error_message ? ": #{error_message}" : ''}
|
26
|
+
|
27
|
+
EOT
|
28
|
+
|
29
|
+
body.each do |part|
|
30
|
+
message += part
|
31
|
+
end
|
32
|
+
|
33
|
+
if @agent.is_a?(String)
|
34
|
+
IO.popen(@agent,"w") do |pipe|
|
35
|
+
pipe.puts(message)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
require 'net/smtp' unless defined?(Net::SMTP)
|
39
|
+
smtp = @agent.is_a?(Net::SMTP) ? @agent : Net::SMTP.new('localhost')
|
40
|
+
smtp.start do |smtp|
|
41
|
+
smtp.send_message(message, @from, *@to)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue
|
45
|
+
end
|
46
|
+
|
47
|
+
body = <<-EOT
|
48
|
+
<html>
|
49
|
+
<head>
|
50
|
+
<title>Error</title>
|
51
|
+
</head>
|
52
|
+
<body>
|
53
|
+
<h1>Application Server Error</h1>
|
54
|
+
<p>Sorry, an error occurred. Our technical staff has been notified and will investigate this issue.</p>
|
55
|
+
</body>
|
56
|
+
</html>
|
57
|
+
EOT
|
58
|
+
headers['Content-Length'] = body.length.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
[ code, headers, body ]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::Facebook formats HTTP responses to remove certain status codes
|
3
|
+
# and HTML entities that are invalid as FBML responses.
|
4
|
+
class Facebook
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
code, headers, body = @app.call(env)
|
11
|
+
|
12
|
+
if code >= 500 && code < 600
|
13
|
+
code = 200
|
14
|
+
end
|
15
|
+
|
16
|
+
contents = ''
|
17
|
+
body.each {|p| contents += p }
|
18
|
+
|
19
|
+
contents.gsub!(/txmt:\/\//, 'http://textmate.local/')
|
20
|
+
contents.gsub!('<body>','<div class="body">')
|
21
|
+
contents.gsub!('</body>','</div>')
|
22
|
+
|
23
|
+
headers['Content-Length'] = contents.length.to_s
|
24
|
+
|
25
|
+
[ code, headers, contents ]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::FileNotFound rescues Kiss::TemplateFileNotFound exceptions
|
3
|
+
# (raised when action template files are not found) and returns an
|
4
|
+
# HTTP 404 error response.
|
5
|
+
class FileNotFound
|
6
|
+
def initialize(app,path = nil)
|
7
|
+
@app = app
|
8
|
+
@body = path ? (
|
9
|
+
::File.file?(path) ?
|
10
|
+
::File.read(path) :
|
11
|
+
template('could not find specified FileNotFound error document')
|
12
|
+
) : template
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
code, headers, body = @app.call(env)
|
17
|
+
rescue Kiss::TemplateFileNotFound => e
|
18
|
+
[ 404, {
|
19
|
+
"Content-Type" => "text/html",
|
20
|
+
"Content-Length" => @body.length.to_s
|
21
|
+
}, @body ]
|
22
|
+
end
|
23
|
+
|
24
|
+
def template(error = nil)
|
25
|
+
<<EOT
|
26
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
27
|
+
<html lang="en">
|
28
|
+
<head>
|
29
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
30
|
+
<meta name="robots" content="NONE,NOARCHIVE" />
|
31
|
+
<title>File Not Found</title>
|
32
|
+
</head>
|
33
|
+
<body>
|
34
|
+
<h1>404 File Not Found</h1>
|
35
|
+
|
36
|
+
#{error ? "<p>Additionally, #{error}.</p>" : ''}
|
37
|
+
</body>
|
38
|
+
</html>
|
39
|
+
EOT
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::ShowExceptions catches exceptions raised from the app,
|
3
|
+
# showing a useful backtrace with clickable stack frames and
|
4
|
+
# TextMate links to source files, as well as the last database
|
5
|
+
# query, GET/POST params, cookies, and Rack environment variables.
|
6
|
+
#
|
7
|
+
# Be careful using this on public-facing sites as it could reveal
|
8
|
+
# potentially sensitive information to malicious users.
|
9
|
+
|
10
|
+
class LogExceptions
|
11
|
+
def initialize(app,path)
|
12
|
+
@app = app
|
13
|
+
@@file = ::File.open(path,'w')
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
@app.call(env)
|
18
|
+
rescue StandardError, LoadError, SyntaxError => e
|
19
|
+
@@file.print Kiss::ExceptionReport.generate(env, e) + "\n--- End of exception report --- \n\n"
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
$debug_messages = []
|
2
|
+
def debug(object)
|
3
|
+
$debug_messages.push( [object.inspect, Kernel.caller[0]] )
|
4
|
+
object
|
5
|
+
end
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
# Rack::ShowDebug displays messages logged by the debug function.
|
9
|
+
#
|
10
|
+
# Be careful using this on public-facing sites as it could reveal
|
11
|
+
# potentially sensitive information to malicious users.
|
12
|
+
|
13
|
+
class ShowDebug
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
$debug_messages = []
|
20
|
+
code, headers, body = @app.call(env)
|
21
|
+
|
22
|
+
if $debug_messages.size > 0
|
23
|
+
contents = <<-EOT
|
24
|
+
<style>
|
25
|
+
.kiss_debug {
|
26
|
+
text-align: left;
|
27
|
+
padding: 3px 7px;
|
28
|
+
border: 1px solid #ebe;
|
29
|
+
border-top: 1px solid #fdf;
|
30
|
+
border-bottom: 1px solid #d6d;
|
31
|
+
background-color: #fbf;
|
32
|
+
font-size: 12px;
|
33
|
+
color: #101;
|
34
|
+
}
|
35
|
+
.kiss_debug a {
|
36
|
+
color: #707;
|
37
|
+
text-decoration: none;
|
38
|
+
}
|
39
|
+
.kiss_debug a:hover {
|
40
|
+
color: #707;
|
41
|
+
text-decoration: underline;
|
42
|
+
}
|
43
|
+
</style>
|
44
|
+
EOT
|
45
|
+
contents += $debug_messages.map do |object,context|
|
46
|
+
filename, line, method = context.split(/:/)
|
47
|
+
textmate_url = "txmt://open?url=file://" + h(absolute_path(filename)) + '&line=' + line
|
48
|
+
<<-EOT
|
49
|
+
<div class="kiss_debug">
|
50
|
+
<tt><b>#{object.gsub(/\</,'<')}</b></tt>
|
51
|
+
<br><small>kiss debug output at <a href="#{textmate_url}">#{filename}:#{line}</a> #{method}</small>
|
52
|
+
</div>
|
53
|
+
EOT
|
54
|
+
end.join
|
55
|
+
else
|
56
|
+
contents = ''
|
57
|
+
end
|
58
|
+
|
59
|
+
body.each {|p| contents += p }
|
60
|
+
headers['Content-Length'] = contents.length.to_s
|
61
|
+
|
62
|
+
[ code, headers, contents ]
|
63
|
+
end
|
64
|
+
|
65
|
+
def absolute_path(filename)
|
66
|
+
filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
|
67
|
+
end
|
68
|
+
|
69
|
+
def textmate_href(frame)
|
70
|
+
"txmt://open?url=file://" + h(absolute_path(context)).sub(/:/,'&line=')
|
71
|
+
end
|
72
|
+
|
73
|
+
def h(obj) # :nodoc:
|
74
|
+
case obj
|
75
|
+
when String
|
76
|
+
Utils.escape_html(obj).gsub(/^(\s+)/) {' ' * $1.length}
|
77
|
+
else
|
78
|
+
Utils.escape_html(obj.inspect)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::ShowExceptions catches exceptions raised from the app,
|
3
|
+
# showing a useful backtrace with clickable stack frames and
|
4
|
+
# TextMate links to source files, as well as the last database
|
5
|
+
# query, GET/POST params, cookies, and Rack environment variables.
|
6
|
+
#
|
7
|
+
# Be careful using this on public-facing sites as it could reveal
|
8
|
+
# potentially sensitive information to malicious users.
|
9
|
+
|
10
|
+
class ShowExceptions
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
@app.call(env)
|
17
|
+
rescue StandardError, LoadError, SyntaxError => e
|
18
|
+
body = Kiss::ExceptionReport.generate(env, e)
|
19
|
+
[500, {
|
20
|
+
"Content-Type" => "text/html",
|
21
|
+
"Content-Length" => body.length.to_s,
|
22
|
+
"X-Kiss-Error-Type" => e.class.name,
|
23
|
+
"X-Kiss-Error-Message" => e.message.sub(/\n.*/m,'')
|
24
|
+
}, body]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sequel
|
2
|
+
module MySQL
|
3
|
+
class Dataset < Sequel::Dataset
|
4
|
+
# Returns results from dataset query as array of arrays,
|
5
|
+
# instead of array of hashes.
|
6
|
+
def all_arrays(opts = nil, &block)
|
7
|
+
a = []
|
8
|
+
fetch_arrays(select_sql(opts)) {|r| a << r}
|
9
|
+
a.each(&block) if block
|
10
|
+
a
|
11
|
+
end
|
12
|
+
|
13
|
+
# Fixes bug in Sequel 1.5; shouldn't be needed for Sequel 2.x
|
14
|
+
# (need to double-check, however).
|
15
|
+
def fetch_arrays(sql)
|
16
|
+
@db.execute_select(sql) do |r|
|
17
|
+
r.each_array {|row| yield row}
|
18
|
+
end
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|