hanami-controller 1.3.3 → 2.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -7
- data/README.md +295 -537
- data/hanami-controller.gemspec +3 -3
- data/lib/hanami/action.rb +653 -38
- data/lib/hanami/action/base_params.rb +2 -2
- data/lib/hanami/action/cache.rb +1 -139
- data/lib/hanami/action/cache/cache_control.rb +4 -4
- data/lib/hanami/action/cache/conditional_get.rb +4 -5
- data/lib/hanami/action/cache/directives.rb +1 -1
- data/lib/hanami/action/cache/expires.rb +3 -3
- data/lib/hanami/action/cookie_jar.rb +3 -3
- data/lib/hanami/action/cookies.rb +3 -62
- data/lib/hanami/action/flash.rb +2 -2
- data/lib/hanami/action/glue.rb +5 -31
- data/lib/hanami/action/halt.rb +12 -0
- data/lib/hanami/action/mime.rb +77 -491
- data/lib/hanami/action/params.rb +3 -3
- data/lib/hanami/action/rack/file.rb +1 -1
- data/lib/hanami/action/request.rb +30 -20
- data/lib/hanami/action/response.rb +174 -0
- data/lib/hanami/action/session.rb +8 -117
- data/lib/hanami/action/validatable.rb +2 -2
- data/lib/hanami/controller.rb +0 -210
- data/lib/hanami/controller/configuration.rb +51 -506
- data/lib/hanami/controller/version.rb +1 -1
- metadata +12 -21
- data/lib/hanami/action/callable.rb +0 -92
- data/lib/hanami/action/callbacks.rb +0 -214
- data/lib/hanami/action/configurable.rb +0 -50
- data/lib/hanami/action/exposable.rb +0 -126
- data/lib/hanami/action/exposable/guard.rb +0 -104
- data/lib/hanami/action/head.rb +0 -121
- data/lib/hanami/action/rack.rb +0 -411
- data/lib/hanami/action/rack/callable.rb +0 -47
- data/lib/hanami/action/rack/errors.rb +0 -53
- data/lib/hanami/action/redirect.rb +0 -59
- data/lib/hanami/action/throwable.rb +0 -169
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49b2d026e0d36ca0e450dd4ac0b3b0cf6f4c61e17cc8c81f71b802d23d8fbb29
|
4
|
+
data.tar.gz: 59f0c51a316fba2fb712806f16a129c36e9bc875a35820a6ac042066d79d1126
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b7d144f8a8a076d2e4e5149d0cd3415041a49049888671a67f586db1e107cf749dec6d8f4141099205eb8eb6bbb3f670420c78df3b9509845de364310ceb9b1
|
7
|
+
data.tar.gz: 2dbc46c1fec018bb5e7bf5ee63d019a925eeda300948037d78c420a4c81034d53e43784ba4d39a4d3f8d63c957a2c00d8307b1127e9b5c292b5033f5599232ce
|
data/CHANGELOG.md
CHANGED
@@ -1,15 +1,54 @@
|
|
1
1
|
# Hanami::Controller
|
2
2
|
Complete, fast and testable actions for Rack
|
3
3
|
|
4
|
-
##
|
4
|
+
## v2.0.0.alpha1 - 2019-01-30
|
5
5
|
### Added
|
6
|
-
- [Luca Guidi]
|
7
|
-
- [Luca Guidi]
|
8
|
-
- [Luca Guidi]
|
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
|
-
|
11
|
-
|
12
|
-
- [
|
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
|
[](https://badge.fury.io/rb/hanami-controller)
|
8
|
-
[](https://travis-ci.org/hanami/controller)
|
9
9
|
[](https://circleci.com/gh/hanami/controller/tree/master)
|
10
10
|
[](https://codecov.io/gh/hanami/controller)
|
11
11
|
[](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.
|
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
|
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
|
-
|
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
|
-
|
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
|
73
|
-
|
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
|
-
|
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
|
-
|
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
|
91
|
-
|
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
|
-
|
96
|
-
action.
|
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 `#
|
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
|
-
|
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
|
-
|
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
|
-
|
136
|
-
|
137
|
-
def call(params)
|
130
|
+
class Show < Hanami::Action
|
131
|
+
def handle(req, *)
|
138
132
|
# ...
|
139
|
-
puts params # => { id: 23, key:
|
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(
|
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
|
154
|
-
require
|
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
|
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] # =>
|
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
|
197
|
-
require
|
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
|
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
|
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({}) # =>
|
219
|
+
action = Show.new(configuration: configuration)
|
220
|
+
action.call({}) # => #<Hanami::Action::Response:0x00007fe8be968418 @status=200 ...>
|
246
221
|
```
|
247
222
|
|
248
|
-
|
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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
270
|
-
`Hanami::Action`'s solution is the simple and powerful DSL: `expose`.
|
271
|
-
It's a thin layer on top of `attr_reader`.
|
272
|
-
|
273
|
-
Using `expose` creates a getter for the given attribute, and adds it to the _exposures_.
|
274
|
-
Exposures (`#exposures`) are a set of attributes exposed to the view.
|
275
|
-
That is to say the variables necessary for rendering a view.
|
276
|
-
|
277
|
-
By default, all `Hanami::Action` objects expose `#params` and `#errors`.
|
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
|
-
|
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
|
291
|
-
action.call(
|
250
|
+
action = Show.new(configuration: configuration)
|
251
|
+
response = action.call(id: 23)
|
292
252
|
|
293
|
-
|
253
|
+
article = response[:article]
|
254
|
+
article.class # => Article
|
255
|
+
article.id # => 23
|
294
256
|
|
295
|
-
|
257
|
+
response.exposures.keys # => [:params, :article]
|
296
258
|
```
|
297
259
|
|
298
260
|
### Callbacks
|
299
261
|
|
300
|
-
|
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
|
269
|
+
def handle(*)
|
309
270
|
end
|
310
271
|
|
311
272
|
private
|
273
|
+
|
312
274
|
def authenticate
|
313
275
|
# ...
|
314
276
|
end
|
315
277
|
|
316
|
-
# `
|
317
|
-
def set_article(
|
318
|
-
|
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 { |
|
290
|
+
before { |req, res| res[:article] = ArticleRepository.new.find(req.params[:id]) }
|
331
291
|
|
332
|
-
def
|
292
|
+
def handle(*)
|
333
293
|
end
|
334
294
|
end
|
335
295
|
```
|
336
296
|
|
337
297
|
### Exceptions management
|
338
298
|
|
339
|
-
When an exception
|
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
|
-
|
305
|
+
class Show < Hanami::Action
|
306
|
+
handle_exception StandardError => 500
|
344
307
|
|
345
|
-
def
|
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
|
362
|
-
|
323
|
+
def handle(*)
|
324
|
+
raise RecordNotFound
|
363
325
|
end
|
364
326
|
end
|
365
327
|
|
366
|
-
action = Show.new
|
367
|
-
action.call({
|
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
|
-
|
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
|
-
|
400
|
-
|
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 =
|
408
|
-
action.call({
|
350
|
+
action = Create.new(configuration: configuration)
|
351
|
+
action.call({}) # => [400, {}, ["Invalid arguments"]]
|
409
352
|
```
|
410
353
|
|
411
|
-
|
354
|
+
Exception policies can be defined globally via configuration:
|
412
355
|
|
413
356
|
```ruby
|
414
|
-
Hanami::Controller.
|
415
|
-
|
357
|
+
configuration = Hanami::Controller::Configuration.new do |config|
|
358
|
+
config.handle_exception RecordNotFound => 404
|
416
359
|
end
|
417
360
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
class Show
|
422
|
-
include Hanami::Action
|
423
|
-
|
424
|
-
configure do
|
425
|
-
handle_exceptions false
|
426
|
-
end
|
427
|
-
|
428
|
-
def call(params)
|
429
|
-
@article = ArticleRepository.new.find(params[:id])
|
430
|
-
end
|
361
|
+
class Show < Hanami::Action
|
362
|
+
def handle(*)
|
363
|
+
raise RecordNotFound
|
431
364
|
end
|
432
365
|
end
|
433
366
|
|
434
|
-
action =
|
435
|
-
action.call({
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
535
|
-
require
|
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
|
468
|
+
def handle(req, *)
|
542
469
|
# ...
|
543
|
-
cookies[:foo] # =>
|
470
|
+
req.cookies[:foo] # => "bar"
|
544
471
|
end
|
545
472
|
end
|
546
473
|
|
547
|
-
action = ReadCookiesFromRackEnv.new
|
548
|
-
action.call({
|
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
|
555
|
-
require
|
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
|
487
|
+
def handle(*, res)
|
562
488
|
# ...
|
563
|
-
cookies[:foo] =
|
489
|
+
res.cookies[:foo] = "bar"
|
564
490
|
end
|
565
491
|
end
|
566
492
|
|
567
|
-
action = SetCookies.new
|
568
|
-
action.call({}) # => [200, {
|
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
|
575
|
-
require
|
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
|
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, {
|
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
|
595
|
-
require
|
519
|
+
require "hanami/controller"
|
520
|
+
require "hanami/action/cookies"
|
596
521
|
|
597
|
-
Hanami::Controller.
|
598
|
-
cookies
|
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
|
529
|
+
def handle(*, res)
|
606
530
|
# ...
|
607
|
-
cookies[:foo] = { value:
|
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, {
|
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
|
-
|
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
|
621
|
-
require
|
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
|
552
|
+
def handle(req, *)
|
628
553
|
# ...
|
629
|
-
session[:age] # =>
|
554
|
+
req.session[:age] # => "35"
|
630
555
|
end
|
631
556
|
end
|
632
557
|
|
633
|
-
action = ReadSessionFromRackEnv.new
|
634
|
-
action.call({
|
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
|
641
|
-
require
|
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
|
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
|
661
|
-
require
|
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
|
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
|
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
|
-
###
|
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
|
693
|
-
require
|
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
|
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
|
711
|
-
require
|
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
|
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
|
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
|
735
|
-
require
|
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
|
659
|
+
def handle(*)
|
742
660
|
# ...
|
743
|
-
fresh etag:
|
744
|
-
# => halt 304 with header IfNoneMatch =
|
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
|
667
|
+
If `resource.cache_key` is equal to `IfNoneMatch` header, then hanami will `halt 304`.
|
750
668
|
|
751
|
-
|
669
|
+
An alterative to hashing based check, is the time based check:
|
752
670
|
|
753
671
|
```ruby
|
754
|
-
require
|
755
|
-
require
|
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
|
678
|
+
def handle(*)
|
762
679
|
# ...
|
763
|
-
fresh last_modified:
|
764
|
-
# => halt 304 with header IfModifiedSince =
|
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
|
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
|
690
|
+
If you need to redirect the client to another resource, use `res.redirect_to`:
|
774
691
|
|
775
692
|
```ruby
|
776
|
-
class Create
|
777
|
-
|
778
|
-
|
779
|
-
def call(params)
|
693
|
+
class Create < Hanami::Action
|
694
|
+
def handle(*, res)
|
780
695
|
# ...
|
781
|
-
redirect_to
|
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:
|
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
|
-
|
794
|
-
|
795
|
-
def call(params)
|
707
|
+
class Create < Hanami::Action
|
708
|
+
def handle(*, res)
|
796
709
|
# ...
|
797
|
-
redirect_to
|
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:
|
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
|
-
|
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({
|
820
|
-
|
730
|
+
response = action.call({ "HTTP_ACCEPT" => "*/*" }) # Content-Type "application/octet-stream"
|
731
|
+
response.format # :all
|
821
732
|
|
822
|
-
action.call({
|
823
|
-
|
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
|
-
|
831
|
-
|
832
|
-
def call(params)
|
740
|
+
class Show < Hanami::Action
|
741
|
+
def handle(*, res)
|
833
742
|
# ...
|
834
|
-
|
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({
|
841
|
-
|
749
|
+
response = action.call({ "HTTP_ACCEPT" => "*/*" }) # Content-Type "application/json"
|
750
|
+
response.format # :json
|
842
751
|
|
843
|
-
action.call({
|
844
|
-
|
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
|
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
|
-
|
870
|
-
|
871
|
-
def call(params)
|
776
|
+
class Show < Hanami::Action
|
777
|
+
def handle(req, res)
|
872
778
|
# ...
|
873
|
-
# @_env[
|
779
|
+
# @_env["HTTP_ACCEPT"] # => "text/html,application/xhtml+xml,application/xml;q=0.9"
|
874
780
|
|
875
|
-
accept?(
|
876
|
-
accept?(
|
877
|
-
accept?(
|
878
|
-
|
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[
|
788
|
+
# @_env["HTTP_ACCEPT"] # => "*/*"
|
883
789
|
|
884
|
-
accept?(
|
885
|
-
accept?(
|
886
|
-
accept?(
|
887
|
-
|
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.
|
897
|
-
format custom:
|
802
|
+
configuration = Hanami::Controller::Configuration.new do |config|
|
803
|
+
config.format custom: "application/custom"
|
898
804
|
end
|
899
805
|
|
900
|
-
class Index
|
901
|
-
|
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
|
-
|
913
|
-
|
813
|
+
response = action.call({ "HTTP_ACCEPT" => "application/custom" }) # => Content-Type "application/custom"
|
814
|
+
response.format # => :custom
|
914
815
|
|
915
|
-
|
816
|
+
class Show < Hanami::Action
|
817
|
+
def handle(*, res)
|
916
818
|
# ...
|
917
|
-
|
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({
|
924
|
-
|
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.
|
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
|
-
|
939
|
-
|
940
|
-
|
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
|
-
|
995
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
909
|
+
Hanami::Controller can be configured via `Hanami::Controller::Configuration`.
|
1067
910
|
It supports a few options:
|
1068
911
|
|
1069
912
|
```ruby
|
1070
|
-
require
|
1071
|
-
|
1072
|
-
Hanami::Controller.configure do
|
1073
|
-
# Handle exceptions with HTTP statuses (true) or don't catch them (false)
|
1074
|
-
# Argument: boolean, defaults to `true`
|
1075
|
-
#
|
1076
|
-
handle_exceptions true
|
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:
|
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
|
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 **
|
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-
|
966
|
+
Copyright © 2014-2019 Luca Guidi – Released under MIT License
|
1209
967
|
|
1210
968
|
This project was formerly known as Lotus (`lotus-controller`).
|