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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/LICENSE.md +22 -0
- data/README.md +1180 -9
- data/hanami-controller.gemspec +19 -12
- data/lib/hanami-controller.rb +1 -0
- data/lib/hanami/action.rb +85 -0
- data/lib/hanami/action/cache.rb +174 -0
- data/lib/hanami/action/cache/cache_control.rb +70 -0
- data/lib/hanami/action/cache/conditional_get.rb +93 -0
- data/lib/hanami/action/cache/directives.rb +99 -0
- data/lib/hanami/action/cache/expires.rb +73 -0
- data/lib/hanami/action/callable.rb +94 -0
- data/lib/hanami/action/callbacks.rb +210 -0
- data/lib/hanami/action/configurable.rb +49 -0
- data/lib/hanami/action/cookie_jar.rb +181 -0
- data/lib/hanami/action/cookies.rb +85 -0
- data/lib/hanami/action/exposable.rb +115 -0
- data/lib/hanami/action/flash.rb +182 -0
- data/lib/hanami/action/glue.rb +66 -0
- data/lib/hanami/action/head.rb +122 -0
- data/lib/hanami/action/mime.rb +493 -0
- data/lib/hanami/action/params.rb +285 -0
- data/lib/hanami/action/rack.rb +270 -0
- data/lib/hanami/action/rack/callable.rb +47 -0
- data/lib/hanami/action/rack/file.rb +33 -0
- data/lib/hanami/action/redirect.rb +59 -0
- data/lib/hanami/action/request.rb +86 -0
- data/lib/hanami/action/session.rb +154 -0
- data/lib/hanami/action/throwable.rb +194 -0
- data/lib/hanami/action/validatable.rb +128 -0
- data/lib/hanami/controller.rb +250 -2
- data/lib/hanami/controller/configuration.rb +705 -0
- data/lib/hanami/controller/error.rb +7 -0
- data/lib/hanami/controller/version.rb +4 -1
- data/lib/hanami/http/status.rb +62 -0
- metadata +124 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- 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
|