hanami-controller 2.0.0.beta1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The Hanami::Action::Flash implementation is derived from Roda's FlashHash, also released under the
4
+ # MIT Licence:
5
+ #
6
+ # Copyright (c) 2014-2020 Jeremy Evans
7
+ # Copyright (c) 2010-2014 Michel Martens, Damian Janowski and Cyril David
8
+ # Copyright (c) 2008-2009 Christian Neukirchen
9
+
3
10
  module Hanami
4
11
  class Action
5
- # A container to transport data with the HTTP session, with a lifespan of
6
- # just one HTTP request or redirect.
7
- #
8
- # Behaves like a hash, returning entries for the current request, except for
9
- # {#[]=}, which updates the hash for the next request.
12
+ # A container to transport data with the HTTP session, with a lifespan of just one HTTP request
13
+ # or redirect.
10
14
  #
11
- # This implementation is derived from Roda's FlashHash, also released under
12
- # the MIT Licence:
13
- #
14
- # Copyright (c) 2014-2020 Jeremy Evans
15
- # Copyright (c) 2010-2014 Michel Martens, Damian Janowski and Cyril David
16
- # Copyright (c) 2008-2009 Christian Neukirchen
15
+ # Behaves like a hash, returning entries for the current request, except for {#[]=}, which
16
+ # updates the hash for the next request.
17
17
  #
18
18
  # @since 0.3.0
19
19
  # @api public
@@ -30,9 +30,9 @@ module Hanami
30
30
  # @api public
31
31
  attr_reader :next
32
32
 
33
- # Initializes a new flash instance
33
+ # Returns a new flash object.
34
34
  #
35
- # @param hash [Hash, nil] the flash hash for the current request. nil will become an empty hash.
35
+ # @param hash [Hash, nil] the flash hash for the current request; `nil` will become an empty hash.
36
36
  #
37
37
  # @since 0.3.0
38
38
  # @api public
@@ -41,7 +41,9 @@ module Hanami
41
41
  @next = {}
42
42
  end
43
43
 
44
- # @return [Hash] The flash hash for the current request
44
+ # Returns the flash hash for the current request.
45
+ #
46
+ # @return [Hash] the flash hash for the current request
45
47
  #
46
48
  # @since 2.0.0
47
49
  # @api public
@@ -49,7 +51,7 @@ module Hanami
49
51
  @flash
50
52
  end
51
53
 
52
- # Returns the value for the given key in the current hash
54
+ # Returns the value for the given key in the current hash.
53
55
  #
54
56
  # @param key [Object] the key
55
57
  #
@@ -61,7 +63,7 @@ module Hanami
61
63
  @flash[key]
62
64
  end
63
65
 
64
- # Updates the next hash with the given key and value
66
+ # Updates the next hash with the given key and value.
65
67
  #
66
68
  # @param key [Object] the key
67
69
  # @param value [Object] the value
@@ -72,9 +74,12 @@ module Hanami
72
74
  @next[key] = value
73
75
  end
74
76
 
75
- # Calls the given block once for each element in the current hash
77
+ # Calls the given block once for each element in the current hash.
76
78
  #
77
- # @param block [Proc]
79
+ # @yieldparam element [Array<(Object, Object)>] array containing the key and value from the
80
+ # hash
81
+ #
82
+ # @return [now]
78
83
  #
79
84
  # @since 1.2.0
80
85
  # @api public
@@ -82,10 +87,12 @@ module Hanami
82
87
  @flash.each(&block)
83
88
  end
84
89
 
85
- # Returns a new array with the results of running block once for every
86
- # element in the current hash
90
+ # Returns an array of objects returned by the block, called once for each element in the
91
+ # current hash.
92
+ #
93
+ # @yieldparam element [Array<(Object, Object)>] array containing the key and value from the
94
+ # hash
87
95
  #
88
- # @param block [Proc]
89
96
  # @return [Array]
90
97
  #
91
98
  # @since 1.2.0
@@ -114,7 +121,7 @@ module Hanami
114
121
  @flash.key?(key)
115
122
  end
116
123
 
117
- # Removes entries from the next hash
124
+ # Removes entries from the next hash.
118
125
  #
119
126
  # @overload discard(key)
120
127
  # Removes the given key from the next hash
@@ -4,9 +4,11 @@ require "hanami/http/status"
4
4
 
5
5
  module Hanami
6
6
  class Action
7
+ # @api private
8
+ # @since 2.0.0
7
9
  module Halt
8
- # @since 2.0.0
9
10
  # @api private
11
+ # @since 2.0.0
10
12
  def self.call(status, body = nil)
