hanami-controller 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +1180 -9
  5. data/hanami-controller.gemspec +19 -12
  6. data/lib/hanami-controller.rb +1 -0
  7. data/lib/hanami/action.rb +85 -0
  8. data/lib/hanami/action/cache.rb +174 -0
  9. data/lib/hanami/action/cache/cache_control.rb +70 -0
  10. data/lib/hanami/action/cache/conditional_get.rb +93 -0
  11. data/lib/hanami/action/cache/directives.rb +99 -0
  12. data/lib/hanami/action/cache/expires.rb +73 -0
  13. data/lib/hanami/action/callable.rb +94 -0
  14. data/lib/hanami/action/callbacks.rb +210 -0
  15. data/lib/hanami/action/configurable.rb +49 -0
  16. data/lib/hanami/action/cookie_jar.rb +181 -0
  17. data/lib/hanami/action/cookies.rb +85 -0
  18. data/lib/hanami/action/exposable.rb +115 -0
  19. data/lib/hanami/action/flash.rb +182 -0
  20. data/lib/hanami/action/glue.rb +66 -0
  21. data/lib/hanami/action/head.rb +122 -0
  22. data/lib/hanami/action/mime.rb +493 -0
  23. data/lib/hanami/action/params.rb +285 -0
  24. data/lib/hanami/action/rack.rb +270 -0
  25. data/lib/hanami/action/rack/callable.rb +47 -0
  26. data/lib/hanami/action/rack/file.rb +33 -0
  27. data/lib/hanami/action/redirect.rb +59 -0
  28. data/lib/hanami/action/request.rb +86 -0
  29. data/lib/hanami/action/session.rb +154 -0
  30. data/lib/hanami/action/throwable.rb +194 -0
  31. data/lib/hanami/action/validatable.rb +128 -0
  32. data/lib/hanami/controller.rb +250 -2
  33. data/lib/hanami/controller/configuration.rb +705 -0
  34. data/lib/hanami/controller/error.rb +7 -0
  35. data/lib/hanami/controller/version.rb +4 -1
  36. data/lib/hanami/http/status.rb +62 -0
  37. metadata +124 -16
  38. data/.gitignore +0 -9
  39. data/Gemfile +0 -4
  40. data/Rakefile +0 -2
  41. data/bin/console +0 -14
  42. data/bin/setup +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9b882cec7ffc28e71c3b39a1107c33e36bc53a4d
4
- data.tar.gz: 634865a8c1fc74e07e22fcd952a46fcc80bcb59a
3
+ metadata.gz: 6aa09c8d919968060861da431f0ba61eb8c58cac
4
+ data.tar.gz: 6b46109f745c50ee376811c08469792ed00eb053
5
5
  SHA512:
