sinatra 1.2.1 → 1.2.2

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.

@@ -1584,6 +1584,45 @@ application (Rails/Ramaze/Camping/...):
1584
1584
  get('/') { "Hello #{session['user_name']}." }
1585
1585
  end
1586
1586
 
1587
+ === Dynamic Application Creation
1588
+
1589
+ Sometimes you want to create new applications at runtime without having to
1590
+ assign them to a constant, you can do this with `Sinatra.new`:
1591
+
1592
+ require 'sinatra/base'
1593
+ my_app = Sinatra.new { get('/') { "hi" } }
1594
+ my_app.run!
1595
+
1596
+ It takes the application to inherit from as optional argument:
1597
+
1598
+ require 'sinatra/base'
1599
+
1600
+ controller = Sinatra.new do
1601
+ enable :logging
1602
+ helpers MyHelpers
1603
+ end
1604
+
1605
+ map('/a') do
1606
+ run Sinatra.new(controller) { get('/') { 'a' } }
1607
+ end
1608
+
1609
+ map('/b') do
1610
+ run Sinatra.new(controller) { get('/') { 'b' } }
1611
+ end
1612
+
1613
+ This is especially useful for testing Sinatra extensions or using Sinatra in
1614
+ your own library.
1615
+
1616
+ This also makes using Sinatra as middleware extremely easy:
1617
+
1618
+ require 'sinatra/base'
1619
+
1620
+ use Sinatra do
1621
+ get('/') { ... }
1622
+ end
1623
+
1624
+ run RailsProject::Application
1625
+
1587
1626
  == Scopes and Binding
1588
1627
 
1589
1628
  The scope you are currently in determines what methods and variables are
@@ -1616,6 +1655,7 @@ You have the application scope binding inside:
1616
1655
  * Methods defined by extensions
1617
1656
  * The block passed to +helpers+
1618
1657
  * Procs/blocks used as value for +set+
1658
+ * The block passed to <tt>Sinatra.new</tt>
1619
1659
 
1620
1660
  You can reach the scope object (the class) like this:
1621
1661
 
data/Rakefile CHANGED
@@ -15,6 +15,15 @@ def source_version
15
15
  end
16
16
  end
17
17
 
18
+ def prev_feature
19
+ source_version.gsub(/^(\d\.)(\d+)\..*$/) { $1 + ($2.to_i - 1).to_s }
20
+ end
21
+
22
+ def prev_version
23
+ return prev_feature + '.0' if source_version.end_with? '.0'
24
+ source_version.gsub(/\d+$/) { |s| s.to_i - 1 }
25
+ end
26
+
18
27
  # SPECS ===============================================================
19
28
 
20
29
  task :test do
@@ -69,13 +78,14 @@ end
69
78
  # Thanks in announcement ===============================================
70
79
 
71
80
  team = ["Ryan Tomayko", "Blake Mizerany", "Simon Rozet", "Konstantin Haase"]
72
- desc "list of contributors: rake thanks[1.1.0..master,1.1.0..1.1.x]"
81
+ desc "list of contributors"
73
82
  task :thanks, [:release,:backports] do |t, a|
74
- a.with_defaults :release => "1.1.0..master", :backports => "1.1.0..1.1.x"
83
+ a.with_defaults :release => "#{prev_version}..HEAD",
84
+ :backports => "#{prev_feature}.0..#{prev_feature}.x"
75
85
  included = `git log --format=format:"%aN\t%s" #{a.release}`.lines.to_a
76
86
  excluded = `git log --format=format:"%aN\t%s" #{a.backports}`.lines.to_a
77
- commits = (included - excluded).group_by { |c| c[/^[^\t]+/] }
78
- authors = commits.keys.sort_by { |n| - commits[n].size } - team
87
+ commits = (included - excluded).group_by { |c| c[/^[^\t]+/] }
88
+ authors = commits.keys.sort_by { |n| - commits[n].size } - team
79
89
  puts authors[0..-2].join(', ') << " and " << authors.last,
