hanami-controller 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +1180 -9
  5. data/hanami-controller.gemspec +19 -12
  6. data/lib/hanami-controller.rb +1 -0
  7. data/lib/hanami/action.rb +85 -0
  8. data/lib/hanami/action/cache.rb +174 -0
  9. data/lib/hanami/action/cache/cache_control.rb +70 -0
  10. data/lib/hanami/action/cache/conditional_get.rb +93 -0
  11. data/lib/hanami/action/cache/directives.rb +99 -0
  12. data/lib/hanami/action/cache/expires.rb +73 -0
  13. data/lib/hanami/action/callable.rb +94 -0
  14. data/lib/hanami/action/callbacks.rb +210 -0
  15. data/lib/hanami/action/configurable.rb +49 -0
  16. data/lib/hanami/action/cookie_jar.rb +181 -0
  17. data/lib/hanami/action/cookies.rb +85 -0
  18. data/lib/hanami/action/exposable.rb +115 -0
  19. data/lib/hanami/action/flash.rb +182 -0
  20. data/lib/hanami/action/glue.rb +66 -0
  21. data/lib/hanami/action/head.rb +122 -0
  22. data/lib/hanami/action/mime.rb +493 -0
  23. data/lib/hanami/action/params.rb +285 -0
  24. data/lib/hanami/action/rack.rb +270 -0
  25. data/lib/hanami/action/rack/callable.rb +47 -0
  26. data/lib/hanami/action/rack/file.rb +33 -0
  27. data/lib/hanami/action/redirect.rb +59 -0
  28. data/lib/hanami/action/request.rb +86 -0
  29. data/lib/hanami/action/session.rb +154 -0
  30. data/lib/hanami/action/throwable.rb +194 -0
  31. data/lib/hanami/action/validatable.rb +128 -0
  32. data/lib/hanami/controller.rb +250 -2
  33. data/lib/hanami/controller/configuration.rb +705 -0
  34. data/lib/hanami/controller/error.rb +7 -0
  35. data/lib/hanami/controller/version.rb +4 -1
  36. data/lib/hanami/http/status.rb +62 -0
  37. metadata +124 -16
  38. data/.gitignore +0 -9
  39. data/Gemfile +0 -4
  40. data/Rakefile +0 -2
  41. data/bin/console +0 -14
  42. data/bin/setup +0 -8
