hanami-controller 2.0.0.beta1 → 2.0.0.rc1

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.
data/lib/hanami/action.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  begin
4
+ require "dry/core"
5
+ require "dry/types"
4
6
  require "hanami/validations"
5
7
  require "hanami/action/validatable"
6
8
  rescue LoadError # rubocop:disable Lint/SuppressedException
7
9
  end
8
10
 
9
- require "hanami/utils/class_attribute"
11
+ require "dry/configurable"
10
12
  require "hanami/utils/callbacks"
11
13
  require "hanami/utils"
12
14
  require "hanami/utils/string"
@@ -14,14 +16,15 @@ require "hanami/utils/kernel"
14
16
  require "rack"
15
17
  require "rack/utils"
16
18
 
19
+ require_relative "action/config"
17
20
  require_relative "action/constants"
18
21
  require_relative "action/base_params"
19
- require_relative "action/configuration"
20
22
  require_relative "action/halt"
21
23
  require_relative "action/mime"
22
24
  require_relative "action/rack/file"
23
25
  require_relative "action/request"
24
26
  require_relative "action/response"
27
+ require_relative "action/errors"
25
28
 
26
29
  module Hanami
27
30
  # An HTTP endpoint
@@ -36,7 +39,52 @@ module Hanami
36
39
  # # ...
37
40
  # end
38
41
  # end
42
+ #
43
+ # @api public
39
44
  class Action
45
+ extend Dry::Configurable(config_class: Config)
46
+
47
+ # See {Config} for individual setting accessor API docs
48
+ setting :handled_exceptions, default: {}
49
+ setting :formats, default: Config::DEFAULT_FORMATS
50
+ setting :default_request_format, constructor: -> (format) {
51
+ Utils::Kernel.Symbol(format) unless format.nil?
52
+ }
53
+ setting :default_response_format, constructor: -> (format) {
54
+ Utils::Kernel.Symbol(format) unless format.nil?
55
+ }
56
+ setting :accepted_formats, default: []
57
+ setting :default_charset
58
+ setting :default_headers, default: {}, constructor: -> (headers) { headers.compact }
59
+ setting :cookies, default: {}, constructor: -> (cookie_options) {
60
+ # Call `to_h` here to permit `ApplicationConfiguration::Cookies` object to be
61
+ # provided when application actions are configured
62
+ cookie_options.to_h.compact
63
+ }
64
+ setting :root_directory, constructor: -> (dir) {
65
+ Pathname(File.expand_path(dir || Dir.pwd)).realpath
66
+ }
67
+ setting :public_directory, default: Config::DEFAULT_PUBLIC_DIRECTORY
68
+ setting :before_callbacks, default: Utils::Callbacks::Chain.new, cloneable: true
69
+ setting :after_callbacks, default: Utils::Callbacks::Chain.new, cloneable: true
70
+
71
+ # @!scope class
72
+
73
+ # @!method config
74
+ # Returns the action's config. Use this to configure your action.
75
+ #
76
+ # @example Access inside class body
77
+ # class Show < Hanami::Action
78
+ # config.default_response_format = :json
79
+ # end
80
+ #
81
+ # @return [Config]
82
+ #
83
+ # @api public
84
+ # @since 2.0.0
85
+
86
+ # @!scope instance
87
+
40
88
  # Override Ruby's hook for modules.
41
89
  # It includes basic Hanami::Action modules to the given class.
42
90
  #
@@ -49,31 +97,13 @@ module Hanami
49
97
 
50
98
  if subclass.superclass == Action
51
99
  subclass.class_eval do
52
- include Utils::ClassAttribute
53
-
54
- class_attribute :before_callbacks
55
- self.before_callbacks = Utils::Callbacks::Chain.new
56
-
57
- class_attribute :after_callbacks
58
- self.after_callbacks = Utils::Callbacks::Chain.new
59
-
60
100
  include Validatable if defined?(Validatable)
61
101
  end
62
102
  end
63
103
 
64
- subclass.instance_variable_set "@configuration", configuration.dup
65
- end
66
-
67
- # @since 2.0.0
68
- # @api private
69
- def self.configuration
70
- @configuration ||= Configuration.new
71
- end
72
-
73
- class << self
74
- # @since 2.0.0
75
- # @api private
76
- alias_method :config, :configuration
104
+ if instance_variable_defined?(:@params_class)
105
+ subclass.instance_variable_set(:@params_class, @params_class)
106
+ end
77
107
  end
