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