hanami-controller 1.3.0 → 2.0.0.alpha2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +297 -538
  5. data/hanami-controller.gemspec +6 -5
  6. data/lib/hanami/action.rb +129 -73
  7. data/lib/hanami/action/application_action.rb +111 -0
  8. data/lib/hanami/action/application_configuration.rb +92 -0
  9. data/lib/hanami/action/application_configuration/cookies.rb +29 -0
  10. data/lib/hanami/action/application_configuration/sessions.rb +46 -0
  11. data/lib/hanami/action/base_params.rb +2 -2
  12. data/lib/hanami/action/cache.rb +1 -139
  13. data/lib/hanami/action/cache/cache_control.rb +4 -4
  14. data/lib/hanami/action/cache/conditional_get.rb +7 -2
  15. data/lib/hanami/action/cache/directives.rb +1 -1
  16. data/lib/hanami/action/cache/expires.rb +3 -3
  17. data/lib/hanami/action/configuration.rb +430 -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 +3 -3
  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 +581 -0
  31. data/lib/hanami/action/validatable.rb +2 -2
  32. data/lib/hanami/action/view_name_inferrer.rb +46 -0
  33. data/lib/hanami/controller.rb +0 -227
  34. data/lib/hanami/controller/version.rb +1 -1
  35. data/lib/hanami/http/status.rb +2 -2
  36. metadata +47 -30
  37. data/lib/hanami-controller.rb +0 -1
  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.rb +0 -126
  42. data/lib/hanami/action/exposable/guard.rb +0 -104
  43. data/lib/hanami/action/head.rb +0 -121
  44. data/lib/hanami/action/rack.rb +0 -399
  45. data/lib/hanami/action/rack/callable.rb +0 -47
  46. data/lib/hanami/action/redirect.rb +0 -59
  47. data/lib/hanami/action/throwable.rb +0 -196
  48. data/lib/hanami/controller/configuration.rb +0 -763
@@ -17,13 +17,14 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = []
18
18
  spec.test_files = spec.files.grep(%r{^(spec)/})
19
19
  spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.3.0'
20
+ spec.required_ruby_version = '>= 2.6.0'
21
21
 
22
22
  spec.add_dependency 'rack', '~> 2.0'
23
- spec.add_dependency 'hanami-utils', '~> 1.3'
23
+ spec.add_dependency 'hanami-utils', '~> 2.0.alpha'
24
+ spec.add_dependency 'dry-configurable', '~> 0.12'
24
25
 
25
- spec.add_development_dependency 'bundler', '~> 1.6'
26
+ spec.add_development_dependency 'bundler', '>= 1.6', '< 3'
26
27
  spec.add_development_dependency 'rack-test', '~> 1.0'
27
- spec.add_development_dependency 'rake', '~> 12'
28
- spec.add_development_dependency 'rspec', '~> 3.7'
28
+ spec.add_development_dependency 'rake', '~> 13'
29
+ spec.add_development_dependency 'rspec', '~> 3.9'
29
30
  end
data/lib/hanami/action.rb CHANGED
@@ -1,17 +1,5 @@
1
- require 'hanami/action/configurable'
2
- require 'hanami/action/rack'
3
- require 'hanami/action/mime'
4
- require 'hanami/action/redirect'
5
- require 'hanami/action/exposable'
6
- require 'hanami/action/throwable'
7
- require 'hanami/action/callbacks'
8
- begin
9
- require 'hanami/validations'
10
- require 'hanami/action/validatable'
11
- rescue LoadError
12
- end
13
- require 'hanami/action/head'
14
- require 'hanami/action/callable'
1
+ require_relative 'action/application_action'
2
+ require_relative 'action/standalone_action'
15
3
 
16
4
  module Hanami
17
5
  # An HTTP endpoint
@@ -28,84 +16,152 @@ module Hanami
28
16
  # # ...
29
17
  # end
30
18
  # end
31
- module Action
32
- # Override Ruby's hook for modules.
33
- # It includes basic Hanami::Action modules to the given class.
19
+ class Action
20
+ # Rack SPEC response code
34
21
  #
35
- # @param base [Class] the target action
22
+ # @since 1.0.0
23
+ # @api private
24
+ RESPONSE_CODE = 0
25
+
26
+ # Rack SPEC response headers
36
27
  #
