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.
Files changed (58) hide show
  1. data/AUTHORS +43 -0
  2. data/CHANGES +511 -0
  3. data/LICENSE +22 -0
  4. data/README.jp.rdoc +552 -0
  5. data/README.rdoc +636 -0
  6. data/Rakefile +116 -0
  7. data/lib/sinatra.rb +7 -0
  8. data/lib/sinatra/base.rb +1167 -0
  9. data/lib/sinatra/images/404.png +0 -0
  10. data/lib/sinatra/images/500.png +0 -0
  11. data/lib/sinatra/main.rb +28 -0
  12. data/lib/sinatra/showexceptions.rb +307 -0
  13. data/lib/sinatra/tilt.rb +746 -0
  14. data/sinatra-base.gemspec +94 -0
  15. data/test/base_test.rb +160 -0
  16. data/test/builder_test.rb +65 -0
  17. data/test/contest.rb +64 -0
  18. data/test/erb_test.rb +81 -0
  19. data/test/erubis_test.rb +82 -0
  20. data/test/extensions_test.rb +100 -0
  21. data/test/filter_test.rb +221 -0
  22. data/test/haml_test.rb +95 -0
  23. data/test/helper.rb +76 -0
  24. data/test/helpers_test.rb +582 -0
  25. data/test/less_test.rb +37 -0
  26. data/test/mapped_error_test.rb +197 -0
  27. data/test/middleware_test.rb +68 -0
  28. data/test/public/favicon.ico +0 -0
  29. data/test/request_test.rb +33 -0
  30. data/test/response_test.rb +42 -0
  31. data/test/result_test.rb +98 -0
  32. data/test/route_added_hook_test.rb +59 -0
  33. data/test/routing_test.rb +860 -0
  34. data/test/sass_test.rb +85 -0
  35. data/test/server_test.rb +47 -0
  36. data/test/settings_test.rb +368 -0
  37. data/test/sinatra_test.rb +13 -0
  38. data/test/static_test.rb +93 -0
  39. data/test/templates_test.rb +159 -0
  40. data/test/views/error.builder +3 -0
  41. data/test/views/error.erb +3 -0
  42. data/test/views/error.erubis +3 -0
  43. data/test/views/error.haml +3 -0
  44. data/test/views/error.sass +2 -0
  45. data/test/views/foo/hello.test +1 -0
  46. data/test/views/hello.builder +1 -0
  47. data/test/views/hello.erb +1 -0
  48. data/test/views/hello.erubis +1 -0
  49. data/test/views/hello.haml +1 -0
  50. data/test/views/hello.less +5 -0
  51. data/test/views/hello.sass +2 -0
  52. data/test/views/hello.test +1 -0
  53. data/test/views/layout2.builder +3 -0
  54. data/test/views/layout2.erb +2 -0
  55. data/test/views/layout2.erubis +2 -0
  56. data/test/views/layout2.haml +2 -0
  57. data/test/views/layout2.test +1 -0
  58. metadata +257 -0
@@ -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
@@ -0,0 +1,7 @@
1
+ libdir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
+
4
+ require 'sinatra/base'
5
+ #require 'sinatra/main'
6
+
7
+ #enable :inline_templates
@@ -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