80
90
  "(based on commits included in #{a.release}, but not in #{a.backports})"
81
91
  end
@@ -7,14 +7,30 @@ require 'sinatra/showexceptions'
7
7
  require 'tilt'
8
8
 
9
9
  module Sinatra
10
- VERSION = '1.2.1'
10
+ VERSION = '1.2.2'
11
11
 
12
12
  # The request object. See Rack::Request for more info:
13
13
  # http://rack.rubyforge.org/doc/classes/Rack/Request.html
14
14
  class Request < Rack::Request
15
+ def self.new(env)
16
+ env['sinatra.request'] ||= super
17
+ end
18
+
15
19
  # Returns an array of acceptable media types for the response
16
20
  def accept
17
- @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.split(';')[0].strip }
21
+ @env['sinatra.accept'] ||= begin
22
+ entries = @env['HTTP_ACCEPT'].to_s.split(',')
23
+ entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
24
+ end
25
+ end
26
+
27
+ def preferred_type(*types)
28
+ return accept.first if types.empty?
29
+ types.flatten!
30
+ accept.detect do |pattern|
31
+ type = types.detect { |t| File.fnmatch(pattern, t) }
32
+ return type if type
33
+ end
18
34
  end
19
35
 
20
36
  if Rack.release <= "1.2"
@@ -34,16 +50,22 @@ module Sinatra
34
50
  end
35
51
 
36
52
  def route
37
- @route ||= begin
38
- path = Rack::Utils.unescape(path_info)
39
- path.empty? ? "/" : path
40
- end
53
+ @route ||= Rack::Utils.unescape(path_info)
41
54
  end
42
55
 
43
56
  def path_info=(value)
44
57
  @route = nil
45
58
  super
46
59
  end
60
+
61
+ private
62
+
63
+ def accept_entry(entry)
64
+ type, *options = entry.gsub(/\s/, '').split(';')
65
+ quality = 0 # we sort smalles first
66
+ options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
67
+ [type, [quality, type.count('*'), 1 - options.size]]
68
+ end
47
69
  end
48
70
 
49
71
  # The response object. See Rack::Response and Rack::ResponseHelpers for
@@ -99,14 +121,14 @@ module Sinatra
99
121
 
100
122
  # According to RFC 2616 section 14.30, "the field value consists of a
101
123
  # single absolute URI"
102
- response['Location'] = url(uri, settings.absolute_redirects?, settings.prefixed_redirects?)
124
+ response['Location'] = uri(uri, settings.absolute_redirects?, settings.prefixed_redirects?)
103
125
  halt(*args)
104
126
  end
105
127
 
106
128
  # Generates the absolute URI for a given path in the app.
107
129
  # Takes Rack routers and reverse proxies into account.
108
130
  def uri(addr = nil, absolute = true, add_script_name = true)
109
- return addr if addr =~ /^https?:\/\//
131
+ return addr if addr =~ /\A[A-z][A-z0-9\+\.\-]*:/
110
132
  uri = [host = ""]
111
133
  if absolute
112
134
  host << 'http'
@@ -156,7 +178,8 @@ module Sinatra
156
178
 
157
179
  # Set the Content-Type of the response body given a media type or file
158
180
  # extension.
159
- def content_type(type, params={})
181
+ def content_type(type = nil, params={})
182
+ return response['Content-Type'] unless type
160
183
  default = params.delete :default
161
184
  mime_type = mime_type(type) || default
162
185
  fail "Unknown media type: %p" % type if mime_type.nil?
@@ -164,7 +187,11 @@ module Sinatra
164
187
  unless params.include? :charset or settings.add_charset.all? { |p| not p === mime_type }
165
188
  params[:charset] = params.delete('charset') || settings.default_encoding
166
189
  end