@@ -0,0 +1,66 @@
1
+ module Hanami
2
+ module Action
3
+ # Glue code for full stack Hanami applications
4
+ #
5
+ # This includes missing rendering logic that it makes sense to include
6
+ # only for web applications.
7
+ #
8
+ # @api private
9
+ # @since 0.3.0
10
+ module Glue
11
+ # Rack environment key that indicates where the action instance is passed
12
+ #
13
+ # @api private
14
+ # @since 0.3.0
15
+ ENV_KEY = 'hanami.action'.freeze
16
+
17
+ # @api private
18
+ # @since 0.3.2
19
+ ADDITIONAL_HTTP_STATUSES_WITHOUT_BODY = Set.new([301, 302]).freeze
20
+
21
+ # Override Ruby's Module#included
22
+ #
23
+ # @api private
24
+ # @since 0.3.0
25
+ def self.included(base)
26
+ base.class_eval { expose(:format) if respond_to?(:expose) }
27
+ end
28
+
29
+ # Check if the current HTTP request is renderable.
30
+ #
31
+ # It verifies if the verb isn't HEAD, if the status demands to omit
32
+ # the body and if it isn't sending a file.
33
+ #
34
+ # @return [TrueClass,FalseClass] the result of the check
35
+ #
36
+ # @api private
37
+ # @since 0.3.2
38
+ def renderable?
39
+ !_requires_no_body? &&
40
+ !sending_file? &&
41
+ !ADDITIONAL_HTTP_STATUSES_WITHOUT_BODY.include?(@_status)
42
+ end
43
+
44
+ protected
45
+ # Put the current instance into the Rack environment
46
+ #
47
+ # @api private
48
+ # @since 0.3.0
49
+ #
50
+ # @see Hanami::Action#finish
51
+ def finish
52
+ super
53
+ @_env[ENV_KEY] = self
54
+ end
55
+
56
+ # Check if the request's body is a file
57
+ #
58
+ # @return [TrueClass,FalseClass] the result of the check
59
+ #
60
+ # @since 0.4.3
61
+ def sending_file?
62
+ @_body.is_a?(::Rack::File)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,122 @@
1
+ module Hanami
2
+ module Action
3
+ # Ensures to not send body or headers for HEAD requests and/or for status
4
+ # codes that doesn't allow them.
5
+ #
6
+ # @since 0.3.2
7
+ #
8
+ # @see http://www.ietf.org/rfc/rfc2616.txt
9
+ module Head
10
+
11
+ # Status codes that by RFC must not include a message body
12
+ #
13
+ # @since 0.3.2
14
+ # @api private
15
+ HTTP_STATUSES_WITHOUT_BODY = Set.new((100..199).to_a << 204 << 205 << 304).freeze
16
+
17
+
18
+ # Entity headers allowed in blank body responses, according to
19
+ # RFC 2616 - Section 10 (HTTP 1.1).
20
+ #
21
+ # "The response MAY include new or updated metainformation in the form
22
+ # of entity-headers".
23
+ #
24
+ # @since 0.4.0
25
+ # @api private
26
+ #
27
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
28
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html
29
+ ENTITY_HEADERS = {
30
+ 'Allow' => true,
31
+ 'Content-Encoding' => true,
32
+ 'Content-Language' => true,
33
+ 'Content-Location' => true,
34
+ 'Content-MD5' => true,
35
+ 'Content-Range' => true,
36
+ 'Expires' => true,
37
+ 'Last-Modified' => true,
38
+ 'extension-header' => true
39
+ }.freeze
40
+
41
+ # Ensures to not send body or headers for HEAD requests and/or for status
42
+ # codes that doesn't allow them.
43
+ #
44
+ # @since 0.3.2
45
+ # @api private
46
+ #
47
+ # @see Hanami::Action#finish
48
+ def finish
49
+ super
50
+
51
+ if _requires_no_body?
52
+ @_body = nil
53
+ @headers.reject! {|header,_| !keep_response_header?(header) }
54
+ end
55
+ end
56
+
57
+ protected
58
+ # @since 0.3.2
59
+ # @api private
60
+ def _requires_no_body?
61
+ HTTP_STATUSES_WITHOUT_BODY.include?(@_status) || head?
62
+ end
63
+
64
+ private
65
+ # According to RFC 2616, when a response MUST have an empty body, it only
66
+ # allows Entity Headers.
67
+ #
68
+ # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
69
+ # other custom header.
70
+ #
71
+ # This restriction is enforced by <tt>Hanami::Action::Head#finish</tt>.
72
+ #
73
+ # However, there are cases that demand to bypass this rule to set meta
74
+ # informations via headers.
75
+ #
76
+ # An example is a <tt>DELETE</tt> request for a JSON API application.
77
+ # It returns a <tt>204</tt> but still wants to specify the rate limit
78
+ # quota via <tt>X-Rate-Limit</tt>.
79
+ #
80
+ # @since 0.5.0
81
+ # @api public
82
+ #
83
+ # @see Hanami::Action::HEAD#finish
84
+ #
85
+ # @example
86
+ # require 'hanami/controller'
87
+ #
88
+ # module Books
89
+ # class Destroy
90
+ # include Hanami::Action
91
+ #
92
+ # def call(params)
93
+ # # ...
94
+ # self.headers.merge!(
95
+ # 'Last-Modified' => 'Fri, 27 Nov 2015 13:32:36 GMT',
96
+ # 'X-Rate-Limit' => '4000',
97
+ # 'Content-Type' => 'application/json',
98
+ # 'X-No-Pass' => 'true'
99
+ # )
100
+ #
101
+ # self.status = 204
102
+ # end
103
+ #
104
+ # private
105
+ #
106
+ # def keep_response_header?(header)
107
+ # super || header == 'X-Rate-Limit'
108
+ # end
109
+ # end
110
+ # end
111
+ #
112
+ # # Only the following headers will be sent:
113
+ # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
114
+ # # * X-Rate-Limit - because we explicitely allow it
115
+ #
116
+ # # Both Content-Type and X-No-Pass are removed because they're not allowed
117
+ def keep_response_header?(header)
118
+ ENTITY_HEADERS.include?(header)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,493 @@
1
+ require 'rack/utils'
2
+ require 'hanami/utils'
3
+ require 'hanami/utils/kernel'
4
+ require 'hanami/utils/deprecation'
5
+
6
+ module Hanami
7
+ module Action
8
+ # Mime type API
9
+ #
10
+ # @since 0.1.0
11
+ #
12
+ # @see Hanami::Action::Mime::ClassMethods#accept
13
+ module Mime
14
+ # The key that returns accepted mime types from the Rack env
15
+ #
16
+ # @since 0.1.0
17
+ # @api private
18
+ HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
19
+
20
+ # The header key to set the mime type of the response
21
+ #
22
+ # @since 0.1.0
23
+ # @api private
24
+ CONTENT_TYPE = 'Content-Type'.freeze
25
+
26
+ # The default mime type for an incoming HTTP request
27
+ #
28
+ # @since 0.1.0
29
+ # @api private
30
+ DEFAULT_ACCEPT = '*/*'.freeze
31
+
32
+ # The default mime type that is returned in the response
33
+ #
34
+ # @since 0.1.0
35
+ # @api private
36
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
37
+
38
+ # The default charset that is returned in the response
39
+ #
40
+ # @since 0.3.0
41
+ # @api private
42
+ DEFAULT_CHARSET = 'utf-8'.freeze
43
+
44
+ # Override Ruby's hook for modules.
45
+ # It includes Mime types logic
46
+ #
47
+ # @param base [Class] the target action
48
+ #
49
+ # @since 0.1.0
50
+ # @api private
51
+ #
52
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
53
+ def self.included(base)
54
+ base.extend ClassMethods
55
+ end
56
+
57
+ module ClassMethods
58
+ # @since 0.2.0
59
+ # @api private
60
+ def format_to_mime_type(format)
61
+ configuration.mime_type_for(format) ||
62
+ ::Rack::Mime.mime_type(".#{ format }", nil) or
63
+ raise Hanami::Controller::UnknownFormatError.new(format)
64
+ end
65
+
66
+ private
67
+
68
+ # Restrict the access to the specified mime type symbols.
69
+ #
70
+ # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
71
+ #
72
+ # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
73
+ # be converted into a mime type
74
+ #
75
+ # @since 0.1.0
76
+ #
77
+ # @see Hanami::Controller::Configuration#format
78
+ #
79
+ # @example
80
+ # require 'hanami/controller'
81
+ #
82
+ # class Show
83
+ # include Hanami::Action
84
+ # accept :html, :json
85
+ #
86
+ # def call(params)
87
+ # # ...
88
+ # end
89
+ # end
90
+ #
91
+ # # When called with "*/*" => 200
92
+ # # When called with "text/html" => 200
93
+ # # When called with "application/json" => 200
94
+ # # When called with "application/xml" => 406
95
+ def accept(*formats)
96
+ mime_types = formats.map do |format|
97
+ format_to_mime_type(format)
98
+ end
99
+
100
+ before do
101
+ unless mime_types.find {|mt| accept?(mt) }
102
+ halt 406
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Returns a symbol representation of the content type.
109
+ #
110
+ # The framework automatically detects the request mime type, and returns
111
+ # the corresponding format.
112
+ #
113
+ # However, if this value was explicitely set by `#format=`, it will return
114
+ # that value
115
+ #
116
+ # @return [Symbol] a symbol that corresponds to the content type
117
+ #
118
+ # @since 0.2.0
119
+ #
120
+ # @see Hanami::Action::Mime#format=
121
+ # @see Hanami::Action::Mime#content_type
122
+ #
123
+ # @example Default scenario
124
+ # require 'hanami/controller'
125
+ #
126
+ # class Show
127
+ # include Hanami::Action
128
+ #
129
+ # def call(params)
130
+ # end
131
+ # end
132
+ #
133
+ # action = Show.new
134
+ #
135
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
136
+ # headers['Content-Type'] # => 'text/html'
137
+ # action.format # => :html
138
+ #
139
+ # @example Set value
140
+ # require 'hanami/controller'
141
+ #
142
+ # class Show
143
+ # include Hanami::Action
144
+ #
145
+ # def call(params)
146
+ # self.format = :xml
147
+ # end
148
+ # end
149
+ #
150
+ # action = Show.new
151
+ #
152
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
153
+ # headers['Content-Type'] # => 'application/xml'
154
+ # action.format # => :xml
155
+ def format
156
+ @format ||= detect_format
157
+ end
158
+
159
+ # The content type that will be automatically set in the response.
160
+ #
161
+ # It prefers, in order:
162
+ # * Explicit set value (see #format=)
163
+ # * Weighted value from Accept
164
+ # * Default content type
165
+ #
166
+ # To override the value, use <tt>#format=</tt>
167
+ #
168
+ # @return [String] the content type from the request.
169
+ #
170
+ # @since 0.1.0
171
+ #
172
+ # @see Hanami::Action::Mime#format=
173
+ # @see Hanami::Configuration#default_request_format
174
+ # @see Hanami::Action::Mime#default_content_type
175
+ # @see Hanami::Action::Mime#DEFAULT_CONTENT_TYPE
176
+ #
177
+ # @example
178
+ # require 'hanami/controller'
179
+ #
180
+ # class Show
181
+ # include Hanami::Action
182
+ #
183
+ # def call(params)
184
+ # # ...
185
+ # content_type # => 'text/html'
186
+ # end
187
+ # end
188
+ def content_type
189
+ @content_type || default_response_type || accepts || default_content_type || DEFAULT_CONTENT_TYPE
190
+ end
191
+
192
+ # Action charset setter, receives new charset value
193
+ #
194
+ # @return [String] the charset of the request.
195
+ #
196
+ # @since 0.3.0
197
+ #
198
+ # @example
199
+ # require 'hanami/controller'
200
+ #
201
+ # class Show
202
+ # include Hanami::Action
203
+ #
204
+ # def call(params)
205
+ # # ...
206
+ # self.charset = 'koi8-r'
207
+ # end
208
+ # end
209
+ def charset=(value)
210
+ @charset = value
211
+ end
212
+
213
+ # The charset that will be automatically set in the response.
214
+ #
215
+ # It prefers, in order:
216
+ # * Explicit set value (see #charset=)
217
+ # * Default configuration charset
218
+ # * Default content type
219
+ #
220
+ # To override the value, use <tt>#charset=</tt>
221
+ #
222
+ # @return [String] the charset of the request.
223
+ #
224
+ # @since 0.3.0
225
+ #
226
+ # @see Hanami::Action::Mime#charset=
227
+ # @see Hanami::Configuration#default_charset
228
+ # @see Hanami::Action::Mime#default_charset
229
+ # @see Hanami::Action::Mime#DEFAULT_CHARSET
230
+ #
231
+ # @example
232
+ # require 'hanami/controller'
233
+ #
234
+ # class Show
235
+ # include Hanami::Action
236
+ #
237
+ # def call(params)
238
+ # # ...
239
+ # charset # => 'text/html'
240
+ # end
241
+ # end
242
+ def charset
243
+ @charset || default_charset || DEFAULT_CHARSET
244
+ end
245
+
246
+ private
247
+
248
+ # Finalize the response by setting the current content type
249
+ #
250
+ # @since 0.1.0
251
+ # @api private
252
+ #
253
+ # @see Hanami::Action#finish
254
+ def finish
255
+ super
256
+ headers[CONTENT_TYPE] ||= content_type_with_charset
257
+ end
258
+
259
+ # Sets the given format and corresponding content type.
260
+ #
261
+ # The framework detects the `HTTP_ACCEPT` header of the request and sets
262
+ # the proper `Content-Type` header in the response.
263
+ # Within this default scenario, `#format` returns a symbol that
264
+ # corresponds to `#content_type`.
265
+ # For instance, if a client sends an `HTTP_ACCEPT` with `text/html`,
266
+ # `#content_type` will return `text/html` and `#format` `:html`.
267
+ #
268
+ # However, it's possible to override what the framework have detected.
269
+ # If a client asks for an `HTTP_ACCEPT` `*/*`, but we want to force the
270
+ # response to be a `text/html` we can use this method.
271
+ #
272
+ # When the format is set, the framework searchs for a corresponding mime
273
+ # type to be set as the `Content-Type` header of the response.
274
+ # This lookup is performed first in the configuration, and then in
275
+ # `Rack::Mime::MIME_TYPES`. If the lookup fails, it raises an error.
276
+ #
277
+ # PERFORMANCE: Because `Hanami::Controller::Configuration#formats` is
278
+ # smaller and looked up first than `Rack::Mime::MIME_TYPES`, we suggest to
279
+ # configure the most common mime types used by your application, **even
280
+ # if they are already present in that Rack constant**.
281
+ #
282
+ # @param format [#to_sym] the format
283
+ #
284
+ # @return [void]
285
+ #
286
+ # @raise [TypeError] if the format cannot be coerced into a Symbol
287
+ # @raise [Hanami::Controller::UnknownFormatError] if the format doesn't
288
+ # have a corresponding mime type
289
+ #
290
+ # @since 0.2.0
291
+ #
292
+ # @see Hanami::Action::Mime#format
293
+ # @see Hanami::Action::Mime#content_type
294
+ # @see Hanami::Controller::Configuration#format
295
+ #
296
+ # @example Default scenario
297
+ # require 'hanami/controller'
298
+ #
299
+ # class Show
300
+ # include Hanami::Action
301
+ #
302
+ # def call(params)
303
+ # end
304
+ # end
305
+ #
306
+ # action = Show.new
307
+ #
308
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
309
+ # headers['Content-Type'] # => 'application/octet-stream'
310
+ # action.format # => :all
311
+ #
312
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
313
+ # headers['Content-Type'] # => 'text/html'
314
+ # action.format # => :html
315
+ #
316
+ # @example Simple usage
317
+ # require 'hanami/controller'
318
+ #
319
+ # class Show
320
+ # include Hanami::Action
321
+ #
322
+ # def call(params)
323
+ # # ...
324
+ # self.format = :json
325
+ # end
326
+ # end
327
+ #
328
+ # action = Show.new
329
+ #
330
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
331
+ # headers['Content-Type'] # => 'application/json'
332
+ # action.format # => :json
333
+ #
334
+ # @example Unknown format
335
+ # require 'hanami/controller'
336
+ #
337
+ # class Show
338
+ # include Hanami::Action
339
+ #
340
+ # def call(params)
341
+ # # ...
342
+ # self.format = :unknown
343
+ # end
344
+ # end
345
+ #
346
+ # action = Show.new
347
+ # action.call({ 'HTTP_ACCEPT' => '*/*' })
348
+ # # => raise Hanami::Controller::UnknownFormatError
349
+ #
350
+ # @example Custom mime type/format
351
+ # require 'hanami/controller'
352
+ #
353
+ # Hanami::Controller.configure do
354
+ # format :custom, 'application/custom'
355
+ # end
356
+ #
357
+ # class Show
358
+ # include Hanami::Action
359
+ #
360
+ # def call(params)
361
+ # # ...
362
+ # self.format = :custom
363
+ # end
364
+ # end
365
+ #
366
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
367
+ # headers['Content-Type'] # => 'application/custom'
368
+ # action.format # => :custom
369
+ def format=(format)
370
+ @format = Utils::Kernel.Symbol(format)
371
+ @content_type = self.class.format_to_mime_type(@format)
372
+ end
373
+
374
+ # Match the given mime type with the Accept header
375
+ #
376
+ # @return [Boolean] true if the given mime type matches Accept
377
+ #
378
+ # @since 0.1.0
379
+ #
380
+ # @example
381
+ # require 'hanami/controller'
382
+ #
383
+ # class Show
384
+ # include Hanami::Action
385
+ #
386
+ # def call(params)
387
+ # # ...
388
+ # # @_env['HTTP_ACCEPT'] # => 'text/html,application/xhtml+xml,application/xml;q=0.9'
389
+ #
390
+ # accept?('text/html') # => true
391
+ # accept?('application/xml') # => true
392
+ # accept?('application/json') # => false
393
+ #
394
+ #
395
+ #
396
+ # # @_env['HTTP_ACCEPT'] # => '*/*'
397
+ #
398
+ # accept?('text/html') # => true
399
+ # accept?('application/xml') # => true
400
+ # accept?('application/json') # => true
401
+ # end
402
+ # end
403
+ def accept?(mime_type)
404
+ !!::Rack::Utils.q_values(accept).find do |mime, _|
405
+ ::Rack::Mime.match?(mime_type, mime)
406
+ end
407
+ end
408
+
409
+ private
410
+
411
+ # @since 0.1.0
412
+ # @api private
413
+ def accept
414
+ @accept ||= @_env[HTTP_ACCEPT] || DEFAULT_ACCEPT
415
+ end
416
+
417
+ # @since 0.1.0
418
+ # @api private
419
+ def accepts
420
+ unless accept == DEFAULT_ACCEPT
421
+ best_q_match(accept, ::Rack::Mime::MIME_TYPES.values)
422
+ end
423
+ end
424
+
425
+ # @since 0.5.0
426
+ # @api private
427
+ def default_response_type
428
+ self.class.format_to_mime_type(configuration.default_response_format) if configuration.default_response_format
429
+ end
430
+
431
+ # @since 0.2.0
432
+ # @api private
433
+ def default_content_type
434
+ self.class.format_to_mime_type(
435
+ configuration.default_request_format
436
+ ) if configuration.default_request_format
437
+ end
438
+
439
+ # @since 0.2.0
440
+ # @api private
441
+ def detect_format
442
+ configuration.format_for(content_type) ||
443
+ ::Rack::Mime::MIME_TYPES.key(content_type).gsub(/\A\./, '').to_sym
444
+ end
445
+
446
+ # @since 0.3.0
447
+ # @api private
448
+ def default_charset
449
+ configuration.default_charset
450
+ end
451
+
452
+ # @since 0.3.0
453
+ # @api private
454
+ def content_type_with_charset
455
+ "#{content_type}; charset=#{charset}"
456
+ end
457
+
458
+ # Patched version of <tt>Rack::Utils.best_q_match</tt>.
459
+ #
460
+ # @since 0.4.1
461
+ # @api private
462
+ #
463
+ # @see http://www.rubydoc.info/gems/rack/Rack/Utils#best_q_match-class_method
464
+ # @see https://github.com/rack/rack/pull/659
465
+ # @see https://github.com/hanami/controller/issues/59
466
+ # @see https://github.com/hanami/controller/issues/104
467
+ def best_q_match(q_value_header, available_mimes)
468
+ values = ::Rack::Utils.q_values(q_value_header)
469
+
470
+ values = values.map do |req_mime, quality|
471
+ match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
472
+ next unless match
473
+ [match, quality]
474
+ end.compact
475
+
476
+ if Hanami::Utils.jruby?
477
+ # See https://github.com/hanami/controller/issues/59
478
+ # See https://github.com/hanami/controller/issues/104
479
+ values.reverse!
480
+ else
481
+ # See https://github.com/jruby/jruby/issues/3004
482
+ values.sort!
483
+ end
484
+
485
+ value = values.sort_by do |match, quality|
486
+ (match.split('/'.freeze, 2).count('*'.freeze) * -10) + quality
487
+ end.last
488
+
489
+ value.first if value
490
+ end
491
+ end
492
+ end
493
+ end