37
- # @since 0.1.0
28
+ # @since 1.0.0
38
29
  # @api private
30
+ RESPONSE_HEADERS = 1
31
+
32
+ # Rack SPEC response body
39
33
  #
40
- # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
41
- #
42
- # @see Hanami::Action::Rack
43
- # @see Hanami::Action::Mime
44
- # @see Hanami::Action::Http
45
- # @see Hanami::Action::Redirect
46
- # @see Hanami::Action::Exposable
47
- # @see Hanami::Action::Throwable
48
- # @see Hanami::Action::Callbacks
49
- # @see Hanami::Action::Validatable
50
- # @see Hanami::Action::Configurable
51
- # @see Hanami::Action::Callable
52
- def self.included(base)
53
- base.class_eval do
54
- include Rack
55
- include Mime
56
- include Redirect
57
- include Exposable
58
- include Throwable
59
- include Callbacks
60
- include Validatable if defined?(Validatable)
61
- include Configurable
62
- include Head
63
- prepend Callable
64
- end
65
- end
34
+ # @since 1.0.0
35
+ # @api private
36
+ RESPONSE_BODY = 2
66
37
 
67
- private
38
+ DEFAULT_ERROR_CODE = 500
68
39
 
69
- # Raise error when `Hanami::Action::Session` isn't included.
40
+ # Status codes that by RFC must not include a message body
70
41
  #
71
- # To use `session`, include `Hanami::Action::Session`.
42
+ # @since 0.3.2
43
+ # @api private
44
+ HTTP_STATUSES_WITHOUT_BODY = Set.new((100..199).to_a << 204 << 205 << 304).freeze
45
+
46
+ # Not Found
72
47
  #
73
- # @raise [Hanami::Controller::MissingSessionError]
48
+ # @since 1.0.0
49
+ # @api private
50
+ NOT_FOUND = 404
51
+
52
+ # Entity headers allowed in blank body responses, according to
53
+ # RFC 2616 - Section 10 (HTTP 1.1).
74
54
  #
75
- # @since 1.2.0
76
- def session
77
- raise Hanami::Controller::MissingSessionError.new(:session)
78
- end
55
+ # "The response MAY include new or updated metainformation in the form
56
+ # of entity-headers".
57
+ #
58
+ # @since 0.4.0
59
+ # @api private
60
+ #
61
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
62
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html
63
+ ENTITY_HEADERS = {
64
+ 'Allow' => true,
65
+ 'Content-Encoding' => true,
66
+ 'Content-Language' => true,
67
+ 'Content-Location' => true,
68
+ 'Content-MD5' => true,
69
+ 'Content-Range' => true,
70
+ 'Expires' => true,
71
+ 'Last-Modified' => true,
72
+ 'extension-header' => true
73
+ }.freeze
79
74
 
80
- # Raise error when `Hanami::Action::Session` isn't included.
75
+ # The request method
81
76
  #
82
- # To use `flash`, include `Hanami::Action::Session`.
77
+ # @since 0.3.2
78
+ # @api private
79
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
80
+
81
+ # The Content-Length HTTP header
83
82
  #
84
- # @raise [Hanami::Controller::MissingSessionError]
83
+ # @since 1.0.0
84
+ # @api private
85
+ CONTENT_LENGTH = 'Content-Length'.freeze
86
+
87
+ # The non-standard HTTP header to pass the control over when a resource
88
+ # cannot be found by the current endpoint
85
89
  #
86
- # @since 1.2.0
87
- def flash
88
- raise Hanami::Controller::MissingSessionError.new(:flash)
89
- end
90
+ # @since 1.0.0
91
+ # @api private
92
+ X_CASCADE = 'X-Cascade'.freeze
93
+
94
+ # HEAD request
95
+ #
96
+ # @since 0.3.2
97
+ # @api private
98
+ HEAD = 'HEAD'.freeze
99
+
100
+ # The key that returns accepted mime types from the Rack env
101
+ #
102
+ # @since 0.1.0
103
+ # @api private
104
+ HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
90
105
 
91
- # Finalize the response
106
+ # The header key to set the mime type of the response
92
107
  #
93
- # This method is abstract and COULD be implemented by included modules in
94
- # order to prepare their data before the response will be returned to the
95
- # webserver.
108
+ # @since 0.1.0
109
+ # @api private
110
+ CONTENT_TYPE = 'Content-Type'.freeze
111
+
112
+ # The default mime type for an incoming HTTP request
96
113
  #