167
- mime_type << ";#{params.map { |kv| kv.join('=') }.join(', ')}" unless params.empty?
190
+ params.delete :charset if mime_type.include? 'charset'
191
+ unless params.empty?
192
+ mime_type << (mime_type.include?(';') ? ', ' : ';')
193
+ mime_type << params.map { |kv| kv.join('=') }.join(', ')
194
+ end
168
195
  response['Content-Type'] = mime_type
169
196
  end
170
197
 
@@ -557,7 +584,7 @@ module Sinatra
557
584
 
558
585
  case
559
586
  when data.is_a?(Symbol)
560
- body, path, line = self.class.templates[data]
587
+ body, path, line = settings.templates[data]
561
588
  if body
562
589
  body = body.call if body.respond_to?(:call)
563
590
  template.new(path, line.to_i, options) { body }
@@ -575,7 +602,7 @@ module Sinatra
575
602
  end
576
603
  when data.is_a?(Proc) || data.is_a?(String)
577
604
  body = data.is_a?(String) ? Proc.new { data } : data
578
- path, line = self.class.caller_locations.first
605
+ path, line = settings.caller_locations.first
579
606
  template.new(path, line.to_i, options, &body)
580
607
  else
581
608
  raise ArgumentError
@@ -680,13 +707,13 @@ module Sinatra
680
707
 
681
708
  private
682
709
  # Run filters defined on the class and all superclasses.
683
- def filter!(type, base = self.class)
710
+ def filter!(type, base = settings)
684
711
  filter! type, base.superclass if base.superclass.respond_to?(:filters)
685
712
  base.filters[type].each { |block| instance_eval(&block) }
686
713
  end
687
714
 
688
715
  # Run routes defined on the class and all superclasses.
689
- def route!(base=self.class, pass_block=nil)
716
+ def route!(base = settings, pass_block=nil)
690
717
  if routes = base.routes[@request.request_method]
691
718
  routes.each do |pattern, keys, conditions, block|
692
719
  pass_block = process_route(pattern, keys, conditions) do
@@ -716,7 +743,9 @@ module Sinatra
716
743
  # Returns pass block.
717
744
  def process_route(pattern, keys, conditions)
718
745
  @original_params ||= @params
719
- if match = pattern.match(@request.route)
746
+ route = @request.route
747
+ route = '/' if route.empty? and not settings.empty_path_info?
748
+ if match = pattern.match(route)
720
749
  values = match.captures.to_a
721
750
  params =
722
751
  if keys.any?
@@ -861,7 +890,7 @@ module Sinatra
861
890
  # Find an custom error block for the key(s) specified.
862
891
  def error_block!(*keys)
863
892
  keys.each do |key|
864
- base = self.class
893
+ base = settings
865
894
  while base.respond_to?(:errors)
866
895
  if block = base.errors[key]
867
896
  # found a handler, eval and return result
@@ -1013,6 +1042,14 @@ module Sinatra
1013
1042
  Rack::Mime::MIME_TYPES[type] = value
1014
1043
  end
1015
1044
 
1045
+ # provides all mime types matching type, including deprecated types:
1046
+ # mime_types :html # => ['text/html']
1047
+ # mime_types :js # => ['application/javascript', 'text/javascript']
1048
+ def mime_types(type)
1049
+ type = mime_type type
1050
+ type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type]
1051
+ end
1052
+
1016
1053
  # Define a before filter; runs before all requests within the same
1017
1054
  # context as route handlers and may access/modify the request and
1018
1055
  # response.
@@ -1065,12 +1102,11 @@ module Sinatra
1065
1102
 
1066
1103
  # Condition for matching mimetypes. Accepts file extensions.
1067
1104
  def provides(*types)
1068
- types.map! { |t| mime_type(t) }
1069
-
1105
+ types.map! { |t| mime_types(t) }
1106
+ types.flatten!
1070
1107
  condition do