78
108
 
79
109
  # Returns the class which defines the params
@@ -87,12 +117,7 @@ module Hanami
87
117
  # @api private
88
118
  # @since 0.7.0
89
119
  def self.params_class
90
- @params_class ||= BaseParams
91
- end
92
-
93
- # FIXME: make this thread-safe
94
- def self.accepted_formats
95
- @accepted_formats ||= []
120
+ @params_class || BaseParams
96
121
  end
97
122
 
98
123
  # Placeholder implementation for params class method
@@ -101,73 +126,74 @@ module Hanami
101
126
  #
102
127
  # @raise [NoMethodError]
103
128
  #
104
- # @api private
129
+ # @api public
105
130
  # @since 2.0.0
106
131
  def self.params(_klass = nil)
107
132
  raise NoMethodError,
108
133
  "To use `params`, please add 'hanami/validations' gem to your Gemfile"
109
134
  end
110
135
 
111
- # Define a callback for an Action.
112
- # The callback will be executed **before** the action is called, in the
113
- # order they are added.
136
+ # @overload self.append_before(*callbacks, &block)
137
+ # Define a callback for an Action.
138
+ # The callback will be executed **before** the action is called, in the
139
+ # order they are added.
114
140
  #
115
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
116
- # each of them is representing a name of a method available in the
117
- # context of the Action.
141
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
142
+ # each of them is representing a name of a method available in the
143
+ # context of the Action.
118
144
  #
119
- # @param blk [Proc] an anonymous function to be executed
145
+ # @param blk [Proc] an anonymous function to be executed
120
146
  #
121
- # @return [void]
147
+ # @return [void]
122
148
  #
123
- # @since 0.3.2
149
+ # @since 0.3.2
124
150
  #
125
- # @see Hanami::Action::Callbacks::ClassMethods#append_after
151
+ # @see Hanami::Action::Callbacks::ClassMethods#append_after
126
152
  #
127
- # @example Method names (symbols)
128
- # require "hanami/controller"
153
+ # @example Method names (symbols)
154
+ # require "hanami/controller"
129
155
  #
130
- # class Show < Hanami::Action
131
- # before :authenticate, :set_article
156
+ # class Show < Hanami::Action
157
+ # before :authenticate, :set_article
132
158
  #
133
- # def handle(req, res)
134
- # end
159
+ # def handle(req, res)
160
+ # end
135
161
  #
136
- # private
137
- # def authenticate
138
- # # ...
139
- # end
162
+ # private
163
+ # def authenticate
164
+ # # ...
165
+ # end
140
166
  #
141
- # # `params` in the method signature is optional
142
- # def set_article(params)
143
- # @article = Article.find params[:id]
167
+ # # `params` in the method signature is optional
168
+ # def set_article(params)
169
+ # @article = Article.find params[:id]
170
+ # end
144
171
  # end
145
- # end
146
172
  #
147
- # # The order of execution will be:
148
- # #
149
- # # 1. #authenticate
150
- # # 2. #set_article
151
- # # 3. #call
173
+ # # The order of execution will be:
174
+ # #
175
+ # # 1. #authenticate
176
+ # # 2. #set_article
177
+ # # 3. #call
152
178
  #
153
- # @example Anonymous functions (Procs)
154
- # require "hanami/controller"
179
+ # @example Anonymous functions (Procs)
180
+ # require "hanami/controller"
155
181
  #
156
- # class Show < Hanami::Action
157
- # before { ... } # 1 do some authentication stuff
158
- # before {|req, res| @article = Article.find params[:id] } # 2
182
+ # class Show < Hanami::Action
183
+ # before { ... } # 1 do some authentication stuff
184
+ # before {|req, res| @article = Article.find params[:id] } # 2
159
185
  #
160
- # def handle(req, res)
186
+ # def handle(req, res)
187
+ # end
161
188
  # end
162
- # end
163
189
  #
164
- # # The order of execution will be:
165
- # #
166
- # # 1. authentication
167
- # # 2. set the article
168
- # # 3. `#handle`
190
+ # # The order of execution will be:
191
+ # #
192
+ # # 1. authentication
193
+ # # 2. set the article
194
+ # # 3. `#handle`
169
195
  def self.append_before(...)
170
- before_callbacks.append(...)
196
+ config.before_callbacks.append(...)
171
197
  end
172
198
 
173
199
  class << self