97
114
  # @since 0.1.0
98
115
  # @api private
99
- # @abstract
116
+ DEFAULT_ACCEPT = '*/*'.freeze
117
+
118
+ # The default mime type that is returned in the response
119
+ #
120
+ # @since 0.1.0
121
+ # @api private
122
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
123
+
124
+ # @since 0.2.0
125
+ # @api private
126
+ RACK_ERRORS = 'rack.errors'.freeze
127
+
128
+ # This isn't part of Rack SPEC
129
+ #
130
+ # Exception notifiers use <tt>rack.exception</tt> instead of
131
+ # <tt>rack.errors</tt>, so we need to support it.
132
+ #
133
+ # @since 0.5.0
134
+ # @api private
100
135
  #
101
- # @see Hanami::Action::Mime#finish
102
- # @see Hanami::Action::Exposable#finish
103
- # @see Hanami::Action::Callable#finish
104
- # @see Hanami::Action::Session#finish
105
- # @see Hanami::Action::Cookies#finish
106
- # @see Hanami::Action::Cache#finish
107
- # @see Hanami::Action::Head#finish
108
- def finish
136
+ # @see Hanami::Action::Throwable::RACK_ERRORS
137
+ # @see http://www.rubydoc.info/github/rack/rack/file/SPEC#The_Error_Stream
138
+ # @see https://github.com/hanami/controller/issues/133
139
+ RACK_EXCEPTION = 'rack.exception'.freeze
140
+
141
+ # The HTTP header for redirects
142
+ #
143
+ # @since 0.2.0
144
+ # @api private
145
+ LOCATION = 'Location'.freeze
146
+
147
+ include StandaloneAction
148
+
149
+ def self.inherited(subclass)
150
+ super
151
+
152
+ # When inheriting within an Hanami app, and the application provider has
153
+ # changed from the superclass, (re-)configure the action for the provider,
154
+ # i.e. for the slice and/or the application itself
155
+ if (provider = application_provider(subclass)) && provider != application_provider(subclass.superclass)
156
+ subclass.include ApplicationAction.new(provider)
157
+ end
158
+ end
159
+
160
+ def self.application_provider(subclass)
161
+ if Hanami.respond_to?(:application?) && Hanami.application?
162
+ Hanami.application.component_provider(subclass)
163
+ end
109
164
  end
165
+ private_class_method :application_provider
110
166
  end
