hanami-controller 1.3.0 → 2.0.0.alpha2

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