@@ -175,23 +201,24 @@ module Hanami
175
201
  alias_method :before, :append_before
176
202
  end
177
203
 
178
- # Define a callback for an Action.
179
- # The callback will be executed **after** the action is called, in the
180
- # order they are added.
204
+ # @overload self.append_after(*callbacks, &block)
205
+ # Define a callback for an Action.
206
+ # The callback will be executed **after** the action is called, in the
207
+ # order they are added.
181
208
  #
182
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
183
- # each of them is representing a name of a method available in the
184
- # context of the Action.
209
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
210
+ # each of them is representing a name of a method available in the
211
+ # context of the Action.
185
212
  #
186
- # @param blk [Proc] an anonymous function to be executed
213
+ # @param blk [Proc] an anonymous function to be executed
187
214
  #
188
- # @return [void]
215
+ # @return [void]
189
216
  #
190
- # @since 0.3.2
217
+ # @since 0.3.2
191
218
  #
192
- # @see Hanami::Action::Callbacks::ClassMethods#append_before
219
+ # @see Hanami::Action::Callbacks::ClassMethods#append_before
193
220
  def self.append_after(...)
194
- after_callbacks.append(...)
221
+ config.after_callbacks.append(...)
195
222
  end
196
223
 
197
224
  class << self
@@ -199,54 +226,56 @@ module Hanami
199
226
  alias_method :after, :append_after
200
227
  end
201
228
 
202
- # Define a callback for an Action.
203
- # The callback will be executed **before** the action is called.
204
- # It will add the callback at the beginning of the callbacks' chain.
229
+ # @overload self.prepend_before(*callbacks, &block)
230
+ # Define a callback for an Action.
231
+ # The callback will be executed **before** the action is called.
232
+ # It will add the callback at the beginning of the callbacks' chain.
205
233
  #
206
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
207
- # each of them is representing a name of a method available in the
208
- # context of the Action.
234
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
235
+ # each of them is representing a name of a method available in the
236
+ # context of the Action.
209
237
  #
210
- # @param blk [Proc] an anonymous function to be executed
238
+ # @param blk [Proc] an anonymous function to be executed
211
239
  #
212
- # @return [void]
240
+ # @return [void]
213
241
  #
214
- # @since 0.3.2
242
+ # @since 0.3.2
215
243
  #
216
- # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
244
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
217
245
  def self.prepend_before(...)
218
- before_callbacks.prepend(...)
246
+ config.before_callbacks.prepend(...)
219
247
  end
220
248
 
221
- # Define a callback for an Action.
222
- # The callback will be executed **after** the action is called.
223
- # It will add the callback at the beginning of the callbacks' chain.
249
+ # @overload self.prepend_after(*callbacks, &block)
250
+ # Define a callback for an Action.
251
+ # The callback will be executed **after** the action is called.
252
+ # It will add the callback at the beginning of the callbacks' chain.
224
253
  #
225
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
226
- # each of them is representing a name of a method available in the
227
- # context of the Action.
254
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
255
+ # each of them is representing a name of a method available in the
256
+ # context of the Action.
228
257
  #
229
- # @param blk [Proc] an anonymous function to be executed
258
+ # @param blk [Proc] an anonymous function to be executed
230
259
  #
231
- # @return [void]
260
+ # @return [void]
232
261
  #
233
- # @since 0.3.2
262
+ # @since 0.3.2
234
263
  #
235
- # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
264
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
236
265
  def self.prepend_after(...)
237
- after_callbacks.prepend(...)
266
+ config.after_callbacks.prepend(...)
238
267
  end
239
268
 
240
269
  # Restrict the access to the specified mime type symbols.
241
270
  #
242
271
  # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
243
272
  #
244
- # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
273
+ # @raise [Hanami::Action::UnknownFormatError] if the symbol cannot
245
274
  # be converted into a mime type
246
275
  #
247
276
  # @since 0.1.0
248
277
  #
249
- # @see Hanami::Action::Configuration#format
278
+ # @see Config#format
250
279
  #
251
280
  # @example
252
281
  # require "hanami/controller"
@@ -262,58 +291,28 @@ module Hanami
262
291
  # # When called with "*/*" => 200
263
292
  # # When called with "text/html" => 200
264
293
  # # When called with "application/json" => 200
265
- # # When called with "application/xml" => 406
294
+ # # When called with "application/xml" => 415
266
295
  def self.accept(*formats)