111
167
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationAction < Module
6
+ attr_reader :provider
7
+ attr_reader :application
8
+
9
+ def initialize(provider)
10
+ @provider = provider
11
+ @application = provider.respond_to?(:application) ? provider.application : Hanami.application
12
+ end
13
+
14
+ def included(action_class)
15
+ action_class.include InstanceMethods
16
+
17
+ define_initialize action_class
18
+ configure_action action_class
19
+ extend_behavior action_class
20
+ end
21
+
22
+ def inspect
23
+ "#<#{self.class.name}[#{provider.name}]>"
24
+ end
25
+
26
+ private
27
+
28
+ def define_initialize(action_class)
29
+ resolve_view = method(:resolve_paired_view)
30
+ resolve_context = method(:resolve_view_context)
31
+
32
+ define_method :initialize do |**deps|
33
+ # Conditionally assign these to repsect any explictly auto-injected
34
+ # dependencies provided by the class
35
+ @view ||= deps[:view] || resolve_view.(self.class)
36
+ @view_context ||= deps[:view_context] || resolve_context.()
37
+
38
+ super(**deps)
39
+ end
40
+ end
41
+
42
+ def resolve_view_context
43
+ identifier = application.config.actions.view_context_identifier
44
+
45
+ if provider.key?(identifier)
46
+ provider[identifier]
47
+ elsif application.key?(identifier)
48
+ application[identifier]
49
+ end
50
+ end
51
+
52
+ def resolve_paired_view(action_class)
53
+ view_identifiers = application.config.actions.view_name_inferrer.(
54
+ action_name: action_class.name,
55
+ provider: provider
56
+ )
57
+
58
+ view_identifiers.detect { |identifier|
59
+ break provider[identifier] if provider.key?(identifier)
60
+ }
61
+ end
62
+
63
+ def configure_action(action_class)
64
+ action_class.config.settings.each do |setting|
65
+ application_value = application.config.actions.public_send(:"#{setting}")
66
+ action_class.config.public_send :"#{setting}=", application_value
67
+ end
68
+ end
69
+
70
+ def extend_behavior(action_class)
71
+ if application.config.actions.csrf_protection
72
+ require "hanami/action/csrf_protection"
73
+ action_class.include Hanami::Action::CSRFProtection
74
+ end
75
+
76
+ if application.config.actions.cookies.enabled?
77
+ require "hanami/action/cookies"
78
+ action_class.include Hanami::Action::Cookies
79
+ end
80
+ end
81
+
82
+ module InstanceMethods
83
+ attr_reader :view
84
+ attr_reader :view_context
85
+
86
+ protected
87
+
88
+ def handle(request, response)
89
+ if view
90
+ response.render view, **request.params
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def build_response(**options)
97
+ options = options.merge(view_options: method(:view_options))
98
+ super(**options)
99
+ end
100
+
101
+ def view_options(req, res)
102
+ {context: view_context&.with(**view_context_options(req, res))}.compact
103
+ end
104
+
105
+ def view_context_options(req, res)
106
+ {request: req, response: res}
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "application_configuration/cookies"
4
+ require_relative "application_configuration/sessions"
5
+ require_relative "configuration"
6
+ require_relative "view_name_inferrer"
7
+
8
+ module Hanami
9
+ class Action
10
+ class ApplicationConfiguration
11
+ include Dry::Configurable
12
+
13
+ setting(:cookies, {}) { |options| Cookies.new(options) }
14
+ setting(:sessions) { |storage, *options| Sessions.new(storage, *options) }
15
+ setting :csrf_protection
16
+
17
+ setting :name_inference_base, "actions"
18
+ setting :view_context_identifier, "view.context"
19
+ setting :view_name_inferrer, ViewNameInferrer
20
+ setting :view_name_inference_base, "views"
21
+
22
+ def initialize(*)
23
+ super
24
+
25
+ @base_configuration = Configuration.new
26
+
27
+ configure_defaults
28
+ end
29
+
30
+ def finalize!
31
+ # A nil value for `csrf_protection` means it has not been explicitly configured
32
+ # (neither true nor false), so we can default it to whether sessions are enabled
33
+ self.csrf_protection = sessions.enabled? if csrf_protection.nil?
34
+ end
35
+
36
+ # Returns the list of available settings
37
+ #
38
+ # @return [Set]
39
+ #
40
+ # @since 2.0.0
41
+ # @api private
42
+ def settings
43
+ base_configuration.settings + self.class.settings
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :base_configuration
49
+
50
+ # Apply defaults for base configuration settings
51
+ def configure_defaults
52
+ self.default_request_format = :html
53
+ self.default_response_format = :html
54
+
55
+ self.default_headers = {
56
+ "X-Frame-Options" => "DENY",
57
+ "X-Content-Type-Options" => "nosniff",
58
+ "X-XSS-Protection" => "1; mode=block",
59
+ "Content-Security-Policy" => \
60
+ "base-uri 'self'; " \
61
+ "child-src 'self'; " \
62
+ "connect-src 'self'; " \
63
+ "default-src 'none'; " \
64
+ "font-src 'self'; " \
65
+ "form-action 'self'; " \
66
+ "frame-ancestors 'self'; " \
67
+ "frame-src 'self'; " \
68
+ "img-src 'self' https: data:; " \
69
+ "media-src 'self'; " \
70
+ "object-src 'none'; " \
71
+ "plugin-types application/pdf; " \
72
+ "script-src 'self'; " \
73
+ "style-src 'self' 'unsafe-inline' https:"
74
+ }
75
+ end
76
+
77
+ def method_missing(name, *args, &block)
78
+ if config.respond_to?(name)
79
+ config.public_send(name, *args, &block)
80
+ elsif base_configuration.respond_to?(name)
81
+ base_configuration.public_send(name, *args, &block)
82
+ else
83
+ super
84
+ end
85
+ end
86
+
87
+ def respond_to_missing?(name, _incude_all = false)
88
+ config.respond_to?(name) || base_configuration.respond_to?(name) || super
89
+ end
90
+ end
91
+ end
92
+ end