hanami-controller 1.3.0 → 2.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
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