hanami-controller 1.3.3 → 2.0.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +299 -537
  5. data/hanami-controller.gemspec +4 -3
  6. data/lib/hanami/action/application_action.rb +131 -0
  7. data/lib/hanami/action/application_configuration/content_security_policy.rb +118 -0
  8. data/lib/hanami/action/application_configuration/cookies.rb +29 -0
  9. data/lib/hanami/action/application_configuration/sessions.rb +46 -0
  10. data/lib/hanami/action/application_configuration.rb +90 -0
  11. data/lib/hanami/action/base_params.rb +2 -2
  12. data/lib/hanami/action/cache/cache_control.rb +4 -4
  13. data/lib/hanami/action/cache/conditional_get.rb +3 -1
  14. data/lib/hanami/action/cache/directives.rb +1 -1
  15. data/lib/hanami/action/cache/expires.rb +3 -3
  16. data/lib/hanami/action/cache.rb +1 -139
  17. data/lib/hanami/action/configuration.rb +428 -0
  18. data/lib/hanami/action/cookie_jar.rb +3 -3
  19. data/lib/hanami/action/cookies.rb +3 -62
  20. data/lib/hanami/action/csrf_protection.rb +214 -0
  21. data/lib/hanami/action/flash.rb +102 -207
  22. data/lib/hanami/action/glue.rb +5 -31
  23. data/lib/hanami/action/halt.rb +12 -0
  24. data/lib/hanami/action/mime.rb +78 -485
  25. data/lib/hanami/action/params.rb +2 -2
  26. data/lib/hanami/action/rack/file.rb +1 -1
  27. data/lib/hanami/action/request.rb +30 -20
  28. data/lib/hanami/action/response.rb +193 -0
  29. data/lib/hanami/action/session.rb +11 -128
  30. data/lib/hanami/action/standalone_action.rb +578 -0
  31. data/lib/hanami/action/validatable.rb +1 -1
  32. data/lib/hanami/action/view_name_inferrer.rb +46 -0
  33. data/lib/hanami/action.rb +129 -73
  34. data/lib/hanami/controller/version.rb +1 -1
  35. data/lib/hanami/controller.rb +0 -227
  36. data/lib/hanami/http/status.rb +2 -2
  37. metadata +44 -26
  38. data/lib/hanami/action/callable.rb +0 -92
  39. data/lib/hanami/action/callbacks.rb +0 -214
  40. data/lib/hanami/action/configurable.rb +0 -50
  41. data/lib/hanami/action/exposable/guard.rb +0 -104
  42. data/lib/hanami/action/exposable.rb +0 -126
  43. data/lib/hanami/action/head.rb +0 -121
  44. data/lib/hanami/action/rack/callable.rb +0 -47
  45. data/lib/hanami/action/rack/errors.rb +0 -53
  46. data/lib/hanami/action/rack.rb +0 -411
  47. data/lib/hanami/action/redirect.rb +0 -59
  48. data/lib/hanami/action/throwable.rb +0 -169
  49. data/lib/hanami/controller/configuration.rb +0 -763
  50. data/lib/hanami-controller.rb +0 -1