11
13
  body ||= Http::Status.message_for(status)
12
14
  throw :halt, [status, body]
@@ -3,6 +3,7 @@
3
3
  require "hanami/utils"
4
4
  require "rack/utils"
5
5
  require "rack/mime"
6
+ require_relative "errors"
6
7
 
7
8
  module Hanami
8
9
  class Action
@@ -84,13 +85,13 @@ module Hanami
84
85
  #
85
86
  # @since 2.0.0
86
87
  # @api private
87
- def self.content_type(configuration, request, accepted_mime_types)
88
+ def self.content_type(config, request, accepted_mime_types)
88
89
  if request.accept_header?
89
90
  type = best_q_match(request.accept, accepted_mime_types)
90
91
  return type if type
91
92
  end
92
93
 
93
- default_response_type(configuration) || default_content_type(configuration) || Action::DEFAULT_CONTENT_TYPE
94
+ default_response_type(config) || default_content_type(config) || Action::DEFAULT_CONTENT_TYPE
94
95
  end
95
96
 
96
97
  # @since 2.0.0
@@ -101,23 +102,23 @@ module Hanami
101
102
 
102
103
  # @since 2.0.0
103
104
  # @api private
104
- def self.default_response_type(configuration)
105
- format_to_mime_type(configuration.default_response_format, configuration)
105
+ def self.default_response_type(config)
106
+ format_to_mime_type(config.default_response_format, config)
106
107
  end
107
108
 
108
109
  # @since 2.0.0
109
110
  # @api private
110
- def self.default_content_type(configuration)
111
- format_to_mime_type(configuration.default_request_format, configuration)
111
+ def self.default_content_type(config)
112
+ format_to_mime_type(config.default_request_format, config)
112
113
  end
113
114
 
114
115
  # @since 2.0.0
115
116
  # @api private
116
- def self.format_to_mime_type(format, configuration)
117
+ def self.format_to_mime_type(format, config)
117
118
  return if format.nil?
118
119
 
119
- configuration.mime_type_for(format) ||
120
- TYPES.fetch(format) { raise Hanami::Controller::UnknownFormatError.new(format) }
120
+ config.mime_type_for(format) ||
121
+ TYPES.fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
121
122
  end
122
123
 
123
124
  # Transforms MIME Types to symbol
@@ -125,17 +126,17 @@ module Hanami
125
126
  #
126
127
  # @see Hanami::Action::Mime#finish
127
128
  # @example
128
- # detect_format("text/html; charset=utf-8", configuration) #=> :html
129
+ # detect_format("text/html; charset=utf-8", config) #=> :html
129
130
  #
130
131
  # @return [Symbol, nil]
131
132
  #
132
133
  # @since 2.0.0
133
134
  # @api private
134
- def self.detect_format(content_type, configuration)
135
+ def self.detect_format(content_type, config)
135
136
  return if content_type.nil?
136
137
 
137
138
  ct = content_type.split(";").first
138
- configuration.format_for(ct) || format_for(ct)
139
+ config.format_for(ct) || format_for(ct)
139
140
  end
140
141
 
141
142
  # @since 2.0.0
@@ -144,63 +145,108 @@ module Hanami
144
145
  TYPES.key(content_type)
145
146
  end
146
147
 
148
+ # @since 2.0.0
149
+ # @api private
150
+ def self.detect_format_and_content_type(value, config)
151
+ case value
152
+ when Symbol
153
+ [value, format_to_mime_type(value, config)]
154
+ when String
155
+ [detect_format(value, config), value]
156
+ else
157
+ raise UnknownFormatError.new(value)
158
+ end
159
+ end
160
+
147
161
  # Transforms symbols to MIME Types
148
162
  # @example
149
- # restrict_mime_types(configuration, [:json]) #=> ["application/json"]
163
+ # restrict_mime_types(config, [:json]) #=> ["application/json"]
150
164
  #
151
165
  # @return [Array<String>, nil]
152
166
  #
153
- # @raise [Hanami::Controller::UnknownFormatError] if the format is invalid
167
+ # @raise [Hanami::Action::UnknownFormatError] if the format is invalid
154
168
  #
155
169
  # @since 2.0.0
156
170
  # @api private
157
- def self.restrict_mime_types(configuration, accepted_formats)
158
- return if accepted_formats.empty?
171
+ def self.restrict_mime_types(config)
172
+ return if config.accepted_formats.empty?
159
173
 
160
- mime_types = accepted_formats.map do |format|
161
- format_to_mime_type(format, configuration)
174
+ mime_types = config.accepted_formats.map do |format|
175
+ format_to_mime_type(format, config)
162
176
  end
163
177
 
