sinatra 0.3.3 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sinatra might be problematic. Click here for more details.
- data/AUTHORS +40 -0
- data/CHANGES +189 -0
- data/README.rdoc +146 -117
- data/Rakefile +33 -10
- data/{test → compat}/app_test.rb +11 -10
- data/{test → compat}/application_test.rb +10 -5
- data/compat/builder_test.rb +101 -0
- data/{test → compat}/custom_error_test.rb +0 -0
- data/compat/erb_test.rb +136 -0
- data/{test → compat}/events_test.rb +16 -3
- data/compat/filter_test.rb +30 -0
- data/compat/haml_test.rb +233 -0
- data/compat/helper.rb +30 -0
- data/compat/mapped_error_test.rb +72 -0
- data/{test → compat}/pipeline_test.rb +9 -4
- data/{test → compat}/public/foo.xml +0 -0
- data/compat/sass_test.rb +57 -0
- data/{test → compat}/sessions_test.rb +0 -0
- data/{test → compat}/streaming_test.rb +4 -1
- data/{test → compat}/sym_params_test.rb +0 -0
- data/{test → compat}/template_test.rb +0 -0
- data/{test → compat}/use_in_file_templates_test.rb +0 -0
- data/{test → compat}/views/foo.builder +0 -0
- data/{test → compat}/views/foo.erb +0 -0
- data/{test → compat}/views/foo.haml +0 -0
- data/{test → compat}/views/foo.sass +0 -0
- data/{test → compat}/views/foo_layout.erb +0 -0
- data/{test → compat}/views/foo_layout.haml +0 -0
- data/{test → compat}/views/layout_test/foo.builder +0 -0
- data/{test → compat}/views/layout_test/foo.erb +0 -0
- data/{test → compat}/views/layout_test/foo.haml +0 -0
- data/{test → compat}/views/layout_test/foo.sass +0 -0
- data/{test → compat}/views/layout_test/layout.builder +0 -0
- data/{test → compat}/views/layout_test/layout.erb +0 -0
- data/{test → compat}/views/layout_test/layout.haml +0 -0
- data/{test → compat}/views/layout_test/layout.sass +0 -0
- data/{test → compat}/views/no_layout/no_layout.builder +0 -0
- data/{test → compat}/views/no_layout/no_layout.haml +0 -0
- data/lib/sinatra.rb +6 -1484
- data/lib/sinatra/base.rb +838 -0
- data/lib/sinatra/compat.rb +239 -0
- data/{images → lib/sinatra/images}/404.png +0 -0
- data/{images → lib/sinatra/images}/500.png +0 -0
- data/lib/sinatra/main.rb +48 -0
- data/lib/sinatra/test.rb +114 -0
- data/lib/sinatra/test/bacon.rb +17 -0
- data/lib/sinatra/test/rspec.rb +7 -8
- data/lib/sinatra/test/spec.rb +3 -4
- data/lib/sinatra/test/unit.rb +3 -5
- data/sinatra.gemspec +68 -35
- data/test/base_test.rb +68 -0
- data/test/builder_test.rb +50 -87
- data/test/data/reload_app_file.rb +3 -0
- data/test/erb_test.rb +38 -124
- data/test/filter_test.rb +27 -22
- data/test/haml_test.rb +51 -216
- data/test/helper.rb +22 -6
- data/test/helpers_test.rb +361 -0
- data/test/mapped_error_test.rb +137 -49
- data/test/middleware_test.rb +58 -0
- data/test/options_test.rb +97 -0
- data/test/reload_test.rb +61 -0
- data/test/request_test.rb +18 -0
- data/test/result_test.rb +88 -0
- data/test/routing_test.rb +391 -0
- data/test/sass_test.rb +27 -48
- data/test/sinatra_test.rb +13 -0
- data/test/static_test.rb +57 -0
- data/test/templates_test.rb +88 -0
- data/test/views/hello.builder +1 -0
- data/test/views/hello.erb +1 -0
- data/test/views/hello.haml +1 -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.haml +2 -0
- data/test/views/layout2.test +1 -0
- metadata +80 -48
- data/ChangeLog +0 -96
- data/lib/sinatra/test/methods.rb +0 -76
- data/test/event_context_test.rb +0 -15
data/lib/sinatra/base.rb
ADDED
@@ -0,0 +1,838 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'uri'
|
3
|
+
require 'rack'
|
4
|
+
require 'rack/builder'
|
5
|
+
|
6
|
+
module Sinatra
|
7
|
+
VERSION = '0.9.0'
|
8
|
+
|
9
|
+
class Request < Rack::Request
|
10
|
+
def user_agent
|
11
|
+
@env['HTTP_USER_AGENT']
|
12
|
+
end
|
13
|
+
|
14
|
+
def accept
|
15
|
+
@env['HTTP_ACCEPT'].split(',').map { |a| a.strip }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override Rack 0.9.x's #params implementation (see #72 in lighthouse)
|
19
|
+
def params
|
20
|
+
self.GET.update(self.POST)
|
21
|
+
rescue EOFError => boom
|
22
|
+
self.GET
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Response < Rack::Response
|
27
|
+
def initialize
|
28
|
+
@status, @body = 200, []
|
29
|
+
@header = Rack::Utils::HeaderHash.new({'Content-Type' => 'text/html'})
|
30
|
+
end
|
31
|
+
|
32
|
+
def write(str)
|
33
|
+
@body << str.to_s
|
34
|
+
str
|
35
|
+
end
|
36
|
+
|
37
|
+
def finish
|
38
|
+
@body = block if block_given?
|
39
|
+
if [204, 304].include?(status.to_i)
|
40
|
+
header.delete "Content-Type"
|
41
|
+
[status.to_i, header.to_hash, []]
|
42
|
+
else
|
43
|
+
body = @body || []
|
44
|
+
body = [body] if body.respond_to? :to_str
|
45
|
+
if header["Content-Length"].nil? && body.respond_to?(:to_ary)
|
46
|
+
header["Content-Length"] = body.to_ary.
|
47
|
+
inject(0) { |len, part| len + part.length }.to_s
|
48
|
+
end
|
49
|
+
[status.to_i, header.to_hash, body]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class NotFound < NameError # :)
|
55
|
+
def code ; 404 ; end
|
56
|
+
end
|
57
|
+
|
58
|
+
module Helpers
|
59
|
+
# Set or retrieve the response status code.
|
60
|
+
def status(value=nil)
|
61
|
+
response.status = value if value
|
62
|
+
response.status
|
63
|
+
end
|
64
|
+
|
65
|
+
# Set or retrieve the response body. When a block is given,
|
66
|
+
# evaluation is deferred until the body is read with #each.
|
67
|
+
def body(value=nil, &block)
|
68
|
+
if block_given?
|
69
|
+
def block.each ; yield call ; end
|
70
|
+
response.body = block
|
71
|
+
else
|
72
|
+
response.body = value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Halt processing and redirect to the URI provided.
|
77
|
+
def redirect(uri, *args)
|
78
|
+
status 302
|
79
|
+
response['Location'] = uri
|
80
|
+
halt(*args)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Halt processing and return the error status provided.
|
84
|
+
def error(code, body=nil)
|
85
|
+
code, body = 500, code.to_str if code.respond_to? :to_str
|
86
|
+
response.body = body unless body.nil?
|
87
|
+
halt code
|
88
|
+
end
|
89
|
+
|
90
|
+
# Halt processing and return a 404 Not Found.
|
91
|
+
def not_found(body=nil)
|
92
|
+
error 404, body
|
93
|
+
end
|
94
|
+
|
95
|
+
# Access the underlying Rack session.
|
96
|
+
def session
|
97
|
+
env['rack.session'] ||= {}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Look up a media type by file extension in Rack's mime registry.
|
101
|
+
def media_type(type)
|
102
|
+
Base.media_type(type)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Set the Content-Type of the response body given a media type or file
|
106
|
+
# extension.
|
107
|
+
def content_type(type, params={})
|
108
|
+
media_type = self.media_type(type)
|
109
|
+
fail "Unknown media type: %p" % type if media_type.nil?
|
110
|
+
if params.any?
|
111
|
+
params = params.collect { |kv| "%s=%s" % kv }.join(', ')
|
112
|
+
response['Content-Type'] = [media_type, params].join(";")
|
113
|
+
else
|
114
|
+
response['Content-Type'] = media_type
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Set the Content-Disposition to "attachment" with the specified filename,
|
119
|
+
# instructing the user agents to prompt to save.
|
120
|
+
def attachment(filename=nil)
|
121
|
+
response['Content-Disposition'] = 'attachment'
|
122
|
+
if filename
|
123
|
+
params = '; filename="%s"' % File.basename(filename)
|
124
|
+
response['Content-Disposition'] << params
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Use the contents of the file as the response body and attempt to
|
129
|
+
def send_file(path, opts={})
|
130
|
+
stat = File.stat(path)
|
131
|
+
last_modified stat.mtime
|
132
|
+
content_type media_type(opts[:type]) ||
|
133
|
+
media_type(File.extname(path)) ||
|
134
|
+
response['Content-Type'] ||
|
135
|
+
'application/octet-stream'
|
136
|
+
response['Content-Length'] ||= (opts[:length] || stat.size).to_s
|
137
|
+
halt StaticFile.open(path, 'rb')
|
138
|
+
rescue Errno::ENOENT
|
139
|
+
not_found
|
140
|
+
end
|
141
|
+
|
142
|
+
class StaticFile < ::File #:nodoc:
|
143
|
+
alias_method :to_path, :path
|
144
|
+
def each
|
145
|
+
while buf = read(8192)
|
146
|
+
yield buf
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
|
152
|
+
# and halt if conditional GET matches. The +time+ argument is a Time,
|
153
|
+
# DateTime, or other object that responds to +to_time+.
|
154
|
+
#
|
155
|
+
# When the current request includes an 'If-Modified-Since' header that
|
156
|
+
# matches the time specified, execution is immediately halted with a
|
157
|
+
# '304 Not Modified' response.
|
158
|
+
def last_modified(time)
|
159
|
+
time = time.to_time if time.respond_to?(:to_time)
|
160
|
+
time = time.httpdate if time.respond_to?(:httpdate)
|
161
|
+
response['Last-Modified'] = time
|
162
|
+
halt 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
|
163
|
+
time
|
164
|
+
end
|
165
|
+
|
166
|
+
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
|
167
|
+
# GET matches. The +value+ argument is an identifier that uniquely
|
168
|
+
# identifies the current version of the resource. The +strength+ argument
|
169
|
+
# indicates whether the etag should be used as a :strong (default) or :weak
|
170
|
+
# cache validator.
|
171
|
+
#
|
172
|
+
# When the current request includes an 'If-None-Match' header with a
|
173
|
+
# matching etag, execution is immediately halted. If the request method is
|
174
|
+
# GET or HEAD, a '304 Not Modified' response is sent.
|
175
|
+
def etag(value, kind=:strong)
|
176
|
+
raise TypeError, ":strong or :weak expected" if ![:strong,:weak].include?(kind)
|
177
|
+
value = '"%s"' % value
|
178
|
+
value = 'W/' + value if kind == :weak
|
179
|
+
response['ETag'] = value
|
180
|
+
|
181
|
+
# Conditional GET check
|
182
|
+
if etags = env['HTTP_IF_NONE_MATCH']
|
183
|
+
etags = etags.split(/\s*,\s*/)
|
184
|
+
halt 304 if etags.include?(value) || etags.include?('*')
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
module Templates
|
190
|
+
def render(engine, template, options={})
|
191
|
+
data = lookup_template(engine, template, options)
|
192
|
+
output = __send__("render_#{engine}", template, data, options)
|
193
|
+
layout, data = lookup_layout(engine, options)
|
194
|
+
if layout
|
195
|
+
__send__("render_#{engine}", layout, data, options) { output }
|
196
|
+
else
|
197
|
+
output
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def lookup_template(engine, template, options={})
|
202
|
+
case template
|
203
|
+
when Symbol
|
204
|
+
if cached = self.class.templates[template]
|
205
|
+
lookup_template(engine, cached, options)
|
206
|
+
else
|
207
|
+
::File.read(template_path(engine, template, options))
|
208
|
+
end
|
209
|
+
when Proc
|
210
|
+
template.call
|
211
|
+
when String
|
212
|
+
template
|
213
|
+
else
|
214
|
+
raise ArgumentError
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def lookup_layout(engine, options)
|
219
|
+
return if options[:layout] == false
|
220
|
+
options.delete(:layout) if options[:layout] == true
|
221
|
+
template = options[:layout] || :layout
|
222
|
+
data = lookup_template(engine, template, options)
|
223
|
+
[template, data]
|
224
|
+
rescue Errno::ENOENT
|
225
|
+
nil
|
226
|
+
end
|
227
|
+
|
228
|
+
def template_path(engine, template, options={})
|
229
|
+
views_dir =
|
230
|
+
options[:views_directory] || self.options.views || "./views"
|
231
|
+
"#{views_dir}/#{template}.#{engine}"
|
232
|
+
end
|
233
|
+
|
234
|
+
def erb(template, options={})
|
235
|
+
require 'erb' unless defined? ::ERB
|
236
|
+
render :erb, template, options
|
237
|
+
end
|
238
|
+
|
239
|
+
def render_erb(template, data, options, &block)
|
240
|
+
data = data.call if data.kind_of? Proc
|
241
|
+
instance = ::ERB.new(data)
|
242
|
+
locals = options[:locals] || {}
|
243
|
+
locals_assigns = locals.to_a.collect { |k,v| "#{k} = locals[:#{k}]" }
|
244
|
+
src = "#{locals_assigns.join("\n")}\n#{instance.src}"
|
245
|
+
eval src, binding, '(__ERB__)', locals_assigns.length + 1
|
246
|
+
instance.result(binding)
|
247
|
+
end
|
248
|
+
|
249
|
+
def haml(template, options={})
|
250
|
+
require 'haml' unless defined? ::Haml
|
251
|
+
options[:options] ||= self.class.haml if self.class.respond_to? :haml
|
252
|
+
render :haml, template, options
|
253
|
+
end
|
254
|
+
|
255
|
+
def render_haml(template, data, options, &block)
|
256
|
+
engine = ::Haml::Engine.new(data, options[:options] || {})
|
257
|
+
engine.render(self, options[:locals] || {}, &block)
|
258
|
+
end
|
259
|
+
|
260
|
+
def sass(template, options={}, &block)
|
261
|
+
require 'sass' unless defined? ::Sass
|
262
|
+
options[:layout] = false
|
263
|
+
render :sass, template, options
|
264
|
+
end
|
265
|
+
|
266
|
+
def render_sass(template, data, options, &block)
|
267
|
+
engine = ::Sass::Engine.new(data, options[:sass] || {})
|
268
|
+
engine.render
|
269
|
+
end
|
270
|
+
|
271
|
+
def builder(template=nil, options={}, &block)
|
272
|
+
require 'builder' unless defined? ::Builder
|
273
|
+
options, template = template, nil if template.is_a?(Hash)
|
274
|
+
template = lambda { block } if template.nil?
|
275
|
+
render :builder, template, options
|
276
|
+
end
|
277
|
+
|
278
|
+
def render_builder(template, data, options, &block)
|
279
|
+
xml = ::Builder::XmlMarkup.new(:indent => 2)
|
280
|
+
if data.respond_to?(:to_str)
|
281
|
+
eval data.to_str, binding, '<BUILDER>', 1
|
282
|
+
elsif data.kind_of?(Proc)
|
283
|
+
data.call(xml)
|
284
|
+
end
|
285
|
+
xml.target!
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
class Base
|
291
|
+
include Rack::Utils
|
292
|
+
include Helpers
|
293
|
+
include Templates
|
294
|
+
|
295
|
+
attr_accessor :app
|
296
|
+
|
297
|
+
def initialize(app=nil)
|
298
|
+
@app = app
|
299
|
+
yield self if block_given?
|
300
|
+
end
|
301
|
+
|
302
|
+
def call(env)
|
303
|
+
dup.call!(env)
|
304
|
+
end
|
305
|
+
|
306
|
+
attr_accessor :env, :request, :response, :params
|
307
|
+
|
308
|
+
def call!(env)
|
309
|
+
@env = env
|
310
|
+
@request = Request.new(env)
|
311
|
+
@response = Response.new
|
312
|
+
@params = nil
|
313
|
+
error_detection { dispatch! }
|
314
|
+
@response.finish
|
315
|
+
end
|
316
|
+
|
317
|
+
def options
|
318
|
+
self.class
|
319
|
+
end
|
320
|
+
|
321
|
+
def halt(*response)
|
322
|
+
throw :halt, *response
|
323
|
+
end
|
324
|
+
|
325
|
+
def pass
|
326
|
+
throw :pass
|
327
|
+
end
|
328
|
+
|
329
|
+
private
|
330
|
+
def dispatch!
|
331
|
+
self.class.filters.each {|block| instance_eval(&block)}
|
332
|
+
if routes = self.class.routes[@request.request_method]
|
333
|
+
path = @request.path_info
|
334
|
+
original_params = nested_params(@request.params)
|
335
|
+
|
336
|
+
routes.each do |pattern, keys, conditions, method_name|
|
337
|
+
if pattern =~ path
|
338
|
+
values = $~.captures.map{|val| val && unescape(val) }
|
339
|
+
params =
|
340
|
+
if keys.any?
|
341
|
+
keys.zip(values).inject({}) do |hash,(k,v)|
|
342
|
+
if k == 'splat'
|
343
|
+
(hash[k] ||= []) << v
|
344
|
+
else
|
345
|
+
hash[k] = v
|
346
|
+
end
|
347
|
+
hash
|
348
|
+
end
|
349
|
+
elsif values.any?
|
350
|
+
{'captures' => values}
|
351
|
+
else
|
352
|
+
{}
|
353
|
+
end
|
354
|
+
@params = original_params.merge(params)
|
355
|
+
|
356
|
+
catch(:pass) {
|
357
|
+
conditions.each { |cond|
|
358
|
+
throw :pass if instance_eval(&cond) == false }
|
359
|
+
return invoke(method_name)
|
360
|
+
}
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
raise NotFound
|
365
|
+
end
|
366
|
+
|
367
|
+
def nested_params(params)
|
368
|
+
return indifferent_hash.merge(params) if !params.keys.join.include?('[')
|
369
|
+
params.inject indifferent_hash do |res, (key,val)|
|
370
|
+
if key =~ /\[.*\]/
|
371
|
+
splat = key.scan(/(^[^\[]+)|\[([^\]]+)\]/).flatten.compact
|
372
|
+
head, last = splat[0..-2], splat[-1]
|
373
|
+
head.inject(res){ |s,v| s[v] ||= indifferent_hash }[last] = val
|
374
|
+
end
|
375
|
+
res
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def indifferent_hash
|
380
|
+
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
381
|
+
end
|
382
|
+
|
383
|
+
def invoke(block)
|
384
|
+
res = catch(:halt) { instance_eval(&block) }
|
385
|
+
case
|
386
|
+
when res.respond_to?(:to_str)
|
387
|
+
@response.body = [res]
|
388
|
+
when res.respond_to?(:to_ary)
|
389
|
+
res = res.to_ary
|
390
|
+
if Fixnum === res.first
|
391
|
+
if res.length == 3
|
392
|
+
@response.status, headers, body = res
|
393
|
+
@response.body = body if body
|
394
|
+
headers.each { |k, v| @response.headers[k] = v } if headers
|
395
|
+
elsif res.length == 2
|
396
|
+
@response.status = res.first
|
397
|
+
@response.body = res.last
|
398
|
+
else
|
399
|
+
raise TypeError, "#{res.inspect} not supported"
|
400
|
+
end
|
401
|
+
else
|
402
|
+
@response.body = res
|
403
|
+
end
|
404
|
+
when res.respond_to?(:each)
|
405
|
+
@response.body = res
|
406
|
+
when (100...599) === res
|
407
|
+
@response.status = res
|
408
|
+
when res.nil?
|
409
|
+
@response.body = []
|
410
|
+
end
|
411
|
+
res
|
412
|
+
end
|
413
|
+
|
414
|
+
def error_detection
|
415
|
+
errmap = self.class.errors
|
416
|
+
yield
|
417
|
+
rescue NotFound => boom
|
418
|
+
@env['sinatra.error'] = boom
|
419
|
+
@response.status = 404
|
420
|
+
@response.body = ['<h1>Not Found</h1>']
|
421
|
+
handler = errmap[boom.class] || errmap[NotFound]
|
422
|
+
invoke handler unless handler.nil?
|
423
|
+
rescue ::Exception => boom
|
424
|
+
@env['sinatra.error'] = boom
|
425
|
+
|
426
|
+
if options.dump_errors?
|
427
|
+
msg = ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n ")
|
428
|
+
@env['rack.errors'] << msg
|
429
|
+
end
|
430
|
+
|
431
|
+
raise boom if options.raise_errors?
|
432
|
+
@response.status = 500
|
433
|
+
invoke errmap[boom.class] || errmap[Exception]
|
434
|
+
ensure
|
435
|
+
if @response.status >= 400 && errmap.key?(response.status)
|
436
|
+
invoke errmap[response.status]
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
@routes = {}
|
441
|
+
@filters = []
|
442
|
+
@conditions = []
|
443
|
+
@templates = {}
|
444
|
+
@middleware = []
|
445
|
+
@callsite = nil
|
446
|
+
@errors = {}
|
447
|
+
|
448
|
+
class << self
|
449
|
+
attr_accessor :routes, :filters, :conditions, :templates,
|
450
|
+
:middleware, :errors
|
451
|
+
|
452
|
+
def set(option, value=self)
|
453
|
+
if value.kind_of?(Proc)
|
454
|
+
metadef(option, &value)
|
455
|
+
metadef("#{option}?") { !!__send__(option) }
|
456
|
+
metadef("#{option}=") { |val| set(option, Proc.new{val}) }
|
457
|
+
elsif value == self && option.respond_to?(:to_hash)
|
458
|
+
option.to_hash.each(&method(:set))
|
459
|
+
elsif respond_to?("#{option}=")
|
460
|
+
__send__ "#{option}=", value
|
461
|
+
else
|
462
|
+
set option, Proc.new{value}
|
463
|
+
end
|
464
|
+
self
|
465
|
+
end
|
466
|
+
|
467
|
+
def enable(*opts)
|
468
|
+
opts.each { |key| set(key, true) }
|
469
|
+
end
|
470
|
+
|
471
|
+
def disable(*opts)
|
472
|
+
opts.each { |key| set(key, false) }
|
473
|
+
end
|
474
|
+
|
475
|
+
def error(codes=Exception, &block)
|
476
|
+
if codes.respond_to? :each
|
477
|
+
codes.each { |err| error(err, &block) }
|
478
|
+
else
|
479
|
+
@errors[codes] = block
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
def not_found(&block)
|
484
|
+
error 404, &block
|
485
|
+
end
|
486
|
+
|
487
|
+
def template(name, &block)
|
488
|
+
templates[name] = block
|
489
|
+
end
|
490
|
+
|
491
|
+
def layout(name=:layout, &block)
|
492
|
+
template name, &block
|
493
|
+
end
|
494
|
+
|
495
|
+
def use_in_file_templates!
|
496
|
+
line = caller.detect do |s|
|
497
|
+
[
|
498
|
+
/lib\/sinatra.*\.rb/,
|
499
|
+
/\(.*\)/,
|
500
|
+
/rubygems\/custom_require\.rb/
|
501
|
+
].all? { |x| s !~ x }
|
502
|
+
end
|
503
|
+
file = line.sub(/:\d+.*$/, '')
|
504
|
+
if data = ::IO.read(file).split('__END__')[1]
|
505
|
+
data.gsub!(/\r\n/, "\n")
|
506
|
+
template = nil
|
507
|
+
data.each_line do |line|
|
508
|
+
if line =~ /^@@\s*(.*)/
|
509
|
+
template = templates[$1.to_sym] = ''
|
510
|
+
elsif template
|
511
|
+
template << line
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
# Look up a media type by file extension in Rack's mime registry.
|
518
|
+
def media_type(type)
|
519
|
+
return type if type.nil? || type.to_s.include?('/')
|
520
|
+
type = ".#{type}" unless type.to_s[0] == ?.
|
521
|
+
Rack::Mime.mime_type(type, nil)
|
522
|
+
end
|
523
|
+
|
524
|
+
def before(&block)
|
525
|
+
@filters << block
|
526
|
+
end
|
527
|
+
|
528
|
+
def condition(&block)
|
529
|
+
@conditions << block
|
530
|
+
end
|
531
|
+
|
532
|
+
def host_name(pattern)
|
533
|
+
condition { pattern === request.host }
|
534
|
+
end
|
535
|
+
|
536
|
+
def user_agent(pattern)
|
537
|
+
condition {
|
538
|
+
if request.user_agent =~ pattern
|
539
|
+
@params[:agent] = $~[1..-1]
|
540
|
+
true
|
541
|
+
else
|
542
|
+
false
|
543
|
+
end
|
544
|
+
}
|
545
|
+
end
|
546
|
+
|
547
|
+
def accept_mime_types(types)
|
548
|
+
types = [types] unless types.kind_of? Array
|
549
|
+
types.map!{|t| media_type(t)}
|
550
|
+
|
551
|
+
condition {
|
552
|
+
matching_types = (request.accept & types)
|
553
|
+
unless matching_types.empty?
|
554
|
+
response.headers['Content-Type'] = matching_types.first
|
555
|
+
true
|
556
|
+
else
|
557
|
+
false
|
558
|
+
end
|
559
|
+
}
|
560
|
+
end
|
561
|
+
|
562
|
+
def get(path, opts={}, &block)
|
563
|
+
conditions = @conditions.dup
|
564
|
+
route('GET', path, opts, &block)
|
565
|
+
|
566
|
+
@conditions = conditions
|
567
|
+
head(path, opts) { invoke(block) ; [] }
|
568
|
+
end
|
569
|
+
|
570
|
+
def put(path, opts={}, &bk); route 'PUT', path, opts, &bk; end
|
571
|
+
def post(path, opts={}, &bk); route 'POST', path, opts, &bk; end
|
572
|
+
def delete(path, opts={}, &bk); route 'DELETE', path, opts, &bk; end
|
573
|
+
def head(path, opts={}, &bk); route 'HEAD', path, opts, &bk; end
|
574
|
+
|
575
|
+
private
|
576
|
+
def route(verb, path, opts={}, &block)
|
577
|
+
host_name opts[:host] if opts.key?(:host)
|
578
|
+
user_agent opts[:agent] if opts.key?(:agent)
|
579
|
+
accept_mime_types opts[:provides] if opts.key?(:provides)
|
580
|
+
|
581
|
+
pattern, keys = compile(path)
|
582
|
+
conditions, @conditions = @conditions, []
|
583
|
+
|
584
|
+
define_method "#{verb} #{path}", &block
|
585
|
+
unbound_method = instance_method("#{verb} #{path}")
|
586
|
+
block = lambda { unbound_method.bind(self).call }
|
587
|
+
|
588
|
+
(routes[verb] ||= []).
|
589
|
+
push([pattern, keys, conditions, block]).last
|
590
|
+
end
|
591
|
+
|
592
|
+
def compile(path)
|
593
|
+
keys = []
|
594
|
+
if path.respond_to? :to_str
|
595
|
+
pattern =
|
596
|
+
URI.encode(path).gsub(/((:\w+)|\*)/) do |match|
|
597
|
+
if match == "*"
|
598
|
+
keys << 'splat'
|
599
|
+
"(.*?)"
|
600
|
+
else
|
601
|
+
keys << $2[1..-1]
|
602
|
+
"([^/?&#\.]+)"
|
603
|
+
end
|
604
|
+
end
|
605
|
+
[/^#{pattern}$/, keys]
|
606
|
+
elsif path.respond_to? :=~
|
607
|
+
[path, keys]
|
608
|
+
else
|
609
|
+
raise TypeError, path
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
public
|
614
|
+
def development? ; environment == :development ; end
|
615
|
+
def test? ; environment == :test ; end
|
616
|
+
def production? ; environment == :production ; end
|
617
|
+
|
618
|
+
def configure(*envs, &block)
|
619
|
+
yield if envs.empty? || envs.include?(environment.to_sym)
|
620
|
+
end
|
621
|
+
|
622
|
+
def use(middleware, *args, &block)
|
623
|
+
reset_middleware
|
624
|
+
@middleware << [middleware, args, block]
|
625
|
+
end
|
626
|
+
|
627
|
+
def run!(options={})
|
628
|
+
set(options)
|
629
|
+
handler = Rack::Handler.get(server)
|
630
|
+
handler_name = handler.name.gsub(/.*::/, '')
|
631
|
+
puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
|
632
|
+
"on #{port} for #{environment} with backup from #{handler_name}"
|
633
|
+
handler.run self, :Host => host, :Port => port do |server|
|
634
|
+
trap(:INT) do
|
635
|
+
## Use thins' hard #stop! if available, otherwise just #stop
|
636
|
+
server.respond_to?(:stop!) ? server.stop! : server.stop
|
637
|
+
puts "\n== Sinatra has ended his set (crowd applauds)"
|
638
|
+
end
|
639
|
+
end
|
640
|
+
rescue Errno::EADDRINUSE => e
|
641
|
+
puts "== Someone is already performing on port #{port}!"
|
642
|
+
end
|
643
|
+
|
644
|
+
def call(env)
|
645
|
+
construct_middleware if @callsite.nil?
|
646
|
+
@callsite.call(env)
|
647
|
+
end
|
648
|
+
|
649
|
+
private
|
650
|
+
def construct_middleware(builder=Rack::Builder.new)
|
651
|
+
builder.use Rack::Session::Cookie if sessions?
|
652
|
+
builder.use Rack::CommonLogger if logging?
|
653
|
+
builder.use Rack::MethodOverride if methodoverride?
|
654
|
+
@middleware.each { |c, args, bk| builder.use(c, *args, &bk) }
|
655
|
+
builder.run new
|
656
|
+
@callsite = builder.to_app
|
657
|
+
end
|
658
|
+
|
659
|
+
def reset_middleware
|
660
|
+
@callsite = nil
|
661
|
+
end
|
662
|
+
|
663
|
+
def inherited(subclass)
|
664
|
+
subclass.routes = dupe_routes
|
665
|
+
subclass.templates = templates.dup
|
666
|
+
subclass.conditions = []
|
667
|
+
subclass.filters = filters.dup
|
668
|
+
subclass.errors = errors.dup
|
669
|
+
subclass.middleware = middleware.dup
|
670
|
+
subclass.send :reset_middleware
|
671
|
+
super
|
672
|
+
end
|
673
|
+
|
674
|
+
def dupe_routes
|
675
|
+
routes.inject({}) do |hash,(request_method,routes)|
|
676
|
+
hash[request_method] = routes.dup
|
677
|
+
hash
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
def metadef(message, &block)
|
682
|
+
(class << self; self; end).
|
683
|
+
send :define_method, message, &block
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
set :raise_errors, true
|
688
|
+
set :dump_errors, false
|
689
|
+
set :sessions, false
|
690
|
+
set :logging, false
|
691
|
+
set :methodoverride, false
|
692
|
+
set :static, false
|
693
|
+
set :environment, (ENV['RACK_ENV'] || :development).to_sym
|
694
|
+
|
695
|
+
set :run, false
|
696
|
+
set :server, (defined?(Rack::Handler::Thin) ? "thin" : "mongrel")
|
697
|
+
set :host, '0.0.0.0'
|
698
|
+
set :port, 4567
|
699
|
+
|
700
|
+
set :app_file, nil
|
701
|
+
set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
|
702
|
+
set :views, Proc.new { root && File.join(root, 'views') }
|
703
|
+
set :public, Proc.new { root && File.join(root, 'public') }
|
704
|
+
|
705
|
+
# static files route
|
706
|
+
get(/.*[^\/]$/) do
|
707
|
+
pass unless options.static? && options.public?
|
708
|
+
path = options.public + unescape(request.path_info)
|
709
|
+
pass unless File.file?(path)
|
710
|
+
send_file path, :disposition => nil
|
711
|
+
end
|
712
|
+
|
713
|
+
error ::Exception do
|
714
|
+
response.status = 500
|
715
|
+
content_type 'text/html'
|
716
|
+
'<h1>Internal Server Error</h1>'
|
717
|
+
end
|
718
|
+
|
719
|
+
configure :development do
|
720
|
+
get '/__sinatra__/:image.png' do
|
721
|
+
filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png"
|
722
|
+
content_type :png
|
723
|
+
send_file filename
|
724
|
+
end
|
725
|
+
|
726
|
+
error NotFound do
|
727
|
+
(<<-HTML).gsub(/^ {8}/, '')
|
728
|
+
<!DOCTYPE html>
|
729
|
+
<html>
|
730
|
+
<head>
|
731
|
+
<style type="text/css">
|
732
|
+
body { text-align:center;font-family:helvetica,arial;font-size:22px;
|
733
|
+
color:#888;margin:20px}
|
734
|
+
#c {margin:0 auto;width:500px;text-align:left}
|
735
|
+
</style>
|
736
|
+
</head>
|
737
|
+
<body>
|
738
|
+
<h2>Sinatra doesn't know this ditty.</h2>
|
739
|
+
<img src='/__sinatra__/404.png'>
|
740
|
+
<div id="c">
|
741
|
+
Try this:
|
742
|
+
<pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
|
743
|
+
</div>
|
744
|
+
</body>
|
745
|
+
</html>
|
746
|
+
HTML
|
747
|
+
end
|
748
|
+
|
749
|
+
error do
|
750
|
+
next unless err = request.env['sinatra.error']
|
751
|
+
heading = err.class.name + ' - ' + err.message.to_s
|
752
|
+
(<<-HTML).gsub(/^ {8}/, '')
|
753
|
+
<!DOCTYPE html>
|
754
|
+
<html>
|
755
|
+
<head>
|
756
|
+
<style type="text/css">
|
757
|
+
body {font-family:verdana;color:#333}
|
758
|
+
#c {margin-left:20px}
|
759
|
+
h1 {color:#1D6B8D;margin:0;margin-top:-30px}
|
760
|
+
h2 {color:#1D6B8D;font-size:18px}
|
761
|
+
pre {border-left:2px solid #ddd;padding-left:10px;color:#000}
|
762
|
+
img {margin-top:10px}
|
763
|
+
</style>
|
764
|
+
</head>
|
765
|
+
<body>
|
766
|
+
<div id="c">
|
767
|
+
<img src="/__sinatra__/500.png">
|
768
|
+
<h1>#{escape_html(heading)}</h1>
|
769
|
+
<pre class='trace'>#{escape_html(err.backtrace.join("\n"))}</pre>
|
770
|
+
<h2>Params</h2>
|
771
|
+
<pre>#{escape_html(params.inspect)}</pre>
|
772
|
+
</div>
|
773
|
+
</body>
|
774
|
+
</html>
|
775
|
+
HTML
|
776
|
+
end
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
class Default < Base
|
781
|
+
set :raise_errors, false
|
782
|
+
set :dump_errors, true
|
783
|
+
set :sessions, false
|
784
|
+
set :logging, true
|
785
|
+
set :methodoverride, true
|
786
|
+
set :static, true
|
787
|
+
set :run, false
|
788
|
+
set :reload, Proc.new { app_file? && development? }
|
789
|
+
|
790
|
+
def self.reloading?
|
791
|
+
@reloading ||= false
|
792
|
+
end
|
793
|
+
|
794
|
+
def self.configure(*envs)
|
795
|
+
super unless reloading?
|
796
|
+
end
|
797
|
+
|
798
|
+
def self.call(env)
|
799
|
+
reload! if reload?
|
800
|
+
super
|
801
|
+
end
|
802
|
+
|
803
|
+
def self.reload!
|
804
|
+
@reloading = true
|
805
|
+
superclass.send :inherited, self
|
806
|
+
$LOADED_FEATURES.delete("sinatra.rb")
|
807
|
+
::Kernel.load app_file
|
808
|
+
@reloading = false
|
809
|
+
end
|
810
|
+
|
811
|
+
end
|
812
|
+
|
813
|
+
class Application < Default
|
814
|
+
end
|
815
|
+
|
816
|
+
module Delegator
|
817
|
+
METHODS = %w[
|
818
|
+
get put post delete head template layout before error not_found
|
819
|
+
configures configure set set_option set_options enable disable use
|
820
|
+
development? test? production? use_in_file_templates!
|
821
|
+
]
|
822
|
+
|
823
|
+
METHODS.each do |method_name|
|
824
|
+
eval <<-RUBY, binding, '(__DELEGATE__)', 1
|
825
|
+
def #{method_name}(*args, &b)
|
826
|
+
::Sinatra::Application.#{method_name}(*args, &b)
|
827
|
+
end
|
828
|
+
private :#{method_name}
|
829
|
+
RUBY
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
def self.new(base=Base, options={}, &block)
|
834
|
+
base = Class.new(base)
|
835
|
+
base.send :class_eval, &block if block_given?
|
836
|
+
base
|
837
|
+
end
|
838
|
+
end
|