sinatra 0.1.7 → 0.2.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.

Files changed (95) hide show
  1. data/CHANGELOG +1 -8
  2. data/Manifest +42 -49
  3. data/README.rdoc +430 -0
  4. data/Rakefile +22 -28
  5. data/images/404.png +0 -0
  6. data/images/500.png +0 -0
  7. data/index.html +9 -0
  8. data/lib/sinatra.rb +1144 -46
  9. data/lib/sinatra/test/methods.rb +56 -0
  10. data/lib/sinatra/test/spec.rb +10 -0
  11. data/lib/sinatra/test/unit.rb +13 -0
  12. data/sinatra.gemspec +44 -40
  13. data/test/app_test.rb +150 -0
  14. data/test/application_test.rb +175 -0
  15. data/test/builder_test.rb +101 -0
  16. data/test/custom_error_test.rb +67 -0
  17. data/test/diddy_test.rb +41 -0
  18. data/test/erb_test.rb +116 -0
  19. data/test/event_context_test.rb +15 -0
  20. data/test/events_test.rb +50 -0
  21. data/test/haml_test.rb +181 -0
  22. data/test/helper.rb +3 -16
  23. data/test/mapped_error_test.rb +61 -0
  24. data/test/public/foo.xml +1 -0
  25. data/test/rest_test.rb +16 -0
  26. data/test/sass_test.rb +57 -0
  27. data/test/sessions_test.rb +40 -0
  28. data/test/streaming_test.rb +112 -0
  29. data/test/sym_params_test.rb +19 -0
  30. data/test/template_test.rb +30 -0
  31. data/test/use_in_file_templates_test.rb +48 -0
  32. data/test/views/foo.builder +1 -0
  33. data/test/views/foo.erb +1 -0
  34. data/test/views/foo.haml +1 -0
  35. data/test/views/foo.sass +2 -0
  36. data/test/views/foo_layout.erb +2 -0
  37. data/test/views/foo_layout.haml +2 -0
  38. data/test/views/layout_test/foo.builder +1 -0
  39. data/test/views/layout_test/foo.erb +1 -0
  40. data/test/views/layout_test/foo.haml +1 -0
  41. data/test/views/layout_test/foo.sass +2 -0
  42. data/test/views/layout_test/layout.builder +3 -0
  43. data/test/views/layout_test/layout.erb +1 -0
  44. data/test/views/layout_test/layout.haml +1 -0
  45. data/test/views/layout_test/layout.sass +2 -0
  46. data/test/views/no_layout/no_layout.builder +1 -0
  47. data/test/views/no_layout/no_layout.haml +1 -0
  48. metadata +122 -98
  49. data/LICENSE +0 -22
  50. data/README +0 -100
  51. data/RakeFile +0 -35
  52. data/examples/hello/hello.rb +0 -28
  53. data/examples/hello/views/hello.erb +0 -1
  54. data/examples/todo/todo.rb +0 -38
  55. data/files/default_index.erb +0 -42
  56. data/files/error.erb +0 -9
  57. data/files/logo.png +0 -0
  58. data/files/not_found.erb +0 -52
  59. data/lib/sinatra/context.rb +0 -88
  60. data/lib/sinatra/context/renderer.rb +0 -75
  61. data/lib/sinatra/core_ext/array.rb +0 -5
  62. data/lib/sinatra/core_ext/class.rb +0 -49
  63. data/lib/sinatra/core_ext/hash.rb +0 -7
  64. data/lib/sinatra/core_ext/kernel.rb +0 -16
  65. data/lib/sinatra/core_ext/metaid.rb +0 -18
  66. data/lib/sinatra/core_ext/module.rb +0 -11
  67. data/lib/sinatra/core_ext/symbol.rb +0 -5
  68. data/lib/sinatra/dispatcher.rb +0 -27
  69. data/lib/sinatra/dsl.rb +0 -176
  70. data/lib/sinatra/environment.rb +0 -15
  71. data/lib/sinatra/event.rb +0 -238
  72. data/lib/sinatra/irb.rb +0 -56
  73. data/lib/sinatra/loader.rb +0 -31
  74. data/lib/sinatra/logger.rb +0 -22
  75. data/lib/sinatra/options.rb +0 -49
  76. data/lib/sinatra/rack_ext/request.rb +0 -15
  77. data/lib/sinatra/route.rb +0 -65
  78. data/lib/sinatra/server.rb +0 -57
  79. data/lib/sinatra/sessions.rb +0 -21
  80. data/lib/sinatra/test_methods.rb +0 -55
  81. data/site/index.htm +0 -104
  82. data/site/index.html +0 -104
  83. data/site/logo.png +0 -0
  84. data/test/sinatra/dispatcher_test.rb +0 -91
  85. data/test/sinatra/event_test.rb +0 -46
  86. data/test/sinatra/renderer_test.rb +0 -47
  87. data/test/sinatra/request_test.rb +0 -21
  88. data/test/sinatra/route_test.rb +0 -21
  89. data/test/sinatra/static_files/foo.txt +0 -1
  90. data/test/sinatra/static_files_test.rb +0 -48
  91. data/test/sinatra/url_test.rb +0 -18
  92. data/vendor/erb/init.rb +0 -3
  93. data/vendor/erb/lib/erb.rb +0 -41
  94. data/vendor/haml/init.rb +0 -3
  95. data/vendor/haml/lib/haml.rb +0 -41
data/Rakefile CHANGED
@@ -1,35 +1,29 @@
1
+ require 'rubygems'
1
2
  require 'rake/testtask'
2
- require 'ftools'
3
+ require 'rake/rdoctask'
4
+ require 'echoe'
3
5
 
4
- Version = '0.1.0'
6
+ task :default => :test
5
7
 
