hanami-controller 1.3.2 → 2.0.0.alpha3
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 +83 -0
- data/LICENSE.md +1 -1
- data/README.md +299 -537
- data/hanami-controller.gemspec +5 -4
- data/lib/hanami/action/application_action.rb +112 -0
- data/lib/hanami/action/application_configuration/cookies.rb +29 -0
- data/lib/hanami/action/application_configuration/sessions.rb +46 -0
- data/lib/hanami/action/application_configuration.rb +92 -0
- data/lib/hanami/action/base_params.rb +2 -2
- data/lib/hanami/action/cache/cache_control.rb +4 -4
- data/lib/hanami/action/cache/conditional_get.rb +3 -1
- data/lib/hanami/action/cache/directives.rb +1 -1
- data/lib/hanami/action/cache/expires.rb +3 -3
- data/lib/hanami/action/cache.rb +1 -139
- data/lib/hanami/action/configuration.rb +428 -0
- data/lib/hanami/action/cookie_jar.rb +3 -3
- data/lib/hanami/action/cookies.rb +3 -62
- data/lib/hanami/action/csrf_protection.rb +214 -0
- data/lib/hanami/action/flash.rb +102 -207
- data/lib/hanami/action/glue.rb +5 -31
- data/lib/hanami/action/halt.rb +12 -0
- data/lib/hanami/action/mime.rb +78 -485
- data/lib/hanami/action/params.rb +2 -2
- data/lib/hanami/action/rack/file.rb +1 -1
- data/lib/hanami/action/request.rb +30 -20
- data/lib/hanami/action/response.rb +193 -0
- data/lib/hanami/action/session.rb +11 -128
- data/lib/hanami/action/standalone_action.rb +579 -0
- data/lib/hanami/action/validatable.rb +1 -1
- data/lib/hanami/action/view_name_inferrer.rb +46 -0
- data/lib/hanami/action.rb +129 -73
- data/lib/hanami/controller/version.rb +1 -1
- data/lib/hanami/controller.rb +0 -227
- data/lib/hanami/http/status.rb +2 -2
- metadata +45 -27
- data/lib/hanami/action/callable.rb +0 -92
- data/lib/hanami/action/callbacks.rb +0 -214
- data/lib/hanami/action/configurable.rb +0 -50
- data/lib/hanami/action/exposable/guard.rb +0 -104
- data/lib/hanami/action/exposable.rb +0 -126
- data/lib/hanami/action/head.rb +0 -121
- data/lib/hanami/action/rack/callable.rb +0 -47
- data/lib/hanami/action/rack.rb +0 -399
- data/lib/hanami/action/redirect.rb +0 -59
- data/lib/hanami/action/throwable.rb +0 -196
- data/lib/hanami/controller/configuration.rb +0 -763
- data/lib/hanami-controller.rb +0 -1
data/README.md
CHANGED
@@ -2,12 +2,15 @@
|
|
2
2
|
|
3
3
|
Complete, fast and testable actions for Rack and [Hanami](http://hanamirb.org)
|
4
4
|
|
5
|
+
## Version
|
6
|
+
|
7
|
+
**This branch contains the code for `hanami-controller` 2.x.**
|
8
|
+
|
5
9
|
## Status
|
6
10
|
|
7
11
|
[](https://badge.fury.io/rb/hanami-controller)
|
8
|
-
[](https://codecov.io/gh/hanami/controller)
|
12
|
+
[](https://github.com/hanami/controller/actions?query=workflow%3Aci+branch%3Amain)
|
13
|
+
[](https://codecov.io/gh/hanami/controller)
|
11
14
|
[](https://depfu.com/github/hanami/controller?project=Bundler)
|
12
15
|
[](http://inch-ci.org/github/hanami/controller)
|
13
16
|
|
@@ -23,14 +26,14 @@ Complete, fast and testable actions for Rack and [Hanami](http://hanamirb.org)
|
|
23
26
|
|
24
27
|
## Rubies
|
25
28
|
|
26
|
-
__Hanami::Controller__ supports Ruby (MRI) 2.
|
29
|
+
__Hanami::Controller__ supports Ruby (MRI) 2.6+
|
27
30
|
|
28
31
|
## Installation
|
29
32
|
|
30
33
|
Add this line to your application's Gemfile:
|
31
34
|
|
32
35
|
```ruby
|
33
|
-
gem
|
36
|
+
gem "hanami/controller"
|
34
37
|
```
|
35
38
|
|
36
39
|
And then execute:
|
@@ -57,22 +60,19 @@ The core of this framework are the actions.
|
|
57
60
|
They are the endpoints that respond to incoming HTTP requests.
|
58
61
|
|
59
62
|
```ruby
|
60
|
-
class Show
|
61
|
-
|
62
|
-
|
63
|
-
def call(params)
|
64
|
-
@article = ArticleRepository.new.find(params[:id])
|
63
|
+
class Show < Hanami::Action
|
64
|
+
def handle(req, res)
|
65
|
+
res[:article] = ArticleRepository.new.find(req.params[:id])
|
65
66
|
end
|
66
67
|
end
|
67
68
|
```
|
68
69
|
|
69
|
-
|
70
|
-
In this case, the interface is one method: `#call(params)`.
|
70
|
+
`Hanami::Action` follows the Hanami philosophy: a single purpose object with a minimal interface.
|
71
71
|
|
72
|
-
Hanami
|
73
|
-
|
72
|
+
In this case, `Hanami::Action` provides the key public interface of `#call(env)`, making your actions Rack-compatible.
|
73
|
+
To provide custom behaviour when your actions are being called, you can implement `#handle(req, res)`
|
74
74
|
|
75
|
-
|
75
|
+
**An action is an object** and **you have full control over it**.
|
76
76
|
In other words, you have the freedom to instantiate, inject dependencies and test it, both at the unit and integration level.
|
77
77
|
|
78
78
|
In the example below, the default repository is `ArticleRepository`. During a unit test we can inject a stubbed version, and invoke `#call` with the params.
|
@@ -80,37 +80,39 @@ __We're avoiding HTTP calls__, we're also going to avoid hitting the database (i
|
|
80
80
|
Imagine how **fast** the unit test could be.
|
81
81
|
|
82
82
|
```ruby
|
83
|
-
class Show
|
84
|
-
|
85
|
-
|
86
|
-
def initialize(repository = ArticleRepository.new)
|
83
|
+
class Show < Hanami::Action
|
84
|
+
def initialize(configuration:, repository: ArticleRepository.new)
|
87
85
|
@repository = repository
|
86
|
+
super(configuration: configuration)
|
88
87
|
end
|
89
88
|
|
90
|
-
def
|
91
|
-
|
89
|
+
def handle(req, res)
|
90
|
+
res[:article] = repository.find(req.params[:id])
|
92
91
|
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :repository
|
93
96
|
end
|
94
97
|
|
95
|
-
|
96
|
-
action.
|
98
|
+
configuration = Hanami::Controller::Configuration.new
|
99
|
+
action = Show.new(configuration: configuration, repository: ArticleRepository.new)
|
100
|
+
action.call(id: 23)
|
97
101
|
```
|
98
102
|
|
99
103
|
### Params
|
100
104
|
|
101
|
-
The request params are passed as an argument to the `#
|
105
|
+
The request params are part of the request passed as an argument to the `#handle` method.
|
102
106
|
If routed with *Hanami::Router*, it extracts the relevant bits from the Rack `env` (eg the requested `:id`).
|
103
107
|
Otherwise everything is passed as is: the full Rack `env` in production, and the given `Hash` for unit tests.
|
104
108
|
|
105
|
-
With Hanami::Router
|
109
|
+
With `Hanami::Router`:
|
106
110
|
|
107
111
|
```ruby
|
108
|
-
class Show
|
109
|
-
|
110
|
-
|
111
|
-
def call(params)
|
112
|
+
class Show < Hanami::Action
|
113
|
+
def handle(req, *)
|
112
114
|
# ...
|
113
|
-
puts params # => { id: 23 } extracted from Rack env
|
115
|
+
puts req.params # => { id: 23 } extracted from Rack env
|
114
116
|
end
|
115
117
|
end
|
116
118
|
```
|
@@ -118,12 +120,10 @@ end
|
|
118
120
|
Standalone:
|
119
121
|
|
120
122
|
```ruby
|
121
|
-
class Show
|
122
|
-
|
123
|
-
|
124
|
-
def call(params)
|
123
|
+
class Show < Hanami::Action
|
124
|
+
def handle(req, *)
|
125
125
|
# ...
|
126
|
-
puts params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
|
126
|
+
puts req.params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
|
127
127
|
end
|
128
128
|
end
|
129
129
|
```
|
@@ -131,17 +131,15 @@ end
|
|
131
131
|
Unit Testing:
|
132
132
|
|
133
133
|
```ruby
|
134
|
-
class Show
|
135
|
-
|
136
|
-
|
137
|
-
def call(params)
|
134
|
+
class Show < Hanami::Action
|
135
|
+
def handle(req, *)
|
138
136
|
# ...
|
139
|
-
puts params # => { id: 23, key:
|
137
|
+
puts req.params # => { id: 23, key: "value" } passed as it is from testing
|
140
138
|
end
|
141
139
|
end
|
142
140
|
|
143
|
-
action = Show.new
|
144
|
-
response = action.call(
|
141
|
+
action = Show.new(configuration: configuration)
|
142
|
+
response = action.call(id: 23, key: "value")
|
145
143
|
```
|
146
144
|
|
147
145
|
#### Whitelisting
|
@@ -150,12 +148,10 @@ Params represent an untrusted input.
|
|
150
148
|
For security reasons it's recommended to whitelist them.
|
151
149
|
|
152
150
|
```ruby
|
153
|
-
require
|
154
|
-
require
|
155
|
-
|
156
|
-
class Signup
|
157
|
-
include Hanami::Action
|
151
|
+
require "hanami/validations"
|
152
|
+
require "hanami/controller"
|
158
153
|
|
154
|
+
class Signup < Hanami::Action
|
159
155
|
params do
|
160
156
|
required(:first_name).filled(:str?)
|
161
157
|
required(:last_name).filled(:str?)
|
@@ -168,18 +164,18 @@ class Signup
|
|
168
164
|
end
|
169
165
|
end
|
170
166
|
|
171
|
-
def
|
167
|
+
def handle(req, *)
|
172
168
|
# Describe inheritance hierarchy
|
173
|
-
puts params.class # => Signup::Params
|
174
|
-
puts params.class.superclass # => Hanami::Action::Params
|
169
|
+
puts req.params.class # => Signup::Params
|
170
|
+
puts req.params.class.superclass # => Hanami::Action::Params
|
175
171
|
|
176
172
|
# Whitelist :first_name, but not :admin
|
177
|
-
puts params[:first_name] # => "Luca"
|
178
|
-
puts params[:admin] # => nil
|
173
|
+
puts req.params[:first_name] # => "Luca"
|
174
|
+
puts req.params[:admin] # => nil
|
179
175
|
|
180
176
|
# Whitelist nested params [:address][:line_one], not [:address][:line_two]
|
181
|
-
puts params[:address][:line_one] # =>
|
182
|
-
puts params[:address][:line_two] # => nil
|
177
|
+
puts req.params[:address][:line_one] # => "69 Tender St"
|
178
|
+
puts req.params[:address][:line_two] # => nil
|
183
179
|
end
|
184
180
|
end
|
185
181
|
```
|
@@ -193,12 +189,11 @@ when params are invalid.
|
|
193
189
|
If you specify the `:type` option, the param will be coerced.
|
194
190
|
|
195
191
|
```ruby
|
196
|
-
require
|
197
|
-
require
|
192
|
+
require "hanami/validations"
|
193
|
+
require "hanami/controller"
|
198
194
|
|
199
|
-
class Signup
|
195
|
+
class Signup < Hanami::Action
|
200
196
|
MEGABYTE = 1024 ** 2
|
201
|
-
include Hanami::Action
|
202
197
|
|
203
198
|
params do
|
204
199
|
required(:first_name).filled(:str?)
|
@@ -210,51 +205,33 @@ class Signup
|
|
210
205
|
optional(:avatar).filled(size?: 1..(MEGABYTE * 3))
|
211
206
|
end
|
212
207
|
|
213
|
-
def
|
214
|
-
halt 400 unless params.valid?
|
208
|
+
def handle(req, *)
|
209
|
+
halt 400 unless req.params.valid?
|
215
210
|
# ...
|
216
211
|
end
|
217
212
|
end
|
218
|
-
|
219
|
-
action = Signup.new
|
220
|
-
|
221
|
-
action.call(valid_params) # => [200, {}, ...]
|
222
|
-
action.errors.empty? # => true
|
223
|
-
|
224
|
-
action.call(invalid_params) # => [400, {}, ...]
|
225
|
-
action.errors.empty? # => false
|
226
|
-
|
227
|
-
action.errors.fetch(:email)
|
228
|
-
# => ['is missing', 'is in invalid format']
|
229
213
|
```
|
230
214
|
|
231
215
|
### Response
|
232
216
|
|
233
|
-
The output of `#call` is a
|
217
|
+
The output of `#call` is a `Hanami::Action::Response`:
|
234
218
|
|
235
219
|
```ruby
|
236
|
-
class Show
|
237
|
-
include Hanami::Action
|
238
|
-
|
239
|
-
def call(params)
|
240
|
-
# ...
|
241
|
-
end
|
220
|
+
class Show < Hanami::Action
|
242
221
|
end
|
243
222
|
|
244
|
-
action = Show.new
|
245
|
-
action.call({}) # =>
|
223
|
+
action = Show.new(configuration: configuration)
|
224
|
+
action.call({}) # => #<Hanami::Action::Response:0x00007fe8be968418 @status=200 ...>
|
246
225
|
```
|
247
226
|
|
248
|
-
|
227
|
+
This is the same `res` response object passed to `#handle`, where you can use its accessors to explicitly set status, headers, and body:
|
249
228
|
|
250
229
|
```ruby
|
251
|
-
class Show
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
self.body = 'Hi!'
|
257
|
-
self.headers.merge!({ 'X-Custom' => 'OK' })
|
230
|
+
class Show < Hanami::Action
|
231
|
+
def handle(*, res)
|
232
|
+
res.status = 201
|
233
|
+
res.body = "Hi!"
|
234
|
+
res.headers.merge!("X-Custom" => "OK")
|
258
235
|
end
|
259
236
|
end
|
260
237
|
|
@@ -264,58 +241,47 @@ action.call({}) # => [201, { "X-Custom" => "OK" }, ["Hi!"]]
|
|
264
241
|
|
265
242
|
### Exposures
|
266
243
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
`Hanami::Action`'s solution is the simple and powerful DSL: `expose`.
|
271
|
-
It's a thin layer on top of `attr_reader`.
|
272
|
-
|
273
|
-
Using `expose` creates a getter for the given attribute, and adds it to the _exposures_.
|
274
|
-
Exposures (`#exposures`) are a set of attributes exposed to the view.
|
275
|
-
That is to say the variables necessary for rendering a view.
|
276
|
-
|
277
|
-
By default, all `Hanami::Action` objects expose `#params` and `#errors`.
|
244
|
+
In case you need to send data from the action to other layers of your application, you can use exposures.
|
245
|
+
By default, an action exposes the received params.
|
278
246
|
|
279
247
|
```ruby
|
280
|
-
class Show
|
281
|
-
|
282
|
-
|
283
|
-
expose :article
|
284
|
-
|
285
|
-
def call(params)
|
286
|
-
@article = ArticleRepository.new.find(params[:id])
|
248
|
+
class Show < Hanami::Action
|
249
|
+
def handle(req, res)
|
250
|
+
res[:article] = ArticleRepository.new.find(req.params[:id])
|
287
251
|
end
|
288
252
|
end
|
289
253
|
|
290
|
-
action
|
291
|
-
action.call(
|
254
|
+
action = Show.new(configuration: configuration)
|
255
|
+
response = action.call(id: 23)
|
292
256
|
|
293
|
-
|
257
|
+
article = response[:article]
|
258
|
+
article.class # => Article
|
259
|
+
article.id # => 23
|
294
260
|
|
295
|
-
|
261
|
+
response.exposures.keys # => [:params, :article]
|
296
262
|
```
|
297
263
|
|
298
264
|
### Callbacks
|
299
265
|
|
300
|
-
|
266
|
+
If you need to execute logic **before** or **after** `#handle` is invoked, you can use _callbacks_.
|
267
|
+
They are useful for shared logic like authentication checks.
|
301
268
|
|
302
269
|
```ruby
|
303
|
-
class Show
|
304
|
-
include Hanami::Action
|
305
|
-
|
270
|
+
class Show < Hanami::Action
|
306
271
|
before :authenticate, :set_article
|
307
272
|
|
308
|
-
def
|
273
|
+
def handle(*)
|
309
274
|
end
|
310
275
|
|
311
276
|
private
|
277
|
+
|
312
278
|
def authenticate
|
313
279
|
# ...
|
314
280
|
end
|
315
281
|
|
316
|
-
# `
|
317
|
-
def set_article(
|
318
|
-
|
282
|
+
# `req` and `res` in the method signature is optional
|
283
|
+
def set_article(req, res)
|
284
|
+
res[:article] = ArticleRepository.new.find(req.params[:id])
|
319
285
|
end
|
320
286
|
end
|
321
287
|
```
|
@@ -323,116 +289,87 @@ end
|
|
323
289
|
Callbacks can also be expressed as anonymous lambdas:
|
324
290
|
|
325
291
|
```ruby
|
326
|
-
class Show
|
327
|
-
include Hanami::Action
|
328
|
-
|
292
|
+
class Show < Hanami::Action
|
329
293
|
before { ... } # do some authentication stuff
|
330
|
-
before { |
|
294
|
+
before { |req, res| res[:article] = ArticleRepository.new.find(req.params[:id]) }
|
331
295
|
|
332
|
-
def
|
296
|
+
def handle(*)
|
333
297
|
end
|
334
298
|
end
|
335
299
|
```
|
336
300
|
|
337
301
|
### Exceptions management
|
338
302
|
|
339
|
-
When an exception
|
303
|
+
When the app raises an exception, `hanami-controller`, does **NOT** manage it.
|
304
|
+
You can write custom exception handling on per action or configuration basis.
|
305
|
+
|
306
|
+
An exception handler can be a valid HTTP status code (eg. `500`, `401`), or a `Symbol` that represents an action method.
|
340
307
|
|
341
308
|
```ruby
|
342
|
-
class Show
|
343
|
-
|
309
|
+
class Show < Hanami::Action
|
310
|
+
handle_exception StandardError => 500
|
344
311
|
|
345
|
-
def
|
312
|
+
def handle(*)
|
346
313
|
raise
|
347
314
|
end
|
348
315
|
end
|
349
316
|
|
350
|
-
action = Show.new
|
317
|
+
action = Show.new(configuration: configuration)
|
351
318
|
action.call({}) # => [500, {}, ["Internal Server Error"]]
|
352
319
|
```
|
353
320
|
|
354
321
|
You can map a specific raised exception to a different HTTP status.
|
355
322
|
|
356
323
|
```ruby
|
357
|
-
class Show
|
358
|
-
include Hanami::Action
|
324
|
+
class Show < Hanami::Action
|
359
325
|
handle_exception RecordNotFound => 404
|
360
326
|
|
361
|
-
def
|
362
|
-
|
327
|
+
def handle(*)
|
328
|
+
raise RecordNotFound
|
363
329
|
end
|
364
330
|
end
|
365
331
|
|
366
|
-
action = Show.new
|
367
|
-
action.call({
|
332
|
+
action = Show.new(configuration: configuration)
|
333
|
+
action.call({}) # => [404, {}, ["Not Found"]]
|
368
334
|
```
|
369
335
|
|
370
336
|
You can also define custom handlers for exceptions.
|
371
337
|
|
372
338
|
```ruby
|
373
|
-
class Create
|
374
|
-
include Hanami::Action
|
339
|
+
class Create < Hanami::Action
|
375
340
|
handle_exception ArgumentError => :my_custom_handler
|
376
341
|
|
377
|
-
|
342
|
+
gle(*)
|
378
343
|
raise ArgumentError.new("Invalid arguments")
|
379
344
|
end
|
380
345
|
|
381
346
|
private
|
382
|
-
def my_custom_handler(exception)
|
383
|
-
status 400, exception.message
|
384
|
-
end
|
385
|
-
end
|
386
|
-
|
387
|
-
action = Create.new
|
388
|
-
action.call({}) # => [400, {}, ["Invalid arguments"]]
|
389
|
-
```
|
390
|
-
|
391
|
-
Exception policies can be defined globally, **before** the controllers/actions
|
392
|
-
are loaded.
|
393
347
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
end
|
398
|
-
|
399
|
-
class Show
|
400
|
-
include Hanami::Action
|
401
|
-
|
402
|
-
def call(params)
|
403
|
-
@article = ArticleRepository.new.find(params[:id])
|
348
|
+
def my_custom_handler(req, res, exception)
|
349
|
+
res.status = 400
|
350
|
+
res.body = exception.message
|
404
351
|
end
|
405
352
|
end
|
406
353
|
|
407
|
-
action =
|
408
|
-
action.call({
|
354
|
+
action = Create.new(configuration: configuration)
|
355
|
+
action.call({}) # => [400, {}, ["Invalid arguments"]]
|
409
356
|
```
|
410
357
|
|
411
|
-
|
358
|
+
Exception policies can be defined globally via configuration:
|
412
359
|
|
413
360
|
```ruby
|
414
|
-
Hanami::Controller.
|
415
|
-
|
361
|
+
configuration = Hanami::Controller::Configuration.new do |config|
|
362
|
+
config.handle_exception RecordNotFound => 404
|
416
363
|
end
|
417
364
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
class Show
|
422
|
-
include Hanami::Action
|
423
|
-
|
424
|
-
configure do
|
425
|
-
handle_exceptions false
|
426
|
-
end
|
427
|
-
|
428
|
-
def call(params)
|
429
|
-
@article = ArticleRepository.new.find(params[:id])
|
430
|
-
end
|
365
|
+
class Show < Hanami::Action
|
366
|
+
def handle(*)
|
367
|
+
raise RecordNotFound
|
431
368
|
end
|
432
369
|
end
|
433
370
|
|
434
|
-
action =
|
435
|
-
action.call({
|
371
|
+
action = Show.new(configuration: configuration)
|
372
|
+
action.call({}) # => [404, {}, ["Not Found"]]
|
436
373
|
```
|
437
374
|
|
438
375
|
#### Inherited Exceptions
|
@@ -442,34 +379,30 @@ class MyCustomException < StandardError
|
|
442
379
|
end
|
443
380
|
|
444
381
|
module Articles
|
445
|
-
class Index
|
446
|
-
include Hanami::Action
|
447
|
-
|
382
|
+
class Index < Hanami::Action
|
448
383
|
handle_exception MyCustomException => :handle_my_exception
|
449
384
|
|
450
|
-
def
|
385
|
+
def handle(*)
|
451
386
|
raise MyCustomException
|
452
387
|
end
|
453
388
|
|
454
389
|
private
|
455
390
|
|
456
|
-
def handle_my_exception
|
391
|
+
def handle_my_exception(req, res, exception)
|
457
392
|
# ...
|
458
393
|
end
|
459
394
|
end
|
460
395
|
|
461
|
-
class Show
|
462
|
-
include Hanami::Action
|
463
|
-
|
396
|
+
class Show < Hanami::Action
|
464
397
|
handle_exception StandardError => :handle_standard_error
|
465
398
|
|
466
|
-
def
|
399
|
+
def handle(*)
|
467
400
|
raise MyCustomException
|
468
401
|
end
|
469
402
|
|
470
403
|
private
|
471
404
|
|
472
|
-
def handle_standard_error
|
405
|
+
def handle_standard_error(req, res, exception)
|
473
406
|
# ...
|
474
407
|
end
|
475
408
|
end
|
@@ -485,220 +418,212 @@ Articles::Show.new.call({}) # => `handle_standard_error` will be invoked,
|
|
485
418
|
When `#halt` is used with a valid HTTP code, it stops the execution and sets the proper status and body for the response:
|
486
419
|
|
487
420
|
```ruby
|
488
|
-
class Show
|
489
|
-
include Hanami::Action
|
490
|
-
|
421
|
+
class Show < Hanami::Action
|
491
422
|
before :authenticate!
|
492
423
|
|
493
|
-
def
|
424
|
+
def handle(*)
|
494
425
|
# ...
|
495
426
|
end
|
496
427
|
|
497
428
|
private
|
429
|
+
|
498
430
|
def authenticate!
|
499
431
|
halt 401 unless authenticated?
|
500
432
|
end
|
501
433
|
end
|
502
434
|
|
503
|
-
action = Show.new
|
435
|
+
action = Show.new(configuration: configuration)
|
504
436
|
action.call({}) # => [401, {}, ["Unauthorized"]]
|
505
437
|
```
|
506
438
|
|
507
439
|
Alternatively, you can specify a custom message.
|
508
440
|
|
509
441
|
```ruby
|
510
|
-
class Show
|
511
|
-
|
512
|
-
|
513
|
-
def call(params)
|
514
|
-
DroidRepository.new.find(params[:id]) or not_found
|
442
|
+
class Show < Hanami::Action
|
443
|
+
def handle(req, res)
|
444
|
+
res[:droid] = DroidRepository.new.find(req.params[:id]) or not_found
|
515
445
|
end
|
516
446
|
|
517
447
|
private
|
448
|
+
|
518
449
|
def not_found
|
519
450
|
halt 404, "This is not the droid you're looking for"
|
520
451
|
end
|
521
452
|
end
|
522
453
|
|
523
|
-
action = Show.new
|
454
|
+
action = Show.new(configuration: configuration)
|
524
455
|
action.call({}) # => [404, {}, ["This is not the droid you're looking for"]]
|
525
456
|
```
|
526
457
|
|
527
458
|
### Cookies
|
528
459
|
|
529
|
-
|
460
|
+
You can read the original cookies sent from the HTTP client via `req.cookies`.
|
461
|
+
If you want to send cookies in the response, use `res.cookies`.
|
530
462
|
|
531
463
|
They are read as a Hash from Rack env:
|
532
464
|
|
533
465
|
```ruby
|
534
|
-
require
|
535
|
-
require
|
466
|
+
require "hanami/controller"
|
467
|
+
require "hanami/action/cookies"
|
536
468
|
|
537
|
-
class ReadCookiesFromRackEnv
|
538
|
-
include Hanami::Action
|
469
|
+
class ReadCookiesFromRackEnv < Hanami::Action
|
539
470
|
include Hanami::Action::Cookies
|
540
471
|
|
541
|
-
def
|
472
|
+
def handle(req, *)
|
542
473
|
# ...
|
543
|
-
cookies[:foo] # =>
|
474
|
+
req.cookies[:foo] # => "bar"
|
544
475
|
end
|
545
476
|
end
|
546
477
|
|
547
|
-
action = ReadCookiesFromRackEnv.new
|
548
|
-
action.call({
|
478
|
+
action = ReadCookiesFromRackEnv.new(configuration: configuration)
|
479
|
+
action.call({"HTTP_COOKIE" => "foo=bar"})
|
549
480
|
```
|
550
481
|
|
551
482
|
They are set like a Hash:
|
552
483
|
|
553
484
|
```ruby
|
554
|
-
require
|
555
|
-
require
|
485
|
+
require "hanami/controller"
|
486
|
+
require "hanami/action/cookies"
|
556
487
|
|
557
|
-
class SetCookies
|
558
|
-
include Hanami::Action
|
488
|
+
class SetCookies < Hanami::Action
|
559
489
|
include Hanami::Action::Cookies
|
560
490
|
|
561
|
-
def
|
491
|
+
def handle(*, res)
|
562
492
|
# ...
|
563
|
-
cookies[:foo] =
|
493
|
+
res.cookies[:foo] = "bar"
|
564
494
|
end
|
565
495
|
end
|
566
496
|
|
567
|
-
action = SetCookies.new
|
568
|
-
action.call({}) # => [200, {
|
497
|
+
action = SetCookies.new(configuration: configuration)
|
498
|
+
action.call({}) # => [200, {"Set-Cookie" => "foo=bar"}, "..."]
|
569
499
|
```
|
570
500
|
|
571
501
|
They are removed by setting their value to `nil`:
|
572
502
|
|
573
503
|
```ruby
|
574
|
-
require
|
575
|
-
require
|
504
|
+
require "hanami/controller"
|
505
|
+
require "hanami/action/cookies"
|
576
506
|
|
577
|
-
class RemoveCookies
|
578
|
-
include Hanami::Action
|
507
|
+
class RemoveCookies < Hanami::Action
|
579
508
|
include Hanami::Action::Cookies
|
580
509
|
|
581
|
-
def
|
510
|
+
def handle(*, res)
|
582
511
|
# ...
|
583
|
-
cookies[:foo] = nil
|
512
|
+
res.cookies[:foo] = nil
|
584
513
|
end
|
585
514
|
end
|
586
515
|
|
587
|
-
action = RemoveCookies.new
|
588
|
-
action.call({}) # => [200, {
|
516
|
+
action = RemoveCookies.new(configuration: configuration)
|
517
|
+
action.call({}) # => [200, {"Set-Cookie" => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"}, "..."]
|
589
518
|
```
|
590
519
|
|
591
520
|
Default values can be set in configuration, but overridden case by case.
|
592
521
|
|
593
522
|
```ruby
|
594
|
-
require
|
595
|
-
require
|
523
|
+
require "hanami/controller"
|
524
|
+
require "hanami/action/cookies"
|
596
525
|
|
597
|
-
Hanami::Controller.
|
598
|
-
cookies
|
526
|
+
configuration = Hanami::Controller::Configuration.new do |config|
|
527
|
+
config.cookies(max_age: 300) # 5 minutes
|
599
528
|
end
|
600
529
|
|
601
|
-
class SetCookies
|
602
|
-
include Hanami::Action
|
530
|
+
class SetCookies < Hanami::Action
|
603
531
|
include Hanami::Action::Cookies
|
604
532
|
|
605
|
-
def
|
533
|
+
def handle(*, res)
|
606
534
|
# ...
|
607
|
-
cookies[:foo] = { value:
|
535
|
+
res.cookies[:foo] = { value: "bar", max_age: 100 }
|
608
536
|
end
|
609
537
|
end
|
610
538
|
|
611
|
-
action = SetCookies.new
|
612
|
-
action.call({}) # => [200, {
|
539
|
+
action = SetCookies.new(configuration: configuration)
|
540
|
+
action.call({}) # => [200, {"Set-Cookie" => "foo=bar; max-age=100;"}, "..."]
|
613
541
|
```
|
614
542
|
|
615
543
|
### Sessions
|
616
544
|
|
617
|
-
|
545
|
+
Actions have builtin support for Rack sessions.
|
546
|
+
Similarly to cookies, you can read the session sent by the HTTP client via
|
547
|
+
`req.session`, and also manipulate it via `res.ression`.
|
618
548
|
|
619
549
|
```ruby
|
620
|
-
require
|
621
|
-
require
|
550
|
+
require "hanami/controller"
|
551
|
+
require "hanami/action/session"
|
622
552
|
|
623
|
-
class ReadSessionFromRackEnv
|
624
|
-
include Hanami::Action
|
553
|
+
class ReadSessionFromRackEnv < Hanami::Action
|
625
554
|
include Hanami::Action::Session
|
626
555
|
|
627
|
-
def
|
556
|
+
def handle(req, *)
|
628
557
|
# ...
|
629
|
-
session[:age] # =>
|
558
|
+
req.session[:age] # => "35"
|
630
559
|
end
|
631
560
|
end
|
632
561
|
|
633
|
-
action = ReadSessionFromRackEnv.new
|
634
|
-
action.call({
|
562
|
+
action = ReadSessionFromRackEnv.new(configuration: configuration)
|
563
|
+
action.call({ "rack.session" => { "age" => "35" } })
|
635
564
|
```
|
636
565
|
|
637
566
|
Values can be set like a Hash:
|
638
567
|
|
639
568
|
```ruby
|
640
|
-
require
|
641
|
-
require
|
569
|
+
require "hanami/controller"
|
570
|
+
require "hanami/action/session"
|
642
571
|
|
643
|
-
class SetSession
|
644
|
-
include Hanami::Action
|
572
|
+
class SetSession < Hanami::Action
|
645
573
|
include Hanami::Action::Session
|
646
574
|
|
647
|
-
def
|
575
|
+
def handle(*, res)
|
648
576
|
# ...
|
649
|
-
session[:age] = 31
|
577
|
+
res.session[:age] = 31
|
650
578
|
end
|
651
579
|
end
|
652
580
|
|
653
|
-
action = SetSession.new
|
581
|
+
action = SetSession.new(configuration: configuration)
|
654
582
|
action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."]
|
655
583
|
```
|
656
584
|
|
657
585
|
Values can be removed like a Hash:
|
658
586
|
|
659
587
|
```ruby
|
660
|
-
require
|
661
|
-
require
|
588
|
+
require "hanami/controller"
|
589
|
+
require "hanami/action/session"
|
662
590
|
|
663
|
-
class RemoveSession
|
664
|
-
include Hanami::Action
|
591
|
+
class RemoveSession < Hanami::Action
|
665
592
|
include Hanami::Action::Session
|
666
593
|
|
667
|
-
def
|
594
|
+
def handle(*, res)
|
668
595
|
# ...
|
669
|
-
session[:age] = nil
|
596
|
+
res.session[:age] = nil
|
670
597
|
end
|
671
598
|
end
|
672
599
|
|
673
|
-
action = RemoveSession.new
|
600
|
+
action = RemoveSession.new(configuration: configuration)
|
674
601
|
action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."] it removes that value from the session
|
675
602
|
```
|
676
603
|
|
677
|
-
While Hanami::Controller supports sessions natively, it's
|
604
|
+
While Hanami::Controller supports sessions natively, it's **session store agnostic**.
|
678
605
|
You have to specify the session store in your Rack middleware configuration (eg `config.ru`).
|
679
606
|
|
680
607
|
```ruby
|
681
608
|
use Rack::Session::Cookie, secret: SecureRandom.hex(64)
|
682
|
-
run Show.new
|
609
|
+
run Show.new(configuration: configuration)
|
683
610
|
```
|
684
611
|
|
685
|
-
###
|
612
|
+
### HTTP Cache
|
686
613
|
|
687
614
|
Hanami::Controller sets your headers correctly according to RFC 2616 / 14.9 for more on standard cache control directives: http://tools.ietf.org/html/rfc2616#section-14.9.1
|
688
615
|
|
689
616
|
You can easily set the Cache-Control header for your actions:
|
690
617
|
|
691
618
|
```ruby
|
692
|
-
require
|
693
|
-
require
|
619
|
+
require "hanami/controller"
|
620
|
+
require "hanami/action/cache"
|
694
621
|
|
695
|
-
class HttpCacheController
|
696
|
-
include Hanami::Action
|
622
|
+
class HttpCacheController < Hanami::Action
|
697
623
|
include Hanami::Action::Cache
|
698
|
-
|
699
624
|
cache_control :public, max_age: 600 # => Cache-Control: public, max-age=600
|
700
625
|
|
701
|
-
def
|
626
|
+
def handle(*)
|
702
627
|
# ...
|
703
628
|
end
|
704
629
|
end
|
@@ -707,16 +632,14 @@ end
|
|
707
632
|
Expires header can be specified using `expires` method:
|
708
633
|
|
709
634
|
```ruby
|
710
|
-
require
|
711
|
-
require
|
635
|
+
require "hanami/controller"
|
636
|
+
require "hanami/action/cache"
|
712
637
|
|
713
|
-
class HttpCacheController
|
714
|
-
include Hanami::Action
|
638
|
+
class HttpCacheController < Hanami::Action
|
715
639
|
include Hanami::Action::Cache
|
716
|
-
|
717
640
|
expires 60, :public, max_age: 600 # => Expires: Sun, 03 Aug 2014 17:47:02 GMT, Cache-Control: public, max-age=600
|
718
641
|
|
719
|
-
def
|
642
|
+
def handle(*)
|
720
643
|
# ...
|
721
644
|
end
|
722
645
|
end
|
@@ -724,82 +647,76 @@ end
|
|
724
647
|
|
725
648
|
### Conditional Get
|
726
649
|
|
727
|
-
According to HTTP specification, conditional GETs provide a way for web servers to inform clients that the response to a GET request hasn't change since the last request returning a Not Modified
|
650
|
+
According to HTTP specification, conditional GETs provide a way for web servers to inform clients that the response to a GET request hasn't change since the last request returning a `304 (Not Modified)` response.
|
728
651
|
|
729
|
-
Passing the HTTP_IF_NONE_MATCH (content identifier) or HTTP_IF_MODIFIED_SINCE (timestamp) headers allows the web server define if the client has a fresh version of a given resource.
|
652
|
+
Passing the `HTTP_IF_NONE_MATCH` (content identifier) or `HTTP_IF_MODIFIED_SINCE` (timestamp) headers allows the web server define if the client has a fresh version of a given resource.
|
730
653
|
|
731
654
|
You can easily take advantage of Conditional Get using `#fresh` method:
|
732
655
|
|
733
656
|
```ruby
|
734
|
-
require
|
735
|
-
require
|
657
|
+
require "hanami/controller"
|
658
|
+
require "hanami/action/cache"
|
736
659
|
|
737
|
-
class ConditionalGetController
|
738
|
-
include Hanami::Action
|
660
|
+
class ConditionalGetController < Hanami::Action
|
739
661
|
include Hanami::Action::Cache
|
740
662
|
|
741
|
-
def
|
663
|
+
def handle(*)
|
742
664
|
# ...
|
743
|
-
fresh etag:
|
744
|
-
# => halt 304 with header IfNoneMatch =
|
665
|
+
fresh etag: resource.cache_key
|
666
|
+
# => halt 304 with header IfNoneMatch = resource.cache_key
|
745
667
|
end
|
746
668
|
end
|
747
669
|
```
|
748
670
|
|
749
|
-
If
|
671
|
+
If `resource.cache_key` is equal to `IfNoneMatch` header, then hanami will `halt 304`.
|
750
672
|
|
751
|
-
|
673
|
+
An alterative to hashing based check, is the time based check:
|
752
674
|
|
753
675
|
```ruby
|
754
|
-
require
|
755
|
-
require
|
676
|
+
require "hanami/controller"
|
677
|
+
require "hanami/action/cache"
|
756
678
|
|
757
|
-
class ConditionalGetController
|
758
|
-
include Hanami::Action
|
679
|
+
class ConditionalGetController < Hanami::Action
|
759
680
|
include Hanami::Action::Cache
|
760
681
|
|
761
|
-
def
|
682
|
+
def handle(*)
|
762
683
|
# ...
|
763
|
-
fresh last_modified:
|
764
|
-
# => halt 304 with header IfModifiedSince =
|
684
|
+
fresh last_modified: resource.update_at
|
685
|
+
# => halt 304 with header IfModifiedSince = resource.update_at.httpdate
|
765
686
|
end
|
766
687
|
end
|
767
688
|
```
|
768
689
|
|
769
|
-
If
|
690
|
+
If `resource.update_at` is equal to `IfModifiedSince` header, then hanami will `halt 304`.
|
770
691
|
|
771
692
|
### Redirect
|
772
693
|
|
773
|
-
If you need to redirect the client to another resource, use
|
694
|
+
If you need to redirect the client to another resource, use `res.redirect_to`:
|
774
695
|
|
775
696
|
```ruby
|
776
|
-
class Create
|
777
|
-
|
778
|
-
|
779
|
-
def call(params)
|
697
|
+
class Create < Hanami::Action
|
698
|
+
def handle(*, res)
|
780
699
|
# ...
|
781
|
-
redirect_to
|
700
|
+
res.redirect_to "http://example.com/articles/23"
|
782
701
|
end
|
783
702
|
end
|
784
703
|
|
785
|
-
action = Create.new
|
786
|
-
action.call({ article: { title:
|
704
|
+
action = Create.new(configuration: configuration)
|
705
|
+
action.call({ article: { title: "Hello" }}) # => [302, {"Location" => "/articles/23"}, ""]
|
787
706
|
```
|
788
707
|
|
789
708
|
You can also redirect with a custom status code:
|
790
709
|
|
791
710
|
```ruby
|
792
|
-
class Create
|
793
|
-
|
794
|
-
|
795
|
-
def call(params)
|
711
|
+
class Create < Hanami::Action
|
712
|
+
def handle(*, res)
|
796
713
|
# ...
|
797
|
-
redirect_to
|
714
|
+
res.redirect_to "http://example.com/articles/23", status: 301
|
798
715
|
end
|
799
716
|
end
|
800
717
|
|
801
|
-
action = Create.new
|
802
|
-
action.call({ article: { title:
|
718
|
+
action = Create.new(configuration: configuration)
|
719
|
+
action.call({ article: { title: "Hello" }}) # => [301, {"Location" => "/articles/23"}, ""]
|
803
720
|
```
|
804
721
|
|
805
722
|
### MIME Types
|
@@ -807,51 +724,46 @@ action.call({ article: { title: 'Hello' }}) # => [301, {'Location' => '/articles
|
|
807
724
|
`Hanami::Action` automatically sets the `Content-Type` header, according to the request.
|
808
725
|
|
809
726
|
```ruby
|
810
|
-
class Show
|
811
|
-
|
812
|
-
|
813
|
-
def call(params)
|
727
|
+
class Show < Hanami::Action
|
728
|
+
def handle(*)
|
814
729
|
end
|
815
730
|
end
|
816
731
|
|
817
|
-
action = Show.new
|
732
|
+
action = Show.new(configuration: configuration)
|
818
733
|
|
819
|
-
action.call({
|
820
|
-
|
734
|
+
response = action.call({ "HTTP_ACCEPT" => "*/*" }) # Content-Type "application/octet-stream"
|
735
|
+
response.format # :all
|
821
736
|
|
822
|
-
action.call({
|
823
|
-
|
737
|
+
response = action.call({ "HTTP_ACCEPT" => "text/html" }) # Content-Type "text/html"
|
738
|
+
response.format # :html
|
824
739
|
```
|
825
740
|
|
826
741
|
However, you can force this value:
|
827
742
|
|
828
743
|
```ruby
|
829
|
-
class Show
|
830
|
-
|
831
|
-
|
832
|
-
def call(params)
|
744
|
+
class Show < Hanami::Action
|
745
|
+
def handle(*, res)
|
833
746
|
# ...
|
834
|
-
|
747
|
+
res.format = format(:json)
|
835
748
|
end
|
836
749
|
end
|
837
750
|
|
838
|
-
action = Show.new
|
751
|
+
action = Show.new(configuration: configuration)
|
839
752
|
|
840
|
-
action.call({
|
841
|
-
|
753
|
+
response = action.call({ "HTTP_ACCEPT" => "*/*" }) # Content-Type "application/json"
|
754
|
+
response.format # :json
|
842
755
|
|
843
|
-
action.call({
|
844
|
-
|
756
|
+
response = action.call({ "HTTP_ACCEPT" => "text/html" }) # Content-Type "application/json"
|
757
|
+
response.format # :json
|
845
758
|
```
|
846
759
|
|
847
760
|
You can restrict the accepted MIME types:
|
848
761
|
|
849
762
|
```ruby
|
850
|
-
class Show
|
851
|
-
include Hanami::Action
|
763
|
+
class Show < Hanami::Action
|
852
764
|
accept :html, :json
|
853
765
|
|
854
|
-
def
|
766
|
+
def handle(*)
|
855
767
|
# ...
|
856
768
|
end
|
857
769
|
end
|
@@ -865,26 +777,24 @@ end
|
|
865
777
|
You can check if the requested MIME type is accepted by the client.
|
866
778
|
|
867
779
|
```ruby
|
868
|
-
class Show
|
869
|
-
|
870
|
-
|
871
|
-
def call(params)
|
780
|
+
class Show < Hanami::Action
|
781
|
+
def handle(req, res)
|
872
782
|
# ...
|
873
|
-
# @_env[
|
783
|
+
# @_env["HTTP_ACCEPT"] # => "text/html,application/xhtml+xml,application/xml;q=0.9"
|
874
784
|
|
875
|
-
accept?(
|
876
|
-
accept?(
|
877
|
-
accept?(
|
878
|
-
|
785
|
+
req.accept?("text/html") # => true
|
786
|
+
req.accept?("application/xml") # => true
|
787
|
+
req.accept?("application/json") # => false
|
788
|
+
res.format # :html
|
879
789
|
|
880
790
|
|
881
791
|
|
882
|
-
# @_env[
|
792
|
+
# @_env["HTTP_ACCEPT"] # => "*/*"
|
883
793
|
|
884
|
-
accept?(
|
885
|
-
accept?(
|
886
|
-
accept?(
|
887
|
-
|
794
|
+
req.accept?("text/html") # => true
|
795
|
+
req.accept?("application/xml") # => true
|
796
|
+
req.accept?("application/json") # => true
|
797
|
+
res.format # :html
|
888
798
|
end
|
889
799
|
end
|
890
800
|
```
|
@@ -893,35 +803,31 @@ Hanami::Controller is shipped with an extensive list of the most common MIME typ
|
|
893
803
|
Also, you can register your own:
|
894
804
|
|
895
805
|
```ruby
|
896
|
-
Hanami::Controller.
|
897
|
-
format custom:
|
806
|
+
configuration = Hanami::Controller::Configuration.new do |config|
|
807
|
+
config.format custom: "application/custom"
|
898
808
|
end
|
899
809
|
|
900
|
-
class Index
|
901
|
-
|
902
|
-
|
903
|
-
def call(params)
|
810
|
+
class Index < Hanami::Action
|
811
|
+
def handle(*)
|
904
812
|
end
|
905
813
|
end
|
906
814
|
|
907
|
-
action = Index.new
|
815
|
+
action = Index.new(configuration: configuration)
|
908
816
|
|
909
|
-
action.call({
|
910
|
-
|
817
|
+
response = action.call({ "HTTP_ACCEPT" => "application/custom" }) # => Content-Type "application/custom"
|
818
|
+
response.format # => :custom
|
911
819
|
|
912
|
-
class Show
|
913
|
-
|
914
|
-
|
915
|
-
def call(params)
|
820
|
+
class Show < Hanami::Action
|
821
|
+
def handle(*, res)
|
916
822
|
# ...
|
917
|
-
|
823
|
+
res.format = format(:custom)
|
918
824
|
end
|
919
825
|
end
|
920
826
|
|
921
|
-
action = Show.new
|
827
|
+
action = Show.new(configuration: configuration)
|
922
828
|
|
923
|
-
action.call({
|
924
|
-
|
829
|
+
response = action.call({ "HTTP_ACCEPT" => "*/*" }) # => Content-Type "application/custom"
|
830
|
+
response.format # => :custom
|
925
831
|
```
|
926
832
|
|
927
833
|
### Streamed Responses
|
@@ -929,17 +835,14 @@ action.format # => :custom
|
|
929
835
|
When the work to be done by the server takes time, it may be a good idea to stream your response. Here's an example of a streamed CSV.
|
930
836
|
|
931
837
|
```ruby
|
932
|
-
Hanami::Controller.
|
933
|
-
format csv: 'text/csv'
|
934
|
-
middleware.use ::Rack::Chunked
|
838
|
+
configuration = Hanami::Controller::Configuration.new do |config|
|
839
|
+
config.format csv: 'text/csv'
|
935
840
|
end
|
936
841
|
|
937
|
-
class Csv
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
self.format = :csv
|
942
|
-
self.body = Enumerator.new do |yielder|
|
842
|
+
class Csv < Hanami::Action
|
843
|
+
def handle(*, res)
|
844
|
+
res.format = format(:csv)
|
845
|
+
res.body = Enumerator.new do |yielder|
|
943
846
|
yielder << csv_header
|
944
847
|
|
945
848
|
# Expensive operation is streamed as each line becomes available
|
@@ -966,230 +869,89 @@ A Controller is nothing more than a logical group of actions: just a Ruby module
|
|
966
869
|
|
967
870
|
```ruby
|
968
871
|
module Articles
|
969
|
-
class Index
|
970
|
-
include Hanami::Action
|
971
|
-
|
872
|
+
class Index < Hanami::Action
|
972
873
|
# ...
|
973
874
|
end
|
974
875
|
|
975
|
-
class Show
|
976
|
-
include Hanami::Action
|
977
|
-
|
876
|
+
class Show < Hanami::Action
|
978
877
|
# ...
|
979
878
|
end
|
980
879
|
end
|
981
880
|
|
982
|
-
Articles::Index.new.call({})
|
881
|
+
Articles::Index.new(configuration: configuration).call({})
|
983
882
|
```
|
984
883
|
|
985
884
|
### Hanami::Router integration
|
986
885
|
|
987
|
-
While Hanami::Router works great with this framework, Hanami::Controller doesn't depend on it.
|
988
|
-
You, the developer, are free to choose your own routing system.
|
989
|
-
|
990
|
-
But, if you use them together, the **only constraint is that an action must support _arity 0_ in its constructor**.
|
991
|
-
The following examples are valid constructors:
|
992
|
-
|
993
886
|
```ruby
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
def initialize(repository = ArticleRepository.new)
|
998
|
-
end
|
999
|
-
|
1000
|
-
def initialize(repository: ArticleRepository.new)
|
1001
|
-
end
|
887
|
+
require "hanami/router"
|
888
|
+
require "hanami/controller"
|
1002
889
|
|
1003
|
-
|
890
|
+
module Web
|
891
|
+
module Controllers
|
892
|
+
module Books
|
893
|
+
class Show < Hanami::Action
|
894
|
+
def handle(*)
|
895
|
+
end
|
896
|
+
end
|
897
|
+
end
|
898
|
+
end
|
1004
899
|
end
|
1005
900
|
|
1006
|
-
|
901
|
+
configuration = Hanami::Controller::Configuration.new
|
902
|
+
router = Hanami::Router.new(configuration: configuration, namespace: Web::Controllers) do
|
903
|
+
get "/books/:id", "books#show"
|
1007
904
|
end
|
1008
905
|
```
|
1009
906
|
|
1010
|
-
__Please note that this is subject to change: we're working to remove this constraint.__
|
1011
|
-
|
1012
|
-
Hanami::Router supports lazy loading for controllers. While this policy can be a
|
1013
|
-
convenient fallback, you should know that it's the slower option. **Be sure of
|
1014
|
-
loading your controllers before you initialize the router.**
|
1015
|
-
|
1016
|
-
|
1017
907
|
### Rack integration
|
1018
908
|
|
1019
|
-
Hanami::Controller is compatible with Rack.
|
1020
|
-
While a Hanami application's architecture is more web oriented, this framework is designed to build pure HTTP endpoints.
|
1021
|
-
|
1022
|
-
### Rack middleware
|
1023
|
-
|
1024
|
-
Rack middleware can be configured globally in `config.ru`. However, consider that they often add
|
1025
|
-
unnecessary overhead for *all* endpoints that aren't direct users of all the configured middleware.
|
1026
|
-
|
1027
|
-
Think about a middleware to create sessions, where only `SessionsController::Create` needs that middleware, but every other action pays the performance price for that middleware.
|
1028
|
-
|
1029
|
-
The solution is that an action can employ one or more Rack middleware, with `.use`.
|
1030
|
-
|
1031
|
-
```ruby
|
1032
|
-
require 'hanami/controller'
|
1033
|
-
|
1034
|
-
module Sessions
|
1035
|
-
class Create
|
1036
|
-
include Hanami::Action
|
1037
|
-
use OmniAuth
|
1038
|
-
|
1039
|
-
def call(params)
|
1040
|
-
# ...
|
1041
|
-
end
|
1042
|
-
end
|
1043
|
-
end
|
1044
|
-
```
|
1045
|
-
|
1046
|
-
```ruby
|
1047
|
-
require 'hanami/controller'
|
1048
|
-
|
1049
|
-
module Sessions
|
1050
|
-
class Create
|
1051
|
-
include Hanami::Controller
|
1052
|
-
|
1053
|
-
use XMiddleware.new('x', 123)
|
1054
|
-
use YMiddleware.new
|
1055
|
-
use ZMiddleware
|
1056
|
-
|
1057
|
-
def call(params)
|
1058
|
-
# ...
|
1059
|
-
end
|
1060
|
-
end
|
1061
|
-
end
|
1062
|
-
```
|
909
|
+
Hanami::Controller is compatible with Rack. If you need to use any Rack middleware, please mount them in `config.ru`.
|
1063
910
|
|
1064
911
|
### Configuration
|
1065
912
|
|
1066
|
-
Hanami::Controller can be configured
|
913
|
+
Hanami::Controller can be configured via `Hanami::Controller::Configuration`.
|
1067
914
|
It supports a few options:
|
1068
915
|
|
1069
916
|
```ruby
|
1070
|
-
require
|
1071
|
-
|
1072
|
-
Hanami::Controller.configure do
|
1073
|
-
# Handle exceptions with HTTP statuses (true) or don't catch them (false)
|
1074
|
-
# Argument: boolean, defaults to `true`
|
1075
|
-
#
|
1076
|
-
handle_exceptions true
|
917
|
+
require "hanami/controller"
|
1077
918
|
|
919
|
+
configuration = Hanami::Controller::Configuration.new do |config|
|
1078
920
|
# If the given exception is raised, return that HTTP status
|
1079
921
|
# It can be used multiple times
|
1080
922
|
# Argument: hash, empty by default
|
1081
923
|
#
|
1082
|
-
handle_exception ArgumentError => 404
|
924
|
+
config.handle_exception ArgumentError => 404
|
1083
925
|
|
1084
926
|
# Register a format to MIME type mapping
|
1085
927
|
# Argument: hash, key: format symbol, value: MIME type string, empty by default
|
1086
928
|
#
|
1087
|
-
format custom:
|
929
|
+
config.format custom: "application/custom"
|
1088
930
|
|
1089
931
|
# Define a fallback format to detect in case of HTTP request with `Accept: */*`
|
1090
932
|
# If not defined here, it will return Rack's default: `application/octet-stream`
|
1091
933
|
# Argument: symbol, it should be already known. defaults to `nil`
|
1092
934
|
#
|
1093
|
-
default_request_format :html
|
935
|
+
config.default_request_format = :html
|
1094
936
|
|
1095
937
|
# Define a default format to set as `Content-Type` header for response,
|
1096
938
|
# unless otherwise specified.
|
1097
939
|
# If not defined here, it will return Rack's default: `application/octet-stream`
|
1098
940
|
# Argument: symbol, it should be already known. defaults to `nil`
|
1099
941
|
#
|
1100
|
-
default_response_format :html
|
942
|
+
config.default_response_format = :html
|
1101
943
|
|
1102
944
|
# Define a default charset to return in the `Content-Type` response header
|
1103
945
|
# If not defined here, it returns `utf-8`
|
1104
946
|
# Argument: string, defaults to `nil`
|
1105
947
|
#
|
1106
|
-
default_charset
|
1107
|
-
|
1108
|
-
# Configure the logic to be executed when Hanami::Action is included
|
1109
|
-
# This is useful to DRY code by having a single place where to configure
|
1110
|
-
# shared behaviors like authentication, sessions, cookies etc.
|
1111
|
-
# Argument: proc
|
1112
|
-
#
|
1113
|
-
prepare do
|
1114
|
-
include Hanami::Action::Sessions
|
1115
|
-
include MyAuthentication
|
1116
|
-
use SomeMiddleWare
|
1117
|
-
|
1118
|
-
before { authenticate! }
|
1119
|
-
end
|
948
|
+
config.default_charset = "koi8-r"
|
1120
949
|
end
|
1121
950
|
```
|
1122
951
|
|
1123
|
-
All of the global configurations can be overwritten at the controller level.
|
1124
|
-
Each controller and action has its own copy of the global configuration.
|
1125
|
-
|
1126
|
-
This means changes are inherited from the top to the bottom, but do not bubble back up.
|
1127
|
-
|
1128
|
-
```ruby
|
1129
|
-
require 'hanami/controller'
|
1130
|
-
|
1131
|
-
Hanami::Controller.configure do
|
1132
|
-
handle_exception ArgumentError => 400
|
1133
|
-
end
|
1134
|
-
|
1135
|
-
module Articles
|
1136
|
-
class Create
|
1137
|
-
include Hanami::Action
|
1138
|
-
|
1139
|
-
configure do
|
1140
|
-
handle_exceptions false
|
1141
|
-
end
|
1142
|
-
|
1143
|
-
def call(params)
|
1144
|
-
raise ArgumentError
|
1145
|
-
end
|
1146
|
-
end
|
1147
|
-
end
|
1148
|
-
|
1149
|
-
module Users
|
1150
|
-
class Create
|
1151
|
-
include Hanami::Action
|
1152
|
-
|
1153
|
-
def call(params)
|
1154
|
-
raise ArgumentError
|
1155
|
-
end
|
1156
|
-
end
|
1157
|
-
end
|
1158
|
-
|
1159
|
-
Users::Create.new.call({}) # => HTTP 400
|
1160
|
-
|
1161
|
-
Articles::Create.new.call({})
|
1162
|
-
# => raises ArgumentError because we set handle_exceptions to false
|
1163
|
-
```
|
1164
|
-
|
1165
952
|
### Thread safety
|
1166
953
|
|
1167
|
-
An Action is **
|
1168
|
-
action for each request. The same advice applies when using
|
1169
|
-
Hanami::Router but NOT routing to `mycontroller#myaction` but instead
|
1170
|
-
routing direct to a class.
|
1171
|
-
|
1172
|
-
```ruby
|
1173
|
-
# config.ru
|
1174
|
-
require 'hanami/controller'
|
1175
|
-
|
1176
|
-
class Action
|
1177
|
-
include Hanami::Action
|
1178
|
-
|
1179
|
-
def self.call(env)
|
1180
|
-
new.call(env)
|
1181
|
-
end
|
1182
|
-
|
1183
|
-
def call(params)
|
1184
|
-
self.body = object_id.to_s
|
1185
|
-
end
|
1186
|
-
end
|
1187
|
-
|
1188
|
-
run Action
|
1189
|
-
```
|
1190
|
-
|
1191
|
-
Hanami::Controller heavely depends on class configuration. To ensure immutability
|
1192
|
-
in deployment environments, use `Hanami::Controller.load!`.
|
954
|
+
An Action is **immutable**, it works without global state, so it's thread-safe by design.
|
1193
955
|
|
1194
956
|
## Versioning
|
1195
957
|
|
@@ -1205,6 +967,6 @@ __Hanami::Controller__ uses [Semantic Versioning 2.0.0](http://semver.org)
|
|
1205
967
|
|
1206
968
|
## Copyright
|
1207
969
|
|
1208
|
-
Copyright © 2014-
|
970
|
+
Copyright © 2014-2021 Luca Guidi – Released under MIT License
|
1209
971
|
|
1210
972
|
This project was formerly known as Lotus (`lotus-controller`).
|