@@ -0,0 +1,578 @@
1
+ begin
2
+ require 'hanami/validations'
3
+ require 'hanami/action/validatable'
4
+ rescue LoadError
5
+ end
6
+
7
+ require 'hanami/utils/class_attribute'
8
+ require 'hanami/utils/callbacks'
9
+ require 'hanami/utils'
10
+ require 'hanami/utils/string'
11
+ require 'hanami/utils/kernel'
12
+ require 'rack/utils'
13
+
14
+ require_relative 'base_params'
15
+ require_relative 'configuration'
16
+ require_relative 'halt'
17
+ require_relative 'mime'
18
+ require_relative 'rack/file'
19
+ require_relative 'request'
20
+ require_relative 'response'
21
+
22
+ module Hanami
23
+ class Action
24
+ module StandaloneAction
25
+ def self.included(klass)
26
+ klass.include InstanceMethods
27
+ klass.extend ClassMethods
28
+ end
29
+
30
+ module ClassMethods
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ alias_method :config, :configuration
36
+
37
+ # Override Ruby's hook for modules.
38
+ # It includes basic Hanami::Action modules to the given class.
39
+ #
40
+ # @param subclass [Class] the target action
41
+ #
42
+ # @since 0.1.0
43
+ # @api private
44
+ def inherited(subclass)
45
+ if subclass.superclass == Action
46
+ subclass.class_eval do
47
+ include Utils::ClassAttribute
48
+
49
+ class_attribute :before_callbacks
50
+ self.before_callbacks = Utils::Callbacks::Chain.new
51
+
52
+ class_attribute :after_callbacks
53
+ self.after_callbacks = Utils::Callbacks::Chain.new
54
+
55
+ include Validatable if defined?(Validatable)
56
+ end
57
+ end
58
+
59
+ subclass.instance_variable_set '@configuration', configuration.dup
60
+ end
61
+
62
+ # Returns the class which defines the params
63
+ #
64
+ # Returns the class which has been provided to define the
65
+ # params. By default this will be Hanami::Action::Params.
66
+ #
67
+ # @return [Class] A params class (when whitelisted) or
68
+ # Hanami::Action::Params
69
+ #
70
+ # @api private
71
+ # @since 0.7.0
72
+ def params_class
73
+ @params_class ||= BaseParams
74
+ end
75
+
76
+ # FIXME: make this thread-safe
77
+ def accepted_formats
78
+ @accepted_formats ||= []
79
+ end
80
+
81
+ # Define a callback for an Action.
82
+ # The callback will be executed **before** the action is called, in the
83
+ # order they are added.
84
+ #
85
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
86
+ # each of them is representing a name of a method available in the
87
+ # context of the Action.
88
+ #
89
+ # @param blk [Proc] an anonymous function to be executed
90
+ #
91
+ # @return [void]
92
+ #
93
+ # @since 0.3.2
94
+ #
95
+ # @see Hanami::Action::Callbacks::ClassMethods#append_after
96
+ #
97
+ # @example Method names (symbols)
98
+ # require 'hanami/controller'
99
+ #
100
+ # class Show
101
+ # include Hanami::Action
102
+ #
103
+ # before :authenticate, :set_article
104
+ #
105
+ # def call(params)
106
+ # end
107
+ #
108
+ # private
109
+ # def authenticate
110
+ # # ...
111
+ # end
112
+ #
113
+ # # `params` in the method signature is optional
114
+ # def set_article(params)
115
+ # @article = Article.find params[:id]
116
+ # end
117
+ # end
118
+ #
119
+ # # The order of execution will be:
120
+ # #
121
+ # # 1. #authenticate
122
+ # # 2. #set_article
123
+ # # 3. #call
124
+ #
125
+ # @example Anonymous functions (Procs)
126
+ # require 'hanami/controller'
127
+ #
128
+ # class Show
129
+ # include Hanami::Action
130
+ #
131
+ # before { ... } # 1 do some authentication stuff
132
+ # before {|params| @article = Article.find params[:id] } # 2
133
+ #
134
+ # def call(params)
135
+ # end
136
+ # end
137
+ #
138
+ # # The order of execution will be:
139
+ # #
140
+ # # 1. authentication
141
+ # # 2. set the article
142
+ # # 3. #call
143
+ def append_before(*callbacks, &blk)
144
+ before_callbacks.append(*callbacks, &blk)
145
+ end
146
+
147
+ alias_method :before, :append_before
148
+
149
+ # class << self
150
+ # # @since 0.1.0
151
+ # alias before append_before
152
+ # end
153
+
154
+ # Define a callback for an Action.
155
+ # The callback will be executed **after** the action is called, in the
156
+ # order they are added.
157
+ #
158
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
159
+ # each of them is representing a name of a method available in the
160
+ # context of the Action.
161
+ #
162
+ # @param blk [Proc] an anonymous function to be executed
163
+ #
164
+ # @return [void]
165
+ #
166
+ # @since 0.3.2
167
+ #
168
+ # @see Hanami::Action::Callbacks::ClassMethods#append_before
169
+ def append_after(*callbacks, &blk)
170
+ after_callbacks.append(*callbacks, &blk)
171
+ end
172
+
173
+ alias_method :after, :append_after
174
+
175
+ # class << self
176
+ # # @since 0.1.0
177
+ # alias after append_after
178
+ # end
179
+
180
+ # Define a callback for an Action.
181
+ # The callback will be executed **before** the action is called.
182
+ # It will add the callback at the beginning of the callbacks' chain.
183
+ #
184
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
185
+ # each of them is representing a name of a method available in the
186
+ # context of the Action.
187
+ #
188
+ # @param blk [Proc] an anonymous function to be executed
189
+ #
190
+ # @return [void]
191
+ #
192
+ # @since 0.3.2
193
+ #
194
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
195
+ def prepend_before(*callbacks, &blk)
196
+ before_callbacks.prepend(*callbacks, &blk)
197
+ end
198
+
199
+ # Define a callback for an Action.
200
+ # The callback will be executed **after** the action is called.
201
+ # It will add the callback at the beginning of the callbacks' chain.
202
+ #
203
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
204
+ # each of them is representing a name of a method available in the
205
+ # context of the Action.
206
+ #
207
+ # @param blk [Proc] an anonymous function to be executed
208
+ #
209
+ # @return [void]
210
+ #
211
+ # @since 0.3.2
212
+ #
213
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
214
+ def prepend_after(*callbacks, &blk)
215
+ after_callbacks.prepend(*callbacks, &blk)
216
+ end
217
+
218
+ # Restrict the access to the specified mime type symbols.
219
+ #
220
+ # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
221
+ #
222
+ # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
223
+ # be converted into a mime type
224
+ #
225
+ # @since 0.1.0
226
+ #
227
+ # @see Hanami::Controller::Configuration#format
228
+ #
229
+ # @example
230
+ # require 'hanami/controller'
231
+ #
232
+ # class Show
233
+ # include Hanami::Action
234
+ # accept :html, :json
235
+ #
236
+ # def call(params)
237
+ # # ...
238
+ # end
239
+ # end
240
+ #
241
+ # # When called with "*/*" => 200
242
+ # # When called with "text/html" => 200
243
+ # # When called with "application/json" => 200
244
+ # # When called with "application/xml" => 406
245
+ def accept(*formats)
246
+ @accepted_formats = *formats
247
+ before :enforce_accepted_mime_types
248
+ end
249
+
250
+ # Returns a new action
251
+ #
252
+ # @overload new(**deps, ...)
253
+ # @param deps [Hash] action dependencies
254
+ #
255
+ # @overload new(configuration:, **deps, ...)
256
+ # @param configuration [Hanami::Controller::Configuration] action configuration
257
+ # @param deps [Hash] action dependencies
258
+ #
259
+ # @return [Hanami::Action] Action object
260
+ #
261
+ # @since 2.0.0
262
+ def new(*args, configuration: self.configuration, **kwargs, &block)
263
+ allocate.tap do |obj|
264
+ obj.instance_variable_set(:@name, Name[name])
265
+ obj.instance_variable_set(:@configuration, configuration.dup.finalize!)
266
+ obj.instance_variable_set(:@accepted_mime_types, Mime.restrict_mime_types(configuration, accepted_formats))
267
+ obj.send(:initialize, *args, **kwargs, &block)
268
+ obj.freeze
269
+ end
270
+ end
271
+
272
+ module Name
273
+ MODULE_SEPARATOR_TRANSFORMER = [:gsub, "::", "."].freeze
274
+
275
+ def self.call(name)
276
+ Utils::String.transform(name, MODULE_SEPARATOR_TRANSFORMER, :underscore) unless name.nil?
277
+ end
278
+
279
+ class << self
280
+ alias_method :[], :call
281
+ end
282
+ end
283
+ end
284
+
285
+ module InstanceMethods
286
+ attr_reader :name
287
+
288
+ # Implements the Rack/Hanami::Action protocol
289
+ #
290
+ # @since 0.1.0
291
+ # @api private
292
+ def call(env)
293
+ request = nil
294
+ response = nil
295
+
296
+ halted = catch :halt do
297
+ begin
298
+ params = self.class.params_class.new(env)
299
+ request = build_request(env, params)
300
+ response = build_response(
301
+ request: request,
302
+ action: name,
303
+ configuration: configuration,
304
+ content_type: Mime.calculate_content_type_with_charset(configuration, request, accepted_mime_types),
305
+ env: env,
306
+ headers: configuration.default_headers
307
+ )
308
+
309
+ _run_before_callbacks(request, response)
310
+ handle(request, response)
311
+ _run_after_callbacks(request, response)
312
+ rescue => exception
313
+ _handle_exception(request, response, exception)
314
+ end
315
+ end
316
+
317
+ finish(request, response, halted)
318
+ end
319
+
320
+ def initialize(**deps)
321
+ @_deps = deps
322
+ end
323
+
324
+ protected
325
+
326
+ # Hook for subclasses to apply behavior as part of action invocation
327
+ #
328
+ # @param request [Hanami::Action::Request]
329
+ # @param response [Hanami::Action::Response]
330
+ #
331
+ # @since 2.0.0
332
+ def handle(request, response)
333
+ end
334
+
335
+ # Halt the action execution with the given HTTP status code and message.
336
+ #
337
+ # When used, the execution of a callback or of an action is interrupted
338
+ # and the control returns to the framework, that decides how to handle
339
+ # the event.
340
+ #
341
+ # If a message is provided, it sets the response body with the message.
342
+ # Otherwise, it sets the response body with the default message associated
343
+ # to the code (eg 404 will set `"Not Found"`).
344
+ #
345
+ # @param status [Fixnum] a valid HTTP status code
346
+ # @param body [String] the response body
347
+ #
348
+ # @raise [StandardError] if the code isn't valid
349
+ #
350
+ # @since 0.2.0
351
+ #
352
+ # @see Hanami::Action::Throwable#handle_exception
353
+ # @see Hanami::Http::Status:ALL
354
+ #
355
+ # @example Basic usage
356
+ # require 'hanami/controller'
357
+ #
358
+ # class Show
359
+ # def call(params)
360
+ # halt 404
361
+ # end
362
+ # end
363
+ #
364
+ # # => [404, {}, ["Not Found"]]
365
+ #
366
+ # @example Custom message
367
+ # require 'hanami/controller'
368
+ #
369
+ # class Show
370
+ # def call(params)
371
+ # halt 404, "This is not the droid you're looking for."
372
+ # end
373
+ # end
374
+ #
375
+ # # => [404, {}, ["This is not the droid you're looking for."]]
376
+ def halt(status, body = nil)
377
+ Halt.call(status, body)
378
+ end
379
+
380
+ # @since 0.3.2
381
+ # @api private
382
+ def _requires_no_body?(res)
383
+ HTTP_STATUSES_WITHOUT_BODY.include?(res.status)
384
+ end
385
+
386
+ # @since 2.0.0
387
+ # @api private
388
+ def _requires_empty_headers?(res)
389
+ _requires_no_body?(res) || res.head?
390
+ end
391
+
392
+ private
393
+
394
+ attr_reader :configuration
395
+
396
+ def accepted_mime_types
397
+ @accepted_mime_types || configuration.mime_types
398
+ end
399
+
400
+ def enforce_accepted_mime_types(req, *)
401
+ Mime.accepted_mime_type?(req, accepted_mime_types, configuration) or halt 406
402
+ end
403
+
404
+ def exception_handler(exception)
405
+ configuration.handled_exceptions.each do |exception_class, handler|
406
+ return handler if exception.kind_of?(exception_class)
407
+ end
408
+
409
+ nil
410
+ end
411
+
412
+ def build_request(env, params)
413
+ Request.new(env, params)
414
+ end
415
+
416
+ def build_response(**options)
417
+ Response.new(**options)
418
+ end
419
+
420
+ # @since 0.2.0
421
+ # @api private
422
+ def _reference_in_rack_errors(req, exception)
423
+ req.env[RACK_EXCEPTION] = exception
424
+
425
+ if errors = req.env[RACK_ERRORS]
426
+ errors.write(_dump_exception(exception))
427
+ errors.flush
428
+ end
429
+ end
430
+
431
+ # @since 0.2.0
432
+ # @api private
433
+ def _dump_exception(exception)
434
+ [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
435
+ end
436
+
437
+ # @since 0.1.0
438
+ # @api private
439
+ def _handle_exception(req, res, exception)
440
+ handler = exception_handler(exception)
441
+
442
+ if handler.nil?
443
+ _reference_in_rack_errors(req, exception)
444
+ raise exception
445
+ end
446
+
447
+ instance_exec(
448
+ req,
449
+ res,
450
+ exception,
451
+ &_exception_handler(handler)
452
+ )
453
+
454
+ nil
455
+ end
456
+
457
+ # @since 0.3.0
458
+ # @api private
459
+ def _exception_handler(handler)
460
+ if respond_to?(handler.to_s, true)
461
+ method(handler)
462
+ else
463
+ ->(*) { halt handler }
464
+ end
465
+ end
466
+
467
+ # @since 0.1.0
468
+ # @api private
469
+ def _run_before_callbacks(*args)
470
+ self.class.before_callbacks.run(self, *args)
471
+ nil
472
+ end
473
+
474
+ # @since 0.1.0
475
+ # @api private
476
+ def _run_after_callbacks(*args)
477
+ self.class.after_callbacks.run(self, *args)
478
+ nil
479
+ end
480
+
481
+ # According to RFC 2616, when a response MUST have an empty body, it only
482
+ # allows Entity Headers.
483
+ #
484
+ # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
485
+ # other custom header.
486
+ #
487
+ # This restriction is enforced by <tt>Hanami::Action#_requires_no_body?</tt>.
488
+ #
489
+ # However, there are cases that demand to bypass this rule to set meta
490
+ # informations via headers.
491
+ #
492
+ # An example is a <tt>DELETE</tt> request for a JSON API application.
493
+ # It returns a <tt>204</tt> but still wants to specify the rate limit
494
+ # quota via <tt>X-Rate-Limit</tt>.
495
+ #
496
+ # @since 0.5.0
497
+ #
498
+ # @see Hanami::Action#_requires_no_body?
499
+ #
500
+ # @example
501
+ # require 'hanami/controller'
502
+ #
503
+ # module Books
504
+ # class Destroy
505
+ # include Hanami::Action
506
+ #
507
+ # def call(params)
508
+ # # ...
509
+ # self.headers.merge!(
510
+ # 'Last-Modified' => 'Fri, 27 Nov 2015 13:32:36 GMT',
511
+ # 'X-Rate-Limit' => '4000',
512
+ # 'Content-Type' => 'application/json',
513
+ # 'X-No-Pass' => 'true'
514
+ # )
515
+ #
516
+ # self.status = 204
517
+ # end
518
+ #
519
+ # private
520
+ #
521
+ # def keep_response_header?(header)
522
+ # super || header == 'X-Rate-Limit'
523
+ # end
524
+ # end
525
+ # end
526
+ #
527
+ # # Only the following headers will be sent:
528
+ # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
529
+ # # * X-Rate-Limit - because we explicitely allow it
530
+ #
531
+ # # Both Content-Type and X-No-Pass are removed because they're not allowed
532
+ def keep_response_header?(header)
533
+ ENTITY_HEADERS.include?(header)
534
+ end
535
+
536
+ # @since 2.0.0
537
+ # @api private
538
+ def _empty_headers(res)
539
+ res.headers.select! { |header, _| keep_response_header?(header) }
540
+ end
541
+
542
+ def format(value)
543
+ case value
544
+ when Symbol
545
+ format = Utils::Kernel.Symbol(value)
546
+ [format, Action::Mime.format_to_mime_type(format, configuration)]
547
+ when String
548
+ [Action::Mime.detect_format(value, configuration), value]
549
+ else
550
+ raise Hanami::Controller::UnknownFormatError.new(value)
551
+ end
552
+ end
553
+
554
+ # Finalize the response
555
+ #
556
+ # Prepare the data before the response will be returned to the webserver
557
+ #
558
+ # @since 0.1.0
559
+ # @api private
560
+ # @abstract
561
+ #
562
+ # @see Hanami::Action::Session#finish
563
+ # @see Hanami::Action::Cookies#finish
564
+ # @see Hanami::Action::Cache#finish
565
+ def finish(req, res, halted)
566
+ res.status, res.body = *halted unless halted.nil?
567
+
568
+ _empty_headers(res) if _requires_empty_headers?(res)
569
+
570
+ res.set_format(Action::Mime.detect_format(res.content_type, configuration))
571
+ res[:params] = req.params
572
+ res[:format] = res.format
573
+ res
574
+ end
575
+ end
576
+ end
577
+ end
578
+ end
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/params'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Validatable
6
6
  # Defines the class name for anonymous params
7
7
  #
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ViewNameInferrer
6
+ ALTERNATIVE_NAMES = {
7
+ "create" => "new",
8
+ "update" => "edit"
9
+ }.freeze
10
+
11
+ class << self
12
+ def call(action_name:, provider:)
13
+ application = provider.respond_to?(:application) ? provider.application : Hanami.application
14
+
15
+ action_identifier_base = application.config.actions.name_inference_base
16
+ view_identifier_base = application.config.actions.view_name_inference_base
17
+
18
+ identifier = action_identifier_name(action_name, provider, action_identifier_base)
19
+
20
+ view_name = [view_identifier_base, identifier].compact.join(".")
21
+
22
+ [view_name, alternative_view_name(view_name)].compact
23
+ end
24
+
25
+ private
26
+
27
+ def action_identifier_name(action_name, provider, name_base)
28
+ provider
29
+ .inflector
30
+ .underscore(action_name)
31
+ .sub(/^#{provider.namespace_path}\//, "")
32
+ .sub(/^#{name_base}\//, "")
33
+ .gsub("/", ".")
34
+ end
35
+
36
+ def alternative_view_name(view_name)
37
+ parts = view_name.split(".")
38
+
39
+ alternative_name = ALTERNATIVE_NAMES[parts.last]
40
+
41
+ [parts[0..-2], alternative_name].join(".") if alternative_name
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end