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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -0
- data/LICENSE.md +1 -1
- data/README.md +297 -538
- data/hanami-controller.gemspec +6 -5
- data/lib/hanami/action.rb +129 -73
- data/lib/hanami/action/application_action.rb +111 -0
- data/lib/hanami/action/application_configuration.rb +92 -0
- data/lib/hanami/action/application_configuration/cookies.rb +29 -0
- data/lib/hanami/action/application_configuration/sessions.rb +46 -0
- data/lib/hanami/action/base_params.rb +2 -2
- data/lib/hanami/action/cache.rb +1 -139
- data/lib/hanami/action/cache/cache_control.rb +4 -4
- data/lib/hanami/action/cache/conditional_get.rb +7 -2
- data/lib/hanami/action/cache/directives.rb +1 -1
- data/lib/hanami/action/cache/expires.rb +3 -3
- data/lib/hanami/action/configuration.rb +430 -0
- data/lib/hanami/action/cookie_jar.rb +3 -3
- data/lib/hanami/action/cookies.rb +3 -62
- data/lib/hanami/action/csrf_protection.rb +214 -0
- data/lib/hanami/action/flash.rb +102 -207
- data/lib/hanami/action/glue.rb +5 -31
- data/lib/hanami/action/halt.rb +12 -0
- data/lib/hanami/action/mime.rb +78 -485
- data/lib/hanami/action/params.rb +3 -3
- data/lib/hanami/action/rack/file.rb +1 -1
- data/lib/hanami/action/request.rb +30 -20
- data/lib/hanami/action/response.rb +193 -0
- data/lib/hanami/action/session.rb +11 -128
- data/lib/hanami/action/standalone_action.rb +581 -0
- data/lib/hanami/action/validatable.rb +2 -2
- data/lib/hanami/action/view_name_inferrer.rb +46 -0
- data/lib/hanami/controller.rb +0 -227
- data/lib/hanami/controller/version.rb +1 -1
- data/lib/hanami/http/status.rb +2 -2
- metadata +47 -30
- data/lib/hanami-controller.rb +0 -1
- data/lib/hanami/action/callable.rb +0 -92
- data/lib/hanami/action/callbacks.rb +0 -214
- data/lib/hanami/action/configurable.rb +0 -50
- data/lib/hanami/action/exposable.rb +0 -126
- data/lib/hanami/action/exposable/guard.rb +0 -104
- data/lib/hanami/action/head.rb +0 -121
- data/lib/hanami/action/rack.rb +0 -399
- data/lib/hanami/action/rack/callable.rb +0 -47
- data/lib/hanami/action/redirect.rb +0 -59
- data/lib/hanami/action/throwable.rb +0 -196
- data/lib/hanami/controller/configuration.rb +0 -763
data/hanami-controller.gemspec
CHANGED
@@ -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.
|
20
|
+
spec.required_ruby_version = '>= 2.6.0'
|
21
21
|
|
22
22
|
spec.add_dependency 'rack', '~> 2.0'
|
23
|
-
spec.add_dependency 'hanami-utils', '~>
|
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', '
|
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', '~>
|
28
|
-
spec.add_development_dependency 'rspec', '~> 3.
|
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
|
-
|
2
|
-
|
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
|
-
|
32
|
-
#
|
33
|
-
# It includes basic Hanami::Action modules to the given class.
|
19
|
+
class Action
|
20
|
+
# Rack SPEC response code
|
34
21
|
#
|
35
|
-
# @
|
22
|
+
# @since 1.0.0
|
23
|
+
# @api private
|
24
|
+
RESPONSE_CODE = 0
|
25
|
+
|
26
|
+
# Rack SPEC response headers
|
36
27
|
#
|
37
|
-
# @since
|
28
|
+
# @since 1.0.0
|
38
29
|
# @api private
|
30
|
+
RESPONSE_HEADERS = 1
|
31
|
+
|
32
|
+
# Rack SPEC response body
|
39
33
|
#
|
40
|
-
# @
|
41
|
-
#
|
42
|
-
|
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
|
-
|
38
|
+
DEFAULT_ERROR_CODE = 500
|
68
39
|
|
69
|
-
#
|
40
|
+
# Status codes that by RFC must not include a message body
|
70
41
|
#
|
71
|
-
#
|
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
|
-
# @
|
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
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
#
|
75
|
+
# The request method
|
81
76
|
#
|
82
|
-
#
|
77
|
+
# @since 0.3.2
|
78
|
+
# @api private
|
79
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
80
|
+
|
81
|
+
# The Content-Length HTTP header
|
83
82
|
#
|
84
|
-
# @
|
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.
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
#
|
106
|
+
# The header key to set the mime type of the response
|
92
107
|
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
|
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
|
-
|
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::
|
102
|
-
# @see
|
103
|
-
# @see
|
104
|
-
|
105
|
-
|
106
|
-
#
|
107
|
-
#
|
108
|
-
|
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
|