164
- accepted_mime_types = mime_types & configuration.mime_types
178
+ accepted_mime_types = mime_types & config.mime_types
165
179
 
166
180
  return if accepted_mime_types.empty?
167
181
 
168
182
  accepted_mime_types
169
183
  end
170
184
 
171
- # Use for checking the Content-Type header to make sure is valid based
172
- # on the accepted_mime_types
185
+ # Yields if an action is configured with `accepted_formats`, the request has an `Accept`
186
+ # header, and none of the Accept types matches the accepted formats. The given block is
187
+ # expected to halt the request handling.
173
188
  #
174
- # If no Content-Type is sent in the request it will check the default_request_format
189
+ # If any of these conditions are not met, then the request is acceptable and the method
190
+ # returns without yielding.
175
191
  #
176
- # @return [TrueClass, FalseClass]
192
+ # @see Action#enforce_accepted_mime_types
193
+ # @see Action.accept
194
+ # @see Config#accepted_formats
177
195
  #
178
196
  # @since 2.0.0
179
197
  # @api private
180
- def self.accepted_mime_type?(request, accepted_mime_types, configuration)
181
- mime_type = request.env[Action::HTTP_CONTENT_TYPE] ||
182
- default_content_type(configuration) ||
183
- Action::DEFAULT_CONTENT_TYPE
198
+ def self.enforce_accept(request, config)
199
+ return unless request.accept_header?
200
+
201
+ accept_types = ::Rack::Utils.q_values(request.accept).map(&:first)
202
+ return if accept_types.any? { |mime_type| accepted_mime_type?(mime_type, config) }
184
203
 
185
- !accepted_mime_types.find { |mt| ::Rack::Mime.match?(mt, mime_type) }.nil?
204
+ yield
186
205
  end
187
206
 
188
- # Use for setting the content_type and charset if the response
207
+ # Yields if an action is configured with `accepted_formats`, the request has a `Content-Type`
208
+ # header (or a `default_requst_format` is configured), and the content type does not match the
209
+ # accepted formats. The given block is expected to halt the request handling.
210
+ #
211
+ # If any of these conditions are not met, then the request is acceptable and the method
212
+ # returns without yielding.
189
213
  #
190
- # @see Hanami::Action::Mime#call
214
+ # @see Action#enforce_accepted_mime_types
215
+ # @see Action.accept
216
+ # @see Config#accepted_formats
217
+ #
218
+ # @since 2.0.0
219
+ # @api private
220
+ def self.enforce_content_type(request, config)
221
+ content_type = request.content_type || default_content_type(config)
222
+
223
+ return if content_type.nil?
224
+
225
+ return if accepted_mime_type?(content_type, config)
226
+
227
+ yield
228
+ end
229
+
230
+ # @since 2.0.0
231
+ # @api private
232
+ def self.accepted_mime_type?(mime_type, config)
233
+ config.accepted_mime_types.any? { |accepted_mime_type|
234
+ ::Rack::Mime.match?(accepted_mime_type, mime_type)
235
+ }
236
+ end
237
+
238
+ # Use for setting the content_type and charset if the response
191
239
  #
192
240
  # @return [String]
193
241
  #
194
242
  # @since 2.0.0
195
243
  # @api private
196
- def self.calculate_content_type_with_charset(configuration, request, accepted_mime_types)
197
- charset = self.charset(configuration.default_charset)
198
- content_type = self.content_type(configuration, request, accepted_mime_types)
244
+ def self.calculate_content_type_with_charset(config, request, accepted_mime_types)
245
+ charset = self.charset(config.default_charset)
246
+ content_type = self.content_type(config, request, accepted_mime_types)
199
247
  content_type_with_charset(content_type, charset)
200
248
  end
201
249
 
202
- # private
203
-
204
250
  # Patched version of <tt>Rack::Utils.best_q_match</tt>.
205
251
  #
206
252
  # @since 2.0.0
@@ -25,7 +25,7 @@ module Hanami
25
25
  # @since 1.1.0
26
26
  # @api private
27
27
  def initialize(errors = {})
28
- super(errors)
28
+ super(errors.dup)
29
29
  end
30
30
 
31
31
  # Add an error to the param validations
@@ -160,9 +160,9 @@ module Hanami
160
160
  def initialize(env)
161
161
  @env = env
162
162
  super(_extract_params)
163
- @result = validate
164
- @params = _params
165
- @errors = Errors.new(@result.messages)
163
+ validation = validate
164
+ @params = validation.to_h
165
+ @errors = Errors.new(validation.messages)
166
166
  freeze
167
167
  end
168
168
 
