hanami-controller 1.3.3 → 2.0.0.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -7
  3. data/README.md +295 -537
  4. data/hanami-controller.gemspec +3 -3
  5. data/lib/hanami/action.rb +653 -38
  6. data/lib/hanami/action/base_params.rb +2 -2
  7. data/lib/hanami/action/cache.rb +1 -139
  8. data/lib/hanami/action/cache/cache_control.rb +4 -4
  9. data/lib/hanami/action/cache/conditional_get.rb +4 -5
  10. data/lib/hanami/action/cache/directives.rb +1 -1
  11. data/lib/hanami/action/cache/expires.rb +3 -3
  12. data/lib/hanami/action/cookie_jar.rb +3 -3
  13. data/lib/hanami/action/cookies.rb +3 -62
  14. data/lib/hanami/action/flash.rb +2 -2
  15. data/lib/hanami/action/glue.rb +5 -31
  16. data/lib/hanami/action/halt.rb +12 -0
  17. data/lib/hanami/action/mime.rb +77 -491
  18. data/lib/hanami/action/params.rb +3 -3
  19. data/lib/hanami/action/rack/file.rb +1 -1
  20. data/lib/hanami/action/request.rb +30 -20
  21. data/lib/hanami/action/response.rb +174 -0
  22. data/lib/hanami/action/session.rb +8 -117
  23. data/lib/hanami/action/validatable.rb +2 -2
  24. data/lib/hanami/controller.rb +0 -210
  25. data/lib/hanami/controller/configuration.rb +51 -506
  26. data/lib/hanami/controller/version.rb +1 -1
  27. metadata +12 -21
  28. data/lib/hanami/action/callable.rb +0 -92
  29. data/lib/hanami/action/callbacks.rb +0 -214
  30. data/lib/hanami/action/configurable.rb +0 -50
  31. data/lib/hanami/action/exposable.rb +0 -126
  32. data/lib/hanami/action/exposable/guard.rb +0 -104
  33. data/lib/hanami/action/head.rb +0 -121
  34. data/lib/hanami/action/rack.rb +0 -411
  35. data/lib/hanami/action/rack/callable.rb +0 -47
  36. data/lib/hanami/action/rack/errors.rb +0 -53
  37. data/lib/hanami/action/redirect.rb +0 -59
  38. data/lib/hanami/action/throwable.rb +0 -169
