hanami-controller 2.0.0.beta1 → 2.0.0.rc1
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 +25 -0
- data/README.md +4 -4
- data/hanami-controller.gemspec +2 -2
- data/lib/hanami/action/base_params.rb +39 -16
- data/lib/hanami/action/config.rb +268 -0
- data/lib/hanami/action/constants.rb +5 -17
- data/lib/hanami/action/csrf_protection.rb +2 -7
- data/lib/hanami/action/errors.rb +69 -0
- data/lib/hanami/action/flash.rb +29 -22
- data/lib/hanami/action/halt.rb +3 -1
- data/lib/hanami/action/mime.rb +81 -35
- data/lib/hanami/action/params.rb +5 -12
- data/lib/hanami/action/rack/file.rb +6 -2
- data/lib/hanami/action/request.rb +67 -5
- data/lib/hanami/action/response.rb +221 -30
- data/lib/hanami/action/session.rb +11 -2
- data/lib/hanami/action/validatable.rb +9 -5
- data/lib/hanami/action.rb +181 -188
- data/lib/hanami/controller/version.rb +2 -4
- data/lib/hanami/controller.rb +0 -16
- data/lib/hanami/http/status.rb +2 -0
- metadata +12 -12
- data/lib/hanami/action/configuration.rb +0 -436
- data/lib/hanami/controller/error.rb +0 -9
@@ -3,15 +3,23 @@
|
|
3
3
|
require "rack"
|
4
4
|
require "rack/response"
|
5
5
|
require "hanami/utils/kernel"
|
6
|
-
require "hanami/action/flash"
|
7
6
|
require "hanami/action/halt"
|
8
7
|
require "hanami/action/cookie_jar"
|
9
8
|
require "hanami/action/cache/cache_control"
|
10
9
|
require "hanami/action/cache/expires"
|
11
10
|
require "hanami/action/cache/conditional_get"
|
11
|
+
require_relative "errors"
|
12
12
|
|
13
13
|
module Hanami
|
14
14
|
class Action
|
15
|
+
# The HTTP response for an action, given to {Action#handle}.
|
16
|
+
#
|
17
|
+
# Inherits from `Rack::Response`, providing compatibility with Rack functionality.
|
18
|
+
#
|
19
|
+
# @see http://www.rubydoc.info/gems/rack/Rack/Response
|
20
|
+
#
|
21
|
+
# @since 2.0.0
|
22
|
+
# @api private
|
15
23
|
class Response < ::Rack::Response
|
16
24
|
# @since 2.0.0
|
17
25
|
# @api private
|
@@ -27,7 +35,7 @@ module Hanami
|
|
27
35
|
|
28
36
|
# @since 2.0.0
|
29
37
|
# @api private
|
30
|
-
attr_reader :request, :
|
38
|
+
attr_reader :request, :exposures, :env, :view_options
|
31
39
|
|
32
40
|
# @since 2.0.0
|
33
41
|
# @api private
|
@@ -36,7 +44,7 @@ module Hanami
|
|
36
44
|
# @since 2.0.0
|
37
45
|
# @api private
|
38
46
|
def self.build(status, env)
|
39
|
-
new(
|
47
|
+
new(config: nil, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r|
|
40
48
|
r.status = status
|
41
49
|
r.body = Http::Status.message_for(status)
|
42
50
|
r.set_format(Mime.format_for(r.content_type))
|
@@ -45,21 +53,25 @@ module Hanami
|
|
45
53
|
|
46
54
|
# @since 2.0.0
|
47
55
|
# @api private
|
48
|
-
def initialize(request:,
|
56
|
+
def initialize(request:, config:, content_type: nil, env: {}, headers: {}, view_options: nil, sessions_enabled: false) # rubocop:disable Layout/LineLength, Metrics/ParameterLists
|
49
57
|
super([], 200, headers.dup)
|
50
|
-
|
58
|
+
self.content_type = content_type if content_type
|
51
59
|
|
52
60
|
@request = request
|
53
|
-
@
|
54
|
-
@configuration = configuration
|
61
|
+
@config = config
|
55
62
|
@charset = ::Rack::MediaType.params(content_type).fetch("charset", nil)
|
56
63
|
@exposures = {}
|
57
64
|
@env = env
|
58
65
|
@view_options = view_options || DEFAULT_VIEW_OPTIONS
|
59
66
|
|
67
|
+
@sessions_enabled = sessions_enabled
|
60
68
|
@sending_file = false
|
61
69
|
end
|
62
70
|
|
71
|
+
# Sets the response body.
|
72
|
+
#
|
73
|
+
# @param str [String] the body string
|
74
|
+
#
|
63
75
|
# @since 2.0.0
|
64
76
|
# @api public
|
65
77
|
def body=(str)
|
@@ -74,50 +86,141 @@ module Hanami
|
|
74
86
|
end
|
75
87
|
end
|
76
88
|
|
77
|
-
#
|
78
|
-
#
|
89
|
+
# This is NOT RELEASED with 2.0.0
|
90
|
+
#
|
91
|
+
# @api private
|
79
92
|
def render(view, **options)
|
80
93
|
self.body = view.(**view_options.(request, self), **exposures.merge(options)).to_str
|
81
94
|
end
|
82
95
|
|
96
|
+
# Returns the format for the response.
|
97
|
+
#
|
98
|
+
# Returns nil if a format has not been assigned and also cannot be determined from the
|
99
|
+
# response's `#content_type`.
|
100
|
+
#
|
101
|
+
# @example
|
102
|
+
# response.format # => :json
|
103
|
+
#
|
104
|
+
# @return [Symbol, nil]
|
105
|
+
#
|
106
|
+
# @since 2.0.0
|
107
|
+
# @api public
|
108
|
+
def format
|
109
|
+
@format ||= Mime.detect_format(content_type, @config)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Sets the format and associated content type for the response.
|
113
|
+
#
|
114
|
+
# Either a format name (`:json`) or a content type string (`"application/json"`) may be given.
|
115
|
+
# In either case, the format or content type will be derived from the given value, and both
|
116
|
+
# will be set.
|
117
|
+
#
|
118
|
+
# @example Assigning via a format name symbol
|
119
|
+
# response.format = :json
|
120
|
+
# response.content_type # => "application/json"
|
121
|
+
# response.headers["Content-Type"] # => "application/json"
|
122
|
+
#
|
123
|
+
# @example Assigning via a content type string
|
124
|
+
# response.format = "application/json"
|
125
|
+
# response.format # => :json
|
126
|
+
# response.content_type # => "application/json"
|
127
|
+
#
|
128
|
+
# @param value [Symbol, String] the format name or content type
|
129
|
+
#
|
130
|
+
# @see Config#formats
|
131
|
+
#
|
83
132
|
# @since 2.0.0
|
84
133
|
# @api public
|
85
|
-
def format=(
|
86
|
-
|
87
|
-
|
88
|
-
|
134
|
+
def format=(value)
|
135
|
+
format, content_type = Mime.detect_format_and_content_type(value, @config)
|
136
|
+
|
137
|
+
self.content_type = Mime.content_type_with_charset(content_type, charset)
|
138
|
+
|
139
|
+
@format = format
|
89
140
|
end
|
90
141
|
|
142
|
+
# Returns the exposure value for the given key.
|
143
|
+
#
|
144
|
+
# @param key [Object]
|
145
|
+
#
|
146
|
+
# @return [Object] the exposure value, if found
|
147
|
+
#
|
148
|
+
# @raise [KeyError] if the exposure was not found
|
149
|
+
#
|
91
150
|
# @since 2.0.0
|
92
151
|
# @api public
|
93
152
|
def [](key)
|
94
153
|
@exposures.fetch(key)
|
95
154
|
end
|
96
155
|
|
156
|
+
# Sets an exposure value for the given key.
|
157
|
+
#
|
158
|
+
# @param key [Object]
|
159
|
+
# @param value [Object]
|
160
|
+
#
|
161
|
+
# @return [Object] the value
|
162
|
+
#
|
97
163
|
# @since 2.0.0
|
98
164
|
# @api public
|
99
165
|
def []=(key, value)
|
100
166
|
@exposures[key] = value
|
101
167
|
end
|
102
168
|
|
169
|
+
# Returns the session for the response.
|
170
|
+
#
|
171
|
+
# This is the same session object as the {Request}.
|
172
|
+
#
|
173
|
+
# @return [Hash] the session object
|
174
|
+
#
|
175
|
+
# @raise [MissingSessionError] if sessions are not enabled
|
176
|
+
#
|
177
|
+
# @see Request#session
|
178
|
+
#
|
103
179
|
# @since 2.0.0
|
104
180
|
# @api public
|
105
181
|
def session
|
106
|
-
|
182
|
+
unless @sessions_enabled
|
183
|
+
raise Hanami::Action::MissingSessionError.new("Hanami::Action::Response#session")
|
184
|
+
end
|
185
|
+
|
186
|
+
request.session
|
107
187
|
end
|
108
188
|
|
189
|
+
# Returns the flash for the request.
|
190
|
+
#
|
191
|
+
# This is the same flash object as the {Request}.
|
192
|
+
#
|
193
|
+
# @return [Flash]
|
194
|
+
#
|
195
|
+
# @raise [MissingSessionError] if sessions are not enabled
|
196
|
+
#
|
197
|
+
# @see Request#flash
|
198
|
+
#
|
109
199
|
# @since 2.0.0
|
110
200
|
# @api public
|
111
|
-
def
|
112
|
-
|
201
|
+
def flash
|
202
|
+
unless @sessions_enabled
|
203
|
+
raise Hanami::Action::MissingSessionError.new("Hanami::Action::Response#flash")
|
204
|
+
end
|
205
|
+
|
206
|
+
request.flash
|
113
207
|
end
|
114
208
|
|
209
|
+
# Returns the set of cookies to be included in the response.
|
210
|
+
#
|
211
|
+
# @return [CookieJar]
|
212
|
+
#
|
115
213
|
# @since 2.0.0
|
116
214
|
# @api public
|
117
|
-
def
|
118
|
-
@
|
215
|
+
def cookies
|
216
|
+
@cookies ||= CookieJar.new(env.dup, headers, @config.cookies)
|
119
217
|
end
|
120
218
|
|
219
|
+
# Sets the response to redirect to the given URL and halts further handling.
|
220
|
+
#
|
221
|
+
# @param url [String]
|
222
|
+
# @param status [Integer] the HTTP status to use for the redirect
|
223
|
+
#
|
121
224
|
# @since 2.0.0
|
122
225
|
# @api public
|
123
226
|
def redirect_to(url, status: 302)
|
@@ -127,19 +230,43 @@ module Hanami
|
|
127
230
|
Halt.call(status)
|
128
231
|
end
|
129
232
|
|
233
|
+
# Sends the file at the given path as the response, for any file within the configured
|
234
|
+
# `public_directory`.
|
235
|
+
#
|
236
|
+
# Handles the following aspects for file responses:
|
237
|
+
#
|
238
|
+
# - Setting `Content-Type` and `Content-Length` headers
|
239
|
+
# - File Not Found responses (returns a 404)
|
240
|
+
# - Conditional GET (via `If-Modified-Since` header)
|
241
|
+
# - Range requests (via `Range` header)
|
242
|
+
#
|
243
|
+
# @param path [String] the file path
|
244
|
+
#
|
245
|
+
# @return [void]
|
246
|
+
#
|
247
|
+
# @see Config#public_directory
|
248
|
+
#
|
130
249
|
# @since 2.0.0
|
131
250
|
# @api public
|
132
251
|
def send_file(path)
|
133
252
|
_send_file(
|
134
|
-
Rack::File.new(path, @
|
253
|
+
Rack::File.new(path, @config.public_directory).call(env)
|
135
254
|
)
|
136
255
|
end
|
137
256
|
|
257
|
+
# Send the file at the given path as the response, for a file anywhere in the file system.
|
258
|
+
#
|
259
|
+
# @see #send_file
|
260
|
+
#
|
261
|
+
# @param path [String, Pathname] path to the file to be sent
|
262
|
+
#
|
263
|
+
# @return [void]
|
264
|
+
#
|
138
265
|
# @since 2.0.0
|
139
266
|
# @api public
|
140
267
|
def unsafe_send_file(path)
|
141
268
|
directory = if Pathname.new(path).relative?
|
142
|
-
@
|
269
|
+
@config.root_directory
|
143
270
|
else
|
144
271
|
FILE_SYSTEM_ROOT
|
145
272
|
end
|
@@ -149,6 +276,37 @@ module Hanami
|
|
149
276
|
)
|
150
277
|
end
|
151
278
|
|
279
|
+
# Specifies the response freshness policy for HTTP caches using the `Cache-Control` header.
|
280
|
+
#
|
281
|
+
# Any number of non-value directives (`:public`, `:private`, `:no_cache`, `:no_store`,
|
282
|
+
# `:must_revalidate`, `:proxy_revalidate`) may be passed along with a Hash of value directives
|
283
|
+
# (`:max_age`, `:min_stale`, `:s_max_age`).
|
284
|
+
#
|
285
|
+
# See [RFC 2616 / 14.9](http://tools.ietf.org/html/rfc2616#section-14.9.1) for more on
|
286
|
+
# standard cache control directives.
|
287
|
+
#
|
288
|
+
# @example
|
289
|
+
# # Set Cache-Control directives
|
290
|
+
# response.cache_control :public, max_age: 900, s_maxage: 86400
|
291
|
+
#
|
292
|
+
# # Overwrite previous Cache-Control directives
|
293
|
+
# response.cache_control :private, :no_cache, :no_store
|
294
|
+
#
|
295
|
+
# response.get_header("Cache-Control") # => "private, no-store, max-age=900"
|
296
|
+
#
|
297
|
+
# @param values [Array<Symbol, Hash>] values to map to `Cache-Control` directives
|
298
|
+
# @option values [Symbol] :public
|
299
|
+
# @option values [Symbol] :private
|
300
|
+
# @option values [Symbol] :no_cache
|
301
|
+
# @option values [Symbol] :no_store
|
302
|
+
# @option values [Symbol] :must_validate
|
303
|
+
# @option values [Symbol] :proxy_revalidate
|
304
|
+
# @option values [Hash] :max_age
|
305
|
+
# @option values [Hash] :min_stale
|
306
|
+
# @option values [Hash] :s_max_age
|
307
|
+
#
|
308
|
+
# @return void
|
309
|
+
#
|
152
310
|
# @since 2.0.0
|
153
311
|
# @api public
|
154
312
|
def cache_control(*values)
|
@@ -156,6 +314,28 @@ module Hanami
|
|
156
314
|
headers.merge!(directives.headers)
|
157
315
|
end
|
158
316
|
|
317
|
+
# Sets the `Expires` header and `Cache-Control`/`max-age` directive for the response.
|
318
|
+
#
|
319
|
+
# You can provide an integer number of seconds in the future, or a Time object indicating when
|
320
|
+
# the response should be considered "stale". The remaining arguments are passed to
|
321
|
+
# {#cache_control}.
|
322
|
+
#
|
323
|
+
# @example
|
324
|
+
# # Set Cache-Control directives and Expires
|
325
|
+
# response.expires 900, :public
|
326
|
+
#
|
327
|
+
# # Overwrite Cache-Control directives and Expires
|
328
|
+
# response.expires 300, :private, :no_cache, :no_store
|
329
|
+
#
|
330
|
+
# response.get_header("Expires") # => "Thu, 26 Jun 2014 12:00:00 GMT"
|
331
|
+
# response.get_header("Cache-Control") # => "private, no-cache, no-store max-age=300"
|
332
|
+
#
|
333
|
+
# @param amount [Integer, Time] number of seconds or point in time
|
334
|
+
# @param values [Array<Symbols>] values to map to `Cache-Control` directives via
|
335
|
+
# {#cache_control}
|
336
|
+
#
|
337
|
+
# @return void
|
338
|
+
#
|
159
339
|
# @since 2.0.0
|
160
340
|
# @api public
|
161
341
|
def expires(amount, *values)
|
@@ -163,6 +343,26 @@ module Hanami
|
|
163
343
|
headers.merge!(directives.headers)
|
164
344
|
end
|
165
345
|
|
346
|
+
# Sets the `etag` and/or `last_modified` headers on the response and halts with a `304 Not
|
347
|
+
# Modified` response if the request is still fresh according to the `IfNoneMatch` and
|
348
|
+
# `IfModifiedSince` request headers.
|
349
|
+
#
|
350
|
+
# @example
|
351
|
+
# # Set etag header and halt 304 if request matches IF_NONE_MATCH header
|
352
|
+
# response.fresh etag: some_resource.updated_at.to_i
|
353
|
+
#
|
354
|
+
# # Set last_modified header and halt 304 if request matches IF_MODIFIED_SINCE
|
355
|
+
# response.fresh last_modified: some_resource.updated_at
|
356
|
+
#
|
357
|
+
# # Set etag and last_modified header and halt 304 if request matches IF_MODIFIED_SINCE and IF_NONE_MATCH
|
358
|
+
# response.fresh last_modified: some_resource.updated_at
|
359
|
+
#
|
360
|
+
# @param options [Hash]
|
361
|
+
# @option options [Integer] :etag for testing IfNoneMatch conditions
|
362
|
+
# @option options [Date] :last_modified for testing IfModifiedSince conditions
|
363
|
+
#
|
364
|
+
# @return void
|
365
|
+
#
|
166
366
|
# @since 2.0.0
|
167
367
|
# @api public
|
168
368
|
def fresh(options)
|
@@ -177,15 +377,6 @@ module Hanami
|
|
177
377
|
|
178
378
|
# @since 2.0.0
|
179
379
|
# @api private
|
180
|
-
def request_id
|
181
|
-
env.fetch(Action::REQUEST_ID) do
|
182
|
-
# FIXME: raise a meaningful error, by inviting devs to include Hanami::Action::Session
|
183
|
-
raise "Can't find request ID"
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# @since 2.0.0
|
188
|
-
# @api public
|
189
380
|
def set_format(value) # rubocop:disable Naming/AccessorMethodName
|
190
381
|
@format = value
|
191
382
|
end
|
@@ -211,7 +402,7 @@ module Hanami
|
|
211
402
|
alias_method :to_ary, :to_a
|
212
403
|
|
213
404
|
# @since 2.0.0
|
214
|
-
# @api
|
405
|
+
# @api private
|
215
406
|
def head?
|
216
407
|
env[Action::REQUEST_METHOD] == Action::HEAD
|
217
408
|
end
|
@@ -4,12 +4,17 @@ require "hanami/action/flash"
|
|
4
4
|
|
5
5
|
module Hanami
|
6
6
|
class Action
|
7
|
-
# Session
|
7
|
+
# Session support for actions.
|
8
8
|
#
|
9
|
-
#
|
9
|
+
# Not included by default; you should include this module manually to enable session support.
|
10
|
+
# For actions within an Hanami app, this module will be included automatically if sessions are
|
11
|
+
# configured in the app config.
|
10
12
|
#
|
13
|
+
# @api public
|
11
14
|
# @since 0.1.0
|
12
15
|
module Session
|
16
|
+
# @api private
|
17
|
+
# @since 0.1.0
|
13
18
|
def self.included(base)
|
14
19
|
base.class_eval do
|
15
20
|
before { |req, _| req.id }
|
@@ -18,6 +23,10 @@ module Hanami
|
|
18
23
|
|
19
24
|
private
|
20
25
|
|
26
|
+
def sessions_enabled?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
21
30
|
# Finalize the response
|
22
31
|
#
|
23
32
|
# @return [void]
|
@@ -4,6 +4,12 @@ require "hanami/action/params"
|
|
4
4
|
|
5
5
|
module Hanami
|
6
6
|
class Action
|
7
|
+
# Support for validating params when calling actions.
|
8
|
+
#
|
9
|
+
# Included only when hanami-validations (and its dependencies) are bundled.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
# @since 0.1.0
|
7
13
|
module Validatable
|
8
14
|
# Defines the class name for anonymous params
|
9
15
|
#
|
@@ -34,13 +40,10 @@ module Hanami
|
|
34
40
|
# Once whitelisted, the params are available as an Hash with symbols
|
35
41
|
# as keys.
|
36
42
|
#
|
37
|
-
#
|
38
|
-
#
|
39
43
|
# It accepts an anonymous block where all the params can be listed.
|
40
44
|
# It internally creates an inner class which inherits from
|
41
45
|
# Hanami::Action::Params.
|
42
46
|
#
|
43
|
-
#
|
44
47
|
# Alternatively, it accepts an concrete class that should inherit from
|
45
48
|
# Hanami::Action::Params.
|
46
49
|
#
|
@@ -49,8 +52,6 @@ module Hanami
|
|
49
52
|
#
|
50
53
|
# @return void
|
51
54
|
#
|
52
|
-
# @since 0.3.0
|
53
|
-
#
|
54
55
|
# @see Hanami::Action::Params
|
55
56
|
# @see https://guides.hanamirb.org//validations/overview
|
56
57
|
#
|
@@ -93,6 +94,9 @@ module Hanami
|
|
93
94
|
# req.params[:admin] # => nil
|
94
95
|
# end
|
95
96
|
# end
|
97
|
+
#
|
98
|
+
# @since 0.3.0
|
99
|
+
# @api public
|
96
100
|
def params(klass = nil, &blk)
|
97
101
|
if klass.nil?
|
98
102
|
klass = const_set(PARAMS_CLASS_NAME, Class.new(Params))
|