6
- metadata.gz: 527fdf6f007ae9eba6bbd5faa9918a1a74e43abb04c61c582a513130d81f4315d5b95f741d3796e9490899746b7c74f8f3e9d66b038f3d2de76a7944f793979d
7
- data.tar.gz: 04812442901425c86f7aed5e5e7f9de25151f243d8f8aa2fc9f202d15a4ee933faba206becb6d3f64cbae862b747a510510c3c842832e505d5f428feefd89fce
6
+ metadata.gz: 5a8524d76af843a0e61350b7b9b550e8ce2d1f7b6ff84fe7ea0cc62c7af1bddde884a8c56fdcc8b7430cd3733598a1a0a9efb35bb4ba7467dff4b01f17bf643c
7
+ data.tar.gz: 5251e851f5d42427d012532b84e26837b5dca7d0c8e1c90b4bad4ab15dff614cca508583b44aa1741b9f5926f07fb36a48647dcd7ce7ec0f921acf6fd0e96bea
data/CHANGELOG.md ADDED
@@ -0,0 +1,155 @@
1
+ # Hanami::Controller
2
+ Complete, fast and testable actions for Rack
3
+
4
+ ## v0.6.0 - 2016-01-22
5
+ ### Changed
6
+ - [Luca Guidi] Renamed the project
7
+
8
+ ## v0.5.1 - 2016-01-19
9
+ ### Fixed
10
+ - [Alfonso Uceda] Ensure `rack.session` cookie to not be sent twice when both `Lotus::Action::Cookies` and `Rack::Session::Cookie` are used together
11
+
12
+ ## v0.5.0 - 2016-01-12
13
+ ### Added
14
+ - [Luca Guidi] Reference a raised exception in Rack env's `rack.exception`. Compatibility with exception reporting SaaS.
15
+
16
+ ### Fixed
17
+ - [Cainã Costa] Ensure Rack environment to be always available for sessions unit tests
18
+ - [Luca Guidi] Ensure superclass exceptions to not shadow subclasses during exception handling (eg. `CustomError` handler will take precedence over `StandardError`)
19
+
20
+ ### Changed
21
+ - [Luca Guidi] Removed `Lotus::Controller::Configuration#default_format`
22
+ - [Cainã Costa] Made `Lotus::Action#session` a public method for improved unit testing
23
+ - [Karim Tarek] Introduced `Lotus::Controller::Error` and let all the framework exceptions to inherit from it.
24
+
25
+ ## v0.4.6 - 2015-12-04
26
+ ### Added
27
+ - [Luca Guidi] Allow to force custom headers for responses that according to RFC shouldn't include them (eg 204). Override `#keep_response_header?(header)` in action
28
+
29
+ ## v0.4.5 - 2015-09-30
30
+ ### Added
31
+ - [Theo Felippe] Added configuration entries: `#default_request_format` and `default_response_format`.
32
+ - [Wellington Santos] Error handling to take account of inherited exceptions.
33
+
34
+ ### Changed
35
+ - [Theo Felippe] Deprecated `#default_format` in favor of: `#default_request_format`.
36
+
37
+ ## v0.4.4 - 2015-06-23
38
+ ### Added
39
+ - [Luca Guidi] Security protection against Cross Site Request Forgery (CSRF).
40
+
41
+ ### Fixed
42
+ - [Matthew Bellantoni] Ensure nested params to be correctly coerced to Hash.
43
+
44
+ ## v0.4.3 - 2015-05-22
45
+ ### Added
46
+ - [Alfonso Uceda Pompa & Luca Guidi] Introduced `Lotus::Action#send_file`
47
+ - [Alfonso Uceda Pompa] Set automatically `Expires` option for cookies when it's missing but `Max-Age` is present. Compatibility with old browsers.
48
+
49
+ ## v0.4.2 - 2015-05-15
50
+ ### Fixed
51
+ - [Luca Guidi] Ensure `Lotus::Action::Params#to_h` to return `::Hash` at the top level
52
+
53
+ ## v0.4.1 - 2015-05-15
54
+ ### Fixed
55
+ - [Luca Guidi] Ensure proper automatic `Content-Type` working well with Internet Explorer.
56
+ - [Luca Guidi] Ensure `Lotus::Action#redirect_to` to return `::String` for Rack servers compatibility.
57
+
58
+ ### Changed
59
+ - [Alfonso Uceda Pompa] Prevent `Content-Type` and `Content-Lenght` to be sent when status code requires no body (eg. `204`).
60
+ This is for compatibility with `Rack::Lint`, not with RFC 2016.
61
+ - [Luca Guidi] Ensure `Lotus::Action::Params#to_h` to return `::Hash`
62
+
63
+ ## v0.4.0 - 2015-03-23
64
+ ### Added
65
+ - [Erol Fornoles] `Action.use` now accepts a block
66
+ - [Alfonso Uceda Pompa] Introduced `Lotus::Controller::Configuration#cookies` as default cookie options.
67
+ - [Alfonso Uceda Pompa] Introduced `Lotus::Controller::Configuration#default_headers` as default HTTP headers to return in all the responses.
68
+ - [Luca Guidi] Introduced `Lotus::Action::Params#get` as a safe API to access nested params.
69
+
70
+ ### Changed
71
+ - [Alfonso Uceda Pompa] `redirect_to` now is a flow control method: it terminates the execution of an action, including the callbacks.
72
+
73
+ ## v0.3.2 - 2015-01-30
74
+ ### Added
75
+ - [Alfonso Uceda Pompa] Callbacks: introduced `append_before` (alias of `before`), `append_after` (alias of `after`), `prepend_before` and `prepend_after`.
76
+ - [Alfonso Uceda Pompa] Introduced `Lotus::Action::Params#raw` which returns unfiltered data as it comes from an HTTP request.
77
+ - [Alfonso Uceda Pompa] `Lotus::Action::Rack.use` now fully supports Rack middleware, by mounting an internal `Rack::Builder` instance.
78
+ - [Simone Carletti] `Lotus::Action::Throwable#halt` now accepts an optional message. If missing it falls back to the corresponding HTTP status message.
79
+ - [Steve Hodgkiss] Nested params validation
80
+
81
+ ### Fixed
82
+ - [Luca Guidi] Ensure HEAD requests will return empty body
83
+ - [Stefano Verna] Ensure HTTP status codes with empty body won't send body and non-entity headers.
84
+ - [Luca Guidi] Only dump exceptions in `rack.errors` if handling is turned off, or the raised exception is not managed.
85
+ - [Luca Guidi] Ensure params will return coerced values
86
+
87
+ ## v0.3.1 - 2015-01-08
88
+ ### Added
89
+ - [Lasse Skindstad Ebert] Introduced `Action#request` which returns an instance a `Rack::Request` compliant object: `Lotus::Action::Request`.
90
+
91
+ ### Fixed
92
+ - [Steve Hodgkiss] Ensure params to return coerced values
93
+
94
+ ## v0.3.0 - 2014-12-23
95
+ ### Added
96
+ - [Luca Guidi] Introduced `Action#request_id` as unique identifier for an incoming HTTP request
97
+ - [Luca Guidi] Introduced `Lotus::Controller.load!` as loading framework entry point
98
+ - [Kir Shatrov] Allow to define a default charset (`default_charset` configuration)
99
+ - [Kir Shatrov] Automatic content type with charset (eg `Content-Type: text/html; charset=utf-8`)
100
+ - [Michał Krzyżanowski] Allow to specify custom exception handlers: procs or methods (`exception_handler` configuration)
101
+ - [Karl Freeman & Lucas Souza] Introduced HTTP caching (`Cache-Control`, `Last-Modified`, ETAG, Conditional GET, expires)
102
+ - [Satoshi Amemiya] Introduced `Action::Params#to_h` and `#to_hash`
103
+ - [Luca Guidi] Added `#params` and `#errors` as default exposures
104
+ - [Luca Guidi] Introduced complete params validations
105
+ - [Luca Guidi & Matthew Bellantoni] Allow to whitelist params
106
+ - [Luca Guidi & Matthew Bellantoni] Allow to define custom classes for params via `Action.params`
107
+ - [Krzysztof Zalewski] Introduced `Action#format` as query method to introspect the requested mime type
108
+ - [Luca Guidi] Official support for Ruby 2.2
109
+
110
+ ### Changed
111
+ - [Trung Lê] Renamed `Configuration#modules` to `#prepare`
112
+ - [Luca Guidi] Update HTTP status codes to IETF RFC 7231
113
+ - [Luca Guidi] When `Lotus::Controller` is included, don't inject code
114
+ - [Luca Guidi] Removed `Controller.action` as a DSL to define actions
115
+ - [Krzysztof Zalewski] Removed `Action#content_type` in favor of `#format=` which accepts a symbol (eg. `:json`)
116
+ - [Fuad Saud] Reduce method visibility where possible (Ruby `private` and `protected`)
117
+
118
+ ### Fixed
119
+ - [Luca Guidi] Don't let exposures definition to override existing methods
120
+
121
+ ## v0.2.0 - 2014-06-23
122
+ ### Added
123
+ - [Luca Guidi] Introduced `Controller.configure` and `Controller.duplicate`
124
+ - [Luca Guidi] Introduced `Action.use`, that let to use a Rack middleware as a before callback
125
+ – [Luca Guidi] Allow to define a default mime type when the request is `Accept: */*` (`default_format` configuration)
126
+ – [Luca Guidi] Allow to register custom mime types and associate them to a symbol (`format` configuration)
127
+ - [Luca Guidi] Introduced `Configuration#handle_exceptions` to associate exceptions to HTTP statuses
128
+ - [Damir Zekic] Allow developers to toggle exception handling (`handle_exceptions` configuration)
129
+ - [Luca Guidi] Introduced `Controller::Configuration`
130
+ - [Luca Guidi] Official support for Ruby 2.1
131
+
132
+ ### Changed
133
+ - [Luca Guidi] `Lotus::Action::Params` doesn't inherit from `Lotus::Utils::Hash` anymore
134
+ - [Luca Guidi] `Lotus::Action::CookieJar` doesn't inherit from `Lotus::Utils::Hash` anymore
135
+ - [Luca Guidi] Make HTTP status messages compliant with IANA and Rack
136
+ - [Damir Zekic] Moved `#throw` override logic into `#halt`, which keeps the same semantic
137
+
138
+ ### Fixed
139
+ - [Krzysztof Zalewski] Reference exception in `rack.errors`
140
+
141
+ ## v0.1.0 - 2014-02-23
142
+ ### Added
143
+ - [Luca Guidi] Introduced `Action.accept` to whitelist accepted mime types
144
+ - [Luca Guidi] Introduced `Action#accept?` as a query method for the current request
145
+ - [Luca Guidi] Allow to whitelist handled exceptions and associate them to an HTTP status
146
+ - [Luca Guidi] Automatic `Content-Type`
147
+ - [Luca Guidi] Use `throw` as a control flow which understands HTTP status
148
+ - [Luca Guidi] Introduced opt-in support for HTTP/Rack cookies
149
+ - [Luca Guidi] Introduced opt-in support for HTTP/Rack sessions
150
+ - [Luca Guidi] Introduced HTTP redirect API
151
+ - [Luca Guidi] Introduced callbacks for actions: before and after
152
+ - [Luca Guidi] Introduced exceptions handling with HTTP statuses
153
+ - [Luca Guidi] Introduced exposures
154
+ - [Luca Guidi] Introduced basic actions compatible with Rack
155
+ - [Luca Guidi] Official support for Ruby 2.0
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright © 2014-2016 Luca Guidi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,8 +1,28 @@
1
1
  # Hanami::Controller
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hanami/controller`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Complete, fast and testable actions for Rack and [Hanami](http://hanamirb.org)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ ## Status
6
+
7
+ [![Gem Version](https://badge.fury.io/rb/hanami-controller.png)](http://badge.fury.io/rb/hanami-controller)
8
+ [![Build Status](https://secure.travis-ci.org/hanami/controller.png?branch=master)](http://travis-ci.org/hanami/controller?branch=master)
9
+ [![Coverage](https://coveralls.io/repos/hanami/controller/badge.png?branch=master)](https://coveralls.io/r/hanami/controller)
10
+ [![Code Climate](https://codeclimate.com/github/hanami/controller.png)](https://codeclimate.com/github/hanami/controller)
11
+ [![Dependencies](https://gemnasium.com/hanami/controller.png)](https://gemnasium.com/hanami/controller)
12
+ [![Inline docs](http://inch-ci.org/github/hanami/controller.png)](http://inch-ci.org/github/hanami/controller)
13
+
14
+ ## Contact
15
+
16
+ * Home page: http://hanamirb.org
17
+ * Mailing List: http://hanamirb.org/mailing-list
18
+ * API Doc: http://rdoc.info/gems/hanami-controller
19
+ * Bugs/Issues: https://github.com/hanami/controller/issues
20
+ * Chat: http://chat.hanamirb.org
21
+ * Chat: https://gitter.im/hanami/chat
22
+
23
+ ## Rubies
24
+
25
+ __Hanami::Controller__ supports Ruby (MRI) 2.2+
6
26
 
7
27
  ## Installation
8
28
 
@@ -14,23 +34,1174 @@ gem 'hanami-controller'
14
34
 
15
35
  And then execute:
16
36
 
17
- $ bundle
37
+ ```shell
38
+ $ bundle
39
+ ```
18
40
 
19
41
  Or install it yourself as:
20
42
 
21
- $ gem install hanami-controller
43
+ ```shell
44
+ $ gem install hanami-controller
45
+ ```
22
46
 
23
47
  ## Usage
24
48
 
25
- TODO: Write usage instructions here
49
+ Hanami::Controller is a micro library for web frameworks.
50
+ It works beautifully with [Hanami::Router](https://github.com/hanami/router), but it can be employed everywhere.
51
+ It's designed to be fast and testable.
52
+
53
+ ### Actions
54
+
55
+ The core of this framework are the actions.
56
+ They are the endpoints that respond to incoming HTTP requests.
57
+
58
+ ```ruby
59
+ class Show
60
+ include Hanami::Action
61
+
62
+ def call(params)
63
+ @article = Article.find params[:id]
64
+ end
65
+ end
66
+ ```
67
+
68
+ The usage of `Hanami::Action` follows the Hanami philosophy: include a module and implement a minimal interface.
69
+ In this case, the interface is one method: `#call(params)`.
70
+
71
+ Hanami is designed to not interfere with inheritance.
72
+ This is important, because you can implement your own initialization strategy.
73
+
74
+ __An action is an object__. That's important because __you have the full control on it__.
75
+ In other words, you have the freedom to instantiate, inject dependencies and test it, both at the unit and integration level.
76
+
77
+ In the example below, the default repository is `Article`. During a unit test we can inject a stubbed version, and invoke `#call` with the params.
78
+ __We're avoiding HTTP calls__, we're also going to avoid hitting the database (it depends on the stubbed repository), __we're just dealing with message passing__.
79
+ Imagine how **fast** the unit test could be.
80
+
81
+ ```ruby
82
+ class Show
83
+ include Hanami::Action
84
+
85
+ def initialize(repository = Article)
86
+ @repository = repository
87
+ end
88
+
89
+ def call(params)
90
+ @article = @repository.find params[:id]
91
+ end
92
+ end
93
+
94
+ action = Show.new(MemoryArticleRepository)
95
+ action.call({ id: 23 })
96
+ ```
97
+
98
+ ### Params
99
+
100
+ The request params are passed as an argument to the `#call` method.
101
+ If routed with *Hanami::Router*, it extracts the relevant bits from the Rack `env` (eg the requested `:id`).
102
+ Otherwise everything is passed as is: the full Rack `env` in production, and the given `Hash` for unit tests.
103
+
104
+ With Hanami::Router:
105
+
106
+ ```ruby
107
+ class Show
108
+ include Hanami::Action
109
+
110
+ def call(params)
111
+ # ...
112
+ puts params # => { id: 23 } extracted from Rack env
113
+ end
114
+ end
115
+ ```
116
+
117
+ Standalone:
118
+
119
+ ```ruby
120
+ class Show
121
+ include Hanami::Action
122
+
123
+ def call(params)
124
+ # ...
125
+ puts params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
126
+ end
127
+ end
128
+ ```
129
+
130
+ Unit Testing:
131
+
132
+ ```ruby
133
+ class Show
134
+ include Hanami::Action
135
+
136
+ def call(params)
137
+ # ...
138
+ puts params # => { id: 23, key: 'value' } passed as it is from testing
139
+ end
140
+ end
141
+
142
+ action = Show.new
143
+ response = action.call({ id: 23, key: 'value' })
144
+ ```
145
+
146
+ #### Whitelisting
147
+
148
+ Params represent an untrusted input.
149
+ For security reasons it's recommended to whitelist them.
150
+
151
+ ```ruby
152
+ require 'hanami/controller'
153
+
154
+ class Signup
155
+ include Hanami::Action
156
+
157
+ params do
158
+ param :first_name
159
+ param :last_name
160
+ param :email
161
+
162
+ param :address do
163
+ param :line_one
164
+ param :state
165
+ param :country
166
+ end
167
+ end
168
+
169
+ def call(params)
170
+ # Describe inheritance hierarchy
171
+ puts params.class # => Signup::Params
172
+ puts params.class.superclass # => Hanami::Action::Params
173
+
174
+ # Whitelist :first_name, but not :admin
175
+ puts params[:first_name] # => "Luca"
176
+ puts params[:admin] # => nil
177
+
178
+ # Whitelist nested params [:address][:line_one], not [:address][:line_two]
179
+ puts params[:address][:line_one] # => '69 Tender St'
180
+ puts params[:address][:line_two] # => nil
181
+ end
182
+ end
183
+ ```
184
+
185
+ #### Validations & Coercions
26
186
 