@@ -1,126 +0,0 @@
1
- require 'hanami/action/exposable/guard'
2
-
3
- module Hanami
4
- module Action
5
- # Exposures API
6
- #
7
- # @since 0.1.0
8
- #
9
- # @see Hanami::Action::Exposable::ClassMethods#expose
10
- module Exposable
11
- # Override Ruby's hook for modules.
12
- # It includes exposures logic
13
- #
14
- # @param base [Class] the target action
15
- #
16
- # @since 0.1.0
17
- # @api private
18
- #
19
- # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
20
- def self.included(base)
21
- base.class_eval do
22
- extend ClassMethods
23
- include Guard
24
-
25
- _expose :params
26
- end
27
- end
28
-
29
- # Exposures API class methods
30
- #
31
- # @since 0.1.0
32
- # @api private
33
- module ClassMethods
34
- # Expose the given attributes on the outside of the object with
35
- # a getter and a special method called #exposures.
36
- #
37
- # @param names [Array<Symbol>] the name(s) of the attribute(s) to be
38
- # exposed
39
- #
40
- # @return [void]
41
- #
42
- # @since 0.1.0
43
- #
44
- # @example
45
- # require 'hanami/controller'
46
- #
47
- # class Show
48
- # include Hanami::Action
49
- #
50
- # expose :article, :tags
51
- #
52
- # def call(params)
53
- # @article = Article.find params[:id]
54
- # @tags = Tag.for(article)
55
- # end
56
- # end
57
- #
58
- # action = Show.new
59
- # action.call({id: 23})
60
- #
61
- # action.article # => #<Article ...>
62
- # action.tags # => [#<Tag ...>, #<Tag ...>]
63
- #
64
- # action.exposures # => { :article => #<Article ...>, :tags => [ ... ] }
65
- def expose(*names)
66
- class_eval do
67
- names.each do |name|
68
- attr_reader(name) unless attr_reader?(name)
69
- end
70
-
71
- exposures.push(*names)
72
- end
73
- end
74
-
75
- # Alias of #expose to be used in internal modules.
76
- # #_expose is not watched by the Guard
77
- alias _expose expose
78
-
79
- # Set of exposures attribute names
80
- #
81
- # @return [Array] the exposures attribute names
82
- #
83
- # @since 0.1.0
84
- # @api private
85
- def exposures
86
- @exposures ||= []
87
- end
88
-
89
- private
90
- # Check if the attr_reader is already defined
91
- #
92
- # @since 0.3.0
93
- # @api private
94
- def attr_reader?(name)
95
- (instance_methods | private_instance_methods).include?(name)
96
- end
97
- end
98
-
99
- # Set of exposures
100
- #
101
- # @return [Hash] the exposures
102
- #
103
- # @since 0.1.0
104
- #
105
- # @see Hanami::Action::Exposable::ClassMethods.expose
106
- def exposures
107
- @exposures ||= {}.tap do |result|
108
- self.class.exposures.each do |name|
109
- result[name] = send(name)
110
- end
111
- end
112
- end
113
-
114
- # Finalize the response
115
- #
116
- # @since 0.3.0
117
- # @api private
118
- #
119
- # @see Hanami::Action#finish
120
- def finish
121
- super
122
- exposures
123
- end
124
- end
125
- end
126
- end
@@ -1,104 +0,0 @@
1
- require 'hanami/controller/error'
2
-
3
- module Hanami
4
- module Controller
5
- # Exposure of reserved words
6
- #
7
- # @since 0.7.1
8
- class IllegalExposureError < Error
9
- end
10
- end
11
-
12
- module Action
13
- module Exposable
14
- # Guard for Exposures API.
15
- # Prevents exposure of reserved words
16
- #
17
- # @since 0.7.1
18
- # @api private
19
- #
20
- # @see Hanami::Action::Exposable::Guard::ClassMethods#expose
21
- # @see Hanami::Action::Exposable::Guard::ClassMethods#reserved_word?
22
- module Guard
23
- # Override Ruby's hook for modules.
24
- # It prepends a guard for the exposures logic
25
- #
26
- # @param base [Class] the target action
27
- #
28
- # @since 0.7.1
29
- # @api private
30
- #
31
- # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
32
- def self.included(base)
33
- class << base
34
- prepend ClassMethods
35
- end
36
- end
37
-
38
- # Exposures API Guard class methods
39
- #
40
- # @since 0.7.1
41
- # @api private
42
- module ClassMethods
43
- # Prevents exposure if names contain a reserved word.
44
- #
45
- # @param names [Array<Symbol>] the name(s) of the attribute(s) to be
46
- # exposed
47
- #
48
- # @return [void]
49
- #
50
- # @since 0.7.1
51
- # @api private
52
- def expose(*names)
53
- detect_reserved_words!(names)
54
-
55
- super
56
- end
57
-
58
- private
59
-
60
- # Raises error if given names contain a reserved word.
61
- #
62
- # @param names [Array<Symbol>] a list of names to be checked.
63
- #
64
- # @return [void]
65
- #
66
- # @raise [IllegalExposeError] if names contain one or more of reserved
67
- # words
68
- #
69
- # @since 0.7.1
70
- # @api private
71
- def detect_reserved_words!(names)
72
- names.each do |name|
73
- if reserved_word?(name)
74
- raise Hanami::Controller::IllegalExposureError.new("#{name} is a reserved word. It cannot be exposed")
75
- end
76
- end
77
- end
78
-
79
- # Checks if a string is a reserved word
80
- #
81
- # Reserved word is a name of the method defined in one of the modules
82
- # of a given namespace.
83
- #
84
- # @param name [Symbol] the word to be checked
85
- # @param namespace [String] the namespace containing internal modules
86
- #
87
- # @return [true, false]
88
- #
89
- # @since 0.7.1
90
- # @api private
91
- def reserved_word?(name, namespace = 'Hanami')
92
- if method_defined?(name) || private_method_defined?(name)
93
- method_owner = instance_method(name).owner
94
-
95
- Utils::String.namespace(method_owner) == namespace
96
- else
97
- false
98
- end
99
- end
100
- end
101
- end
102
- end
103
- end
104
- end
@@ -1,121 +0,0 @@
1
- module Hanami
2
- module Action
3
- # Ensures to not send body or headers for HEAD requests and/or for status
4
- # codes that doesn't allow them.
5
- #
6
- # @since 0.3.2
7
- #
8
- # @see http://www.ietf.org/rfc/rfc2616.txt
9
- module Head
10
-
11
- # Status codes that by RFC must not include a message body
12
- #
13
- # @since 0.3.2
14
- # @api private
15
- HTTP_STATUSES_WITHOUT_BODY = Set.new((100..199).to_a << 204 << 205 << 304).freeze
16
-
17
-
18
- # Entity headers allowed in blank body responses, according to
19
- # RFC 2616 - Section 10 (HTTP 1.1).
20
- #
21
- # "The response MAY include new or updated metainformation in the form
22
- # of entity-headers".
23
- #
24
- # @since 0.4.0
25
- # @api private
26
- #
27
- # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
28
- # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html
29
- ENTITY_HEADERS = {
30
- 'Allow' => true,
31
- 'Content-Encoding' => true,
32
- 'Content-Language' => true,
33
- 'Content-Location' => true,
34
- 'Content-MD5' => true,
35
- 'Content-Range' => true,
36
- 'Expires' => true,
37
- 'Last-Modified' => true,
38
- 'extension-header' => true
39
- }.freeze
40
-
41
- # Ensures to not send body or headers for HEAD requests and/or for status
42
- # codes that doesn't allow them.
43
- #
44
- # @since 0.3.2
45
- # @api private
46
- #
47
- # @see Hanami::Action#finish
48
- def finish
49
- super
50
-
51
- if _requires_no_body?
52
- @_body = nil
53
- @headers.reject! {|header,_| !keep_response_header?(header) }
54
- end
55
- end
56
-
57
- protected
58
- # @since 0.3.2
59
- # @api private
60
- def _requires_no_body?
61
- HTTP_STATUSES_WITHOUT_BODY.include?(@_status) || head?
62
- end
63
-
64
- private
65
- # According to RFC 2616, when a response MUST have an empty body, it only
66
- # allows Entity Headers.
67
- #
68
- # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
69
- # other custom header.
70
- #
71
- # This restriction is enforced by <tt>Hanami::Action::Head#finish</tt>.
72
- #
73
- # However, there are cases that demand to bypass this rule to set meta
74
- # informations via headers.
75
- #
76
- # An example is a <tt>DELETE</tt> request for a JSON API application.
77
- # It returns a <tt>204</tt> but still wants to specify the rate limit
78
- # quota via <tt>X-Rate-Limit</tt>.
79
- #
80
- # @since 0.5.0
81
- #
82
- # @see Hanami::Action::HEAD#finish
83
- #
84
- # @example
85
- # require 'hanami/controller'
86
- #
87
- # module Books
88
- # class Destroy
89
- # include Hanami::Action
90
- #
91
- # def call(params)
92
- # # ...
93
- # self.headers.merge!(
94
- # 'Last-Modified' => 'Fri, 27 Nov 2015 13:32:36 GMT',
95
- # 'X-Rate-Limit' => '4000',
96
- # 'Content-Type' => 'application/json',
97
- # 'X-No-Pass' => 'true'
98
- # )
99
- #
100
- # self.status = 204
101
- # end
102
- #
103
- # private
104
- #
105
- # def keep_response_header?(header)
106
- # super || header == 'X-Rate-Limit'
107
- # end
108
- # end
109
- # end
110
- #
111
- # # Only the following headers will be sent:
112
- # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
113
- # # * X-Rate-Limit - because we explicitely allow it
114
- #
115
- # # Both Content-Type and X-No-Pass are removed because they're not allowed
116
- def keep_response_header?(header)
117
- ENTITY_HEADERS.include?(header)
118
- end
119
- end
120
- end
121
- end
@@ -1,411 +0,0 @@
1
- require 'securerandom'
2
- require 'hanami/action/request'
3
- require 'hanami/action/base_params'
4
- require 'hanami/action/rack/callable'
5
- require 'hanami/action/rack/file'
6
- require 'hanami/utils/deprecation'
7
-
8
- module Hanami
9
- module Action
10
- # Rack integration API
11
- #
12
- # @since 0.1.0
13
- module Rack
14
- # Rack SPEC response code
15
- #
16
- # @since 1.0.0
17
- # @api private
18
- RESPONSE_CODE = 0
19
-
20
- # Rack SPEC response headers
21
- #
22
- # @since 1.0.0
23
- # @api private
24
- RESPONSE_HEADERS = 1
25
-
26
- # Rack SPEC response body
27
- #
28
- # @since 1.0.0
29
- # @api private
30
- RESPONSE_BODY = 2
31
-
32
- # The default HTTP response code
33
- #
34
- # @since 0.1.0
35
- # @api private
36
- DEFAULT_RESPONSE_CODE = 200
37
-
38
- # Not Found
39
- #
40
- # @since 1.0.0
41
- # @api private
42
- NOT_FOUND = 404
43
-
44
- # The default Rack response body
45
- #
46
- # @since 0.1.0
47
- # @api private
48
- DEFAULT_RESPONSE_BODY = []
49
-
50
- # The default HTTP Request ID length
51
- #
52
- # @since 0.3.0
53
- # @api private
54
- #
55
- # @see Hanami::Action::Rack#request_id
56
- DEFAULT_REQUEST_ID_LENGTH = 16
57
-
58
- # The request method
59
- #
60
- # @since 0.3.2
61
- # @api private
62
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
63
-
64
- # The Content-Length HTTP header
65
- #
66
- # @since 1.0.0
67
- # @api private
68
- CONTENT_LENGTH = 'Content-Length'.freeze
69
-
70
- # The non-standard HTTP header to pass the control over when a resource
71
- # cannot be found by the current endpoint
72
- #
73
- # @since 1.0.0
74
- # @api private
75
- X_CASCADE = 'X-Cascade'.freeze
76
-
77
- # HEAD request
78
- #
79
- # @since 0.3.2
80
- # @api private
81
- HEAD = 'HEAD'.freeze
82
-
83
- # The key that returns router parsed body from the Rack env
84
- ROUTER_PARSED_BODY = 'router.parsed_body'.freeze
85
-
86
- # This is the root directory for `#unsafe_send_file`
87
- #
88
- # @since 1.3.3
89
- # @api private
90
- #
91
- # @see #unsafe_send_file
92
- FILE_SYSTEM_ROOT = Pathname.new("/").freeze
93
-
94
- # Override Ruby's hook for modules.
95
- # It includes basic Hanami::Action modules to the given class.
96
- #
97
- # @param base [Class] the target action
98
- #
99
- # @since 0.1.0
100
- # @api private
101
- #
102
- # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
103
- def self.included(base)
104
- base.class_eval do
105
- extend ClassMethods
106
- prepend InstanceMethods
107
- end
108
- end
109
-
110
- # @api private
111
- module ClassMethods
112
- # Build rack builder
113
- #
114
- # @return [Rack::Builder]
115
- # @api private
116
- def rack_builder
117
- @rack_builder ||= begin
118
- extend Hanami::Action::Rack::Callable
119
- rack_builder = ::Rack::Builder.new
120
- rack_builder.run ->(env) { self.new.call(env) }
121
- rack_builder
122
- end
123
- end
124
-
125
- # Use a Rack middleware
126
- #
127
- # The middleware will be used as it is.
128
- #
129
- # At the runtime, the middleware be invoked with the raw Rack env.
130
- #
131
- # Multiple middlewares can be employed, just by using multiple times
132
- # this method.
133
- #
134
- # @param middleware [#call] A Rack middleware
135
- # @param args [Array] Array arguments for middleware
136
- #
137
- # @since 0.2.0
138
- #
139
- # @see Hanami::Action::Callbacks::ClassMethods#before
140
- #
141
- # @example Middleware
142
- # require 'hanami/controller'
143
- #
144
- # module Sessions
145
- # class Create
146
- # include Hanami::Action
147
- # use OmniAuth
148
- #
149
- # def call(params)
150
- # # ...
151
- # end
152
- # end
153
- # end
154
- def use(middleware, *args, &block)
155
- rack_builder.use middleware, *args, &block
156
- end
157
-
158
- # Returns the class which defines the params
159
- #
160
- # Returns the class which has been provided to define the
161
- # params. By default this will be Hanami::Action::Params.
162
- #
163
- # @return [Class] A params class (when whitelisted) or
164
- # Hanami::Action::Params
165
- #
166
- # @api private
167
- # @since 0.7.0
168
- def params_class
169
- @params_class ||= BaseParams
170
- end
171
- end
172
-
173
- # @since 0.7.0
174
- # @api private
175
- module InstanceMethods
176
- # @since 0.7.0
177
- # @api private
178
- def initialize(*)
179
- super
180
- @_status = nil
181
- @_body = nil
182
- end
183
- end
184
-
185
- protected
186
- # Gets the headers from the response
187
- #
188
- # @return [Hash] the HTTP headers from the response
189
- #
190
- # @since 0.1.0
191
- #
192
- # @example
193
- # require 'hanami/controller'
194
- #
195
- # class Show
196
- # include Hanami::Action
197
- #
198
- # def call(params)
199
- # # ...
200
- # self.headers # => { ... }
201
- # self.headers.merge!({'X-Custom' => 'OK'})
202
- # end
203
- # end
204
- def headers
205
- @headers
206
- end
207
-
208
- # Returns a serialized Rack response (Array), according to the current
209
- # status code, headers, and body.
210
- #
211
- # @return [Array] the serialized response
212
- #
213
- # @since 0.1.0
214
- # @api private
215
- #
216
- # @see Hanami::Action::Rack::DEFAULT_RESPONSE_CODE
217
- # @see Hanami::Action::Rack::DEFAULT_RESPONSE_BODY
218
- # @see Hanami::Action::Rack#status=
219
- # @see Hanami::Action::Rack#headers
220
- # @see Hanami::Action::Rack#body=
221
- def response
222
- [ @_status || DEFAULT_RESPONSE_CODE, headers, @_body || DEFAULT_RESPONSE_BODY.dup ]
223
- end
224
-
225
- # Calculates an unique ID for the current request
226
- #
227
- # @return [String] The unique ID
228
- #
229
- # @since 0.3.0
230
- def request_id
231
- # FIXME make this number configurable and document the probabilities of clashes
232
- @request_id ||= SecureRandom.hex(DEFAULT_REQUEST_ID_LENGTH)
233
- end
234
-
235
- # Returns a Hanami specialized rack request
236
- #
237
- # @return [Hanami::Action::Request] The request
238
- #
239
- # @since 0.3.1
240
- #
241
- # @example
242
- # require 'hanami/controller'
243
- #
244
- # class Create
245
- # include Hanami::Action
246
- #
247
- # def call(params)
248
- # ip = request.ip
249
- # secure = request.ssl?
250
- # end
251
- # end
252
- def request
253
- @request ||= ::Hanami::Action::Request.new(@_env)
254
- end
255
-
256
- # Return parsed request body
257
- #
258
- # @deprecated
259
- def parsed_request_body
260
- Hanami::Utils::Deprecation.new('#parsed_request_body is deprecated and it will be removed in future versions')
261
- @_env.fetch(ROUTER_PARSED_BODY, nil)
262
- end
263
-
264
- private
265
-
266
- # Sets the HTTP status code for the response
267
- #
268
- # @param status [Fixnum] an HTTP status code
269
- # @return [void]
270
- #
271
- # @since 0.1.0
272
- #
273
- # @example
274
- # require 'hanami/controller'
275
- #
276
- # class Create
277
- # include Hanami::Action
278
- #
279
- # def call(params)
280
- # # ...
281
- # self.status = 201
282
- # end
283
- # end
284
- def status=(status)
285
- @_status = status
286
- end
287
-
288
- # Sets the body of the response
289
- #
290
- # @param body [String] the body of the response
291
- # @return [void]
292
- #
293
- # @since 0.1.0
294
- #
295
- # @example
296
- # require 'hanami/controller'
297
- #
298
- # class Show
299
- # include Hanami::Action
300
- #
301
- # def call(params)
302
- # # ...
303
- # self.body = 'Hi!'
304
- # end
305
- # end
306
- def body=(body)
307
- body = Array(body) unless body.respond_to?(:each)
308
- @_body = body
309
- end
310
-
311
- # Send a file as response.
312
- # <tt>This method only sends files from the public directory</tt>
313
- #
314
- # It automatically handle the following cases:
315
- #
316
- # * <tt>Content-Type</tt> and <tt>Content-Length</tt>
317
- # * File Not found (returns a 404)
318
- # * Conditional GET (via <tt>If-Modified-Since</tt> header)
319
- # * Range requests (via <tt>Range</tt> header)
320
- #
321
- # @param path [String, Pathname] the body of the response
322
- # @return [void]
323
- #
324
- # @since 0.4.3
325
- #
326
- # @example
327
- # require 'hanami/controller'
328
- #
329
- # class Show
330
- # include Hanami::Action
331
- #
332
- # def call(params)
333
- # # ...
334
- # send_file Pathname.new('path/to/file')
335
- # end
336
- # end
337
- def send_file(path)
338
- _send_file(
339
- File.new(path, self.class.configuration.public_directory).call(@_env)
340
- )
341
- end
342
-
343
- # Send a file as response from anywhere in the file system.
344
- #
345
- # @see Hanami::Action::Rack#send_file
346
- #
347
- # @param path [String, Pathname] path to the file to be sent
348
- # @return [void]
349
- #
350
- # @since 1.0.0
351
- #
352
- # @example
353
- # require 'hanami/controller'
354
- #
355
- # class Show
356
- # include Hanami::Action
357
- #
358
- # def call(params)
359
- # # ...
360
- # unsafe_send_file Pathname.new('/tmp/path/to/file')
361
- # end
362
- # end
363
- def unsafe_send_file(path)
364
- directory = if Pathname.new(path).relative?
365
- self.class.configuration.root_directory
366
- else
367
- FILE_SYSTEM_ROOT
368
- end
369
-
370
- _send_file(
371
- File.new(path, directory).call(@_env)
372
- )
373
- end
374
-
375
- # Check if the current request is a HEAD
376
- #
377
- # @return [TrueClass,FalseClass] the result of the check
378
- #
379
- # @since 0.3.2
380
- def head?
381
- request_method == HEAD
382
- end
383
-
384
- # NOTE: <tt>Hanami::Action::CSRFProtection</tt> (<tt>hanamirb</tt> gem) depends on this.
385
- #
386
- # @api private
387
- # @since 0.4.4
388
- def request_method
389
- @_env[REQUEST_METHOD]
390
- end
391
-
392
- # @since 1.0.0
393
- # @api private
394
- def _send_file(response)
395
- headers.merge!(response[RESPONSE_HEADERS])
396
-
397
- if response[RESPONSE_CODE] == NOT_FOUND
398
- headers.delete(X_CASCADE)
399
- headers.delete(CONTENT_LENGTH)
400
- halt NOT_FOUND
401
- else
402
- # FIXME: this is a fix for https://github.com/hanami/controller/issues/240
403
- # It's here to maintain the backward compat with 1.1, as we can't remove `#halt`
404
- # We should review the workflow for 2.0, because I don't like callbacks to be referenced from here.
405
- _run_after_callbacks(params)
406
- halt response[RESPONSE_CODE], response[RESPONSE_BODY]
407
- end
408
- end
409
- end
410
- end
411
- end