@@ -235,7 +235,7 @@ module Hanami
235
235
  errors.empty?
236
236
  end
237
237
 
238
- # Serialize params to Hash
238
+ # Serialize validated params to Hash
239
239
  #
240
240
  # @return [::Hash]
241
241
  #
@@ -244,13 +244,6 @@ module Hanami
244
244
  @params
245
245
  end
246
246
  alias_method :to_hash, :to_h
247
-
248
- private
249
-
250
- # @api private
251
- def _params
252
- _router_params.merge(@result.output)
253
- end
254
247
  end
255
248
  end
256
249
  end
@@ -4,13 +4,17 @@ require "rack/file"
4
4
 
5
5
  module Hanami
6
6
  class Action
7
+ # Rack extensions for actions.
8
+ #
9
+ # @api private
10
+ # @since 0.4.3
7
11
  module Rack
8
12
  # File to be sent
9
13
  #
14
+ # @see Hanami::Action::Response#send_file
15
+ #
10
16
  # @since 0.4.3
11
17
  # @api private
12
- #
13
- # @see Hanami::Action::Rack#send_file
14
18
  class File
15
19
  # @param path [String,Pathname] file path
16
20
  #
@@ -1,37 +1,99 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack/utils"
3
+ require "hanami/action/flash"
4
4
  require "rack/mime"
5
5
  require "rack/request"
6
+ require "rack/utils"
6
7
  require "securerandom"
8
+ require_relative "errors"
7
9
 
8
10
  module Hanami
9
11
  class Action
10
- # An HTTP request based on top of Rack::Request.
11
- # This guarantees backwards compatibility with with Rack.
12
+ # The HTTP request for an action, given to {Action#handle}.
12
13
  #
13
- # @since 0.3.1
14
+ # Inherits from `Rack::Request`, providing compatibility with Rack functionality.
14
15
  #
15
16
  # @see http://www.rubydoc.info/gems/rack/Rack/Request
17
+ #
18
+ # @since 0.3.1
16
19
  class Request < ::Rack::Request
20
+ # Returns the request's params.
21
+ #
22
+ # For an action with {Validatable} included, this will be a {Params} instance, otherwise a
23
+ # {BaseParams}.
24
+ #
25
+ # @return [BaseParams,Params]
26
+ #
27
+ # @since 2.0.0
28
+ # @api public
17
29
  attr_reader :params
18
30
 
19
- def initialize(env, params)
31
+ # @since 2.0.0
32
+ # @api private
33
+ def initialize(env:, params:, sessions_enabled: false)
20
34
  super(env)
35
+
21
36
  @params = params
37
+ @sessions_enabled = sessions_enabled
22
38
  end
23
39
 
40
+ # Returns the request's ID
41
+ #
42
+ # @return [String]
43
+ #
44
+ # @since 2.0.0
45
+ # @api public
24
46
  def id
25
47
  # FIXME: make this number configurable and document the probabilities of clashes
26
48
  @id ||= @env[Action::REQUEST_ID] = SecureRandom.hex(Action::DEFAULT_ID_LENGTH)
27
49
  end
28
50
 
51
+ # Returns the session for the request.
52
+ #
53
+ # @return [Hash] the session object
54
+ #
55
+ # @raise [MissingSessionError] if sessions are not enabled
56
+ #
57
+ # @see Response#session
58
+ #
59
+ # @since 2.0.0
60
+ # @api public
61
+ def session
62
+ unless @sessions_enabled
63
+ raise Hanami::Action::MissingSessionError.new("Hanami::Action::Request#session")
64
+ end
65
+
66
+ super
67
+ end
68
+
69
+ # Returns the flash for the request.
70
+ #
71
+ # @return [Flash]
72
+ #
73
+ # @raise [MissingSessionError] if sessions are not enabled
74
+ #
75
+ # @see Response#flash
76
+ #
77
+ # @since 2.0.0
78
+ # @api public
79
+ def flash
80
+ unless @sessions_enabled
81
+ raise Hanami::Action::MissingSessionError.new("Hanami::Action::Request#flash")
82
+ end
83
+
84
+ @flash ||= Flash.new(session[Flash::KEY])
85
+ end
86
+
87
+ # @since 2.0.0
88
+ # @api private
29
89
  def accept?(mime_type)
30
90
  !!::Rack::Utils.q_values(accept).find do |mime, _|
31
91
  ::Rack::Mime.match?(mime_type, mime)
32
92
  end
33
93
  end
34
94
 
95
+ # @since 2.0.0
96
+ # @api private
35
97
  def accept_header?
36
98
  accept != Action::DEFAULT_ACCEPT
37
99
  end