hanami-controller 0.0.0 → 0.6.0

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