27
- ## Development
187
+ Because params are a well defined set of data required to fulfill a feature
188
+ in your application, you can validate them. So you can avoid hitting lower MVC layers
189
+ when params are invalid.
28
190
 
29
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
191
+ If you specify the `:type` option, the param will be coerced.
192
+
193
+ ```ruby
194
+ require 'hanami/controller'
195
+
196
+ class Signup
197
+ MEGABYTE = 1024 ** 2
198
+ include Hanami::Action
199
+
200
+ params do
201
+ param :first_name, presence: true
202
+ param :last_name, presence: true
203
+ param :email, presence: true, format: /@/, confirmation: true
204
+ param :password, presence: true, confirmation: true
205
+ param :terms_of_service, acceptance: true
206
+ param :avatar, size: 0..(MEGABYTE * 3)
207
+ param :age, type: Integer, size: 18..99
208
+ end
209
+
210
+ def call(params)
211
+ halt 400 unless params.valid?
212
+ # ...
213
+ end
214
+ end
215
+
216
+ action = Signup.new
217
+
218
+ action.call(valid_params) # => [200, {}, ...]
219
+ action.errors.empty? # => true
220
+
221
+ action.call(invalid_params) # => [400, {}, ...]
222
+ action.errors # => #<Hanami::Validations::Errors:0x007fabe4b433d0 @errors={...}>
223
+
224
+ action.errors.for(:email)
225
+ # => [#<Hanami::Validations::Error:0x007fabe4b432e0 @attribute=:email, @validation=:presence, @expected=true, @actual=nil>]
226
+ ```
227
+
228
+ ### Response
229
+
230
+ The output of `#call` is a serialized Rack::Response (see [#finish](http://rubydoc.info/github/rack/rack/master/Rack/Response#finish-instance_method)):
231
+
232
+ ```ruby
233
+ class Show
234
+ include Hanami::Action
235
+
236
+ def call(params)
237
+ # ...
238
+ end
239
+ end
240
+
241
+ action = Show.new
242
+ action.call({}) # => [200, {}, [""]]
243
+ ```
244
+
245
+ It has private accessors to explicitly set status, headers, and body:
246
+
247
+ ```ruby
248
+ class Show
249
+ include Hanami::Action
250
+
251
+ def call(params)
252
+ self.status = 201
253
+ self.body = 'Hi!'
254
+ self.headers.merge!({ 'X-Custom' => 'OK' })
255
+ end
256
+ end
257
+
258
+ action = Show.new
259
+ action.call({}) # => [201, { "X-Custom" => "OK" }, ["Hi!"]]
260
+ ```
261
+
262
+ ### Exposures
263
+
264
+ We know that actions are objects and `Hanami::Action` respects one of the pillars of OOP: __encapsulation__.
265
+ Other frameworks extract instance variables (`@ivar`) and make them available to the view context.
266
+
267
+ `Hanami::Action`'s solution is the simple and powerful DSL: `expose`.
268
+ It's a thin layer on top of `attr_reader`.
269
+
270
+ Using `expose` creates a getter for the given attribute, and adds it to the _exposures_.
271
+ Exposures (`#exposures`) are a set of attributes exposed to the view.
272
+ That is to say the variables necessary for rendering a view.
273
+
274
+ By default, all `Hanami::Action` objects expose `#params` and `#errors`.
275
+
276
+ ```ruby
277
+ class Show
278
+ include Hanami::Action
279
+
280
+ expose :article
281
+
282
+ def call(params)
283
+ @article = Article.find params[:id]
284
+ end
285
+ end
286
+
287
+ action = Show.new
288
+ action.call({ id: 23 })
289
+
290
+ assert_equal 23, action.article.id
291
+
292
+ puts action.exposures # => { article: <Article:0x007f965c1d0318 @id=23> }
293
+ ```
294
+
295
+ ### Callbacks
296
+
297
+ It offers a powerful, inheritable callback chain which is executed before and/or after your `#call` method invocation:
298
+
299
+ ```ruby
300
+ class Show
301
+ include Hanami::Action
302
+
303
+ before :authenticate, :set_article
304
+
305
+ def call(params)
306
+ end
307
+
308
+ private
309
+ def authenticate
310
+ # ...
311
+ end
312
+
313
+ # `params` in the method signature is optional
314
+ def set_article(params)
315
+ @article = Article.find params[:id]
316
+ end
317
+ end
318
+ ```
319
+
320
+ Callbacks can also be expressed as anonymous lambdas:
321
+
322
+ ```ruby
323
+ class Show
324
+ include Hanami::Action
325
+
326
+ before { ... } # do some authentication stuff
327
+ before { |params| @article = Article.find params[:id] }
328
+
329
+ def call(params)
330
+ end
331
+ end
332
+ ```
30
333
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
334
+ ### Exceptions management
335
+
336
+ When an exception is raised, it automatically sets the HTTP status to [500](http://httpstatus.es/500):
337
+
338
+ ```ruby
339
+ class Show
340
+ include Hanami::Action
341
+
342
+ def call(params)
343
+ raise
344
+ end
345
+ end
346
+
347
+ action = Show.new
348
+ action.call({}) # => [500, {}, ["Internal Server Error"]]
349
+ ```
350
+
351
+ You can map a specific raised exception to a different HTTP status.
352
+
353
+ ```ruby
354
+ class Show
355
+ include Hanami::Action
356
+ handle_exception RecordNotFound => 404
357
+
358
+ def call(params)
359
+ @article = Article.find params[:id]
360
+ end
361
+ end
362
+
363
+ action = Show.new
364
+ action.call({id: 'unknown'}) # => [404, {}, ["Not Found"]]
365
+ ```
366
+
367
+ You can also define custom handlers for exceptions.
368
+
369
+ ```ruby
370
+ class Create
371
+ include Hanami::Action
372
+ handle_exception ArgumentError => :my_custom_handler
373
+
374
+ def call(params)
375
+ raise ArgumentError.new("Invalid arguments")
376
+ end
377
+
378
+ private
379
+ def my_custom_handler(exception)
380
+ status 400, exception.message
381
+ end
382
+ end
383
+
384
+ action = Create.new
385
+ action.call({}) # => [400, {}, ["Invalid arguments"]]
386
+ ```
387
+
388
+ Exception policies can be defined globally, **before** the controllers/actions
389
+ are loaded.
390
+
391
+ ```ruby
392
+ Hanami::Controller.configure do
393
+ handle_exception RecordNotFound => 404
394
+ end
395
+
396
+ class Show
397
+ include Hanami::Action
398
+
399
+ def call(params)
400
+ @article = Article.find params[:id]
401
+ end
402
+ end
403
+
404
+ action = Show.new
405
+ action.call({id: 'unknown'}) # => [404, {}, ["Not Found"]]
406
+ ```
407
+
408
+ This feature can be turned off globally, in a controller or in a single action.
409
+
410
+ ```ruby
411
+ Hanami::Controller.configure do
412
+ handle_exceptions false
413
+ end
414
+
415
+ # or
416
+
417
+ module Articles
418
+ class Show
419
+ include Hanami::Action
420
+
421
+ configure do
422
+ handle_exceptions false
423
+ end
424
+
425
+ def call(params)
426
+ @article = Article.find params[:id]
427
+ end
428
+ end
429
+ end
430
+
431
+ action = Articles::Show.new
432
+ action.call({id: 'unknown'}) # => raises RecordNotFound
433
+ ```
434
+
435
+ #### Inherited Exceptions
436
+
437
+ ```ruby
438
+ class MyCustomException < StandardError
439
+ end
440
+
441
+ module Articles
442
+ class Index
443
+ include Hanami::Action
444
+
445
+ handle_exception MyCustomException => :handle_my_exception
446
+
447
+ def call(params)
448
+ raise MyCustomException
449
+ end
450
+
451
+ private
452
+
453
+ def handle_my_exception
454
+ # ...
455
+ end
456
+ end
457
+
458
+ class Show
459
+ include Hanami::Action
460
+
461
+ handle_exception StandardError => :handle_standard_error
462
+
463
+ def call(params)
464
+ raise MyCustomException
465
+ end
466
+
467
+ private
468
+
469
+ def handle_standard_error
470
+ # ...
471
+ end
472
+ end
473
+ end
474
+
475
+ Articles::Index.new.call({}) # => `handle_my_exception` will be invoked
476
+ Articles::Show.new.call({}) # => `handle_standard_error` will be invoked,
477
+ # because `MyCustomException` inherits from `StandardError`
478
+ ```
479
+
480
+ ### Throwable HTTP statuses
481
+
482
+ When `#halt` is used with a valid HTTP code, it stops the execution and sets the proper status and body for the response:
483
+
484
+ ```ruby
485
+ class Show
486
+ include Hanami::Action
487
+
488
+ before :authenticate!
489
+
490
+ def call(params)
491
+ # ...
492
+ end
493
+
494
+ private
495
+ def authenticate!
496
+ halt 401 unless authenticated?
497
+ end
498
+ end
499
+
500
+ action = Show.new
501
+ action.call({}) # => [401, {}, ["Unauthorized"]]
502
+ ```
503
+
504
+ Alternatively, you can specify a custom message.
505
+
506
+ ```ruby
507
+ class Show
508
+ include Hanami::Action
509
+
510
+ def call(params)
511
+ DroidRepository.find(params[:id]) or not_found
512
+ end
513
+
514
+ private
515
+ def not_found
516
+ halt 404, "This is not the droid you're looking for"
517
+ end
518
+ end
519
+
520
+ action = Show.new
521
+ action.call({}) # => [404, {}, ["This is not the droid you're looking for"]]
522
+ ```
523
+
524
+ ### Cookies
525
+
526
+ Hanami::Controller offers convenient access to cookies.
527
+
528
+ They are read as a Hash from Rack env:
529
+
530
+ ```ruby
531
+ require 'hanami/controller'
532
+ require 'hanami/action/cookies'
533
+
534
+ class ReadCookiesFromRackEnv
535
+ include Hanami::Action
536
+ include Hanami::Action::Cookies
537
+
538
+ def call(params)
539
+ # ...
540
+ cookies[:foo] # => 'bar'
541
+ end
542
+ end
543
+
544
+ action = ReadCookiesFromRackEnv.new
545
+ action.call({'HTTP_COOKIE' => 'foo=bar'})
546
+ ```
547
+
548
+ They are set like a Hash:
549
+
550
+ ```ruby
551
+ require 'hanami/controller'
552
+ require 'hanami/action/cookies'
553
+
554
+ class SetCookies
555
+ include Hanami::Action
556
+ include Hanami::Action::Cookies
557
+
558
+ def call(params)
559
+ # ...
560
+ cookies[:foo] = 'bar'
561
+ end
562
+ end
563
+
564
+ action = SetCookies.new
565
+ action.call({}) # => [200, {'Set-Cookie' => 'foo=bar'}, '...']
566
+ ```
567
+
568
+ They are removed by setting their value to `nil`:
569
+
570
+ ```ruby
571
+ require 'hanami/controller'
572
+ require 'hanami/action/cookies'
573
+
574
+ class RemoveCookies
575
+ include Hanami::Action
576
+ include Hanami::Action::Cookies
577
+
578
+ def call(params)
579
+ # ...
580
+ cookies[:foo] = nil
581
+ end
582
+ end
583
+
584
+ action = RemoveCookies.new
585
+ action.call({}) # => [200, {'Set-Cookie' => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"}, '...']
586
+ ```
587
+
588
+ Default values can be set in configuration, but overriden case by case.
589
+
590
+ ```ruby
591
+ require 'hanami/controller'
592
+ require 'hanami/action/cookies'
593
+
594
+ Hanami::Controller.configure do
595
+ cookies max_age: 300 # 5 minutes
596
+ end
597
+
598
+ class SetCookies
599
+ include Hanami::Action
600
+ include Hanami::Action::Cookies
601
+
602
+ def call(params)
603
+ # ...
604
+ cookies[:foo] = { value: 'bar', max_age: 100 }
605
+ end
606
+ end
607
+
608
+ action = SetCookies.new
609
+ action.call({}) # => [200, {'Set-Cookie' => "foo=bar; max-age=100;"}, '...']
610
+ ```
611
+
612
+ ### Sessions
613
+
614
+ It has builtin support for Rack sessions:
615
+
616
+ ```ruby
617
+ require 'hanami/controller'
618
+ require 'hanami/action/session'
619
+
620
+ class ReadSessionFromRackEnv
621
+ include Hanami::Action
622
+ include Hanami::Action::Session
623
+
624
+ def call(params)
625
+ # ...
626
+ session[:age] # => '31'
627
+ end
628
+ end
629
+
630
+ action = ReadSessionFromRackEnv.new
631
+ action.call({ 'rack.session' => { 'age' => '31' }})
632
+ ```
633
+
634
+ Values can be set like a Hash:
635
+
636
+ ```ruby
637
+ require 'hanami/controller'
638
+ require 'hanami/action/session'
639
+
640
+ class SetSession
641
+ include Hanami::Action
642
+ include Hanami::Action::Session
643
+
644
+ def call(params)
645
+ # ...
646
+ session[:age] = 31
647
+ end
648
+ end
649
+
650
+ action = SetSession.new
651
+ action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."]
652
+ ```
653
+
654
+ Values can be removed like a Hash:
655
+
656
+ ```ruby
657
+ require 'hanami/controller'
658
+ require 'hanami/action/session'
659
+
660
+ class RemoveSession
661
+ include Hanami::Action
662
+ include Hanami::Action::Session
663
+
664
+ def call(params)
665
+ # ...
666
+ session[:age] = nil
667
+ end
668
+ end
669
+
670
+ action = RemoveSession.new
671
+ action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."] it removes that value from the session
672
+ ```
673
+
674
+ While Hanami::Controller supports sessions natively, it's __session store agnostic__.
675
+ You have to specify the session store in your Rack middleware configuration (eg `config.ru`).
676
+
677
+ ```ruby
678
+ use Rack::Session::Cookie, secret: SecureRandom.hex(64)
679
+ run Show.new
680
+ ```
681
+
682
+ ### Http Cache
683
+
684
+ 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
685
+
686
+ You can easily set the Cache-Control header for your actions:
687
+
688
+ ```ruby
689
+ require 'hanami/controller'
690
+ require 'hanami/action/cache'
691
+
692
+ class HttpCacheController
693
+ include Hanami::Action
694
+ include Hanami::Action::Cache
695
+
696
+ cache_control :public, max_age: 600 # => Cache-Control: public, max-age=600
697
+
698
+ def call(params)
699
+ # ...
700
+ end
701
+ end
702
+ ```
703
+
704
+ Expires header can be specified using `expires` method:
705
+
706
+ ```ruby
707
+ require 'hanami/controller'
708
+ require 'hanami/action/cache'
709
+
710
+ class HttpCacheController
711
+ include Hanami::Action
712
+ include Hanami::Action::Cache
713
+
714
+ expires 60, :public, max_age: 600 # => Expires: Sun, 03 Aug 2014 17:47:02 GMT, Cache-Control: public, max-age=600
715
+
716
+ def call(params)
717
+ # ...
718
+ end
719
+ end
720
+ ```
721
+
722
+ ### Conditional Get
723
+
724
+ According to HTTP specification, conditional GETs provide a way for web servers to inform clients that the response to a GET request hasn't change since the last request returning a Not Modified header (304).
725
+
726
+ 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.
727
+
728
+ You can easily take advantage of Conditional Get using `#fresh` method:
729
+
730
+ ```ruby
731
+ require 'hanami/controller'
732
+ require 'hanami/action/cache'
733
+
734
+ class ConditionalGetController
735
+ include Hanami::Action
736
+ include Hanami::Action::Cache
737
+
738
+ def call(params)
739
+ # ...
740
+ fresh etag: @resource.cache_key
741
+ # => halt 304 with header IfNoneMatch = @resource.cache_key
742
+ end
743
+ end
744
+ ```
745
+
746
+ If `@resource.cache_key` is equal to `IfNoneMatch` header, then hanami will `halt 304`.
747
+
748
+ The same behavior is accomplished using `last_modified`:
749
+
750
+ ```ruby
751
+ require 'hanami/controller'
752
+ require 'hanami/action/cache'
753
+
754
+ class ConditionalGetController
755
+ include Hanami::Action
756
+ include Hanami::Action::Cache
757
+
758
+ def call(params)
759
+ # ...
760
+ fresh last_modified: @resource.update_at
761
+ # => halt 304 with header IfModifiedSince = @resource.update_at.httpdate
762
+ end
763
+ end
764
+ ```
765
+
766
+ If `@resource.update_at` is equal to `IfModifiedSince` header, then hanami will `halt 304`.
767
+
768
+ ### Redirect
769
+
770
+ If you need to redirect the client to another resource, use `#redirect_to`:
771
+
772
+ ```ruby
773
+ class Create
774
+ include Hanami::Action
775
+
776
+ def call(params)
777
+ # ...
778
+ redirect_to 'http://example.com/articles/23'
779
+ end
780
+ end
781
+
782
+ action = Create.new
783
+ action.call({ article: { title: 'Hello' }}) # => [302, {'Location' => '/articles/23'}, '']
784
+ ```
785
+
786
+ You can also redirect with a custom status code:
787
+
788
+ ```ruby
789
+ class Create
790
+ include Hanami::Action
791
+
792
+ def call(params)
793
+ # ...
794
+ redirect_to 'http://example.com/articles/23', status: 301
795
+ end
796
+ end
797
+
798
+ action = Create.new
799
+ action.call({ article: { title: 'Hello' }}) # => [301, {'Location' => '/articles/23'}, '']
800
+ ```
801
+
802
+ ### MIME Types
803
+
804
+ `Hanami::Action` automatically sets the `Content-Type` header, according to the request.
805
+
806
+ ```ruby
807
+ class Show
808
+ include Hanami::Action
809
+
810
+ def call(params)
811
+ end
812
+ end
813
+
814
+ action = Show.new
815
+
816
+ action.call({ 'HTTP_ACCEPT' => '*/*' }) # Content-Type "application/octet-stream"
817
+ action.format # :all
818
+
819
+ action.call({ 'HTTP_ACCEPT' => 'text/html' }) # Content-Type "text/html"
820
+ action.format # :html
821
+ ```
822
+
823
+ However, you can force this value:
824
+
825
+ ```ruby
826
+ class Show
827
+ include Hanami::Action
828
+
829
+ def call(params)
830
+ # ...
831
+ self.format = :json
832
+ end
833
+ end
834
+
835
+ action = Show.new
836
+
837
+ action.call({ 'HTTP_ACCEPT' => '*/*' }) # Content-Type "application/json"
838
+ action.format # :json
839
+
840
+ action.call({ 'HTTP_ACCEPT' => 'text/html' }) # Content-Type "application/json"
841
+ action.format # :json
842
+ ```
843
+
844
+ You can restrict the accepted MIME types:
845
+
846
+ ```ruby
847
+ class Show
848
+ include Hanami::Action
849
+ accept :html, :json
850
+
851
+ def call(params)
852
+ # ...
853
+ end
854
+ end
855
+
856
+ # When called with "\*/\*" => 200
857
+ # When called with "text/html" => 200
858
+ # When called with "application/json" => 200
859
+ # When called with "application/xml" => 406
860
+ ```
861
+
862
+ You can check if the requested MIME type is accepted by the client.
863
+
864
+ ```ruby
865
+ class Show
866
+ include Hanami::Action
867
+
868
+ def call(params)
869
+ # ...
870
+ # @_env['HTTP_ACCEPT'] # => 'text/html,application/xhtml+xml,application/xml;q=0.9'
871
+
872
+ accept?('text/html') # => true
873
+ accept?('application/xml') # => true
874
+ accept?('application/json') # => false
875
+ self.format # :html
876
+
877
+
878
+
879
+ # @_env['HTTP_ACCEPT'] # => '*/*'
880
+
881
+ accept?('text/html') # => true
882
+ accept?('application/xml') # => true
883
+ accept?('application/json') # => true
884
+ self.format # :html
885
+ end
886
+ end
887
+ ```
888
+
889
+ Hanami::Controller is shipped with an extensive list of the most common MIME types.
890
+ Also, you can register your own:
891
+
892
+ ```ruby
893
+ Hanami::Controller.configure do
894
+ format custom: 'application/custom'
895
+ end
896
+
897
+ class Index
898
+ include Hanami::Action
899
+
900
+ def call(params)
901
+ end
902
+ end
903
+
904
+ action = Index.new
905
+
906
+ action.call({ 'HTTP_ACCEPT' => 'application/custom' }) # => Content-Type 'application/custom'
907
+ action.format # => :custom
908
+
909
+ class Show
910
+ include Hanami::Action
911
+
912
+ def call(params)
913
+ # ...
914
+ self.format = :custom
915
+ end
916
+ end
917
+
918
+ action = Show.new
919
+
920
+ action.call({ 'HTTP_ACCEPT' => '*/*' }) # => Content-Type 'application/custom'
921
+ action.format # => :custom
922
+ ```
923
+
924
+ ### Streamed Responses
925
+
926
+ 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.
927
+
928
+ ```ruby
929
+ Hanami::Controller.configure do
930
+ format csv: 'text/csv'
931
+ middleware.use ::Rack::Chunked
932
+ end
933
+
934
+ class Csv
935
+ include Hanami::Action
936
+
937
+ def call(params)
938
+ self.format = :csv
939
+ self.body = Enumerator.new do |yielder|
940
+ yielder << csv_header
941
+
942
+ # Expensive operation is streamed as each line becomes available
943
+ csv_body.each_line do |line|
944
+ yielder << line
945
+ end
946
+ end
947
+ end
948
+ end
949
+ ```
950
+
951
+ Note:
952
+ * In development, Hanami' code reloading needs to be disabled for streaming to work. This is because `Shotgun` interferes with the streaming action. You can disable it like this `hanami server --code-reloading=false`
953
+ * Streaming does not work with WEBrick as it buffers its response. We recommend using `puma`, though you may find success with other servers
954
+
955
+ ### No rendering, please
956
+
957
+ Hanami::Controller is designed to be a pure HTTP endpoint, rendering belongs to other layers of MVC.
958
+ You can set the body directly (see [response](#response)), or use [Hanami::View](https://github.com/hanami/view).
959
+
960
+ ### Controllers
961
+
962
+ A Controller is nothing more than a logical group of actions: just a Ruby module.
963
+
964
+ ```ruby
965
+ module Articles
966
+ class Index
967
+ include Hanami::Action
968
+
969
+ # ...
970
+ end
971
+
972
+ class Show
973
+ include Hanami::Action
974
+
975
+ # ...
976
+ end
977
+ end
978
+
979
+ Articles::Index.new.call({})
980
+ ```
981
+
982
+ ### Hanami::Router integration
983
+
984
+ While Hanami::Router works great with this framework, Hanami::Controller doesn't depend on it.
985
+ You, the developer, are free to choose your own routing system.
986
+
987
+ But, if you use them together, the **only constraint is that an action must support _arity 0_ in its constructor**.
988
+ The following examples are valid constructors:
989
+
990
+ ```ruby
991
+ def initialize
992
+ end
993
+
994
+ def initialize(repository = Article)
995
+ end
996
+
997
+ def initialize(repository: Article)
998
+ end
999
+
1000
+ def initialize(options = {})
1001
+ end
1002
+
1003
+ def initialize(*args)
1004
+ end
1005
+ ```
1006
+
1007
+ __Please note that this is subject to change: we're working to remove this constraint.__
1008
+
1009
+ Hanami::Router supports lazy loading for controllers. While this policy can be a
1010
+ convenient fallback, you should know that it's the slower option. **Be sure of
1011
+ loading your controllers before you initialize the router.**
1012
+
1013
+
1014
+ ### Rack integration
1015
+
1016
+ Hanami::Controller is compatible with Rack. However, it doesn't mount any middleware.
1017
+ While a Hanami application's architecture is more web oriented, this framework is designed to build pure HTTP endpoints.
1018
+
1019
+ ### Rack middleware
1020
+
1021
+ Rack middleware can be configured globally in `config.ru`. However, consider that they often add
1022
+ unnecessary overhead for *all* endpoints that aren't direct users of all the configured middleware.
1023
+
1024
+ 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.
1025
+
1026
+ The solution is that an action can employ one or more Rack middleware, with `.use`.
1027
+
1028
+ ```ruby
1029
+ require 'hanami/controller'
1030
+
1031
+ module Sessions
1032
+ class Create
1033
+ include Hanami::Action
1034
+ use OmniAuth
1035
+
1036
+ def call(params)
1037
+ # ...
1038
+ end
1039
+ end
1040
+ end
1041
+ ```
1042
+
1043
+ ```ruby
1044
+ require 'hanami/controller'
1045
+
1046
+ module Sessions
1047
+ class Create
1048
+ include Hanami::Controller
1049
+
1050
+ use XMiddleware.new('x', 123)
1051
+ use YMiddleware.new
1052
+ use ZMiddleware
1053
+
1054
+ def call(params)
1055
+ # ...
1056
+ end
1057
+ end
1058
+ end
1059
+ ```
1060
+
1061
+ ### Configuration
1062
+
1063
+ Hanami::Controller can be configured with a DSL.
1064
+ It supports a few options:
1065
+
1066
+ ```ruby
1067
+ require 'hanami/controller'
1068
+
1069
+ Hanami::Controller.configure do
1070
+ # Handle exceptions with HTTP statuses (true) or don't catch them (false)
1071
+ # Argument: boolean, defaults to `true`
1072
+ #
1073
+ handle_exceptions true
1074
+
1075
+ # If the given exception is raised, return that HTTP status
1076
+ # It can be used multiple times
1077
+ # Argument: hash, empty by default
1078
+ #
1079
+ handle_exception ArgumentError => 404
1080
+
1081
+ # Register a format to MIME type mapping
1082
+ # Argument: hash, key: format symbol, value: MIME type string, empty by default
1083
+ #
1084
+ format custom: 'application/custom'
1085
+
1086
+ # Define a fallback format to detect in case of HTTP request with `Accept: */*`
1087
+ # If not defined here, it will return Rack's default: `application/octet-stream`
1088
+ # Argument: symbol, it should be already known. defaults to `nil`
1089
+ #
1090
+ default_request_format :html
1091
+
1092
+ # Define a default format to set as `Content-Type` header for response,
1093
+ # unless otherwise specified.
1094
+ # If not defined here, it will return Rack's default: `application/octet-stream`
1095
+ # Argument: symbol, it should be already known. defaults to `nil`
1096
+ #
1097
+ default_response_format :html
1098
+
1099
+ # Define a default charset to return in the `Content-Type` response header
1100
+ # If not defined here, it returns `utf-8`
1101
+ # Argument: string, defaults to `nil`
1102
+ #
1103
+ default_charset 'koi8-r'
1104
+
1105
+ # Configure the logic to be executed when Hanami::Action is included
1106
+ # This is useful to DRY code by having a single place where to configure
1107
+ # shared behaviors like authentication, sessions, cookies etc.
1108
+ # Argument: proc
1109
+ #
1110
+ prepare do
1111
+ include Hanami::Action::Sessions
1112
+ include MyAuthentication
1113
+ use SomeMiddleWare
1114
+
1115
+ before { authenticate! }
1116
+ end
1117
+ end
1118
+ ```
1119
+
1120
+ All of the global configurations can be overwritten at the controller level.
1121
+ Each controller and action has its own copy of the global configuration.
1122
+
1123
+ This means changes are inherited from the top to the bottom, but do not bubble back up.
1124
+
1125
+ ```ruby
1126
+ require 'hanami/controller'
1127
+
1128
+ Hanami::Controller.configure do
1129
+ handle_exception ArgumentError => 400
1130
+ end
1131
+
1132
+ module Articles
1133
+ class Create
1134
+ include Hanami::Action
1135
+
1136
+ configure do
1137
+ handle_exceptions false
1138
+ end
1139
+
1140
+ def call(params)
1141
+ raise ArgumentError
1142
+ end
1143
+ end
1144
+ end
1145
+
1146
+ module Users
1147
+ class Create
1148
+ include Hanami::Action
1149
+
1150
+ def call(params)
1151
+ raise ArgumentError
1152
+ end
1153
+ end
1154
+ end
1155
+
1156
+ Users::Create.new.call({}) # => HTTP 400
1157
+
1158
+ Articles::Create.new.call({})
1159
+ # => raises ArgumentError because we set handle_exceptions to false
1160
+ ```
1161
+
1162
+ ### Thread safety
1163
+
1164
+ An Action is **mutable**. When used without Hanami::Router, be sure to instantiate an
1165
+ action for each request. The same advice applies when using
1166
+ Hanami::Router but NOT routing to `mycontroller#myaction` but instead
1167
+ routing direct to a class.
1168
+
1169
+ ```ruby
1170
+ # config.ru
1171
+ require 'hanami/controller'
1172
+
1173
+ class Action
1174
+ include Hanami::Action
1175
+
1176
+ def self.call(env)
1177
+ new.call(env)
1178
+ end
1179
+
1180
+ def call(params)
1181
+ self.body = object_id.to_s
1182
+ end
1183
+ end
1184
+
1185
+ run Action
1186
+ ```
1187
+
1188
+ Hanami::Controller heavely depends on class configuration. To ensure immutability
1189
+ in deployment environments, use `Hanami::Controller.load!`.
1190
+
1191
+ ## Versioning
1192
+
1193
+ __Hanami::Controller__ uses [Semantic Versioning 2.0.0](http://semver.org)
32
1194
 
33
1195
  ## Contributing
34
1196
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hanami-controller.
1197
+ 1. Fork it
1198
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
1199
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
1200
+ 4. Push to the branch (`git push origin my-new-feature`)
1201
+ 5. Create new Pull Request
1202
+
1203
+ ## Copyright
1204
+
1205
+ Copyright © 2014-2016 Luca Guidi – Released under MIT License
36
1206
 
1207
+ This project was formerly known as Lotus (`lotus-controller`).