hanami-action 3.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +985 -0
  3. data/LICENSE +20 -0
  4. data/README.md +873 -0
  5. data/hanami-action.gemspec +39 -0
  6. data/lib/hanami/action/body_parser/json.rb +20 -0
  7. data/lib/hanami/action/body_parser/multipart_form.rb +22 -0
  8. data/lib/hanami/action/body_parser.rb +109 -0
  9. data/lib/hanami/action/cache/cache_control.rb +84 -0
  10. data/lib/hanami/action/cache/conditional_get.rb +101 -0
  11. data/lib/hanami/action/cache/directives.rb +126 -0
  12. data/lib/hanami/action/cache/expires.rb +84 -0
  13. data/lib/hanami/action/cache.rb +29 -0
  14. data/lib/hanami/action/config/formats.rb +256 -0
  15. data/lib/hanami/action/config.rb +172 -0
  16. data/lib/hanami/action/constants.rb +283 -0
  17. data/lib/hanami/action/cookie_jar.rb +214 -0
  18. data/lib/hanami/action/cookies.rb +27 -0
  19. data/lib/hanami/action/csrf_protection.rb +217 -0
  20. data/lib/hanami/action/errors.rb +109 -0
  21. data/lib/hanami/action/flash.rb +176 -0
  22. data/lib/hanami/action/halt.rb +18 -0
  23. data/lib/hanami/action/mime/request_mime_weight.rb +66 -0
  24. data/lib/hanami/action/mime.rb +438 -0
  25. data/lib/hanami/action/params.rb +342 -0
  26. data/lib/hanami/action/rack/file.rb +41 -0
  27. data/lib/hanami/action/rack_utils.rb +11 -0
  28. data/lib/hanami/action/request/session.rb +68 -0
  29. data/lib/hanami/action/request.rb +141 -0
  30. data/lib/hanami/action/response.rb +481 -0
  31. data/lib/hanami/action/session.rb +47 -0
  32. data/lib/hanami/action/validatable.rb +166 -0
  33. data/lib/hanami/action/version.rb +13 -0
  34. data/lib/hanami/action/view_name_inferrer.rb +56 -0
  35. data/lib/hanami/action.rb +672 -0
  36. data/lib/hanami/http/status.rb +149 -0
  37. data/lib/hanami-action.rb +3 -0
  38. metadata +153 -0
