hanami-controller 1.3.3 → 2.0.0.alpha1

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