267
- @accepted_formats = *formats
268
- before :enforce_accepted_mime_types
296
+ config.accepted_formats = formats
269
297
  end
270
298
 
271
- # Returns a new action
272
- #
273
- # @overload new(**deps, ...)
274
- # @param deps [Hash] action dependencies
275
- #
276
- # @overload new(configuration:, **deps, ...)
277
- # @param configuration [Hanami::Action::Configuration] action configuration
278
- # @param deps [Hash] action dependencies
279
- #
280
- # @return [Hanami::Action] Action object
299
+ # @see Config#handle_exception
281
300
  #
282
301
  # @since 2.0.0
283
- def self.new(*args, configuration: self.configuration, **kwargs, &block)
284
- allocate.tap do |obj|
285
- obj.instance_variable_set(:@name, Name[name])
286
- obj.instance_variable_set(:@configuration, configuration.dup.finalize!)
287
- obj.instance_variable_set(:@accepted_mime_types, Mime.restrict_mime_types(configuration, accepted_formats))
288
- obj.send(:initialize, *args, **kwargs, &block)
289
- obj.freeze
290
- end
302
+ # @api public
303
+ def self.handle_exception(...)
304
+ config.handle_exception(...)
291
305
  end
292
306
 
307
+ # Returns a new action
308
+ #
293
309
  # @since 2.0.0
294
- # @api private
295
- module Name
296
- # @since 2.0.0
297
- # @api private
298
- MODULE_SEPARATOR_TRANSFORMER = [:gsub, "::", "."].freeze
299
-
300
- # @since 2.0.0
301
- # @api private
302
- def self.call(name)
303
- Utils::String.transform(name, MODULE_SEPARATOR_TRANSFORMER, :underscore) unless name.nil?
304
- end
305
-
306
- class << self
307
- # @since 2.0.0
308
- # @api private
309
- alias_method :[], :call
310
- end
310
+ # @api public
311
+ def initialize(config: self.class.config)
312
+ @config = config
313
+ freeze
311
314
  end
312
315
 
313
- # @since 2.0.0
314
- # @api private
315
- attr_reader :name
316
-
317
316
  # Implements the Rack/Hanami::Action protocol
318
317
  #
319
318
  # @since 0.1.0
@@ -324,16 +323,22 @@ module Hanami
324
323
 
325
324
  halted = catch :halt do
326
325
  params = self.class.params_class.new(env)
327
- request = build_request(env, params)
326
+ request = build_request(
327
+ env: env,
328
+ params: params,
329
+ sessions_enabled: sessions_enabled?
330
+ )
328
331
  response = build_response(
329
332
  request: request,
330
- action: name,
331
- configuration: configuration,
332
- content_type: Mime.calculate_content_type_with_charset(configuration, request, accepted_mime_types),
333
+ config: config,
334
+ content_type: Mime.calculate_content_type_with_charset(config, request, config.accepted_mime_types),
333
335
  env: env,
334
- headers: configuration.default_headers
336
+ headers: config.default_headers,
337
+ sessions_enabled: sessions_enabled?
335
338
  )
336
339
 
340
+ enforce_accepted_mime_types(request)
341
+
337
342
  _run_before_callbacks(request, response)
338
343
  handle(request, response)
339
344
  _run_after_callbacks(request, response)
@@ -344,12 +349,6 @@ module Hanami
344
349
  finish(request, response, halted)
345
350
  end
346
351
 
347
- # @since 2.0.0
348
- # @api public
349
- def initialize(**deps)
350
- @_deps = deps
351
- end
352
-
353
352
  protected
354
353
 
355
354
  # Hook for subclasses to apply behavior as part of action invocation
@@ -421,36 +420,44 @@ module Hanami
421
420
 
422
421
  # @since 2.0.0
423
422
  # @api private
424
- attr_reader :configuration
423
+ attr_reader :config
425
424
 
426
425
  # @since 2.0.0
427
426
  # @api private
428
- def accepted_mime_types
429
- @accepted_mime_types || configuration.mime_types
430
- end
427
+ def enforce_accepted_mime_types(request)
428
+ return if config.accepted_formats.empty?
431
429
 
432
- # @since 2.0.0
433
- # @api private
434
- def enforce_accepted_mime_types(req, *)
435
- Mime.accepted_mime_type?(req, accepted_mime_types, configuration) or halt 406
430
+ Mime.enforce_accept(request, config) { return halt 406 }
431
+ Mime.enforce_content_type(request, config) { return halt 415 }
436
432
  end