data/README.md ADDED
@@ -0,0 +1,873 @@
1
+ <!--- This file is synced from hanakai-rb/repo-sync -->
2
+
3
+ [actions]: https://github.com/hanami/hanami-action/actions
4
+ [chat]: https://discord.gg/naQApPAsZB
5
+ [forum]: https://discourse.hanamirb.org
6
+ [rubygem]: https://rubygems.org/gems/hanami-action
7
+
8
+ # Hanami Action [![Gem Version](https://badge.fury.io/rb/hanami-action.svg)][rubygem] [![CI Status](https://github.com/hanami/hanami-action/workflows/CI/badge.svg)][actions]
9
+
10
+ [![Forum](https://img.shields.io/badge/Forum-dc360f?logo=discourse&logoColor=white)][forum]
11
+ [![Chat](https://img.shields.io/badge/Chat-717cf8?logo=discord&logoColor=white)][chat]
12
+
13
+ > [!NOTE]
14
+ > **Hanami Controller has been renamed to Hanami Action.**
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem "hanami-action"
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```shell
27
+ $ bundle
28
+ ```
29
+
30
+ Or install it yourself as:
31
+
32
+ ```shell
33
+ $ gem install hanami-action
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ Hanami Action is a micro library for web frameworks. It works beautifully with [Hanami Router](https://github.com/hanami/router), but it can be employed everywhere. It's designed to be fast and testable.
39
+
40
+ ### Actions
41
+
42
+ The core of this framework are the actions.
43
+ They are the endpoints that respond to incoming HTTP requests.
44
+
45
+ ```ruby
46
+ class Show < Hanami::Action
47
+ def handle(request, response)
48
+ response[:article] = ArticleRepository.new.find(request.params[:id])
49
+ end
50
+ end
51
+ ```
52
+
53
+ `Hanami::Action` follows the Hanami philosophy: a single purpose object with a minimal interface.
54
+
55
+ In this case, `Hanami::Action` provides the key public interface of `#call(env)`, making your actions Rack-compatible.
56
+ To provide custom behaviour when your actions are being called, you can implement `#handle(request, response)`
57
+
58
+ **An action is an object** and **you have full control over it**.
59
+ In other words, you have the freedom to instantiate, inject dependencies and test it, both at the unit and integration level.
60
+
61
+ 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.
62
+ __We're avoiding HTTP calls__, we're also going to avoid hitting the database (it depends on the stubbed repository), __we're just dealing with message passing__.
63
+ Imagine how **fast** the unit test could be.
64
+
65
+ ```ruby
66
+ class Show < Hanami::Action
67
+ def initialize(configuration:, repository: ArticleRepository.new)
68
+ @repository = repository
69
+ super(configuration: configuration)
70
+ end
71
+
72
+ def handle(request, response)
73
+ response[:article] = repository.find(request.params[:id])
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :repository
79
+ end
80
+
81
+ configuration = Hanami::Action::Configuration.new
82
+ action = Show.new(configuration: configuration, repository: ArticleRepository.new)
83
+ action.call(id: 23)
84
+ ```
85
+
86
+ ### Params
87
+
88
+ The request params are part of the request passed as an argument to the `#handle` method.
89
+ If routed with *Hanami::Router*, it extracts the relevant bits from the Rack `env` (e.g. the requested `:id`).
90
+ Otherwise everything is passed as is: the full Rack `env` in production, and the given `Hash` for unit tests.
91
+
92
+ With `Hanami::Router`:
93
+
94
+ ```ruby
95
+ class Show < Hanami::Action
96
+ def handle(request, *)
97
+ # ...
98
+ puts request.params # => { id: 23 } extracted from Rack env
99
+ end
100
+ end
101
+ ```
102
+
103
+ Standalone:
104
+
105
+ ```ruby
106
+ class Show < Hanami::Action
107
+ def handle(request, *)
108
+ # ...
109
+ puts request.params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
110
+ end
111
+ end
112
+ ```
113
+
114
+ Unit Testing:
115
+
116
+ ```ruby
117
+ class Show < Hanami::Action
118
+ def handle(request, *)
119
+ # ...
120
+ puts request.params # => { id: 23, key: "value" } passed as it is from testing
121
+ end
122
+ end
123
+
124
+ action = Show.new(configuration: configuration)
125
+ response = action.call(id: 23, key: "value")
126
+ ```
127
+
128
+ #### Allowlisting
129
+
130
+ Params represent an untrusted input.
131
+ For security reasons it's recommended to allowlist them.
132
+
133
+ ```ruby
134
+ require "dry/validation"
135
+ require "hanami/action"
136
+
137
+ class Signup < Hanami::Action
138
+ params do
139
+ required(:first_name).filled(:str?)
140
+ required(:last_name).filled(:str?)
141
+ required(:email).filled(:str?)
142
+
143
+ required(:address).schema do
144
+ required(:line_one).filled(:str?)
145
+ required(:state).filled(:str?)
146
+ required(:country).filled(:str?)
147
+ end
148
+ end
149
+
150
+ def handle(request, *)
151
+ # Describe inheritance hierarchy
152
+ puts request.params.class # => Signup::Params
153
+ puts request.params.class.superclass # => Hanami::Action::Params
154
+
155
+ # Allowlist :first_name, but not :admin
156
+ puts request.params[:first_name] # => "Luca"
157
+ puts request.params[:admin] # => nil
158
+
159
+ # Allowlist nested params [:address][:line_one], not [:address][:line_two]
160
+ puts request.params[:address][:line_one] # => "69 Tender St"
161
+ puts request.params[:address][:line_two] # => nil
162
+ end
163
+ end
164
+ ```
165
+
166
+ #### Validations & Coercions
167
+
168
+ Because params are a well defined set of data required to fulfill a feature
169
+ in your application, you can validate them. So you can avoid hitting lower MVC layers
170
+ when params are invalid.
171
+
172
+ If you specify the `:type` option, the param will be coerced.
173
+
174
+ ```ruby
175
+ require "dry/validation"
176
+ require "hanami/action"
177
+
178
+ class Signup < Hanami::Action
179
+ MEGABYTE = 1024 ** 2
180
+
181
+ params do
182
+ required(:first_name).filled(:str?)
183
+ required(:last_name).filled(:str?)
184
+ required(:email).filled?(:str?, format?: /\A.+@.+\z/)
185
+ required(:password).filled(:str?).confirmation
186
+ required(:terms_of_service).filled(:bool?)
187
+ required(:age).filled(:int?, included_in?: 18..99)
188
+ optional(:avatar).filled(size?: 1..(MEGABYTE * 3))
189
+ end
190
+
191
+ def handle(request, *)
192
+ halt 400 unless request.params.valid?
193
+ # ...
194
+ end
195
+ end
196
+ ```
197
+
198
+ ### Response
199
+
200
+ The output of `#call` is a `Hanami::Action::Response`:
201
+
202
+ ```ruby
203
+ class Show < Hanami::Action
204
+ end
205
+
206
+ action = Show.new(configuration: configuration)
207
+ action.call({}) # => #<Hanami::Action::Response:0x00007fe8be968418 @status=200 ...>
208
+ ```
209
+
210
+ This is the same `response` object passed to `#handle`, where you can use its accessors to explicitly set status, headers, and body:
211
+
212
+ ```ruby
213
+ class Show < Hanami::Action
214
+ def handle(*, response)
215
+ response.status = 201
216
+ response.body = "Hi!"
217
+ response.headers.merge!("X-Custom" => "OK")
218
+ end
219
+ end
220
+
221
+ action = Show.new
222
+ action.call({}) # => [201, { "X-Custom" => "OK" }, ["Hi!"]]
223
+ ```
224
+
225
+ ### Exposures
226
+
227
+ In case you need to send data from the action to other layers of your application, you can use exposures.
228
+ By default, an action exposes the received params.
229
+
230
+ ```ruby
231
+ class Show < Hanami::Action
232
+ def handle(request, response)
233
+ response[:article] = ArticleRepository.new.find(request.params[:id])
234
+ end
235
+ end
236
+
237
+ action = Show.new(configuration: configuration)
238
+ response = action.call(id: 23)
239
+
240
+ article = response[:article]
241
+ article.class # => Article
242
+ article.id # => 23
243
+
244
+ response.exposures.keys # => [:params, :article]
245
+ ```
246
+
247
+ ### Callbacks
248
+
249
+ If you need to execute logic **before** or **after** `#handle` is invoked, you can use _callbacks_.
250
+ They are useful for shared logic like authentication checks.
251
+
252
+ ```ruby
253
+ class Show < Hanami::Action
254
+ before :authenticate, :set_article
255
+
256
+ def handle(*)
257
+ end
258
+
259
+ private
260
+
261
+ def authenticate
262
+ # ...
263
+ end
264
+
265
+ # `request` and `response` in the method signature is optional
266
+ def set_article(request, response)
267
+ response[:article] = ArticleRepository.new.find(request.params[:id])
268
+ end
269
+ end
270
+ ```
271
+
272
+ Callbacks can also be expressed as anonymous lambdas:
273
+
274
+ ```ruby
275
+ class Show < Hanami::Action
276
+ before { ... } # do some authentication stuff
277
+ before { |request, response| response[:article] = ArticleRepository.new.find(request.params[:id]) }
278
+
279
+ def handle(*)
280
+ end
281
+ end
282
+ ```
283
+
284
+ ### Exceptions management
285
+
286
+ When the app raises an exception, `hanami-action`, does **NOT** manage it.
287
+ You can write custom exception handling on per action or configuration basis.
288
+
289
+ An exception handler can be a valid HTTP status code (eg. `500`, `401`), or a `Symbol` that represents an action method.
290
+
291
+ ```ruby
292
+ class Show < Hanami::Action
293
+ handle_exception StandardError => 500
294
+
295
+ def handle(*)
296
+ raise
297
+ end
298
+ end
299
+
300
+ action = Show.new(configuration: configuration)
301
+ action.call({}) # => [500, {}, ["Internal Server Error"]]
302
+ ```
303
+
304
+ You can map a specific raised exception to a different HTTP status.
305
+
306
+ ```ruby
307
+ class Show < Hanami::Action
308
+ handle_exception RecordNotFound => 404
309
+
310
+ def handle(*)
311
+ raise RecordNotFound
312
+ end
313
+ end
314
+
315
+ action = Show.new(configuration: configuration)
316
+ action.call({}) # => [404, {}, ["Not Found"]]
317
+ ```
318
+
319
+ You can also define custom handlers for exceptions.
320
+
321
+ ```ruby
322
+ class Create < Hanami::Action
323
+ handle_exception ArgumentError => :my_custom_handler
324
+
325
+ def handle(*)
326
+ raise ArgumentError.new("Invalid arguments")
327
+ end
328
+
329
+ private
330
+
331
+ def my_custom_handler(request, response, exception)
332
+ response.status = 400
333
+ response.body = exception.message
334
+ end
335
+ end
336
+
337
+ action = Create.new(configuration: configuration)
338
+ action.call({}) # => [400, {}, ["Invalid arguments"]]
339
+ ```
340
+
341
+ Exception policies can be defined globally via configuration:
342
+
343
+ ```ruby
344
+ configuration = Hanami::Action::Configuration.new do |config|
345
+ config.handle_exception RecordNotFound => 404
346
+ end
347
+
348
+ class Show < Hanami::Action
349
+ def handle(*)
350
+ raise RecordNotFound
351
+ end
352
+ end
353
+
354
+ action = Show.new(configuration: configuration)
355
+ action.call({}) # => [404, {}, ["Not Found"]]
356
+ ```
357
+
358
+ #### Inherited Exceptions
359
+
360
+ ```ruby
361
+ class MyCustomException < StandardError
362
+ end
363
+
364
+ module Articles
365
+ class Index < Hanami::Action
366
+ handle_exception MyCustomException => :handle_my_exception
367
+
368
+ def handle(*)
369
+ raise MyCustomException
370
+ end
371
+
372
+ private
373
+
374
+ def handle_my_exception(request, response, exception)
375
+ # ...
376
+ end
377
+ end
378
+
379
+ class Show < Hanami::Action
380
+ handle_exception StandardError => :handle_standard_error
381
+
382
+ def handle(*)
383
+ raise MyCustomException
384
+ end
385
+
386
+ private
387
+
388
+ def handle_standard_error(request, response, exception)
389
+ # ...
390
+ end
391
+ end
392
+ end
393
+
394
+ Articles::Index.new.call({}) # => `handle_my_exception` will be invoked
395
+ Articles::Show.new.call({}) # => `handle_standard_error` will be invoked,
396
+ # because `MyCustomException` inherits from `StandardError`
397
+ ```
398
+
399
+ ### Throwable HTTP statuses
400
+
401
+ When `#halt` is used with a valid HTTP code, it stops the execution and sets the proper status and body for the response:
402
+
403
+ ```ruby
404
+ class Show < Hanami::Action
405
+ before :authenticate!
406
+
407
+ def handle(*)
408
+ # ...
409
+ end
410
+
411
+ private
412
+
413
+ def authenticate!
414
+ halt 401 unless authenticated?
415
+ end
416
+ end
417
+
418
+ action = Show.new(configuration: configuration)
419
+ action.call({}) # => [401, {}, ["Unauthorized"]]
420
+ ```
421
+
422
+ Alternatively, you can specify a custom message.
423
+
424
+ ```ruby
425
+ class Show < Hanami::Action
426
+ def handle(request, response)
427
+ response[:droid] = DroidRepository.new.find(request.params[:id]) or not_found
428
+ end
429
+
430
+ private
431
+
432
+ def not_found
433
+ halt 404, "This is not the droid you're looking for"
434
+ end
435
+ end
436
+
437
+ action = Show.new(configuration: configuration)
438
+ action.call({}) # => [404, {}, ["This is not the droid you're looking for"]]
439
+ ```
440
+
441
+ ### Cookies
442
+
443
+ You can read the original cookies sent from the HTTP client via `request.cookies`.
444
+ If you want to send cookies in the response, use `response.cookies`.
445
+
446
+ They are read as a Hash from Rack env:
447
+
448
+ ```ruby
449
+ require "hanami/action"
450
+ require "hanami/action/cookies"
451
+
452
+ class ReadCookiesFromRackEnv < Hanami::Action
453
+ include Hanami::Action::Cookies
454
+
455
+ def handle(request, *)
456
+ # ...
457
+ request.cookies[:foo] # => "bar"
458
+ end
459
+ end
460
+
461
+ action = ReadCookiesFromRackEnv.new(configuration: configuration)
462
+ action.call({"HTTP_COOKIE" => "foo=bar"})
463
+ ```
464
+
465
+ They are set like a Hash:
466
+
467
+ ```ruby
468
+ require "hanami/action"
469
+ require "hanami/action/cookies"
470
+
471
+ class SetCookies < Hanami::Action
472
+ include Hanami::Action::Cookies
473
+
474
+ def handle(*, response)
475
+ # ...
476
+ response.cookies[:foo] = "bar"
477
+ end
478
+ end
479
+
480
+ action = SetCookies.new(configuration: configuration)
481
+ action.call({}) # => [200, {"Set-Cookie" => "foo=bar"}, "..."]
482
+ ```
483
+
484
+ They are removed by setting their value to `nil`:
485
+
486
+ ```ruby
487
+ require "hanami/action"
488
+ require "hanami/action/cookies"
489
+
490
+ class RemoveCookies < Hanami::Action
491
+ include Hanami::Action::Cookies
492
+
493
+ def handle(*, response)
494
+ # ...
495
+ response.cookies[:foo] = nil
496
+ end
497
+ end
498
+
499
+ action = RemoveCookies.new(configuration: configuration)
500
+ action.call({}) # => [200, {"Set-Cookie" => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"}, "..."]
501
+ ```
502
+
503
+ Default values can be set in configuration, but overridden case by case.
504
+
505
+ ```ruby
506
+ require "hanami/action"
507
+ require "hanami/action/cookies"
508
+
509
+ configuration = Hanami::Action::Configuration.new do |config|
510
+ config.cookies(max_age: 300) # 5 minutes
511
+ end
512
+
513
+ class SetCookies < Hanami::Action
514
+ include Hanami::Action::Cookies
515
+
516
+ def handle(*, response)
517
+ # ...
518
+ response.cookies[:foo] = { value: "bar", max_age: 100 }
519
+ end
520
+ end
521
+
522
+ action = SetCookies.new(configuration: configuration)
523
+ action.call({}) # => [200, {"Set-Cookie" => "foo=bar; max-age=100;"}, "..."]
524
+ ```
525
+
526
+ ### Sessions
527
+
528
+ Actions have builtin support for Rack sessions.
529
+ Similarly to cookies, you can read the session sent by the HTTP client via
530
+ `request.session`, and also manipulate it via `response.session`.
531
+
532
+ ```ruby
533
+ require "hanami/action"
534
+ require "hanami/action/session"
535
+
536
+ class ReadSessionFromRackEnv < Hanami::Action
537
+ include Hanami::Action::Session
538
+
539
+ def handle(request, *)
540
+ # ...
541
+ request.session[:age] # => "35"
542
+ end
543
+ end
544
+
545
+ action = ReadSessionFromRackEnv.new(configuration: configuration)
546
+ action.call({ "rack.session" => { "age" => "35" } })
547
+ ```
548
+
549
+ Values can be set like a Hash:
550
+
551
+ ```ruby
552
+ require "hanami/action"
553
+ require "hanami/action/session"
554
+
555
+ class SetSession < Hanami::Action
556
+ include Hanami::Action::Session
557
+
558
+ def handle(*, response)
559
+ # ...
560
+ response.session[:age] = 31
561
+ end
562
+ end
563
+
564
+ action = SetSession.new(configuration: configuration)
565
+ action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."]
566
+ ```
567
+
568
+ Values can be removed like a Hash:
569
+
570
+ ```ruby
571
+ require "hanami/action"
572
+ require "hanami/action/session"
573
+
574
+ class RemoveSession < Hanami::Action
575
+ include Hanami::Action::Session
576
+
577
+ def handle(*, response)
578
+ # ...
579
+ response.session[:age] = nil
580
+ end
581
+ end
582
+
583
+ action = RemoveSession.new(configuration: configuration)
584
+ action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."] it removes that value from the session
585
+ ```
586
+
587
+ While Hanami::Action supports sessions natively, it's **session store agnostic**.
588
+ You have to specify the session store in your Rack middleware configuration (eg `config.ru`).
589
+
590
+ ```ruby
591
+ use Rack::Session::Cookie, secret: SecureRandom.hex(64)
592
+ run Show.new(configuration: configuration)
593
+ ```
594
+
595
+ ### HTTP Cache
596
+
597
+ Hanami::Action 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
598
+
599
+ You can easily set the Cache-Control header for your actions:
600
+
601
+ ```ruby
602
+ require "hanami/action"
603
+ require "hanami/action/cache"
604
+
605
+ class HttpCacheAction < Hanami::Action
606
+ include Hanami::Action::Cache
607
+ cache_control :public, max_age: 600 # => Cache-Control: public, max-age=600
608
+
609
+ def handle(*)
610
+ # ...
611
+ end
612
+ end
613
+ ```
614
+
615
+ Expires header can be specified using `expires` method:
616
+
617
+ ```ruby
618
+ require "hanami/action"
619
+ require "hanami/action/cache"
620
+
621
+ class HttpCacheAction < Hanami::Action
622
+ include Hanami::Action::Cache
623
+ expires 60, :public, max_age: 600 # => Expires: Sun, 03 Aug 2014 17:47:02 GMT, Cache-Control: public, max-age=600
624
+
625
+ def handle(*)
626
+ # ...
627
+ end
628
+ end
629
+ ```
630
+
631
+ ### Conditional Get
632
+
633
+ 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.
634
+
635
+ 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.
636
+
637
+ You can easily take advantage of Conditional Get using `#fresh` method:
638
+
639
+ ```ruby
640
+ require "hanami/action"
641
+ require "hanami/action/cache"
642
+
643
+ class ConditionalGetAction < Hanami::Action
644
+ include Hanami::Action::Cache
645
+
646
+ def handle(*)
647
+ # ...
648
+ fresh etag: resource.cache_key
649
+ # => halt 304 with header IfNoneMatch = resource.cache_key
650
+ end
651
+ end
652
+ ```
653
+
654
+ If `resource.cache_key` is equal to `IfNoneMatch` header, then hanami will `halt 304`.
655
+
656
+ An alternative to hashing based check, is the time based check:
657
+
658
+ ```ruby
659
+ require "hanami/action"
660
+ require "hanami/action/cache"
661
+
662
+ class ConditionalGetAction < Hanami::Action
663
+ include Hanami::Action::Cache
664
+
665
+ def handle(*)
666
+ # ...
667
+ fresh last_modified: resource.updated_at
668
+ # => halt 304 with header IfModifiedSince = resource.updated_at.httpdate
669
+ end
670
+ end
671
+ ```
672
+
673
+ If `resource.updated_at` is equal to `IfModifiedSince` header, then hanami will `halt 304`.
674
+
675
+ ### Redirect
676
+
677
+ If you need to redirect the client to another resource, use `response.redirect_to`:
678
+
679
+ ```ruby
680
+ class Create < Hanami::Action
681
+ def handle(*, response)
682
+ # ...
683
+ response.redirect_to "http://example.com/articles/23"
684
+ end
685
+ end
686
+
687
+ action = Create.new(configuration: configuration)
688
+ action.call({ article: { title: "Hello" }}) # => [302, {"Location" => "/articles/23"}, ""]
689
+ ```
690
+
691
+ You can also redirect with a custom status code:
692
+
693
+ ```ruby
694
+ class Create < Hanami::Action
695
+ def handle(*, response)
696
+ # ...
697
+ response.redirect_to "http://example.com/articles/23", status: 301
698
+ end
699
+ end
700
+
701
+ action = Create.new(configuration: configuration)
702
+ action.call({ article: { title: "Hello" }}) # => [301, {"Location" => "/articles/23"}, ""]
703
+ ```
704
+
705
+ ### MIME Types
706
+
707
+ `Hanami::Action` automatically sets the `Content-Type` header, according to the request.
708
+
709
+ ```ruby
710
+ class Show < Hanami::Action
711
+ def handle(*)
712
+ end
713
+ end
714
+
715
+ action = Show.new(configuration: configuration)
716
+
717
+ response = action.call({ "HTTP_ACCEPT" => "*/*" }) # Content-Type "application/octet-stream"
718
+ response.format # :all
719
+
720
+ response = action.call({ "HTTP_ACCEPT" => "text/html" }) # Content-Type "text/html"
721
+ response.format # :html
722
+ ```
723
+
724
+ However, you can force this value:
725
+
726
+ ```ruby
727
+ class Show < Hanami::Action
728
+ def handle(*, response)
729
+ # ...
730
+ response.format = :json
731
+ end
732
+ end
733
+
734
+ action = Show.new(configuration: configuration)
735
+
736
+ response = action.call({ "HTTP_ACCEPT" => "*/*" }) # Content-Type "application/json"
737
+ response.format # :json
738
+
739
+ response = action.call({ "HTTP_ACCEPT" => "text/html" }) # Content-Type "application/json"
740
+ response.format # :json
741
+ ```
742
+
743
+ You can restrict the accepted MIME types:
744
+
745
+ ```ruby
746
+ class Show < Hanami::Action
747
+ accept :html, :json
748
+
749
+ def handle(*)
750
+ # ...
751
+ end
752
+ end
753
+
754
+ # When called with "*/*" => 200
755
+ # When called with "text/html" => 200
756
+ # When called with "application/json" => 200
757
+ # When called with "application/xml" => 415
758
+ ```
759
+
760
+ You can check if the requested MIME type is accepted by the client.
761
+
762
+ ```ruby
763
+ class Show < Hanami::Action
764
+ def handle(request, response)
765
+ # ...
766
+ # @_env["HTTP_ACCEPT"] # => "text/html,application/xhtml+xml,application/xml;q=0.9"
767
+
768
+ request.accept?("text/html") # => true
769
+ request.accept?("application/xml") # => true
770
+ request.accept?("application/json") # => false
771
+ response.format # :html
772
+
773
+
774
+ # @_env["HTTP_ACCEPT"] # => "*/*"
775
+
776
+ request.accept?("text/html") # => true
777
+ request.accept?("application/xml") # => true
778
+ request.accept?("application/json") # => true
779
+ response.format # :html
780
+ end
781
+ end
782
+ ```
783
+
784
+ Hanami::Action is shipped with an extensive list of the most common MIME types.
785
+ Also, you can register your own:
786
+
787
+ ```ruby
788
+ configuration = Hanami::Action::Configuration.new do |config|
789
+ config.format custom: "application/custom"
790
+ end
791
+
792
+ class Index < Hanami::Action
793
+ def handle(*)
794
+ end
795
+ end
796
+
797
+ action = Index.new(configuration: configuration)
798
+
799
+ response = action.call({ "HTTP_ACCEPT" => "application/custom" }) # => Content-Type "application/custom"
800
+ response.format # => :custom
801
+
802
+ class Show < Hanami::Action
803
+ def handle(*, response)
804
+ # ...
805
+ response.format = :custom
806
+ end
807
+ end
808
+
809
+ action = Show.new(configuration: configuration)
810
+
811
+ response = action.call({ "HTTP_ACCEPT" => "*/*" }) # => Content-Type "application/custom"
812
+ response.format # => :custom
813
+ ```
814
+
815
+ ### Streamed Responses
816
+
817
+ 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.
818
+
819
+ ```ruby
820
+ configuration = Hanami::Action::Configuration.new do |config|
821
+ config.format csv: 'text/csv'
822
+ end
823
+
824
+ class Csv < Hanami::Action
825
+ def handle(*, response)
826
+ response.format = :csv
827
+ response.body = Enumerator.new do |yielder|
828
+ yielder << csv_header
829
+
830
+ # Expensive operation is streamed as each line becomes available
831
+ csv_body.each_line do |line|
832
+ yielder << line
833
+ end
834
+ end
835
+ end
836
+ end
837
+ ```
838
+
839
+ Note:
840
+ * In development, Hanami' code reloading needs to be disabled for streaming to work. This is because `Shotgun` interferes with the streaming action. You can disable it like this `hanami server --code-reloading=false`
841
+ * Streaming does not work with WEBrick as it buffers its response. We recommend using `puma`, though you may find success with other servers
842
+
843
+ ### No rendering, please
844
+
845
+ Hanami::Action is designed to be a pure HTTP endpoint, rendering belongs to other layers of MVC.
846
+ You can set the body directly (see [response](#response)), or use [Hanami::View](https://github.com/hanami/view).
847
+
848
+ ### Rack integration
849
+
850
+ Hanami::Action is compatible with Rack. If you need to use any Rack middleware, please mount them in `config.ru`.
851
+
852
+ ### Thread safety
853
+
854
+ An Action is **immutable**, it works without global state, so it's thread-safe by design.
855
+
856
+ ## Contributing
857
+
858
+ 1. Fork it
859
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
860
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
861
+ 4. Push to the branch (`git push origin my-new-feature`)
862
+ 5. Create new Pull Request
863
+
864
+ ## Links
865
+
866
+ - [User documentation](https://hanamirb.org)
867
+ - [API documentation](http://rubydoc.info/gems/hanami-action)
868
+
869
+
870
+ ## License
871
+
872
+ See `LICENSE` file.
873
+