6
- begin
7
- require 'rubygems'
8
- gem 'echoe'
9
- ENV['RUBY_FLAGS'] = ""
10
- require 'echoe'
11
-
12
- Echoe.new('sinatra') do |p|
13
- p.rubyforge_name = 'sinatra'
14
- p.dependencies = ['mongrel >=1.0.1', 'rack >=0.2.0']
15
- p.summary = "Sinatra is a classy web-framework dressed in a DSL"
16
- p.description = "Sinatra is a classy web-framework dressed in a DSL"
17
- p.url = "http://sinatra.rubyforge.org/"
18
- p.author = 'Blake Mizerany'
19
- p.email = "blake.mizerany@gmail.com"
20
- p.test_pattern = 'test/**/*_test.rb'
21
- p.include_rakefile = true
22
- p.rdoc_pattern = ['README', 'LICENSE'] + Dir.glob('lib/**/*.rb') + Dir.glob('vendor/**/*.rb')
23
- p.docs_host = "bmizerany@rubyforge.org:/var/www/gforge-projects/"
24
- end
8
+ Rake::RDocTask.new do |rd|
9
+ rd.main = "README.rdoc"
10
+ rd.rdoc_files += ["README.rdoc"]
11
+ rd.rdoc_files += Dir.glob("lib/**/*.rb")
12
+ rd.rdoc_dir = 'doc'
13
+ end
25
14
 
26
- rescue LoadError
15
+ Rake::TestTask.new do |t|
16
+ ENV['SINATRA_ENV'] = 'test'
17
+ t.pattern = File.dirname(__FILE__) + "/test/*_test.rb"
27
18
  end
28
19
 
29
- desc 'Clear all the log files from here down'
30
- task :remove_logs do
31
- Dir.glob(Dir.pwd + '/**/*.log') do |logfile|
32
- FileUtils.rm(logfile)
33
- puts 'Removed: %s' % logfile
34
- end
20
+ Echoe.new("sinatra") do |p|
21
+ p.author = "Blake Mizerany"
22
+ p.summary = "Classy web-development dressed in a DSL"
23
+ p.url = "http://www.sinatrarb.com"
24
+ p.docs_host = "sinatrarb.com:/var/www/blakemizerany.com/public/docs/"
25
+ p.dependencies = ["mongrel >=1.0.1", "rack >= 0.3.0"]
26
+ p.install_message = "*** Be sure to checkout the site for helpful tips! sinatrarb.com ***"
27
+ p.include_rakefile = true
35
28
  end
