roda 2.15.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 625daaffa474b5553308633c41d20b76fa3096d8
4
- data.tar.gz: 4bcfc8c8c1258f6860e8ac430ba59679b2c62c69
3
+ metadata.gz: c3051f95d0665c935a0aa391b7f5b3d4ec3905ec
4
+ data.tar.gz: c3a81d9c6384dc8708c941fcb8171295fcaff6bc
5
5
  SHA512:
6
- metadata.gz: 2331dd19bd5efb8c3187df90917c4a23f814d52ec205b553ac1131fb39a787fffc83930ce697011cda5ac6926ab7cf31a209033dfc5589342b9410ff09b6a743
7
- data.tar.gz: 0abd741f20b87dbe497eba823357ce323aa1d82f7a8048895afe4d5b47eefeb340e50fdf9c71daa8c9cb2c064a5e4f30cda800aa751a345f84f57d2424a6a617
6
+ metadata.gz: e6479fd6fffd9fdcefc3c09e3543142668b7cd15b3f52af8fda896d438a174b54c1a8350c5aea6a5745188f5eaec78b5fe3a11405365a1318ff38a0098acd307
7
+ data.tar.gz: 1a837206ad2a4a158e5c939c0a52107752f3e9c9108255e843db681c1f2a62265ee5ca54ba96071b441ca760de7ee2871cf58822ce9f12ed704fa283fcb786b2
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ = 2.16.0 (2016-07-13)
2
+
3
+ * Add type_routing plugin, for routing based on path extensions and Accept headers (Papierkorb, jeremyevans) (#75)
4
+
5
+ * Add unescape_path plugin, for decoding URL-encoded PATH_INFO before routing (jeremyevans) (#74)
6
+
7
+ * Add request_headers plugin, for simpler access to request headers (celsworth) (#72)
8
+
1
9
  = 2.15.0 (2016-06-13)
2
10
 
3
11
  * Add public plugin for r.public method for serving all files in the public directory (jeremyevans)
@@ -0,0 +1,48 @@
1
+ = New Features
2
+
3
+ * A type_routing plugin has been added. This plugin allows routing
4
+ based on the requested type, which can be submitted either via a
5
+ file extension or Accept header:
6
+
7
+ plugin :type_routing
8
+
9
+ route do |r|
10
+ r.get 'a' do
11
+ r.html{ "<h1>This is the HTML response</h1>" }
12
+ r.json{ '{"json": "ok"}' }
13
+ r.xml{ "<root>This is the XML response</root>" }
14
+ end
15
+ end
16
+
17
+ # /a or /a.html => HTML response
18
+ # /a.json => JSON response
19
+ # /a.xml => XML response
20
+
21
+ The response content type is set appropriately when the r.html,
22
+ r.json, or r.xml block is yielded to. Using plugin options, you can
23
+ add support for custom types, and choose whether to use only file
24
+ extensions or only Accept headers for type matching.
25
+
26
+ * A request_headers plugin has been added. This allows easier access
27
+ to request headers. For example, to access a header called
28
+ X-My-Header, by default you would need to use the CGI mangled name:
29
+
30
+ r.env['HTTP_X_MY_HEADER']
31
+
32
+ The request_headers plugin allows the easier to use:
33
+
34
+ r.headers['X-My-Header']
35
+
36
+ * An unescape_path plugin has been added. By default, Roda does not
37
+ unescape a URL-encoded PATH_INFO before routing. This plugin allows
38
+ URL-encoded PATH_INFO to work, supporting %2f as well as / as path
39
+ separators, and having captures return unescaped values:
40
+
41
+ plugin :unescape_path
42
+
43
+ route do |r|
44
+ # Assume /b/a URL encoded at %2f%62%2f%61
45
+ r.on :x, /(.)/ do |*x|
46
+ # x => ['b', 'a']
47
+ end
48
+ end
data/lib/roda.rb CHANGED
@@ -350,7 +350,7 @@ class Roda
350
350
  def initialize(scope, env)
351
351
  @scope = scope
352
352
  @captures = []
353
- @remaining_path = env[PATH_INFO]
353
+ @remaining_path = _remaining_path(env)
354
354
  super(env)
355
355
  end
356
356
 
@@ -722,6 +722,11 @@ class Roda
722
722
  SEGMENT
723
723
  end
724
724
 
725
+ # The base remaining path to use.
726
+ def _remaining_path(env)
727
+ env[PATH_INFO]
728
+ end
729
+
725
730
  # Backbone of the verb method support, using a terminal match if
726
731
  # args is not empty, or a regular match if it is empty.
727
732
  def _verb(args, &block)
@@ -6,13 +6,9 @@ class Roda
6
6
  # The per_thread_caching plugin changes the default cache
7
7
  # from being a shared thread safe cache to a separate cache per
8
8
  # thread. This means getting or setting values no longer
9
- # needs a mutex on non-MRI ruby implementations, which may be
10
- # faster when using a thread pool. However, since the caches
11
- # are no longer shared, this will take up more memory.
12
- #
13
- # Note that it does not make sense to use this plugin on MRI,
14
- # since the default cache on MRI doesn't use a mutex as it
15
- # is already thread safe due to the GVL.
9
+ # needs a mutex, which may be faster when using a thread pool.
10
+ # However, since the caches are no longer shared, this will
11
+ # take up more memory.
16
12
  #
17
13
  # Using this plugin changes the matcher regexp cache to use
18
14
  # per-thread caches, and changes the default for future
@@ -0,0 +1,83 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'set'
4
+
5
+ class Roda
6
+ module RodaPlugins
7
+ # The request_headers plugin provides access to headers sent in the
8
+ # request in a more natural way than directly accessing the env hash.
9
+ #
10
+ # In practise this means you don't need to uppercase, convert dashes
11
+ # to underscores, or add a HTTP_ prefix.
12
+ #
13
+ # For example, to access a header called X-My-Header you
14
+ # would previously need to do:
15
+ #
16
+ # r.env['HTTP_X_MY_HEADER']
17
+ #
18
+ # But with this plugin you can now say:
19
+ #
20
+ # r.headers['X-My-Header']
21
+ #
22
+ # The name is actually case-insensitive so x-my-header will work as well.
23
+ #
24
+ #
25
+ # Example:
26
+ #
27
+ # plugin :request_headers
28
+ #
29
+ module RequestHeaders
30
+ module RequestMethods
31
+ # Provide access to the request headers while normalising indexes.
32
+ def headers
33
+ @request_headers ||= Headers.new(@env)
34
+ end
35
+ end
36
+
37
+ class Headers
38
+ # Set of environment variable names that don't need HTTP_ prepended to them.
39
+ CGI_VARIABLES = Set.new(%w'
40
+ AUTH_TYPE
41
+ CONTENT_LENGTH
42
+ CONTENT_TYPE
43
+ GATEWAY_INTERFACE
44
+ HTTPS
45
+ PATH_INFO
46
+ PATH_TRANSLATED
47
+ QUERY_STRING
48
+ REMOTE_ADDR
49
+ REMOTE_HOST
50
+ REMOTE_IDENT
51
+ REMOTE_USER
52
+ REQUEST_METHOD
53
+ SCRIPT_NAME
54
+ SERVER_NAME
55
+ SERVER_PORT
56
+ SERVER_PROTOCOL
57
+ SERVER_SOFTWARE
58
+ ').freeze
59
+
60
+ def initialize(env)
61
+ @env = env
62
+ end
63
+
64
+ # Returns the value for the given key mapped to @env
65
+ def [](key)
66
+ @env[env_name(key)]
67
+ end
68
+
69
+ private
70
+
71
+ # Convert a HTTP header name into an environment variable name
72
+ def env_name(key)
73
+ key = key.to_s.upcase
74
+ key.tr!('-', '_')
75
+ key = 'HTTP_' + key unless CGI_VARIABLES.include?(key)
76
+ key
77
+ end
78
+ end
79
+ end
80
+
81
+ register_plugin(:request_headers, RequestHeaders)
82
+ end
83
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # Allows to respond to specific request data types. User agents can request
7
+ # specific data types by either supplying an appropriate +Accept+ header
8
+ # or by appending it as file extension to the path.
9
+ #
10
+ # Example:
11
+ #
12
+ # plugin :type_routing
13
+ #
14
+ # route do |r|
15
+ # r.get 'a' do
16
+ # r.html{ "<h1>This is the HTML response</h1>" }
17
+ # r.json{ '{"json": "ok"}' }
18
+ # r.xml{ "<root>This is the XML response</root>" }
19
+ # "Unsupported data type"
20
+ # end
21
+ # end
22
+ #
23
+ # This application will handle the following paths:
24
+ # /a.html :: HTML response
25
+ # /a.json :: JSON response
26
+ # /a.xml :: XML response
27
+ # /a :: HTML, JSON, or XML response, depending on the Accept header
28
+ #
29
+ # The response +Content-Type+ header will be set to a suitable value when
30
+ # the block is matched.
31
+ #
32
+ # Note that if no match is found, code will continue to execute, which can
33
+ # result in unexpected behaviour. This should only happen if you do not
34
+ # handle all supported/configured types. If you want to simplify handling,
35
+ # you can just place the html handling after the other types, without using
36
+ # a separate block:
37
+ #
38
+ # route do |r|
39
+ # r.get 'a' do
40
+ # r.json{ '{"json": "ok"}' }
41
+ # r.xml{ "<root>This is the XML response</root>" }
42
+ #
43
+ # "<h1>This is the HTML response</h1>"
44
+ # end
45
+ # end
46
+ #
47
+ # This works correctly because Roda assumes the html type by default.
48
+ #
49
+ # To match custom extensions, use the :types option:
50
+ #
51
+ # plugin :type_routing, :types => {
52
+ # :yaml => 'application/x-yaml',
53
+ # :js => 'application/javascript; charset=utf-8',
54
+ # }
55
+ #
56
+ # route do |r|
57
+ # r.get 'a' do
58
+ # r.yaml{ YAML.dump "YAML data" }
59
+ # r.js{ "JavaScript code" }
60
+ # # or:
61
+ # r.on_type(:js){ "JavaScript code" }
62
+ # "Unsupported data type"
63
+ # end
64
+ # end
65
+ #
66
+ # = Plugin options
67
+ #
68
+ # The following plugin options are supported:
69
+ #
70
+ # :default_type :: The default data type to assume if the client did not
71
+ # provide one. Defaults to +:html+.
72
+ # :exclude :: Exclude one or more types from the default set (default set
73
+ # is :html, :xml, :json).
74
+ # :types :: Mapping from a data type to its MIME-Type. Used both to match
75
+ # incoming requests and to provide +Content-Type+ values. If the
76
+ # value is +nil+, no +Content-Type+ will be set. The type may
77
+ # contain media type parameters, which will be sent to the client
78
+ # but ignored for request matching.
79
+ # :use_extension :: Whether to take the path extension into account.
80
+ # Default is +true+.
81
+ # :use_header :: Whether to take the +Accept+ header into account.
82
+ # Default is +true+.
83
+ module TypeRouting
84
+ ACCEPT_HEADER = 'HTTP_ACCEPT'.freeze
85
+ CONTENT_TYPE_HEADER = 'Content-Type'.freeze
86
+
87
+ CONFIGURATION = {
88
+ :mimes => {
89
+ 'text/json' => :json,
90
+ 'application/json' => :json,
91
+ 'text/xml' => :xml,
92
+ 'application/xml' => :xml,
93
+ 'text/html' => :html,
94
+ }.freeze,
95
+ :types => {
96
+ :json => 'application/json'.freeze,
97
+ :xml => 'application/xml'.freeze,
98
+ :html => 'text/html'.freeze,
99
+ }.freeze,
100
+ :use_extension => true,
101
+ :use_header => true,
102
+ :default_type => :html
103
+ }.freeze
104
+
105
+ def self.configure(app, opts = {})
106
+ config = (app.opts[:type_routing] || CONFIGURATION).dup
107
+ [:use_extension, :use_header, :default_type].each do |key|
108
+ config[key] = opts[key] if opts.has_key?(key)
109
+ end
110
+
111
+ types = config[:types] = config[:types].dup
112
+ mimes = config[:mimes] = config[:mimes].dup
113
+
114
+ Array(opts[:exclude]).each do |type|
115
+ types.delete(type)
116
+ mimes.reject!{|_, v| v == type}
117
+ end
118
+
119
+ if mapping = opts[:types]
120
+ types.merge!(mapping)
121
+
122
+ mapping.each do |k, v|
123
+ if v
124
+ mimes[v.split(';', 2).first] = k
125
+ end
126
+ end
127
+ end
128
+
129
+ types.freeze
130
+ mimes.freeze
131
+
132
+ type_keys = config[:types].keys
133
+ config[:extension_regexp] = /(.+)\.(#{Regexp.union(type_keys.map(&:to_s))})\z/
134
+
135
+ type_keys.each do |type|
136
+ app::RodaRequest.send(:define_method, type) do |&block|
137
+ on_type(type, &block)
138
+ end
139
+ end
140
+
141
+ app.opts[:type_routing] = config.freeze
142
+ end
143
+
144
+ module RequestMethods
145
+ # Yields if the given +type+ matches the requested data type and halts
146
+ # the request afterwards, returning the result of the block.
147
+ def on_type(type, &block)
148
+ return unless type == requested_type
149
+ response[CONTENT_TYPE_HEADER] ||= @scope.opts[:type_routing][:types][type]
150
+ always(&block)
151
+ end
152
+
153
+ # Returns the data type the client requests.
154
+ def requested_type
155
+ return @requested_type if defined?(@requested_type)
156
+
157
+ opts = @scope.opts[:type_routing]
158
+ @requested_type = accept_response_type if opts[:use_header]
159
+ @requested_type ||= opts[:default_type]
160
+ end
161
+
162
+ private
163
+
164
+ # Removes a trailing file extension from the path, and sets
165
+ # the requested type if so.
166
+ def _remaining_path(env)
167
+ opts = scope.opts[:type_routing]
168
+ path = super
169
+
170
+ if opts[:use_extension]
171
+ if m = path.match(opts[:extension_regexp])
172
+ @requested_type = m[2].to_sym
173
+ path = m[1]
174
+ end
175
+ end
176
+
177
+ path
178
+ end
179
+
180
+ # The response type indicated by the Accept request header.
181
+ def accept_response_type
182
+ mimes = @scope.opts[:type_routing][:mimes]
183
+
184
+ @env[ACCEPT_HEADER].to_s.split(/\s*,\s*/).map do |part|
185
+ mime, _= part.split(/\s*;\s*/, 2)
186
+ if sym = mimes[mime]
187
+ return sym
188
+ end
189
+ end
190
+
191
+ nil
192
+ end
193
+ end
194
+ end
195
+
196
+ register_plugin(:type_routing, TypeRouting)
197
+ end
198
+ end
@@ -0,0 +1,32 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The unescape_path plugin decodes a URL-encoded path
7
+ # before routing. This fixes routing when the slashes
8
+ # are URL-encoded as %2f and returns decoded parameters
9
+ # when matched by symbols or regexps.
10
+ #
11
+ # plugin :unescape_path
12
+ #
13
+ # route do |r|
14
+ # # Assume /b/a URL encoded at %2f%62%2f%61
15
+ # r.on :x, /(.)/ do |*x|
16
+ # # x => ['b', 'a']
17
+ # end
18
+ # end
19
+ module UnescapePath
20
+ module RequestMethods
21
+ private
22
+
23
+ # Unescape the path.
24
+ def _remaining_path(env)
25
+ Rack::Utils.unescape(super)
26
+ end
27
+ end
28
+ end
29
+
30
+ register_plugin(:unescape_path, UnescapePath)
31
+ end
32
+ end
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 2
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 15
7
+ RodaMinorVersion = 16
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -0,0 +1,39 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "request_headers plugin" do
4
+ def header_app(header_name)
5
+ app(:bare) do
6
+ plugin :request_headers
7
+ route do |r|
8
+ r.on do
9
+ # return the value of the request header in the response body,
10
+ # or the static string 'not found' if it hasn't been supplied.
11
+ r.headers[header_name] || 'not found'
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ it "must add HTTP_ prefix when appropriate" do
18
+ header_app('Foo')
19
+ body('/', {'HTTP_FOO' => 'a'}).must_equal 'a'
20
+ end
21
+
22
+ it "must ignore HTTP_ prefix when appropriate" do
23
+ header_app('Content-Type')
24
+ body('/', {'CONTENT_TYPE' => 'a'}).must_equal 'a'
25
+ end
26
+
27
+ it "must return nil for non-existant headers" do
28
+ header_app('X-Non-Existant')
29
+ body('/').must_equal 'not found'
30
+ end
31
+
32
+ it "must be case-insensitive" do
33
+ header_app('X-My-Header')
34
+ body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a'
35
+
36
+ header_app('x-my-header')
37
+ body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a'
38
+ end
39
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
4
+
5
+ describe "type_routing plugin" do
6
+ before do
7
+ app(:type_routing) do |r|
8
+ r.is 'a' do
9
+ r.html{ "HTML: #{r.requested_type}" }
10
+ r.json{ "JSON: #{r.requested_type}" }
11
+ r.xml{ "XML: #{r.requested_type}" }
12
+ "No match"
13
+ end
14
+ end
15
+ end
16
+
17
+ it "uses the file extension in the path" do
18
+ body('/a').must_equal 'HTML: html'
19
+ header('Content-Type', '/a').must_equal 'text/html'
20
+
21
+ body('/a.html').must_equal 'HTML: html'
22
+ header('Content-Type', '/a.html').must_equal 'text/html'
23
+
24
+ body('/a.json').must_equal 'JSON: json'
25
+ header('Content-Type', '/a.json').must_equal 'application/json'
26
+
27
+ body('/a.xml').must_equal 'XML: xml'
28
+ header('Content-Type', '/a.xml').must_equal 'application/xml'
29
+
30
+ status('/a.yadda').must_equal 404
31
+ end
32
+
33
+ it "uses the Accept header value" do
34
+ body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML: html'
35
+ header('Content-Type', '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'text/html'
36
+
37
+ body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON: json'
38
+ header('Content-Type', '/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'application/json'
39
+
40
+ body('/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'XML: xml'
41
+ header('Content-Type', '/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'application/xml'
42
+
43
+ body('/a', 'HTTP_ACCEPT' => 'some/thing').must_equal 'HTML: html'
44
+ header('Content-Type', '/a', 'HTTP_ACCEPT' => 'some/thing').must_equal 'text/html'
45
+ end
46
+
47
+ it "favors the file extension over the Accept header" do
48
+ body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON: json'
49
+ body('/a.xml', 'HTTP_ACCEPT' => 'application/json').must_equal 'XML: xml'
50
+ body('/a.html', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML: html'
51
+ end
52
+
53
+
54
+ it "uses the default if neither file extension nor Accept header are given" do
55
+ body('/a').must_equal 'HTML: html'
56
+ header('Content-Type', '/a').must_equal 'text/html'
57
+ end
58
+ end
59
+
60
+ describe "type_routing plugin" do
61
+ it "does not use the file extension if its disabled" do
62
+ app(:bare) do
63
+ plugin :type_routing, :use_extension => false
64
+
65
+ route do |r|
66
+ r.is 'a' do
67
+ r.html{ "HTML" }
68
+ r.json{ "JSON" }
69
+ end
70
+ end
71
+ end
72
+
73
+ status('/a.json').must_equal 404
74
+ status('/a.html').must_equal 404
75
+ body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML'
76
+ body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON'
77
+ end
78
+
79
+ it "does not use the Accept header if its disabled" do
80
+ app(:bare) do
81
+ plugin :type_routing, :use_header => false
82
+
83
+ route do |r|
84
+ r.is 'a' do
85
+ r.html{ "HTML" }
86
+ r.json{ "JSON" }
87
+ end
88
+ end
89
+ end
90
+
91
+ body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML'
92
+ body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'HTML'
93
+ body('/a.html', 'HTTP_ACCEPT' => 'application/json').must_equal 'HTML'
94
+ body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON'
95
+ end
96
+
97
+ it "only eats known file extensions" do
98
+ app(:bare) do
99
+ plugin :type_routing
100
+
101
+ route do |r|
102
+ r.is 'a' do
103
+ r.html{ "HTML" }
104
+ r.json{ "JSON" }
105
+ r.xml{ "XML" }
106
+ raise "Mismatch!"
107
+ end
108
+
109
+ r.is 'a.jpg' do
110
+ "Okay"
111
+ end
112
+ end
113
+ end
114
+
115
+ body('/a.html').must_equal 'HTML'
116
+ body('/a.json').must_equal 'JSON'
117
+ body('/a.xml').must_equal 'XML'
118
+ body('/a.jpg').must_equal 'Okay'
119
+ end
120
+
121
+ it "uses custom data types" do
122
+ app(:bare) do
123
+ plugin :type_routing, :types => { :yaml => 'application/x-yaml' }
124
+
125
+ route do |r|
126
+ r.is 'a' do
127
+ r.html{ "HTML" }
128
+ r.yaml{ "YAML" }
129
+ raise "Mismatch!"
130
+ end
131
+ end
132
+ end
133
+
134
+ body('/a.html').must_equal 'HTML'
135
+ body('/a.yaml').must_equal 'YAML'
136
+ header('Content-Type', '/a.yaml').must_equal 'application/x-yaml'
137
+ end
138
+
139
+ it "handles response-specific type information when using custom types" do
140
+ app(:bare) do
141
+ plugin :type_routing, :exclude=>:html, :default_type=>:json, :types => { :html => 'text/html; charset=utf-8' }
142
+
143
+ route do |r|
144
+ r.is 'a' do
145
+ r.json{ "JSON" }
146
+ r.html{ "HTML" }
147
+ raise "Mismatch!"
148
+ end
149
+ end
150
+ end
151
+
152
+ body('/a').must_equal 'JSON'
153
+ body('/a.html').must_equal 'HTML'
154
+ header('Content-Type', '/a.html').must_equal 'text/html; charset=utf-8'
155
+ header('Content-Type', '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'text/html; charset=utf-8'
156
+ end
157
+
158
+ it "Handle nil content type when using custom types" do
159
+ app(:bare) do
160
+ plugin :type_routing, :exclude=>:html, :default_type=>:json, :types => { :html => nil}
161
+
162
+ route do |r|
163
+ r.is 'a' do
164
+ r.html{ "HTML" }
165
+ r.json{ "JSON" }
166
+ raise "Mismatch!"
167
+ end
168
+ end
169
+ end
170
+
171
+ body('/a').must_equal 'JSON'
172
+ body('/a.html').must_equal 'HTML'
173
+ header('Content-Type', '/a.html').must_equal 'text/html'
174
+ header('Content-Type', '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'application/json'
175
+ end
176
+
177
+ it "uses custom default type" do
178
+ app(:bare) do
179
+ plugin :type_routing, :default_type => :json
180
+
181
+ route do |r|
182
+ r.is 'a' do
183
+ r.html{ "HTML" }
184
+ r.json{ "JSON" }
185
+ raise "Mismatch!"
186
+ end
187
+ end
188
+ end
189
+
190
+ body('/a').must_equal 'JSON'
191
+ body('/a.html').must_equal 'HTML'
192
+ body('/a.json').must_equal 'JSON'
193
+ end
194
+
195
+ it "supports nil default type" do
196
+ app(:bare) do
197
+ plugin :type_routing, :default_type => nil
198
+
199
+ route do |r|
200
+ r.is 'a' do
201
+ r.html{ "HTML" }
202
+ r.json{ "JSON" }
203
+ "None"
204
+ end
205
+ end
206
+ end
207
+
208
+ body('/a').must_equal 'None'
209
+ body('/a.html').must_equal 'HTML'
210
+ body('/a.json').must_equal 'JSON'
211
+ end
212
+
213
+ it "excludes given types" do
214
+ app(:bare) do
215
+ plugin :type_routing, :exclude => [ :xml ]
216
+
217
+ route do |r|
218
+ r.is 'a' do
219
+ r.html{ "HTML" }
220
+ r.json{ "JSON" }
221
+ r.xml{ raise "Mismatch!" }
222
+ raise "Mismatch"
223
+ end
224
+ end
225
+ end
226
+
227
+ body('/a.html').must_equal 'HTML'
228
+ body('/a.json').must_equal 'JSON'
229
+ status('/a.xml').must_equal 404
230
+
231
+ body('/a', 'HTTP_ACCEPT' => 'text/xml').must_equal 'HTML'
232
+ body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON'
233
+ body('/a', 'HTTP_ACCEPT' => 'text/xml').must_equal 'HTML'
234
+ body('/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML'
235
+ end
236
+
237
+ it "handles loading the plugin multiple times correctly" do
238
+ app(:bare) do
239
+ plugin :type_routing, :default_type => :json
240
+ plugin :type_routing
241
+
242
+ route do |r|
243
+ r.is 'a' do
244
+ r.html{ "HTML" }
245
+ r.json{ "JSON" }
246
+ raise "Mismatch!"
247
+ end
248
+ end
249
+ end
250
+
251
+ body('/a').must_equal 'JSON'
252
+ body('/a.html').must_equal 'HTML'
253
+ body('/a.json').must_equal 'JSON'
254
+ end
255
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "unescape_path_path plugin" do
4
+ it "decodes URL-encoded routing path" do
5
+ app(:unescape_path) do |r|
6
+ r.on 'b' do
7
+ r.get /(.)/ do |a|
8
+ "#{a}-b"
9
+ end
10
+ end
11
+
12
+ r.get :name do |name|
13
+ name
14
+ end
15
+ end
16
+
17
+ body('/a').must_equal 'a'
18
+ body('/%61').must_equal 'a'
19
+ body('%2f%61').must_equal 'a'
20
+ body('%2f%62%2f%61').must_equal 'a-b'
21
+ end
22
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-13 00:00:00.000000000 Z
11
+ date: 2016-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -181,6 +181,7 @@ extra_rdoc_files:
181
181
  - doc/release_notes/2.13.0.txt
182
182
  - doc/release_notes/2.14.0.txt
183
183
  - doc/release_notes/2.15.0.txt
184
+ - doc/release_notes/2.16.0.txt
184
185
  files:
185
186
  - CHANGELOG
186
187
  - MIT-LICENSE
@@ -199,6 +200,7 @@ files:
199
200
  - doc/release_notes/2.13.0.txt
200
201
  - doc/release_notes/2.14.0.txt
201
202
  - doc/release_notes/2.15.0.txt
203
+ - doc/release_notes/2.16.0.txt
202
204
  - doc/release_notes/2.2.0.txt
203
205
  - doc/release_notes/2.3.0.txt
204
206
  - doc/release_notes/2.4.0.txt
@@ -264,6 +266,7 @@ files:
264
266
  - lib/roda/plugins/public.rb
265
267
  - lib/roda/plugins/render.rb
266
268
  - lib/roda/plugins/render_each.rb
269
+ - lib/roda/plugins/request_headers.rb
267
270
  - lib/roda/plugins/response_request.rb
268
271
  - lib/roda/plugins/run_handler.rb
269
272
  - lib/roda/plugins/shared_vars.rb
@@ -276,6 +279,8 @@ files:
276
279
  - lib/roda/plugins/symbol_matchers.rb
277
280
  - lib/roda/plugins/symbol_status.rb
278
281
  - lib/roda/plugins/symbol_views.rb
282
+ - lib/roda/plugins/type_routing.rb
283
+ - lib/roda/plugins/unescape_path.rb
279
284
  - lib/roda/plugins/view_options.rb
280
285
  - lib/roda/plugins/view_subdirs.rb
281
286
  - lib/roda/plugins/websockets.rb
@@ -346,6 +351,7 @@ files:
346
351
  - spec/plugin/public_spec.rb
347
352
  - spec/plugin/render_each_spec.rb
348
353
  - spec/plugin/render_spec.rb
354
+ - spec/plugin/request_headers_spec.rb
349
355
  - spec/plugin/response_request_spec.rb
350
356
  - spec/plugin/run_handler_spec.rb
351
357
  - spec/plugin/shared_vars_spec.rb
@@ -357,6 +363,8 @@ files:
357
363
  - spec/plugin/symbol_matchers_spec.rb
358
364
  - spec/plugin/symbol_status_spec.rb
359
365
  - spec/plugin/symbol_views_spec.rb
366
+ - spec/plugin/type_routing_spec.rb
367
+ - spec/plugin/unescape_path_spec.rb
360
368
  - spec/plugin/view_options_spec.rb
361
369
  - spec/plugin/websockets_spec.rb
362
370
  - spec/plugin_spec.rb