1071
- matching_types = (request.accept & types)
1072
- unless matching_types.empty?
1073
- content_type matching_types.first
1108
+ if type = request.preferred_type(types)
1109
+ content_type(type)
1074
1110
  true
1075
1111
  else
1076
1112
  false
@@ -1099,6 +1135,7 @@ module Sinatra
1099
1135
  def route(verb, path, options={}, &block)
1100
1136
  # Because of self.options.host
1101
1137
  host_name(options.delete(:host)) if options.key?(:host)
1138
+ enable :empty_path_info if path == "" and empty_path_info.nil?
1102
1139
 
1103
1140
  block, pattern, keys, conditions = compile! verb, path, block, options
1104
1141
  invoke_hook(:route_added, verb, path, block)
@@ -1232,10 +1269,10 @@ module Sinatra
1232
1269
  # an instance of this class as end point.
1233
1270
  def build(*args, &bk)
1234
1271
  builder = Rack::Builder.new
1272
+ builder.use Rack::MethodOverride if method_override?
1273
+ builder.use ShowExceptions if show_exceptions?
1274
+ builder.use Rack::CommonLogger if logging?
1235
1275
  setup_sessions builder
1236
- builder.use Rack::CommonLogger if logging?
1237
- builder.use Rack::MethodOverride if method_override?
1238
- builder.use ShowExceptions if show_exceptions?
1239
1276
  middleware.each { |c,a,b| builder.use(c, *a, &b) }
1240
1277
  builder.run new!(*args, &bk)
1241
1278
  builder
@@ -1363,6 +1400,7 @@ module Sinatra
1363
1400
 
1364
1401
  set :absolute_redirects, true
1365
1402
  set :prefixed_redirects, false
1403
+ set :empty_path_info, nil
1366
1404
 
1367
1405
  set :app_file, nil
1368
1406
  set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
@@ -1401,7 +1439,7 @@ module Sinatra
1401
1439
  </head>
1402
1440
  <body>
1403
1441
  <h2>Sinatra doesn't know this ditty.</h2>
1404
- <img src='/__sinatra__/404.png'>
1442
+ <img src='#{uri "/__sinatra__/404.png"}'>
1405
1443
  <div id="c">
1406
1444
  Try this:
1407
1445
  <pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
@@ -174,7 +174,7 @@ TEMPLATE = <<-HTML # :nodoc:
174
174
  <body>
175
175
  <div id="wrap">
176
176
  <div id="header">
177
- <img src="/__sinatra__/500.png" alt="application error" height="161" width="313" />
177
+ <img src="<%= env['SCRIPT_NAME'] %>/__sinatra__/500.png" alt="application error" height="161" width="313" />
178
178
  <div id="summary">
179
179
  <h1><strong><%=h exception.class %></strong> at <strong><%=h path %>
180
180
  </strong></h1>
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
4
 
5
5
  s.name = 'sinatra'
6
- s.version = '1.2.1'
7
- s.date = '2011-03-17'
6
+ s.version = '1.2.2'
7
+ s.date = '2011-04-08'
8
8
 
9
9
  s.description = "Classy web-development dressed in a DSL"
10
10
  s.summary = "Classy web-development dressed in a DSL"
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
24
24
  README.hu.rdoc
25
25
  README.jp.rdoc
26
26
  README.pt-br.rdoc
27
+ README.pt-pt.rdoc
27
28
  README.rdoc
28
29
  README.ru.rdoc
29
30
  README.zh.rdoc
@@ -10,11 +10,11 @@ class BaseTest < Test::Unit::TestCase
10
10
 
11
11
  it 'allows unicode strings in ascii templates per default (1.9)' do
12
12
  next unless defined? Encoding
13
- @base.new.erb(File.read(@base.views + "/ascii.erb").encode("ASCII"), {}, :value => "åkej")
13
+ @base.new!.erb(File.read(@base.views + "/ascii.erb").encode("ASCII"), {}, :value => "åkej")
14
14
  end