29
+
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
2
+ <html>
3
+ <head>
4
+ <title>Your Page Title</title>
5
+ <meta http-equiv="REFRESH" content="0;url=http://sinatrarb.com"></HEAD>
6
+ <BODY>
7
+ This site has <a href="http:://sinatrarb.com">moved</a>.
8
+ </BODY>
9
+ </HTML>
@@ -1,49 +1,1147 @@
1
- # Copyright (c) 2007 Blake Mizerany
2
- #
3
- # Permission is hereby granted, free of charge, to any person
4
- # obtaining a copy of this software and associated documentation
5
- # files (the "Software"), to deal in the Software without
6
- # restriction, including without limitation the rights to use,
7
- # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the
9
- # Software is furnished to do so, subject to the following
10
- # conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
- # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
- # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
- # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
- # OTHER DEALINGS IN THE SOFTWARE.
23
-
24
- %w(rubygems rack).each do |library|
25
- begin
26
- require library
27
- rescue LoadError
28
- raise "== Sinatra cannot run without #{library} installed"
29
- end
30
- end
31
-
32
- SINATRA_ROOT = File.dirname(__FILE__) + '/..'
33
-
34
- require File.dirname(__FILE__) + '/sinatra/loader'
35
-
36
- Sinatra::Loader.load_files Dir.glob(SINATRA_ROOT + '/lib/sinatra/core_ext/*.rb')
37
- Sinatra::Loader.load_files Dir.glob(SINATRA_ROOT + '/lib/sinatra/rack_ext/*.rb')
38
- Sinatra::Loader.load_files Dir.glob(SINATRA_ROOT + '/lib/sinatra/*.rb')
39
- Sinatra::Loader.load_files Dir.glob(SINATRA_ROOT + '/vendor/*/init.rb')
40
-
41
- Sinatra::Loader.load_files Dir.glob(File.dirname($0) + '/vendor/*/init.rb')
42
-
43
- Sinatra::Environment.prepare
1
+ require 'rubygems'
2
+ require 'uri'
3
+ require 'time'
4
+
5
+ if ENV['SWIFT']
6
+ require 'swiftcore/swiftiplied_mongrel'
7
+ puts "Using Swiftiplied Mongrel"
8
+ elsif ENV['EVENT']
9
+ require 'swiftcore/evented_mongrel'
10
+ puts "Using Evented Mongrel"
11
+ end
12
+
13
+ require 'rack'
14
+ require 'ostruct'
15
+
16
+ class Class
17
+ def dslify_writer(*syms)
18
+ syms.each do |sym|
19
+ class_eval <<-end_eval
20
+ def #{sym}(v=nil)
21
+ self.send "#{sym}=", v if v
22
+ v
23
+ end
24
+ end_eval
25
+ end
26
+ end
27
+ end
28
+
29
+ module Rack #:nodoc:
30
+
31
+ class Request #:nodoc:
32
+
33
+ # Set of request method names allowed via the _method parameter hack. By default,
34
+ # all request methods defined in RFC2616 are included, with the exception of
35
+ # TRACE and CONNECT.
36
+ POST_TUNNEL_METHODS_ALLOWED = %w( PUT DELETE OPTIONS HEAD )
37
+
38
+ # Return the HTTP request method with support for method tunneling using the POST
39
+ # _method parameter hack. If the real request method is POST and a _method param is
40
+ # given and the value is one defined in +POST_TUNNEL_METHODS_ALLOWED+, return the value
41
+ # of the _method param instead.
42
+ def request_method
43
+ if post_tunnel_method_hack?
44
+ params['_method'].upcase
45
+ else
46
+ @env['REQUEST_METHOD']
47
+ end
48
+ end
49
+
50
+ def user_agent
51
+ env['HTTP_USER_AGENT']
52
+ end
53
+
54
+ private
55
+
56
+ # Return truthfully if and only if the following conditions are met: 1.) the
57
+ # *actual* request method is POST, 2.) the request content-type is one of
58
+ # 'application/x-www-form-urlencoded' or 'multipart/form-data', 3.) there is a
59
+ # "_method" parameter in the POST body (not in the query string), and 4.) the
60
+ # method parameter is one of the verbs listed in the POST_TUNNEL_METHODS_ALLOWED
61
+ # list.
62
+ def post_tunnel_method_hack?
63
+ @env['REQUEST_METHOD'] == 'POST' &&
64
+ POST_TUNNEL_METHODS_ALLOWED.include?(self.POST.fetch('_method', '').upcase)
65
+ end
66
+
67
+ end
68
+
69
+ module Utils
70
+ extend self
71
+ end
72
+
73
+ end
74
+
75
+ module Sinatra
76
+ extend self
77
+
78
+ module Version
79
+ MAJOR = '0'
80
+ MINOR = '2'
81
+ REVISION = '0'
82
+ def self.combined
83
+ [MAJOR, MINOR, REVISION].join('.')
84
+ end
85
+ end
86
+
87
+ class NotFound < RuntimeError; end
88
+ class ServerError < RuntimeError; end
89
+
90
+ Result = Struct.new(:block, :params, :status) unless defined?(Result)
91
+
92
+ def options
93
+ application.options
94
+ end
95
+
96
+ def application
97
+ unless @app
98
+ @app = Application.new
99
+ Sinatra::Environment.setup!
100
+ end
101
+ @app
102
+ end
103
+
104
+ def application=(app)
105
+ @app = app
106
+ end
107
+
108
+ def port
109
+ application.options.port
110
+ end
111
+
112
+ def env
113
+ application.options.env
114
+ end
115
+
116
+ def build_application
117
+ app = application
118
+ app = Rack::Session::Cookie.new(app) if Sinatra.options.sessions == true
119
+ app = Rack::CommonLogger.new(app) if Sinatra.options.logging == true
120
+ app
121
+ end
122
+
123
+ def run
124
+
125
+ begin
126
+ puts "== Sinatra has taken the stage on port #{port} for #{env}"
127
+ require 'pp'
128
+ Rack::Handler::Mongrel.run(build_application, :Port => port) do |server|
129
+ trap(:INT) do
130
+ server.stop
131
+ puts "\n== Sinatra has ended his set (crowd applauds)"
132
+ end
133
+ end
134
+ rescue Errno::EADDRINUSE => e
135
+ puts "== Someone is already performing on port #{port}!"
136
+ end
137
+
138
+ end
139
+
140
+ class Event
141
+
142
+ URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
143
+ PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
144
+ SPLAT = /(.*?)/
145
+ attr_reader :path, :block, :param_keys, :pattern, :options
146
+
147
+ def initialize(path, options = {}, &b)
148
+ @path = URI.encode(path)
149
+ @block = b
150
+ @param_keys = []
151
+ @options = options
152
+ regex = @path.to_s.gsub(PARAM) do
153
+ @param_keys << $1
154
+ "(#{URI_CHAR}+)"
155
+ end
156
+
157
+ regex.gsub!('*', SPLAT.to_s)
158
+
159
+ @pattern = /^#{regex}$/
160
+ end
161
+
162
+ def invoke(request)
163
+ params = {}
164
+ if agent = options[:agent]
165
+ return unless request.user_agent =~ agent
166
+ params[:agent] = $~[1..-1]
167
+ end
168
+ if host = options[:host]
169
+ return unless host === request.host
170
+ end
171
+ return unless pattern =~ request.path_info.squeeze('/')
172
+ params.merge!(param_keys.zip($~.captures.map(&:from_param)).to_hash)
173
+ Result.new(block, params, 200)
174
+ end
175
+
176
+ end
177
+
178
+ class Error
179
+
180
+ attr_reader :code, :block
181
+
182
+ def initialize(code, &b)
183
+ @code, @block = code, b
184
+ end
185
+
186
+ def invoke(request)
187
+ Result.new(block, {}, 404)
188
+ end
189
+
190
+ end
191
+
192
+ class Static
193
+
194
+ def invoke(request)
195
+ return unless File.file?(
196
+ Sinatra.application.options.public + request.path_info
197
+ )
198
+ Result.new(block, {}, 200)
199
+ end
200
+
201
+ def block
202
+ Proc.new do
203
+ send_file Sinatra.application.options.public + request.path_info,
204
+ :disposition => nil
205
+ end
206
+ end
207
+
208
+ end
209
+
210
+ # Adapted from actionpack
211
+ # Methods for sending files and streams to the browser instead of rendering.
212
+ module Streaming
213
+ DEFAULT_SEND_FILE_OPTIONS = {
214
+ :type => 'application/octet-stream'.freeze,
215
+ :disposition => 'attachment'.freeze,
216
+ :stream => true,
217
+ :buffer_size => 4096
218
+ }.freeze
219
+
220
+ class MissingFile < RuntimeError; end
221
+
222
+ class FileStreamer
223
+
224
+ attr_reader :path, :options
225
+
226
+ def initialize(path, options)
227
+ @path, @options = path, options
228
+ end
229
+
230
+ def to_result(cx, *args)
231
+ self
232
+ end
233
+
234
+ def each
235
+ File.open(path, 'rb') do |file|
236
+ while buf = file.read(options[:buffer_size])
237
+ yield buf
238
+ end
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+ protected
245
+ # Sends the file by streaming it 4096 bytes at a time. This way the
246
+ # whole file doesn't need to be read into memory at once. This makes
247
+ # it feasible to send even large files.
248
+ #
249
+ # Be careful to sanitize the path parameter if it coming from a web
250
+ # page. send_file(params[:path]) allows a malicious user to
251
+ # download any file on your server.
252
+ #
253
+ # Options:
254
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
255
+ # Defaults to File.basename(path).
256
+ # * <tt>:type</tt> - specifies an HTTP content type.
257
+ # Defaults to 'application/octet-stream'.
258
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
259
+ # Valid values are 'inline' and 'attachment' (default). When set to nil, the
260
+ # Content-Disposition and Content-Transfer-Encoding headers are omitted entirely.
261
+ # * <tt>:stream</tt> - whether to send the file to the user agent as it is read (true)
262
+ # or to read the entire file before sending (false). Defaults to true.
263
+ # * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
264
+ # Defaults to 4096.
265
+ # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
266
+ # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value (See Time#httpdate)
267
+ # indicating the last modified time of the file. If the request includes an
268
+ # If-Modified-Since header that matches this value exactly, a 304 Not Modified response
269
+ # is sent instead of the file. Defaults to the file's last modified
270
+ # time.
271
+ #
272
+ # The default Content-Type and Content-Disposition headers are
273
+ # set to download arbitrary binary files in as many browsers as
274
+ # possible. IE versions 4, 5, 5.5, and 6 are all known to have
275
+ # a variety of quirks (especially when downloading over SSL).
276
+ #
277
+ # Simple download:
278
+ # send_file '/path/to.zip'
279
+ #
280
+ # Show a JPEG in the browser:
281
+ # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
282
+ #
283
+ # Show a 404 page in the browser:
284
+ # send_file '/path/to/404.html, :type => 'text/html; charset=utf-8', :status => 404
285
+ #
286
+ # Read about the other Content-* HTTP headers if you'd like to
287
+ # provide the user with more information (such as Content-Description).
288
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
289
+ #
290
+ # Also be aware that the document may be cached by proxies and browsers.
291
+ # The Pragma and Cache-Control headers declare how the file may be cached
292
+ # by intermediaries. They default to require clients to validate with
293
+ # the server before releasing cached responses. See
294
+ # http://www.mnot.net/cache_docs/ for an overview of web caching and
295
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
296
+ # for the Cache-Control header spec.
297
+ def send_file(path, options = {}) #:doc:
298
+ raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
299
+
300
+ options[:length] ||= File.size(path)
301
+ options[:filename] ||= File.basename(path)
302
+ options[:type] ||= Rack::File::MIME_TYPES[File.extname(options[:filename])[1..-1]] || 'text/plain'
303
+ options[:last_modified] ||= File.mtime(path).httpdate
304
+ send_file_headers! options
305
+
306
+ if options[:stream]
307
+ throw :halt, [options[:status] || 200, FileStreamer.new(path, options)]
308
+ else
309
+ File.open(path, 'rb') { |file| throw :halt, [options[:status] || 200, file.read] }
310
+ end
311
+ end
312
+
313
+ # Send binary data to the user as a file download. May set content type, apparent file name,
314
+ # and specify whether to show data inline or download as an attachment.
315
+ #
316
+ # Options:
317
+ # * <tt>:filename</tt> - Suggests a filename for the browser to use.
318
+ # * <tt>:type</tt> - specifies an HTTP content type.
319
+ # Defaults to 'application/octet-stream'.
320
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
321
+ # Valid values are 'inline' and 'attachment' (default).
322
+ # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
323
+ # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value (See Time#httpdate)
324
+ # indicating the last modified time of the response entity. If the request includes an
325
+ # If-Modified-Since header that matches this value exactly, a 304 Not Modified response
326
+ # is sent instead of the data.
327
+ #
328
+ # Generic data download:
329
+ # send_data buffer
330
+ #
331
+ # Download a dynamically-generated tarball:
332
+ # send_data generate_tgz('dir'), :filename => 'dir.tgz'
333
+ #
334
+ # Display an image Active Record in the browser:
335
+ # send_data image.data, :type => image.content_type, :disposition => 'inline'
336
+ #
337
+ # See +send_file+ for more information on HTTP Content-* headers and caching.
338
+ def send_data(data, options = {}) #:doc:
339
+ send_file_headers! options.merge(:length => data.size)
340
+ throw :halt, [options[:status] || 200, data]
341
+ end
342
+
343
+ private
344
+ def send_file_headers!(options)
345
+ options = DEFAULT_SEND_FILE_OPTIONS.merge(options)
346
+ [:length, :type, :disposition].each do |arg|
347
+ raise ArgumentError, ":#{arg} option required" unless options.key?(arg)
348
+ end
349
+
350
+ # Send a "304 Not Modified" if the last_modified option is provided and matches
351
+ # the If-Modified-Since request header value.
352
+ if last_modified = options[:last_modified]
353
+ header 'Last-Modified' => last_modified
354
+ throw :halt, [ 304, '' ] if last_modified == request.env['HTTP_IF_MODIFIED_SINCE']
355
+ end
356
+
357
+ headers(
358
+ 'Content-Length' => options[:length].to_s,
359
+ 'Content-Type' => options[:type].strip # fixes a problem with extra '\r' with some browsers
360
+ )
361
+
362
+ # Omit Content-Disposition and Content-Transfer-Encoding headers if
363
+ # the :disposition option set to nil.
364
+ if !options[:disposition].nil?
365
+ disposition = options[:disposition].dup || 'attachment'
366
+ disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
367
+ headers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary'
368
+ end
369
+
370
+ # Fix a problem with IE 6.0 on opening downloaded files:
371
+ # If Cache-Control: no-cache is set (which Rails does by default),
372
+ # IE removes the file it just downloaded from its cache immediately
373
+ # after it displays the "open/save" dialog, which means that if you
374
+ # hit "open" the file isn't there anymore when the application that
375
+ # is called for handling the download is run, so let's workaround that
376
+ header('Cache-Control' => 'private') if headers['Cache-Control'] == 'no-cache'
377
+ end
378
+ end
379
+
380
+ module ResponseHelpers
381
+
382
+ def redirect(path, *args)
383
+ status(302)
384
+ headers 'Location' => path
385
+ throw :halt, *args
386
+ end
387
+
388
+ def headers(header = nil)
389
+ @response.headers.merge!(header) if header
390
+ @response.headers
391
+ end
392
+ alias :header :headers
393
+
394
+ end
395
+
396
+ module RenderingHelpers
397
+
398
+ def render(renderer, template, options={})
399
+ m = method("render_#{renderer}")
400
+ result = m.call(resolve_template(renderer, template, options), options)
401
+ if layout = determine_layout(renderer, template, options)
402
+ result = m.call(resolve_template(renderer, layout, options), options) { result }
403
+ end
404
+ result
405
+ end
406
+
407
+ def determine_layout(renderer, template, options)
408
+ return if options[:layout] == false
409
+ layout_from_options = options[:layout] || :layout
410
+ resolve_template(renderer, layout_from_options, options, false)
411
+ end
412
+
413
+ private
414
+
415
+ def resolve_template(renderer, template, options, scream = true)
416
+ case template
417
+ when String
418
+ template
419
+ when Proc
420
+ template.call
421
+ when Symbol
422
+ if proc = templates[template]
423
+ resolve_template(renderer, proc, options, scream)
424
+ else
425
+ read_template_file(renderer, template, options, scream)
426
+ end
427
+ else
428
+ nil
429
+ end
430
+ end
431
+
432
+ def read_template_file(renderer, template, options, scream = true)
433
+ path = File.join(
434
+ options[:views_directory] || Sinatra.application.options.views,
435
+ "#{template}.#{renderer}"
436
+ )
437
+ unless File.exists?(path)
438
+ raise Errno::ENOENT.new(path) if scream
439
+ nil
440
+ else
441
+ File.read(path)
442
+ end
443
+ end
444
+
445
+ def templates
446
+ Sinatra.application.templates
447
+ end
448
+
449
+ end
450
+
451
+ module Erb
452
+
453
+ def erb(content, options={})
454
+ require 'erb'
455
+ render(:erb, content, options)
456
+ end
457
+
458
+ private
459
+
460
+ def render_erb(content, options = {})
461
+ ::ERB.new(content).result(binding)
462
+ end
463
+
464
+ end
465
+
466
+ module Haml
467
+
468
+ def haml(content, options={})
469
+ require 'haml'
470
+ render(:haml, content, options)
471
+ end
472
+
473
+ private
474
+
475
+ def render_haml(content, options = {}, &b)
476
+ ::Haml::Engine.new(content).render(options[:scope] || self, options[:locals] || {}, &b)
477
+ end
478
+
479
+ end
480
+
481
+ # Generate valid CSS using Sass (part of Haml)
482
+ #
483
+ # Sass templates can be in external files with <tt>.sass</tt> extension or can use Sinatra's
484
+ # in_file_templates. In either case, the file can be rendered by passing the name of
485
+ # the template to the +sass+ method as a symbol.
486
+ #
487
+ # Unlike Haml, Sass does not support a layout file, so the +sass+ method will ignore both
488
+ # the default <tt>layout.sass</tt> file and any parameters passed in as <tt>:layout</tt> in
489
+ # the options hash.
490
+ #
491
+ # === Sass Template Files
492
+ #
493
+ # Sass templates can be stored in separate files with a <tt>.sass</tt>
494
+ # extension under the view path.
495
+ #
496
+ # Example:
497
+ # get '/stylesheet.css' do
498
+ # header 'Content-Type' => 'text/css; charset=utf-8'
499
+ # sass :stylesheet
500
+ # end
501
+ #
502
+ # The "views/stylesheet.sass" file might contain the following:
503
+ #
504
+ # body
505
+ # #admin
506
+ # :background-color #CCC
507
+ # #main
508
+ # :background-color #000
509
+ # #form
510
+ # :border-color #AAA
511
+ # :border-width 10px
512
+ #
513
+ # And yields the following output:
514
+ #
515
+ # body #admin {
516
+ # background-color: #CCC; }
517
+ # body #main {
518
+ # background-color: #000; }
519
+ #
520
+ # #form {
521
+ # border-color: #AAA;
522
+ # border-width: 10px; }
523
+ #
524
+ #
525
+ # NOTE: Haml must be installed or a LoadError will be raised the first time an
526
+ # attempt is made to render a Sass template.
527
+ #
528
+ # See http://haml.hamptoncatlin.com/docs/rdoc/classes/Sass.html for comprehensive documentation on Sass.
529
+
530
+
531
+ module Sass
532
+
533
+ def sass(content, options = {})
534
+ require 'sass'
535
+
536
+ # Sass doesn't support a layout, so we override any possible layout here
537
+ options[:layout] = false
538
+
539
+ render(:sass, content, options)
540
+ end
541
+
542
+ private
543
+
544
+ def render_sass(content, options = {})
545
+ ::Sass::Engine.new(content).render
546
+ end
547
+ end
548
+
549
+ # Generating conservative XML content using Builder templates.
550
+ #
551
+ # Builder templates can be inline by passing a block to the builder method, or in
552
+ # external files with +.builder+ extension by passing the name of the template
553
+ # to the +builder+ method as a Symbol.
554
+ #
555
+ # === Inline Rendering
556
+ #
557
+ # If the builder method is given a block, the block is called directly with an
558
+ # +XmlMarkup+ instance and the result is returned as String:
559
+ # get '/who.xml' do
560
+ # builder do |xml|
561
+ # xml.instruct!
562
+ # xml.person do
563
+ # xml.name "Francis Albert Sinatra",
564
+ # :aka => "Frank Sinatra"
565
+ # xml.email 'frank@capitolrecords.com'
566
+ # end
567
+ # end
568
+ # end
569
+ #
570
+ # Yields the following XML:
571
+ # <?xml version='1.0' encoding='UTF-8'?>
572
+ # <person>
573
+ # <name aka='Frank Sinatra'>Francis Albert Sinatra</name>
574
+ # <email>Frank Sinatra</email>
575
+ # </person>
576
+ #
577
+ # === Builder Template Files
578
+ #
579
+ # Builder templates can be stored in separate files with a +.builder+
580
+ # extension under the view path. An +XmlMarkup+ object named +xml+ is automatically
581
+ # made available to template.
582
+ #
583
+ # Example:
584
+ # get '/bio.xml' do
585
+ # builder :bio
586
+ # end
587
+ #
588
+ # The "views/bio.builder" file might contain the following:
589
+ # xml.instruct! :xml, :version => '1.1'
590
+ # xml.person do
591
+ # xml.name "Francis Albert Sinatra"
592
+ # xml.aka "Frank Sinatra"
593
+ # xml.aka "Ol' Blue Eyes"
594
+ # xml.aka "The Chairman of the Board"
595
+ # xml.born 'date' => '1915-12-12' do
596
+ # xml.text! "Hoboken, New Jersey, U.S.A."
597
+ # end
598
+ # xml.died 'age' => 82
599
+ # end
600
+ #
601
+ # And yields the following output:
602
+ # <?xml version='1.1' encoding='UTF-8'?>
603
+ # <person>
604
+ # <name>Francis Albert Sinatra</name>
605
+ # <aka>Frank Sinatra</aka>
606
+ # <aka>Ol&apos; Blue Eyes</aka>
607
+ # <aka>The Chairman of the Board</aka>
608
+ # <born date='1915-12-12'>Hoboken, New Jersey, U.S.A.</born>
609
+ # <died age='82' />
610
+ # </person>
611
+ #
612
+ # NOTE: Builder must be installed or a LoadError will be raised the first time an
613
+ # attempt is made to render a builder template.
614
+ #
615
+ # See http://builder.rubyforge.org/ for comprehensive documentation on Builder.
616
+ module Builder
617
+
618
+ def builder(content=nil, options={}, &block)
619
+ options, content = content, nil if content.is_a?(Hash)
620
+ content = Proc.new { block } if content.nil?
621
+ render(:builder, content, options)
622
+ end
623
+
624
+ private
625
+
626
+ def render_builder(content, options = {}, &b)
627
+ require 'builder'
628
+ xml = ::Builder::XmlMarkup.new(:indent => 2)
629
+ case content
630
+ when String
631
+ eval(content, binding, '<BUILDER>', 1)
632
+ when Proc
633
+ content.call(xml)
634
+ end
635
+ xml.target!
636
+ end
637
+
638
+ end
639
+
640
+ class EventContext
641
+
642
+ include ResponseHelpers
643
+ include Streaming
644
+ include RenderingHelpers
645
+ include Erb
646
+ include Haml
647
+ include Builder
648
+ include Sass
649
+
650
+ attr_accessor :request, :response
651
+
652
+ dslify_writer :status, :body
653
+
654
+ def initialize(request, response, route_params)
655
+ @request = request
656
+ @response = response
657
+ @route_params = route_params
658
+ @response.body = nil
659
+ end
660
+
661
+ def params
662
+ @params ||= begin
663
+ h = Hash.new {|h,k| h[k.to_s] if Symbol === k}
664
+ h.merge(@route_params.merge(@request.params))
665
+ end
666
+ end
667
+
668
+ def data
669
+ @data ||= params.keys.first
670
+ end
671
+
672
+ def stop(*args)
673
+ throw :halt, args
674
+ end
675
+
676
+ def complete(returned)
677
+ @response.body || returned
678
+ end
679
+
680
+ def session
681
+ @request.env['rack.session'] || {}
682
+ end
683
+
684
+ private
685
+
686
+ def method_missing(name, *args, &b)
687
+ @response.send(name, *args, &b)
688
+ end
689
+
690
+ end
691
+
692
+ class Application
693
+
694
+ attr_reader :events, :errors, :templates, :filters
695
+ attr_reader :clearables, :reloading
696
+
697
+ attr_writer :options
698
+
699
+ def self.default_options
700
+ @@default_options ||= {
701
+ :run => true,
702
+ :port => 4567,
703
+ :env => :development,
704
+ :root => Dir.pwd,
705
+ :views => Dir.pwd + '/views',
706
+ :public => Dir.pwd + '/public',
707
+ :sessions => false,
708
+ :logging => true,
709
+ }
710
+ end
711
+
712
+ def default_options
713
+ self.class.default_options
714
+ end
715
+
716
+
717
+ ##
718
+ # Load all options given on the command line
719
+ # NOTE: Ignores --name so unit/spec tests can run individually
720
+ def load_options!
721
+ require 'optparse'
722
+ OptionParser.new do |op|
723
+ op.on('-p port') { |port| default_options[:port] = port }
724
+ op.on('-e env') { |env| default_options[:env] = env }
725
+ op.on('-x') { |env| default_options[:mutex] = true }
726
+ end.parse!(ARGV.dup.select { |o| o !~ /--name/ })
727
+ end
728
+
729
+ # Called immediately after the application is initialized or reloaded to
730
+ # register default events. Events added here have dibs on requests since
731
+ # they appear first in the list.
732
+ def load_default_events!
733
+ events[:get] << Static.new
734
+ end
735
+
736
+ def initialize
737
+ @clearables = [
738
+ @events = Hash.new { |hash, key| hash[key] = [] },
739
+ @errors = Hash.new,
740
+ @filters = Hash.new { |hash, key| hash[key] = [] },
741
+ @templates = Hash.new
742
+ ]
743
+ load_options!
744
+ load_default_events!
745
+ end
746
+
747
+ def define_event(method, path, options = {}, &b)
748
+ events[method] << event = Event.new(path, options, &b)
749
+ event
750
+ end
751
+
752
+ def define_template(name=:layout, &b)
753
+ templates[name] = b
754
+ end
755
+
756
+ def define_error(code, options = {}, &b)
757
+ errors[code] = Error.new(code, &b)
758
+ end
759
+
760
+ def define_filter(type, &b)
761
+ filters[:before] << b
762
+ end
763
+
764
+ # Visits and invokes each handler registered for the +request_method+ in
765
+ # definition order until a Result response is produced. If no handler
766
+ # responds with a Result, the NotFound error handler is invoked.
767
+ #
768
+ # When the request_method is "HEAD" and no valid Result is produced by
769
+ # the set of handlers registered for HEAD requests, an attempt is made to
770
+ # invoke the GET handlers to generate the response before resorting to the
771
+ # default error handler.
772
+ def lookup(request)
773
+ method = request.request_method.downcase.to_sym
774
+ events[method].eject(&[:invoke, request]) ||
775
+ (events[:get].eject(&[:invoke, request]) if method == :head) ||
776
+ errors[NotFound].invoke(request)
777
+ end
778
+
779
+ def options
780
+ @options ||= OpenStruct.new(default_options)
781
+ end
782
+
783
+ def development?
784
+ options.env == :development
785
+ end
786
+
787
+ def reload!
788
+ @reloading = true
789
+ clearables.each(&:clear)
790
+ load_default_events!
791
+ Kernel.load $0
792
+ @reloading = false
793
+ Environment.setup!
794
+ end
795
+
796
+ def mutex
797
+ @@mutex ||= Mutex.new
798
+ end
799
+
800
+ def run_safely
801
+ if options.mutex
802
+ mutex.synchronize { yield }
803
+ else
804
+ yield
805
+ end
806
+ end
807
+
808
+ def call(env)
809
+ reload! if development?
810
+ request = Rack::Request.new(env)
811
+ result = lookup(request)
812
+ context = EventContext.new(
813
+ request,
814
+ Rack::Response.new,
815
+ result.params
816
+ )
817
+ context.status(result.status)
818
+ begin
819
+ returned = run_safely do
820
+ catch(:halt) do
821
+ filters[:before].each { |f| context.instance_eval(&f) }
822
+ [:complete, context.instance_eval(&result.block)]
823
+ end
824
+ end
825
+ body = returned.to_result(context)
826
+ rescue => e
827
+ request.env['sinatra.error'] = e
828
+ context.status(500)
829
+ result = (errors[e.class] || errors[ServerError]).invoke(request)
830
+ returned = run_safely do
831
+ catch(:halt) do
832
+ [:complete, context.instance_eval(&result.block)]
833
+ end
834
+ end
835
+ body = returned.to_result(context)
836
+ end
837
+ body = '' unless body.respond_to?(:each)
838
+ body = '' if request.request_method.upcase == 'HEAD'
839
+ context.body = body.kind_of?(String) ? [*body] : body
840
+ context.finish
841
+ end
842
+
843
+ end
844
+
845
+
846
+ module Environment
847
+ extend self
848
+
849
+ def setup!
850
+ configure do
851
+ error do
852
+ raise request.env['sinatra.error'] if Sinatra.options.raise_errors
853
+ '<h1>Internal Server Error</h1>'
854
+ end
855
+ not_found { '<h1>Not Found</h1>'}
856
+ end
857
+
858
+ configures :development do
859
+
860
+ get '/sinatra_custom_images/:image.png' do
861
+ File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
862
+ end
863
+
864
+ not_found do
865
+ %Q(
866
+ <style>
867
+ body {
868
+ text-align: center;
869
+ color: #888;
870
+ font-family: Arial;
871
+ font-size: 22px;
872
+ margin: 20px;
873
+ }
874
+ #content {
875
+ margin: 0 auto;
876
+ width: 500px;
877
+ text-align: left;
878
+ }
879
+ </style>
880
+ <html>
881
+ <body>
882
+ <h2>Sinatra doesn't know this diddy.</h2>
883
+ <img src='/sinatra_custom_images/404.png'></img>
884
+ <div id="content">
885
+ Try this:
886
+ <pre>#{request.request_method.downcase} "#{request.path_info}" do
887
+ .. do something ..
888
+ end<pre>
889
+ </div>
890
+ </body>
891
+ </html>
892
+ )
893
+ end
894
+
895
+ error do
896
+ @error = request.env['sinatra.error']
897
+ %Q(
898
+ <html>
899
+ <body>
900
+ <style type="text/css" media="screen">
901
+ body {
902
+ font-family: Verdana;
903
+ color: #333;
904
+ }
905
+
906
+ #content {
907
+ width: 700px;
908
+ margin-left: 20px;
909
+ }
910
+
911
+ #content h1 {
912
+ width: 99%;
913
+ color: #1D6B8D;
914
+ font-weight: bold;
915
+ }
916
+
917
+ #stacktrace {
918
+ margin-top: -20px;
919
+ }
920
+
921
+ #stacktrace pre {
922
+ font-size: 12px;
923
+ border-left: 2px solid #ddd;
924
+ padding-left: 10px;
925
+ }
926
+
927
+ #stacktrace img {
928
+ margin-top: 10px;
929
+ }
930
+ </style>
931
+ <div id="content">
932
+ <img src="/sinatra_custom_images/500.png" />
933
+ <div class="info">
934
+ Params: <pre>#{params.inspect}
935
+ </div>
936
+ <div id="stacktrace">
937
+ <h1>#{Rack::Utils.escape_html(@error.class.name + ' - ' + @error.message)}</h1>
938
+ <pre><code>#{Rack::Utils.escape_html(@error.backtrace.join("\n"))}</code></pre>
939
+ </div>
940
+ </body>
941
+ </html>
942
+ )
943
+ end
944
+ end
945
+ end
946
+ end
947
+
948
+ end
949
+
950
+ def get(path, options ={}, &b)
951
+ Sinatra.application.define_event(:get, path, options, &b)
952
+ end
953
+
954
+ def post(path, options ={}, &b)
955
+ Sinatra.application.define_event(:post, path, options, &b)
956
+ end
957
+
958
+ def put(path, options ={}, &b)
959
+ Sinatra.application.define_event(:put, path, options, &b)
960
+ end
961
+
962
+ def delete(path, options ={}, &b)
963
+ Sinatra.application.define_event(:delete, path, options, &b)
964
+ end
965
+
966
+ def before(&b)
967
+ Sinatra.application.define_filter(:before, &b)
968
+ end
969
+
970
+ def helpers(&b)
971
+ Sinatra::EventContext.class_eval(&b)
972
+ end
973
+
974
+ def error(type = Sinatra::ServerError, options = {}, &b)
975
+ Sinatra.application.define_error(type, options, &b)
976
+ end
977
+
978
+ def not_found(options = {}, &b)
979
+ Sinatra.application.define_error(Sinatra::NotFound, options, &b)
980
+ end
981
+
982
+ def layout(name = :layout, &b)
983
+ Sinatra.application.define_template(name, &b)
984
+ end
985
+
986
+ def template(name, &b)
987
+ Sinatra.application.define_template(name, &b)
988
+ end
989
+
990
+ def use_in_file_templates!
991
+ require 'stringio'
992
+ templates = IO.read(caller.first.split(':').first).split('__FILE__').last
993
+ data = StringIO.new(templates)
994
+ current_template = nil
995
+ data.each do |line|
996
+ if line =~ /^##\s?(.*)/
997
+ current_template = $1.to_sym
998
+ Sinatra.application.templates[current_template] = ''
999
+ elsif current_template
1000
+ Sinatra.application.templates[current_template] << line
1001
+ end
1002
+ end
1003
+ end
1004
+
1005
+ def configures(*envs, &b)
1006
+ yield if !Sinatra.application.reloading &&
1007
+ (envs.include?(Sinatra.application.options.env) ||
1008
+ envs.empty?)
1009
+ end
1010
+ alias :configure :configures
1011
+
1012
+ def set_options(opts)
1013
+ Sinatra::Application.default_options.merge!(opts)
1014
+ Sinatra.application.options = nil
1015
+ end
1016
+
1017
+ def set_option(key, value)
1018
+ set_options(key => value)
1019
+ end
1020
+
1021
+ def mime(ext, type)
1022
+ Rack::File::MIME_TYPES[ext.to_s] = type
1023
+ end
1024
+
1025
+ ### Misc Core Extensions
1026
+
1027
+ module Kernel
1028
+
1029
+ def silence_warnings
1030
+ old_verbose, $VERBOSE = $VERBOSE, nil
1031
+ yield
1032
+ ensure
1033
+ $VERBOSE = old_verbose
1034
+ end
1035
+
1036
+ end
1037
+
1038
+ class String
1039
+
1040
+ # Converts +self+ to an escaped URI parameter value
1041
+ # 'Foo Bar'.to_param # => 'Foo%20Bar'
1042
+ def to_param
1043
+ URI.escape(self)
1044
+ end
1045
+
1046
+ # Converts +self+ from an escaped URI parameter value
1047
+ # 'Foo%20Bar'.from_param # => 'Foo Bar'
1048
+ def from_param
1049
+ URI.unescape(self)
1050
+ end
1051
+
1052
+ end
1053
+
1054
+ class Hash
1055
+
1056
+ def to_params
1057
+ map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
1058
+ end
1059
+
1060
+ def symbolize_keys
1061
+ self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
1062
+ end
1063
+
1064
+ def pass(*keys)
1065
+ reject { |k,v| !keys.include?(k) }
1066
+ end
1067
+
1068
+ end
1069
+
1070
+ class Symbol
1071
+
1072
+ def to_proc
1073
+ Proc.new { |*args| args.shift.__send__(self, *args) }
1074
+ end
1075
+
1076
+ end
1077
+
1078
+ class Array
1079
+
1080
+ def to_hash
1081
+ self.inject({}) { |h, (k, v)| h[k] = v; h }
1082
+ end
1083
+
1084
+ def to_proc
1085
+ Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
1086
+ end
1087
+
1088
+ end
1089
+
1090
+ module Enumerable
1091
+
1092
+ def eject(&block)
1093
+ find { |e| result = block[e] and break result }
1094
+ end
1095
+
1096
+ end
1097
+
1098
+ ### Core Extension results for throw :halt
1099
+
1100
+ class Proc
1101
+ def to_result(cx, *args)
1102
+ cx.instance_eval(&self)
1103
+ args.shift.to_result(cx, *args)
1104
+ end
1105
+ end
1106
+
1107
+ class String
1108
+ def to_result(cx, *args)
1109
+ args.shift.to_result(cx, *args)
1110
+ self
1111
+ end
1112
+ end
1113
+
1114
+ class Array
1115
+ def to_result(cx, *args)
1116
+ self.shift.to_result(cx, *self)
1117
+ end
1118
+ end
1119
+
1120
+ class Symbol
1121
+ def to_result(cx, *args)
1122
+ cx.send(self, *args)
1123
+ end
1124
+ end
1125
+
1126
+ class Fixnum
1127
+ def to_result(cx, *args)
1128
+ cx.status self
1129
+ args.shift.to_result(cx, *args)
1130
+ end
1131
+ end
1132
+
1133
+ class NilClass
1134
+ def to_result(cx, *args)
1135
+ ''
1136
+ end
1137
+ end
44
1138
 
45
1139
  at_exit do
46
- Sinatra::Environment.prepare_loggers unless Sinatra::Options.environment == :test
47
- Sinatra::Irb.start! if Sinatra::Options.console
48
- Sinatra::Server.new.start unless Sinatra::Server.running
1140
+ raise $! if $!
1141
+ if Sinatra.application.options.run
1142
+ Sinatra.run
1143
+ end
49
1144
  end
1145
+
1146
+ mime :xml, 'application/xml'
1147
+ mime :js, 'application/javascript'