sinatra-base 1.0
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/AUTHORS +43 -0
- data/CHANGES +511 -0
- data/LICENSE +22 -0
- data/README.jp.rdoc +552 -0
- data/README.rdoc +636 -0
- data/Rakefile +116 -0
- data/lib/sinatra.rb +7 -0
- data/lib/sinatra/base.rb +1167 -0
- data/lib/sinatra/images/404.png +0 -0
- data/lib/sinatra/images/500.png +0 -0
- data/lib/sinatra/main.rb +28 -0
- data/lib/sinatra/showexceptions.rb +307 -0
- data/lib/sinatra/tilt.rb +746 -0
- data/sinatra-base.gemspec +94 -0
- data/test/base_test.rb +160 -0
- data/test/builder_test.rb +65 -0
- data/test/contest.rb +64 -0
- data/test/erb_test.rb +81 -0
- data/test/erubis_test.rb +82 -0
- data/test/extensions_test.rb +100 -0
- data/test/filter_test.rb +221 -0
- data/test/haml_test.rb +95 -0
- data/test/helper.rb +76 -0
- data/test/helpers_test.rb +582 -0
- data/test/less_test.rb +37 -0
- data/test/mapped_error_test.rb +197 -0
- data/test/middleware_test.rb +68 -0
- data/test/public/favicon.ico +0 -0
- data/test/request_test.rb +33 -0
- data/test/response_test.rb +42 -0
- data/test/result_test.rb +98 -0
- data/test/route_added_hook_test.rb +59 -0
- data/test/routing_test.rb +860 -0
- data/test/sass_test.rb +85 -0
- data/test/server_test.rb +47 -0
- data/test/settings_test.rb +368 -0
- data/test/sinatra_test.rb +13 -0
- data/test/static_test.rb +93 -0
- data/test/templates_test.rb +159 -0
- data/test/views/error.builder +3 -0
- data/test/views/error.erb +3 -0
- data/test/views/error.erubis +3 -0
- data/test/views/error.haml +3 -0
- data/test/views/error.sass +2 -0
- data/test/views/foo/hello.test +1 -0
- data/test/views/hello.builder +1 -0
- data/test/views/hello.erb +1 -0
- data/test/views/hello.erubis +1 -0
- data/test/views/hello.haml +1 -0
- data/test/views/hello.less +5 -0
- data/test/views/hello.sass +2 -0
- data/test/views/hello.test +1 -0
- data/test/views/layout2.builder +3 -0
- data/test/views/layout2.erb +2 -0
- data/test/views/layout2.erubis +2 -0
- data/test/views/layout2.haml +2 -0
- data/test/views/layout2.test +1 -0
- metadata +257 -0
data/Rakefile
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
task :spec => :test
|
7
|
+
|
8
|
+
def source_version
|
9
|
+
line = File.read('lib/sinatra/base.rb')[/^\s*VERSION = .*/]
|
10
|
+
line.match(/.*VERSION = '(.*)'/)[1]
|
11
|
+
end
|
12
|
+
|
13
|
+
# SPECS ===============================================================
|
14
|
+
|
15
|
+
Rake::TestTask.new(:test) do |t|
|
16
|
+
t.test_files = FileList['test/*_test.rb']
|
17
|
+
t.ruby_opts = ['-rubygems -I.'] if defined? Gem
|
18
|
+
end
|
19
|
+
|
20
|
+
# Rcov ================================================================
|
21
|
+
namespace :test do
|
22
|
+
desc 'Mesures test coverage'
|
23
|
+
task :coverage do
|
24
|
+
rm_f "coverage"
|
25
|
+
rcov = "rcov --text-summary --test-unit-only -Ilib"
|
26
|
+
system("#{rcov} --no-html --no-color test/*_test.rb")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Website =============================================================
|
31
|
+
# Building docs requires HAML and the hanna gem:
|
32
|
+
# gem install mislav-hanna --source=http://gems.github.com
|
33
|
+
|
34
|
+
desc 'Generate RDoc under doc/api'
|
35
|
+
task 'doc' => ['doc:api']
|
36
|
+
|
37
|
+
task 'doc:api' => ['doc/api/index.html']
|
38
|
+
|
39
|
+
file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc'] do |f|
|
40
|
+
require 'rbconfig'
|
41
|
+
hanna = RbConfig::CONFIG['ruby_install_name'].sub('ruby', 'hanna')
|
42
|
+
rb_files = f.prerequisites
|
43
|
+
sh((<<-end).gsub(/\s+/, ' '))
|
44
|
+
#{hanna}
|
45
|
+
--charset utf8
|
46
|
+
--fmt html
|
47
|
+
--inline-source
|
48
|
+
--line-numbers
|
49
|
+
--main README.rdoc
|
50
|
+
--op doc/api
|
51
|
+
--title 'Sinatra API Documentation'
|
52
|
+
#{rb_files.join(' ')}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
CLEAN.include 'doc/api'
|
56
|
+
|
57
|
+
# PACKAGING ============================================================
|
58
|
+
|
59
|
+
if defined?(Gem)
|
60
|
+
# Load the gemspec using the same limitations as github
|
61
|
+
def spec
|
62
|
+
require 'rubygems' unless defined? Gem::Specification
|
63
|
+
@spec ||= eval(File.read('sinatra-base.gemspec'))
|
64
|
+
end
|
65
|
+
|
66
|
+
def package(ext='')
|
67
|
+
"pkg/sinatra-base-#{spec.version}" + ext
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'Build packages'
|
71
|
+
task :package => %w[.gem .tar.gz].map {|e| package(e)}
|
72
|
+
|
73
|
+
desc 'Build and install as local gem'
|
74
|
+
task :install => package('.gem') do
|
75
|
+
sh "gem install #{package('.gem')}"
|
76
|
+
end
|
77
|
+
|
78
|
+
directory 'pkg/'
|
79
|
+
CLOBBER.include('pkg')
|
80
|
+
|
81
|
+
file package('.gem') => %w[pkg/ sinatra-base.gemspec] + spec.files do |f|
|
82
|
+
sh "gem build sinatra-base.gemspec"
|
83
|
+
mv File.basename(f.name), f.name
|
84
|
+
end
|
85
|
+
|
86
|
+
file package('.tar.gz') => %w[pkg/] + spec.files do |f|
|
87
|
+
sh <<-SH
|
88
|
+
git archive \
|
89
|
+
--prefix=sinatra-base-#{source_version}/ \
|
90
|
+
--format=tar \
|
91
|
+
HEAD | gzip > #{f.name}
|
92
|
+
SH
|
93
|
+
end
|
94
|
+
|
95
|
+
task 'sinatra.gemspec' => FileList['{lib,test,compat}/**','Rakefile','CHANGES','*.rdoc'] do |f|
|
96
|
+
# read spec file and split out manifest section
|
97
|
+
spec = File.read(f.name)
|
98
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
99
|
+
# replace version and date
|
100
|
+
head.sub!(/\.version = '.*'/, ".version = '#{source_version}'")
|
101
|
+
head.sub!(/\.date = '.*'/, ".date = '#{Date.today.to_s}'")
|
102
|
+
# determine file list from git ls-files
|
103
|
+
files = `git ls-files`.
|
104
|
+
split("\n").
|
105
|
+
sort.
|
106
|
+
reject{ |file| file =~ /^\./ }.
|
107
|
+
reject { |file| file =~ /^doc/ }.
|
108
|
+
map{ |file| " #{file}" }.
|
109
|
+
join("\n")
|
110
|
+
# piece file back together and write...
|
111
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
112
|
+
spec = [head,manifest,tail].join(" # = MANIFEST =\n")
|
113
|
+
File.open(f.name, 'w') { |io| io.write(spec) }
|
114
|
+
puts "updated #{f.name}"
|
115
|
+
end
|
116
|
+
end
|
data/lib/sinatra.rb
ADDED
data/lib/sinatra/base.rb
ADDED
@@ -0,0 +1,1167 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'time'
|
3
|
+
require 'uri'
|
4
|
+
require 'rack'
|
5
|
+
require 'rack/builder'
|
6
|
+
require 'sinatra/showexceptions'
|
7
|
+
|
8
|
+
# require tilt if available; fall back on bundled version.
|
9
|
+
begin
|
10
|
+
require 'tilt'
|
11
|
+
if Tilt::VERSION < '0.8'
|
12
|
+
warn "WARN: sinatra requires tilt >= 0.8; you have #{Tilt::VERSION}. " +
|
13
|
+
"loading bundled version..."
|
14
|
+
Object.send :remove_const, :Tilt
|
15
|
+
raise LoadError
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
require 'sinatra/tilt'
|
19
|
+
end
|
20
|
+
|
21
|
+
module Sinatra
|
22
|
+
VERSION = '1.0'
|
23
|
+
|
24
|
+
# The request object. See Rack::Request for more info:
|
25
|
+
# http://rack.rubyforge.org/doc/classes/Rack/Request.html
|
26
|
+
class Request < Rack::Request
|
27
|
+
# Returns an array of acceptable media types for the response
|
28
|
+
def accept
|
29
|
+
@env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.strip }
|
30
|
+
end
|
31
|
+
|
32
|
+
def secure?
|
33
|
+
(@env['HTTP_X_FORWARDED_PROTO'] || @env['rack.url_scheme']) == 'https'
|
34
|
+
end
|
35
|
+
|
36
|
+
# Override Rack < 1.1's Request#params implementation (see lh #72 for
|
37
|
+
# more info) and add a Request#user_agent method.
|
38
|
+
# XXX remove when we require rack > 1.1
|
39
|
+
if Rack.release < '1.1'
|
40
|
+
def params
|
41
|
+
self.GET.update(self.POST)
|
42
|
+
rescue EOFError, Errno::ESPIPE
|
43
|
+
self.GET
|
44
|
+
end
|
45
|
+
|
46
|
+
def user_agent
|
47
|
+
@env['HTTP_USER_AGENT']
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# The response object. See Rack::Response and Rack::ResponseHelpers for
|
53
|
+
# more info:
|
54
|
+
# http://rack.rubyforge.org/doc/classes/Rack/Response.html
|
55
|
+
# http://rack.rubyforge.org/doc/classes/Rack/Response/Helpers.html
|
56
|
+
class Response < Rack::Response
|
57
|
+
def finish
|
58
|
+
@body = block if block_given?
|
59
|
+
if [204, 304].include?(status.to_i)
|
60
|
+
header.delete "Content-Type"
|
61
|
+
[status.to_i, header.to_hash, []]
|
62
|
+
else
|
63
|
+
body = @body || []
|
64
|
+
body = [body] if body.respond_to? :to_str
|
65
|
+
if body.respond_to?(:to_ary)
|
66
|
+
header["Content-Length"] = body.to_ary.
|
67
|
+
inject(0) { |len, part| len + Rack::Utils.bytesize(part) }.to_s
|
68
|
+
end
|
69
|
+
[status.to_i, header.to_hash, body]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class NotFound < NameError #:nodoc:
|
75
|
+
def code ; 404 ; end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Methods available to routes, before/after filters, and views.
|
79
|
+
module Helpers
|
80
|
+
# Set or retrieve the response status code.
|
81
|
+
def status(value=nil)
|
82
|
+
response.status = value if value
|
83
|
+
response.status
|
84
|
+
end
|
85
|
+
|
86
|
+
# Set or retrieve the response body. When a block is given,
|
87
|
+
# evaluation is deferred until the body is read with #each.
|
88
|
+
def body(value=nil, &block)
|
89
|
+
if block_given?
|
90
|
+
def block.each ; yield call ; end
|
91
|
+
response.body = block
|
92
|
+
else
|
93
|
+
response.body = value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Halt processing and redirect to the URI provided.
|
98
|
+
def redirect(uri, *args)
|
99
|
+
status 302
|
100
|
+
response['Location'] = uri
|
101
|
+
halt(*args)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Halt processing and return the error status provided.
|
105
|
+
def error(code, body=nil)
|
106
|
+
code, body = 500, code.to_str if code.respond_to? :to_str
|
107
|
+
response.body = body unless body.nil?
|
108
|
+
halt code
|
109
|
+
end
|
110
|
+
|
111
|
+
# Halt processing and return a 404 Not Found.
|
112
|
+
def not_found(body=nil)
|
113
|
+
error 404, body
|
114
|
+
end
|
115
|
+
|
116
|
+
# Set multiple response headers with Hash.
|
117
|
+
def headers(hash=nil)
|
118
|
+
response.headers.merge! hash if hash
|
119
|
+
response.headers
|
120
|
+
end
|
121
|
+
|
122
|
+
# Access the underlying Rack session.
|
123
|
+
def session
|
124
|
+
env['rack.session'] ||= {}
|
125
|
+
end
|
126
|
+
|
127
|
+
# Look up a media type by file extension in Rack's mime registry.
|
128
|
+
def mime_type(type)
|
129
|
+
Base.mime_type(type)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Set the Content-Type of the response body given a media type or file
|
133
|
+
# extension.
|
134
|
+
def content_type(type, params={})
|
135
|
+
mime_type = self.mime_type(type)
|
136
|
+
fail "Unknown media type: %p" % type if mime_type.nil?
|
137
|
+
if params.any?
|
138
|
+
params = params.collect { |kv| "%s=%s" % kv }.join(', ')
|
139
|
+
response['Content-Type'] = [mime_type, params].join(";")
|
140
|
+
else
|
141
|
+
response['Content-Type'] = mime_type
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Set the Content-Disposition to "attachment" with the specified filename,
|
146
|
+
# instructing the user agents to prompt to save.
|
147
|
+
def attachment(filename=nil)
|
148
|
+
response['Content-Disposition'] = 'attachment'
|
149
|
+
if filename
|
150
|
+
params = '; filename="%s"' % File.basename(filename)
|
151
|
+
response['Content-Disposition'] << params
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Use the contents of the file at +path+ as the response body.
|
156
|
+
def send_file(path, opts={})
|
157
|
+
stat = File.stat(path)
|
158
|
+
last_modified stat.mtime
|
159
|
+
|
160
|
+
content_type mime_type(opts[:type]) ||
|
161
|
+
mime_type(File.extname(path)) ||
|
162
|
+
response['Content-Type'] ||
|
163
|
+
'application/octet-stream'
|
164
|
+
|
165
|
+
response['Content-Length'] ||= (opts[:length] || stat.size).to_s
|
166
|
+
|
167
|
+
if opts[:disposition] == 'attachment' || opts[:filename]
|
168
|
+
attachment opts[:filename] || path
|
169
|
+
elsif opts[:disposition] == 'inline'
|
170
|
+
response['Content-Disposition'] = 'inline'
|
171
|
+
end
|
172
|
+
|
173
|
+
halt StaticFile.open(path, 'rb')
|
174
|
+
rescue Errno::ENOENT
|
175
|
+
not_found
|
176
|
+
end
|
177
|
+
|
178
|
+
# Rack response body used to deliver static files. The file contents are
|
179
|
+
# generated iteratively in 8K chunks.
|
180
|
+
class StaticFile < ::File #:nodoc:
|
181
|
+
alias_method :to_path, :path
|
182
|
+
def each
|
183
|
+
rewind
|
184
|
+
while buf = read(8192)
|
185
|
+
yield buf
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Specify response freshness policy for HTTP caches (Cache-Control header).
|
191
|
+
# Any number of non-value directives (:public, :private, :no_cache,
|
192
|
+
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
|
193
|
+
# a Hash of value directives (:max_age, :min_stale, :s_max_age).
|
194
|
+
#
|
195
|
+
# cache_control :public, :must_revalidate, :max_age => 60
|
196
|
+
# => Cache-Control: public, must-revalidate, max-age=60
|
197
|
+
#
|
198
|
+
# See RFC 2616 / 14.9 for more on standard cache control directives:
|
199
|
+
# http://tools.ietf.org/html/rfc2616#section-14.9.1
|
200
|
+
def cache_control(*values)
|
201
|
+
if values.last.kind_of?(Hash)
|
202
|
+
hash = values.pop
|
203
|
+
hash.reject! { |k,v| v == false }
|
204
|
+
hash.reject! { |k,v| values << k if v == true }
|
205
|
+
else
|
206
|
+
hash = {}
|
207
|
+
end
|
208
|
+
|
209
|
+
values = values.map { |value| value.to_s.tr('_','-') }
|
210
|
+
hash.each { |k,v| values << [k.to_s.tr('_', '-'), v].join('=') }
|
211
|
+
|
212
|
+
response['Cache-Control'] = values.join(', ') if values.any?
|
213
|
+
end
|
214
|
+
|
215
|
+
# Set the Expires header and Cache-Control/max-age directive. Amount
|
216
|
+
# can be an integer number of seconds in the future or a Time object
|
217
|
+
# indicating when the response should be considered "stale". The remaining
|
218
|
+
# "values" arguments are passed to the #cache_control helper:
|
219
|
+
#
|
220
|
+
# expires 500, :public, :must_revalidate
|
221
|
+
# => Cache-Control: public, must-revalidate, max-age=60
|
222
|
+
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT
|
223
|
+
#
|
224
|
+
def expires(amount, *values)
|
225
|
+
values << {} unless values.last.kind_of?(Hash)
|
226
|
+
|
227
|
+
if amount.respond_to?(:to_time)
|
228
|
+
max_age = amount.to_time - Time.now
|
229
|
+
time = amount.to_time
|
230
|
+
else
|
231
|
+
max_age = amount
|
232
|
+
time = Time.now + amount
|
233
|
+
end
|
234
|
+
|
235
|
+
values.last.merge!(:max_age => max_age)
|
236
|
+
cache_control(*values)
|
237
|
+
|
238
|
+
response['Expires'] = time.httpdate
|
239
|
+
end
|
240
|
+
|
241
|
+
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
|
242
|
+
# and halt if conditional GET matches. The +time+ argument is a Time,
|
243
|
+
# DateTime, or other object that responds to +to_time+.
|
244
|
+
#
|
245
|
+
# When the current request includes an 'If-Modified-Since' header that
|
246
|
+
# matches the time specified, execution is immediately halted with a
|
247
|
+
# '304 Not Modified' response.
|
248
|
+
def last_modified(time)
|
249
|
+
return unless time
|
250
|
+
time = time.to_time if time.respond_to?(:to_time)
|
251
|
+
time = time.httpdate if time.respond_to?(:httpdate)
|
252
|
+
response['Last-Modified'] = time
|
253
|
+
halt 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
|
254
|
+
time
|
255
|
+
end
|
256
|
+
|
257
|
+
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
|
258
|
+
# GET matches. The +value+ argument is an identifier that uniquely
|
259
|
+
# identifies the current version of the resource. The +kind+ argument
|
260
|
+
# indicates whether the etag should be used as a :strong (default) or :weak
|
261
|
+
# cache validator.
|
262
|
+
#
|
263
|
+
# When the current request includes an 'If-None-Match' header with a
|
264
|
+
# matching etag, execution is immediately halted. If the request method is
|
265
|
+
# GET or HEAD, a '304 Not Modified' response is sent.
|
266
|
+
def etag(value, kind=:strong)
|
267
|
+
raise TypeError, ":strong or :weak expected" if ![:strong,:weak].include?(kind)
|
268
|
+
value = '"%s"' % value
|
269
|
+
value = 'W/' + value if kind == :weak
|
270
|
+
response['ETag'] = value
|
271
|
+
|
272
|
+
# Conditional GET check
|
273
|
+
if etags = env['HTTP_IF_NONE_MATCH']
|
274
|
+
etags = etags.split(/\s*,\s*/)
|
275
|
+
halt 304 if etags.include?(value) || etags.include?('*')
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
## Sugar for redirect (example: redirect back)
|
280
|
+
def back ; request.referer ; end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
# Template rendering methods. Each method takes the name of a template
|
285
|
+
# to render as a Symbol and returns a String with the rendered output,
|
286
|
+
# as well as an optional hash with additional options.
|
287
|
+
#
|
288
|
+
# `template` is either the name or path of the template as symbol
|
289
|
+
# (Use `:'subdir/myview'` for views in subdirectories), or a string
|
290
|
+
# that will be rendered.
|
291
|
+
#
|
292
|
+
# Possible options are:
|
293
|
+
# :layout If set to false, no layout is rendered, otherwise
|
294
|
+
# the specified layout is used (Ignored for `sass` and `less`)
|
295
|
+
# :locals A hash with local variables that should be available
|
296
|
+
# in the template
|
297
|
+
module Templates
|
298
|
+
include Tilt::CompileSite
|
299
|
+
|
300
|
+
def erb(template, options={}, locals={})
|
301
|
+
options[:outvar] = '@_out_buf'
|
302
|
+
render :erb, template, options, locals
|
303
|
+
end
|
304
|
+
|
305
|
+
def erubis(template, options={}, locals={})
|
306
|
+
options[:outvar] = '@_out_buf'
|
307
|
+
render :erubis, template, options, locals
|
308
|
+
end
|
309
|
+
|
310
|
+
def haml(template, options={}, locals={})
|
311
|
+
render :haml, template, options, locals
|
312
|
+
end
|
313
|
+
|
314
|
+
def sass(template, options={}, locals={})
|
315
|
+
options[:layout] = false
|
316
|
+
render :sass, template, options, locals
|
317
|
+
end
|
318
|
+
|
319
|
+
def less(template, options={}, locals={})
|
320
|
+
options[:layout] = false
|
321
|
+
render :less, template, options, locals
|
322
|
+
end
|
323
|
+
|
324
|
+
def builder(template=nil, options={}, locals={}, &block)
|
325
|
+
options, template = template, nil if template.is_a?(Hash)
|
326
|
+
template = Proc.new { block } if template.nil?
|
327
|
+
render :builder, template, options, locals
|
328
|
+
end
|
329
|
+
|
330
|
+
private
|
331
|
+
def render(engine, data, options={}, locals={}, &block)
|
332
|
+
# merge app-level options
|
333
|
+
options = settings.send(engine).merge(options) if settings.respond_to?(engine)
|
334
|
+
|
335
|
+
# extract generic options
|
336
|
+
locals = options.delete(:locals) || locals || {}
|
337
|
+
views = options.delete(:views) || settings.views || "./views"
|
338
|
+
layout = options.delete(:layout)
|
339
|
+
layout = :layout if layout.nil? || layout == true
|
340
|
+
|
341
|
+
# compile and render template
|
342
|
+
template = compile_template(engine, data, options, views)
|
343
|
+
output = template.render(self, locals, &block)
|
344
|
+
|
345
|
+
# render layout
|
346
|
+
if layout
|
347
|
+
begin
|
348
|
+
options = options.merge(:views => views, :layout => false)
|
349
|
+
output = render(engine, layout, options, locals) { output }
|
350
|
+
rescue Errno::ENOENT
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
output
|
355
|
+
end
|
356
|
+
|
357
|
+
def compile_template(engine, data, options, views)
|
358
|
+
@template_cache.fetch engine, data, options do
|
359
|
+
template = Tilt[engine]
|
360
|
+
raise "Template engine not found: #{engine}" if template.nil?
|
361
|
+
|
362
|
+
case
|
363
|
+
when data.is_a?(Symbol)
|
364
|
+
body, path, line = self.class.templates[data]
|
365
|
+
if body
|
366
|
+
body = body.call if body.respond_to?(:call)
|
367
|
+
template.new(path, line.to_i, options) { body }
|
368
|
+
else
|
369
|
+
path = ::File.join(views, "#{data}.#{engine}")
|
370
|
+
template.new(path, 1, options)
|
371
|
+
end
|
372
|
+
when data.is_a?(Proc) || data.is_a?(String)
|
373
|
+
body = data.is_a?(String) ? Proc.new { data } : data
|
374
|
+
path, line = self.class.caller_locations.first
|
375
|
+
template.new(path, line.to_i, options, &body)
|
376
|
+
else
|
377
|
+
raise ArgumentError
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Base class for all Sinatra applications and middleware.
|
384
|
+
class Base
|
385
|
+
include Rack::Utils
|
386
|
+
include Helpers
|
387
|
+
include Templates
|
388
|
+
|
389
|
+
attr_accessor :app
|
390
|
+
|
391
|
+
def initialize(app=nil)
|
392
|
+
@app = app
|
393
|
+
@template_cache = Tilt::Cache.new
|
394
|
+
yield self if block_given?
|
395
|
+
end
|
396
|
+
|
397
|
+
# Rack call interface.
|
398
|
+
def call(env)
|
399
|
+
dup.call!(env)
|
400
|
+
end
|
401
|
+
|
402
|
+
attr_accessor :env, :request, :response, :params
|
403
|
+
|
404
|
+
def call!(env)
|
405
|
+
@env = env
|
406
|
+
@request = Request.new(env)
|
407
|
+
@response = Response.new
|
408
|
+
@params = indifferent_params(@request.params)
|
409
|
+
@template_cache.clear if settings.reload_templates
|
410
|
+
|
411
|
+
invoke { dispatch! }
|
412
|
+
invoke { error_block!(response.status) }
|
413
|
+
|
414
|
+
status, header, body = @response.finish
|
415
|
+
|
416
|
+
# Never produce a body on HEAD requests. Do retain the Content-Length
|
417
|
+
# unless it's "0", in which case we assume it was calculated erroneously
|
418
|
+
# for a manual HEAD response and remove it entirely.
|
419
|
+
if @env['REQUEST_METHOD'] == 'HEAD'
|
420
|
+
body = []
|
421
|
+
header.delete('Content-Length') if header['Content-Length'] == '0'
|
422
|
+
end
|
423
|
+
|
424
|
+
[status, header, body]
|
425
|
+
end
|
426
|
+
|
427
|
+
# Access settings defined with Base.set.
|
428
|
+
def settings
|
429
|
+
self.class
|
430
|
+
end
|
431
|
+
alias_method :options, :settings
|
432
|
+
|
433
|
+
# Exit the current block, halts any further processing
|
434
|
+
# of the request, and returns the specified response.
|
435
|
+
def halt(*response)
|
436
|
+
response = response.first if response.length == 1
|
437
|
+
throw :halt, response
|
438
|
+
end
|
439
|
+
|
440
|
+
# Pass control to the next matching route.
|
441
|
+
# If there are no more matching routes, Sinatra will
|
442
|
+
# return a 404 response.
|
443
|
+
def pass(&block)
|
444
|
+
throw :pass, block
|
445
|
+
end
|
446
|
+
|
447
|
+
# Forward the request to the downstream app -- middleware only.
|
448
|
+
def forward
|
449
|
+
fail "downstream app not set" unless @app.respond_to? :call
|
450
|
+
status, headers, body = @app.call(@request.env)
|
451
|
+
@response.status = status
|
452
|
+
@response.body = body
|
453
|
+
@response.headers.merge! headers
|
454
|
+
nil
|
455
|
+
end
|
456
|
+
|
457
|
+
private
|
458
|
+
# Run before filters defined on the class and all superclasses.
|
459
|
+
def before_filter!(base=self.class)
|
460
|
+
before_filter!(base.superclass) if base.superclass.respond_to?(:before_filters)
|
461
|
+
base.before_filters.each { |block| instance_eval(&block) }
|
462
|
+
end
|
463
|
+
|
464
|
+
# Run after filters defined on the class and all superclasses.
|
465
|
+
def after_filter!(base=self.class)
|
466
|
+
after_filter!(base.superclass) if base.superclass.respond_to?(:after_filters)
|
467
|
+
base.after_filters.each { |block| instance_eval(&block) }
|
468
|
+
end
|
469
|
+
|
470
|
+
# Run routes defined on the class and all superclasses.
|
471
|
+
def route!(base=self.class, pass_block=nil)
|
472
|
+
if routes = base.routes[@request.request_method]
|
473
|
+
original_params = @params
|
474
|
+
path = unescape(@request.path_info)
|
475
|
+
|
476
|
+
routes.each do |pattern, keys, conditions, block|
|
477
|
+
if match = pattern.match(path)
|
478
|
+
values = match.captures.to_a
|
479
|
+
params =
|
480
|
+
if keys.any?
|
481
|
+
keys.zip(values).inject({}) do |hash,(k,v)|
|
482
|
+
if k == 'splat'
|
483
|
+
(hash[k] ||= []) << v
|
484
|
+
else
|
485
|
+
hash[k] = v
|
486
|
+
end
|
487
|
+
hash
|
488
|
+
end
|
489
|
+
elsif values.any?
|
490
|
+
{'captures' => values}
|
491
|
+
else
|
492
|
+
{}
|
493
|
+
end
|
494
|
+
@params = original_params.merge(params)
|
495
|
+
@block_params = values
|
496
|
+
|
497
|
+
pass_block = catch(:pass) do
|
498
|
+
conditions.each { |cond|
|
499
|
+
throw :pass if instance_eval(&cond) == false }
|
500
|
+
route_eval(&block)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
@params = original_params
|
506
|
+
end
|
507
|
+
|
508
|
+
# Run routes defined in superclass.
|
509
|
+
if base.superclass.respond_to?(:routes)
|
510
|
+
route! base.superclass, pass_block
|
511
|
+
return
|
512
|
+
end
|
513
|
+
|
514
|
+
route_eval(&pass_block) if pass_block
|
515
|
+
|
516
|
+
route_missing
|
517
|
+
end
|
518
|
+
|
519
|
+
# Run a route block and throw :halt with the result.
|
520
|
+
def route_eval(&block)
|
521
|
+
throw :halt, instance_eval(&block)
|
522
|
+
end
|
523
|
+
|
524
|
+
# No matching route was found or all routes passed. The default
|
525
|
+
# implementation is to forward the request downstream when running
|
526
|
+
# as middleware (@app is non-nil); when no downstream app is set, raise
|
527
|
+
# a NotFound exception. Subclasses can override this method to perform
|
528
|
+
# custom route miss logic.
|
529
|
+
def route_missing
|
530
|
+
if @app
|
531
|
+
forward
|
532
|
+
else
|
533
|
+
raise NotFound
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
# Attempt to serve static files from public directory. Throws :halt when
|
538
|
+
# a matching file is found, returns nil otherwise.
|
539
|
+
def static!
|
540
|
+
return if (public_dir = settings.public).nil?
|
541
|
+
public_dir = File.expand_path(public_dir)
|
542
|
+
|
543
|
+
path = File.expand_path(public_dir + unescape(request.path_info))
|
544
|
+
return if path[0, public_dir.length] != public_dir
|
545
|
+
return unless File.file?(path)
|
546
|
+
|
547
|
+
env['sinatra.static_file'] = path
|
548
|
+
send_file path, :disposition => nil
|
549
|
+
end
|
550
|
+
|
551
|
+
# Enable string or symbol key access to the nested params hash.
|
552
|
+
def indifferent_params(params)
|
553
|
+
params = indifferent_hash.merge(params)
|
554
|
+
params.each do |key, value|
|
555
|
+
next unless value.is_a?(Hash)
|
556
|
+
params[key] = indifferent_params(value)
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
def indifferent_hash
|
561
|
+
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
562
|
+
end
|
563
|
+
|
564
|
+
# Run the block with 'throw :halt' support and apply result to the response.
|
565
|
+
def invoke(&block)
|
566
|
+
res = catch(:halt) { instance_eval(&block) }
|
567
|
+
return if res.nil?
|
568
|
+
|
569
|
+
case
|
570
|
+
when res.respond_to?(:to_str)
|
571
|
+
@response.body = [res]
|
572
|
+
when res.respond_to?(:to_ary)
|
573
|
+
res = res.to_ary
|
574
|
+
if Fixnum === res.first
|
575
|
+
if res.length == 3
|
576
|
+
@response.status, headers, body = res
|
577
|
+
@response.body = body if body
|
578
|
+
headers.each { |k, v| @response.headers[k] = v } if headers
|
579
|
+
elsif res.length == 2
|
580
|
+
@response.status = res.first
|
581
|
+
@response.body = res.last
|
582
|
+
else
|
583
|
+
raise TypeError, "#{res.inspect} not supported"
|
584
|
+
end
|
585
|
+
else
|
586
|
+
@response.body = res
|
587
|
+
end
|
588
|
+
when res.respond_to?(:each)
|
589
|
+
@response.body = res
|
590
|
+
when (100...599) === res
|
591
|
+
@response.status = res
|
592
|
+
end
|
593
|
+
|
594
|
+
res
|
595
|
+
end
|
596
|
+
|
597
|
+
# Dispatch a request with error handling.
|
598
|
+
def dispatch!
|
599
|
+
static! if settings.static? && (request.get? || request.head?)
|
600
|
+
before_filter!
|
601
|
+
route!
|
602
|
+
rescue NotFound => boom
|
603
|
+
handle_not_found!(boom)
|
604
|
+
rescue ::Exception => boom
|
605
|
+
handle_exception!(boom)
|
606
|
+
ensure
|
607
|
+
after_filter! unless env['sinatra.static_file']
|
608
|
+
end
|
609
|
+
|
610
|
+
def handle_not_found!(boom)
|
611
|
+
@env['sinatra.error'] = boom
|
612
|
+
@response.status = 404
|
613
|
+
@response.headers['X-Cascade'] = 'pass'
|
614
|
+
@response.body = ['<h1>Not Found</h1>']
|
615
|
+
error_block! boom.class, NotFound
|
616
|
+
end
|
617
|
+
|
618
|
+
def handle_exception!(boom)
|
619
|
+
@env['sinatra.error'] = boom
|
620
|
+
|
621
|
+
dump_errors!(boom) if settings.dump_errors?
|
622
|
+
raise boom if settings.show_exceptions?
|
623
|
+
|
624
|
+
@response.status = 500
|
625
|
+
if res = error_block!(boom.class)
|
626
|
+
res
|
627
|
+
elsif settings.raise_errors?
|
628
|
+
raise boom
|
629
|
+
else
|
630
|
+
error_block!(Exception)
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
# Find an custom error block for the key(s) specified.
|
635
|
+
def error_block!(*keys)
|
636
|
+
keys.each do |key|
|
637
|
+
base = self.class
|
638
|
+
while base.respond_to?(:errors)
|
639
|
+
if block = base.errors[key]
|
640
|
+
# found a handler, eval and return result
|
641
|
+
return instance_eval(&block)
|
642
|
+
else
|
643
|
+
base = base.superclass
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
647
|
+
nil
|
648
|
+
end
|
649
|
+
|
650
|
+
def dump_errors!(boom)
|
651
|
+
msg = ["#{boom.class} - #{boom.message}:",
|
652
|
+
*boom.backtrace].join("\n ")
|
653
|
+
@env['rack.errors'].puts(msg)
|
654
|
+
end
|
655
|
+
|
656
|
+
class << self
|
657
|
+
attr_reader :routes, :before_filters, :after_filters, :templates, :errors
|
658
|
+
|
659
|
+
def reset!
|
660
|
+
@conditions = []
|
661
|
+
@routes = {}
|
662
|
+
@before_filters = []
|
663
|
+
@after_filters = []
|
664
|
+
@errors = {}
|
665
|
+
@middleware = []
|
666
|
+
@prototype = nil
|
667
|
+
@extensions = []
|
668
|
+
|
669
|
+
if superclass.respond_to?(:templates)
|
670
|
+
@templates = Hash.new { |hash,key| superclass.templates[key] }
|
671
|
+
else
|
672
|
+
@templates = {}
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
# Extension modules registered on this class and all superclasses.
|
677
|
+
def extensions
|
678
|
+
if superclass.respond_to?(:extensions)
|
679
|
+
(@extensions + superclass.extensions).uniq
|
680
|
+
else
|
681
|
+
@extensions
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
# Middleware used in this class and all superclasses.
|
686
|
+
def middleware
|
687
|
+
if superclass.respond_to?(:middleware)
|
688
|
+
superclass.middleware + @middleware
|
689
|
+
else
|
690
|
+
@middleware
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
# Sets an option to the given value. If the value is a proc,
|
695
|
+
# the proc will be called every time the option is accessed.
|
696
|
+
def set(option, value=self, &block)
|
697
|
+
raise ArgumentError if block && value != self
|
698
|
+
value = block if block
|
699
|
+
if value.kind_of?(Proc)
|
700
|
+
metadef(option, &value)
|
701
|
+
metadef("#{option}?") { !!__send__(option) }
|
702
|
+
metadef("#{option}=") { |val| metadef(option, &Proc.new{val}) }
|
703
|
+
elsif value == self && option.respond_to?(:to_hash)
|
704
|
+
option.to_hash.each { |k,v| set(k, v) }
|
705
|
+
elsif respond_to?("#{option}=")
|
706
|
+
__send__ "#{option}=", value
|
707
|
+
else
|
708
|
+
set option, Proc.new{value}
|
709
|
+
end
|
710
|
+
self
|
711
|
+
end
|
712
|
+
|
713
|
+
# Same as calling `set :option, true` for each of the given options.
|
714
|
+
def enable(*opts)
|
715
|
+
opts.each { |key| set(key, true) }
|
716
|
+
end
|
717
|
+
|
718
|
+
# Same as calling `set :option, false` for each of the given options.
|
719
|
+
def disable(*opts)
|
720
|
+
opts.each { |key| set(key, false) }
|
721
|
+
end
|
722
|
+
|
723
|
+
# Define a custom error handler. Optionally takes either an Exception
|
724
|
+
# class, or an HTTP status code to specify which errors should be
|
725
|
+
# handled.
|
726
|
+
def error(codes=Exception, &block)
|
727
|
+
Array(codes).each { |code| @errors[code] = block }
|
728
|
+
end
|
729
|
+
|
730
|
+
# Sugar for `error(404) { ... }`
|
731
|
+
def not_found(&block)
|
732
|
+
error 404, &block
|
733
|
+
end
|
734
|
+
|
735
|
+
# Define a named template. The block must return the template source.
|
736
|
+
def template(name, &block)
|
737
|
+
filename, line = caller_locations.first
|
738
|
+
templates[name] = [block, filename, line.to_i]
|
739
|
+
end
|
740
|
+
|
741
|
+
# Define the layout template. The block must return the template source.
|
742
|
+
def layout(name=:layout, &block)
|
743
|
+
template name, &block
|
744
|
+
end
|
745
|
+
|
746
|
+
# Load embeded templates from the file; uses the caller's __FILE__
|
747
|
+
# when no file is specified.
|
748
|
+
def inline_templates=(file=nil)
|
749
|
+
file = (file.nil? || file == true) ? caller_files.first : file
|
750
|
+
|
751
|
+
begin
|
752
|
+
app, data =
|
753
|
+
::IO.read(file).gsub("\r\n", "\n").split(/^__END__$/, 2)
|
754
|
+
rescue Errno::ENOENT
|
755
|
+
app, data = nil
|
756
|
+
end
|
757
|
+
|
758
|
+
if data
|
759
|
+
lines = app.count("\n") + 1
|
760
|
+
template = nil
|
761
|
+
data.each_line do |line|
|
762
|
+
lines += 1
|
763
|
+
if line =~ /^@@\s*(.*)/
|
764
|
+
template = ''
|
765
|
+
templates[$1.to_sym] = [template, file, lines]
|
766
|
+
elsif template
|
767
|
+
template << line
|
768
|
+
end
|
769
|
+
end
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
# Lookup or register a mime type in Rack's mime registry.
|
774
|
+
def mime_type(type, value=nil)
|
775
|
+
return type if type.nil? || type.to_s.include?('/')
|
776
|
+
type = ".#{type}" unless type.to_s[0] == ?.
|
777
|
+
return Rack::Mime.mime_type(type, nil) unless value
|
778
|
+
Rack::Mime::MIME_TYPES[type] = value
|
779
|
+
end
|
780
|
+
|
781
|
+
# Define a before filter; runs before all requests within the same
|
782
|
+
# context as route handlers and may access/modify the request and
|
783
|
+
# response.
|
784
|
+
def before(&block)
|
785
|
+
@before_filters << block
|
786
|
+
end
|
787
|
+
|
788
|
+
# Define an after filter; runs after all requests within the same
|
789
|
+
# context as route handlers and may access/modify the request and
|
790
|
+
# response.
|
791
|
+
def after(&block)
|
792
|
+
@after_filters << block
|
793
|
+
end
|
794
|
+
|
795
|
+
# Add a route condition. The route is considered non-matching when the
|
796
|
+
# block returns false.
|
797
|
+
def condition(&block)
|
798
|
+
@conditions << block
|
799
|
+
end
|
800
|
+
|
801
|
+
private
|
802
|
+
def host_name(pattern)
|
803
|
+
condition { pattern === request.host }
|
804
|
+
end
|
805
|
+
|
806
|
+
def user_agent(pattern)
|
807
|
+
condition {
|
808
|
+
if request.user_agent =~ pattern
|
809
|
+
@params[:agent] = $~[1..-1]
|
810
|
+
true
|
811
|
+
else
|
812
|
+
false
|
813
|
+
end
|
814
|
+
}
|
815
|
+
end
|
816
|
+
alias_method :agent, :user_agent
|
817
|
+
|
818
|
+
def provides(*types)
|
819
|
+
types = [types] unless types.kind_of? Array
|
820
|
+
types.map!{|t| mime_type(t)}
|
821
|
+
|
822
|
+
condition {
|
823
|
+
matching_types = (request.accept & types)
|
824
|
+
unless matching_types.empty?
|
825
|
+
response.headers['Content-Type'] = matching_types.first
|
826
|
+
true
|
827
|
+
else
|
828
|
+
false
|
829
|
+
end
|
830
|
+
}
|
831
|
+
end
|
832
|
+
|
833
|
+
public
|
834
|
+
# Defining a `GET` handler also automatically defines
|
835
|
+
# a `HEAD` handler.
|
836
|
+
def get(path, opts={}, &block)
|
837
|
+
conditions = @conditions.dup
|
838
|
+
route('GET', path, opts, &block)
|
839
|
+
|
840
|
+
@conditions = conditions
|
841
|
+
route('HEAD', path, opts, &block)
|
842
|
+
end
|
843
|
+
|
844
|
+
def put(path, opts={}, &bk); route 'PUT', path, opts, &bk end
|
845
|
+
def post(path, opts={}, &bk); route 'POST', path, opts, &bk end
|
846
|
+
def delete(path, opts={}, &bk); route 'DELETE', path, opts, &bk end
|
847
|
+
def head(path, opts={}, &bk); route 'HEAD', path, opts, &bk end
|
848
|
+
|
849
|
+
private
|
850
|
+
def route(verb, path, options={}, &block)
|
851
|
+
# Because of self.options.host
|
852
|
+
host_name(options.delete(:bind)) if options.key?(:host)
|
853
|
+
|
854
|
+
options.each {|option, args| send(option, *args)}
|
855
|
+
|
856
|
+
pattern, keys = compile(path)
|
857
|
+
conditions, @conditions = @conditions, []
|
858
|
+
|
859
|
+
define_method "#{verb} #{path}", &block
|
860
|
+
unbound_method = instance_method("#{verb} #{path}")
|
861
|
+
block =
|
862
|
+
if block.arity != 0
|
863
|
+
proc { unbound_method.bind(self).call(*@block_params) }
|
864
|
+
else
|
865
|
+
proc { unbound_method.bind(self).call }
|
866
|
+
end
|
867
|
+
|
868
|
+
invoke_hook(:route_added, verb, path, block)
|
869
|
+
|
870
|
+
(@routes[verb] ||= []).
|
871
|
+
push([pattern, keys, conditions, block]).last
|
872
|
+
end
|
873
|
+
|
874
|
+
def invoke_hook(name, *args)
|
875
|
+
extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
|
876
|
+
end
|
877
|
+
|
878
|
+
def compile(path)
|
879
|
+
keys = []
|
880
|
+
if path.respond_to? :to_str
|
881
|
+
special_chars = %w{. + ( )}
|
882
|
+
pattern =
|
883
|
+
path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
|
884
|
+
case match
|
885
|
+
when "*"
|
886
|
+
keys << 'splat'
|
887
|
+
"(.*?)"
|
888
|
+
when *special_chars
|
889
|
+
Regexp.escape(match)
|
890
|
+
else
|
891
|
+
keys << $2[1..-1]
|
892
|
+
"([^/?&#]+)"
|
893
|
+
end
|
894
|
+
end
|
895
|
+
[/^#{pattern}$/, keys]
|
896
|
+
elsif path.respond_to?(:keys) && path.respond_to?(:match)
|
897
|
+
[path, path.keys]
|
898
|
+
elsif path.respond_to? :match
|
899
|
+
[path, keys]
|
900
|
+
else
|
901
|
+
raise TypeError, path
|
902
|
+
end
|
903
|
+
end
|
904
|
+
|
905
|
+
public
|
906
|
+
# Makes the methods defined in the block and in the Modules given
|
907
|
+
# in `extensions` available to the handlers and templates
|
908
|
+
def helpers(*extensions, &block)
|
909
|
+
class_eval(&block) if block_given?
|
910
|
+
include(*extensions) if extensions.any?
|
911
|
+
end
|
912
|
+
|
913
|
+
def register(*extensions, &block)
|
914
|
+
extensions << Module.new(&block) if block_given?
|
915
|
+
@extensions += extensions
|
916
|
+
extensions.each do |extension|
|
917
|
+
extend extension
|
918
|
+
extension.registered(self) if extension.respond_to?(:registered)
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
def development?; environment == :development end
|
923
|
+
def production?; environment == :production end
|
924
|
+
def test?; environment == :test end
|
925
|
+
|
926
|
+
# Set configuration options for Sinatra and/or the app.
|
927
|
+
# Allows scoping of settings for certain environments.
|
928
|
+
def configure(*envs, &block)
|
929
|
+
yield self if envs.empty? || envs.include?(environment.to_sym)
|
930
|
+
end
|
931
|
+
|
932
|
+
# Use the specified Rack middleware
|
933
|
+
def use(middleware, *args, &block)
|
934
|
+
@prototype = nil
|
935
|
+
@middleware << [middleware, args, block]
|
936
|
+
end
|
937
|
+
|
938
|
+
# Run the Sinatra app as a self-hosted server using
|
939
|
+
# Thin, Mongrel or WEBrick (in that order)
|
940
|
+
def run!(options={})
|
941
|
+
set options
|
942
|
+
handler = detect_rack_handler
|
943
|
+
handler_name = handler.name.gsub(/.*::/, '')
|
944
|
+
puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
|
945
|
+
"on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
|
946
|
+
handler.run self, :Host => bind, :Port => port do |server|
|
947
|
+
trap(:INT) do
|
948
|
+
## Use thins' hard #stop! if available, otherwise just #stop
|
949
|
+
server.respond_to?(:stop!) ? server.stop! : server.stop
|
950
|
+
puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
|
951
|
+
end
|
952
|
+
set :running, true
|
953
|
+
end
|
954
|
+
rescue Errno::EADDRINUSE => e
|
955
|
+
puts "== Someone is already performing on port #{port}!"
|
956
|
+
end
|
957
|
+
|
958
|
+
# The prototype instance used to process requests.
|
959
|
+
def prototype
|
960
|
+
@prototype ||= new
|
961
|
+
end
|
962
|
+
|
963
|
+
# Create a new instance of the class fronted by its middleware
|
964
|
+
# pipeline. The object is guaranteed to respond to #call but may not be
|
965
|
+
# an instance of the class new was called on.
|
966
|
+
def new(*args, &bk)
|
967
|
+
builder = Rack::Builder.new
|
968
|
+
builder.use Rack::Session::Cookie if sessions?
|
969
|
+
builder.use Rack::CommonLogger if logging?
|
970
|
+
builder.use Rack::MethodOverride if method_override?
|
971
|
+
builder.use ShowExceptions if show_exceptions?
|
972
|
+
middleware.each { |c,a,b| builder.use(c, *a, &b) }
|
973
|
+
|
974
|
+
builder.run super
|
975
|
+
builder.to_app
|
976
|
+
end
|
977
|
+
|
978
|
+
def call(env)
|
979
|
+
synchronize { prototype.call(env) }
|
980
|
+
end
|
981
|
+
|
982
|
+
private
|
983
|
+
def detect_rack_handler
|
984
|
+
servers = Array(self.server)
|
985
|
+
servers.each do |server_name|
|
986
|
+
begin
|
987
|
+
return Rack::Handler.get(server_name.downcase)
|
988
|
+
rescue LoadError
|
989
|
+
rescue NameError
|
990
|
+
end
|
991
|
+
end
|
992
|
+
fail "Server handler (#{servers.join(',')}) not found."
|
993
|
+
end
|
994
|
+
|
995
|
+
def inherited(subclass)
|
996
|
+
subclass.reset!
|
997
|
+
super
|
998
|
+
end
|
999
|
+
|
1000
|
+
@@mutex = Mutex.new
|
1001
|
+
def synchronize(&block)
|
1002
|
+
if lock?
|
1003
|
+
@@mutex.synchronize(&block)
|
1004
|
+
else
|
1005
|
+
yield
|
1006
|
+
end
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
def metadef(message, &block)
|
1010
|
+
(class << self; self; end).
|
1011
|
+
send :define_method, message, &block
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
public
|
1015
|
+
CALLERS_TO_IGNORE = [
|
1016
|
+
/\/sinatra(\/(base|main|showexceptions))?\.rb$/, # all sinatra code
|
1017
|
+
/lib\/tilt.*\.rb$/, # all tilt code
|
1018
|
+
/\(.*\)/, # generated code
|
1019
|
+
/custom_require\.rb$/, # rubygems require hacks
|
1020
|
+
/active_support/, # active_support require hacks
|
1021
|
+
]
|
1022
|
+
|
1023
|
+
# add rubinius (and hopefully other VM impls) ignore patterns ...
|
1024
|
+
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS)
|
1025
|
+
|
1026
|
+
# Like Kernel#caller but excluding certain magic entries and without
|
1027
|
+
# line / method information; the resulting array contains filenames only.
|
1028
|
+
def caller_files
|
1029
|
+
caller_locations.
|
1030
|
+
map { |file,line| file }
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
def caller_locations
|
1034
|
+
caller(1).
|
1035
|
+
map { |line| line.split(/:(?=\d|in )/)[0,2] }.
|
1036
|
+
reject { |file,line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
|
1037
|
+
end
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
reset!
|
1041
|
+
|
1042
|
+
set :environment, (ENV['RACK_ENV'] || :development).to_sym
|
1043
|
+
set :raise_errors, Proc.new { test? }
|
1044
|
+
set :dump_errors, Proc.new { !test? }
|
1045
|
+
set :show_exceptions, Proc.new { development? }
|
1046
|
+
set :sessions, false
|
1047
|
+
set :logging, false
|
1048
|
+
set :method_override, false
|
1049
|
+
|
1050
|
+
class << self
|
1051
|
+
alias_method :methodoverride?, :method_override?
|
1052
|
+
alias_method :methodoverride=, :method_override=
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
set :run, false # start server via at-exit hook?
|
1056
|
+
set :running, false # is the built-in server running now?
|
1057
|
+
set :server, %w[thin mongrel webrick]
|
1058
|
+
set :bind, '0.0.0.0'
|
1059
|
+
set :port, 4567
|
1060
|
+
|
1061
|
+
set :app_file, nil
|
1062
|
+
set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
|
1063
|
+
set :views, Proc.new { root && File.join(root, 'views') }
|
1064
|
+
set :reload_templates, Proc.new { development? }
|
1065
|
+
set :lock, false
|
1066
|
+
|
1067
|
+
set :public, Proc.new { root && File.join(root, 'public') }
|
1068
|
+
set :static, Proc.new { self.public && File.exist?(self.public) }
|
1069
|
+
|
1070
|
+
error ::Exception do
|
1071
|
+
response.status = 500
|
1072
|
+
content_type 'text/html'
|
1073
|
+
'<h1>Internal Server Error</h1>'
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
configure :development do
|
1077
|
+
get '/__sinatra__/:image.png' do
|
1078
|
+
filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png"
|
1079
|
+
content_type :png
|
1080
|
+
send_file filename
|
1081
|
+
end
|
1082
|
+
|
1083
|
+
error NotFound do
|
1084
|
+
content_type 'text/html'
|
1085
|
+
|
1086
|
+
(<<-HTML).gsub(/^ {8}/, '')
|
1087
|
+
<!DOCTYPE html>
|
1088
|
+
<html>
|
1089
|
+
<head>
|
1090
|
+
<style type="text/css">
|
1091
|
+
body { text-align:center;font-family:helvetica,arial;font-size:22px;
|
1092
|
+
color:#888;margin:20px}
|
1093
|
+
#c {margin:0 auto;width:500px;text-align:left}
|
1094
|
+
</style>
|
1095
|
+
</head>
|
1096
|
+
<body>
|
1097
|
+
<h2>Sinatra doesn't know this ditty.</h2>
|
1098
|
+
<img src='/__sinatra__/404.png'>
|
1099
|
+
<div id="c">
|
1100
|
+
Try this:
|
1101
|
+
<pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
|
1102
|
+
</div>
|
1103
|
+
</body>
|
1104
|
+
</html>
|
1105
|
+
HTML
|
1106
|
+
end
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
# Execution context for classic style (top-level) applications. All
|
1111
|
+
# DSL methods executed on main are delegated to this class.
|
1112
|
+
#
|
1113
|
+
# The Application class should not be subclassed, unless you want to
|
1114
|
+
# inherit all settings, routes, handlers, and error pages from the
|
1115
|
+
# top-level. Subclassing Sinatra::Base is heavily recommended for
|
1116
|
+
# modular applications.
|
1117
|
+
class Application < Base
|
1118
|
+
set :logging, Proc.new { ! test? }
|
1119
|
+
set :method_override, true
|
1120
|
+
set :run, Proc.new { ! test? }
|
1121
|
+
|
1122
|
+
def self.register(*extensions, &block) #:nodoc:
|
1123
|
+
added_methods = extensions.map {|m| m.public_instance_methods }.flatten
|
1124
|
+
Delegator.delegate(*added_methods)
|
1125
|
+
super(*extensions, &block)
|
1126
|
+
end
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
# Sinatra delegation mixin. Mixing this module into an object causes all
|
1130
|
+
# methods to be delegated to the Sinatra::Application class. Used primarily
|
1131
|
+
# at the top-level.
|
1132
|
+
module Delegator #:nodoc:
|
1133
|
+
def self.delegate(*methods)
|
1134
|
+
methods.each do |method_name|
|
1135
|
+
eval <<-RUBY, binding, '(__DELEGATE__)', 1
|
1136
|
+
def #{method_name}(*args, &b)
|
1137
|
+
::Sinatra::Application.send(#{method_name.inspect}, *args, &b)
|
1138
|
+
end
|
1139
|
+
private #{method_name.inspect}
|
1140
|
+
RUBY
|
1141
|
+
end
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
delegate :get, :put, :post, :delete, :head, :template, :layout,
|
1145
|
+
:before, :after, :error, :not_found, :configure, :set, :mime_type,
|
1146
|
+
:enable, :disable, :use, :development?, :test?, :production?,
|
1147
|
+
:helpers, :settings
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
# Create a new Sinatra application. The block is evaluated in the new app's
|
1151
|
+
# class scope.
|
1152
|
+
def self.new(base=Base, options={}, &block)
|
1153
|
+
base = Class.new(base)
|
1154
|
+
base.send :class_eval, &block if block_given?
|
1155
|
+
base
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
# Extend the top-level DSL with the modules provided.
|
1159
|
+
def self.register(*extensions, &block)
|
1160
|
+
Application.register(*extensions, &block)
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
# Include the helper modules provided in Sinatra's request context.
|
1164
|
+
def self.helpers(*extensions, &block)
|
1165
|
+
Application.helpers(*extensions, &block)
|
1166
|
+
end
|
1167
|
+
end
|