sinatra-base 1.0

Sign up to get free protection for your applications and to get access to all the features.
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