437
433
 
438
434
  # @since 2.0.0
439
435
  # @api private
440
436
  def exception_handler(exception)
441
- configuration.handled_exceptions.each do |exception_class, handler|
437
+ config.handled_exceptions.each do |exception_class, handler|
442
438
  return handler if exception.is_a?(exception_class)
443
439
  end
444
440
 
445
441
  nil
446
442
  end
447
443
 
444
+ # @see Session#sessions_enabled?
445
+ # @since 2.0.0
446
+ # @api private
447
+ def sessions_enabled?
448
+ false
449
+ end
450
+
451
+ # Hook to be overridden by `Hanami::Extensions::Action` for integrated actions
452
+ #
448
453
  # @since 2.0.0
449
454
  # @api private
450
- def build_request(env, params)
451
- Request.new(env, params)
455
+ def build_request(**options)
456
+ Request.new(**options)
452
457
  end
453
458
 
459
+ # Hook to be overridden by `Hanami::Extensions::Action` for integrated actions
460
+ #
454
461
  # @since 2.0.0
455
462
  # @api private
456
463
  def build_response(**options)
@@ -507,14 +514,14 @@ module Hanami
507
514
  # @since 0.1.0
508
515
  # @api private
509
516
  def _run_before_callbacks(*args)
510
- self.class.before_callbacks.run(self, *args)
517
+ config.before_callbacks.run(self, *args)
511
518
  nil
512
519
  end
513
520
 
514
521
  # @since 0.1.0
515
522
  # @api private
516
523
  def _run_after_callbacks(*args)
517
- self.class.after_callbacks.run(self, *args)
524
+ config.after_callbacks.run(self, *args)
518
525
  nil
519
526
  end
520
527
 
@@ -583,20 +590,6 @@ module Hanami
583
590
  res.body = Response::EMPTY_BODY
584
591
  end
585
592
 
586
- # @since 2.0.0
587
- # @api private
588
- def format(value)
589
- case value
590
- when Symbol
591
- format = Utils::Kernel.Symbol(value)
592
- [format, Action::Mime.format_to_mime_type(format, configuration)]
593
- when String
594
- [Action::Mime.detect_format(value, configuration), value]
595
- else
596
- raise Hanami::Controller::UnknownFormatError.new(value)
597
- end
598
- end
599
-
600
593
  # Finalize the response
601
594
  #
602
595
  # Prepare the data before the response will be returned to the webserver
@@ -614,7 +607,7 @@ module Hanami
614
607
  _empty_headers(res) if _requires_empty_headers?(res)
615
608
  _empty_body(res) if res.head?
616
609
 
617
- res.set_format(Action::Mime.detect_format(res.content_type, configuration))
610
+ res.set_format(Action::Mime.detect_format(res.content_type, config))
618
611
  res[:params] = req.params
619
612
  res[:format] = res.format
620
613
  res
@@ -2,9 +2,7 @@
2
2
 
3
3
  module Hanami
4
4
  module Controller
5
- # Defines the version
6
- #
7
- # @since 0.1.0
8
- VERSION = "2.0.0.beta1"
5
+ # @api public
6
+ VERSION = "2.0.0.rc1"
9
7
  end
10
8
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "hanami/action"
4
4
  require "hanami/controller/version"
5
- require "hanami/controller/error"
6
5
 
7
6
  # Hanami
8
7
  #
@@ -27,20 +26,5 @@ module Hanami
27
26
  # end
28
27
  # end
29
28
  module Controller
30
- # Unknown format error
31
- #
32
- # This error is raised when a action sets a format that it isn't recognized
33
- # both by `Hanami::Action::Configuration` and the list of Rack mime types
34
- #
35
- # @since 0.2.0
36
- #
37
- # @see Hanami::Action::Mime#format=
38
- class UnknownFormatError < Hanami::Controller::Error
39
- # @since 0.2.0
40
- # @api private
41
- def initialize(format)
42
- super("Cannot find a corresponding Mime type for '#{format}'. Please configure it with Hanami::Controller::Configuration#format.") # rubocop:disable Layout/LineLength
43
- end
44
- end
45
29
  end
46
30
  end
@@ -3,6 +3,8 @@
3
3
  require "rack/utils"
4
4
 
5
5
  module Hanami
6
+ # @since 0.1.0
7
+ # @api private
6
8
  module Http
7
9
  # An HTTP status
8
10
  #