15
15
 
16
16
  it 'allows ascii strings in unicode templates per default (1.9)' do
17
17
  next unless defined? Encoding
18
- @base.new.erb(:utf8, {}, :value => "Some Lyrics".encode("ASCII"))
18
+ @base.new!.erb(:utf8, {}, :value => "Some Lyrics".encode("ASCII"))
19
19
  end
20
20
  end
@@ -123,6 +123,34 @@ class HelpersTest < Test::Unit::TestCase
123
123
  response = request.get('/', 'HTTP_X_FORWARDED_HOST' => 'example.com', 'SERVER_PORT' => '8080')
124
124
  assert_equal 'http://example.com/foo', response['Location']
125
125
  end
126
+
127
+ it 'accepts absolute URIs' do
128
+ mock_app do
129
+ get '/' do
130
+ redirect 'http://google.com'
131
+ fail 'redirect should halt'
132
+ end
133
+ end
134
+
135
+ get '/'
136
+ assert_equal 302, status
137
+ assert_equal '', body
138
+ assert_equal 'http://google.com', response['Location']
139
+ end
140
+
141
+ it 'accepts absolute URIs with a different schema' do
142
+ mock_app do
143
+ get '/' do
144
+ redirect 'mailto:jsmith@example.com'
145
+ fail 'redirect should halt'
146
+ end
147
+ end
148
+
149
+ get '/'
150
+ assert_equal 302, status
151
+ assert_equal '', body
152
+ assert_equal 'mailto:jsmith@example.com', response['Location']
153
+ end
126
154
  end
127
155
 
128
156
  describe 'error' do
@@ -401,6 +429,28 @@ class HelpersTest < Test::Unit::TestCase
401
429
  get '/'
402
430
  assert tests_ran
403
431
  end
432
+
433
+ it 'handles already present params' do
434
+ mock_app do
435
+ get '/' do
436
+ content_type 'foo/bar;level=1', :charset => 'utf-8'
437
+ 'ok'
438
+ end
439
+ end
440
+ get '/'
441
+ assert_equal 'foo/bar;level=1, charset=utf-8', response['Content-Type']
442
+ end
443
+
444
+ it 'does not add charset if present' do
445
+ mock_app do
446
+ get '/' do
447
+ content_type 'text/plain;charset=utf-16'
448
+ 'ok'
449
+ end
450
+ end
451
+ get '/'
452
+ assert_equal 'text/plain;charset=utf-16', response['Content-Type']
453
+ end
404
454
  end
405
455
 
406
456
  describe 'send_file' do
@@ -795,6 +845,18 @@ class HelpersTest < Test::Unit::TestCase
795
845
  assert_equal 'http://example.org/foo/bar', body
796
846
  end
797
847
 
848
+ it 'handles absolute URIs' do
849
+ mock_app { get('/') { uri 'http://google.com' }}
850
+ get '/'
851
+ assert_equal 'http://google.com', body
852
+ end
853
+
854
+ it 'handles different protocols' do
855
+ mock_app { get('/') { uri 'mailto:jsmith@example.com' }}
856
+ get '/'
857
+ assert_equal 'mailto:jsmith@example.com', body
858
+ end
859
+
798
860
  it 'is aliased to #url' do
799
861
  mock_app { get('/') { url }}
800
862
  get '/'
@@ -93,12 +93,28 @@ class RoutingTest < Test::Unit::TestCase
93
93
  assert_equal "<h1>Not Found</h1>", response.body
94
94
  end
95
95
 
96
- it 'matches empty PATH_INFO to "/"' do
97
- mock_app {
96
+ it 'matches empty PATH_INFO to "/" if no route is defined for ""' do
97
+ mock_app do
98
98
  get '/' do
99
99
  'worked'
100
100
  end
101
- }
101
+ end
102
+
103
+ get '/', {}, "PATH_INFO" => ""
104
+ assert ok?
105
+ assert_equal 'worked', body
106
+ end
107
+
108
+ it 'matches empty PATH_INFO to "" if a route is defined for ""' do
109
+ mock_app do
110
+ get '/' do
111
+ 'did not work'
112
+ end
113
+
114
+ get '' do
115
+ 'worked'
116
+ end
117
+ end
102
118
 
103
119
  get '/', {}, "PATH_INFO" => ""
104
120
  assert ok?
@@ -724,6 +740,77 @@ class RoutingTest < Test::Unit::TestCase
724
740
  assert_equal 'default', body
725
741
  end
726
742
 
743
+ it 'respects user agent prefferences for the content type' do
744
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
745
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5,text/html;q=0.8' }
746
+ assert_body 'text/html;charset=utf-8'
747
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.8,text/html;q=0.5' }
748
+ assert_body 'image/png'
749
+ end
750
+
751
+ it 'accepts generic types' do
752
+ mock_app do
753
+ get('/', :provides => :xml) { content_type }
754
+ get('/') { 'no match' }
755
+ end
756
+ get '/'
757
+ assert_body 'no match'
758
+ get '/', {}, { 'HTTP_ACCEPT' => 'foo/*' }
759
+ assert_body 'no match'
760
+ get '/', {}, { 'HTTP_ACCEPT' => 'application/*' }
761
+ assert_body 'application/xml;charset=utf-8'
762
+ get '/', {}, { 'HTTP_ACCEPT' => '*/*' }
763
+ assert_body 'application/xml;charset=utf-8'
764
+ end
765
+
766
+ it 'prefers concrete over partly generic types' do
767
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
768
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/*, text/html' }
769
+ assert_body 'text/html;charset=utf-8'
770
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png, text/*' }
771
+ assert_body 'image/png'
772
+ end
773
+
774
+ it 'prefers concrete over fully generic types' do
775
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
776
+ get '/', {}, { 'HTTP_ACCEPT' => '*/*, text/html' }
777
+ assert_body 'text/html;charset=utf-8'
778
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png, */*' }
779
+ assert_body 'image/png'
780
+ end
781
+
782
+ it 'prefers partly generic over fully generic types' do
783
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
784
+ get '/', {}, { 'HTTP_ACCEPT' => '*/*, text/*' }
785
+ assert_body 'text/html;charset=utf-8'
786
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/*, */*' }
787
+ assert_body 'image/png'
788
+ end
789
+
790
+ it 'respects quality with generic types' do
791
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
792
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/*;q=1, text/html;q=0' }
793
+ assert_body 'image/png'
794
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5, text/*;q=0.7' }
795
+ assert_body 'text/html;charset=utf-8'
796
+ end
797
+
798
+ it 'accepts both text/javascript and application/javascript for js' do
799
+ mock_app { get('/', :provides => :js) { content_type }}
800
+ get '/', {}, { 'HTTP_ACCEPT' => 'application/javascript' }
801
+ assert_body 'application/javascript;charset=utf-8'
802
+ get '/', {}, { 'HTTP_ACCEPT' => 'text/javascript' }
803
+ assert_body 'text/javascript;charset=utf-8'
804
+ end
805
+
806
+ it 'accepts both text/xml and application/xml for xml' do
807
+ mock_app { get('/', :provides => :xml) { content_type }}
808
+ get '/', {}, { 'HTTP_ACCEPT' => 'application/xml' }
809
+ assert_body 'application/xml;charset=utf-8'
810
+ get '/', {}, { 'HTTP_ACCEPT' => 'text/xml' }
811
+ assert_body 'text/xml;charset=utf-8'
812
+ end
813
+
727
814
  it 'passes a single url param as block parameters when one param is specified' do
728
815
  mock_app {
729
816
  get '/:foo' do |foo|