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/lib/hanami/action/params.rb
CHANGED
@@ -2,7 +2,7 @@ require 'hanami/action/base_params'
|
|
2
2
|
require 'hanami/validations/form'
|
3
3
|
|
4
4
|
module Hanami
|
5
|
-
|
5
|
+
class Action
|
6
6
|
# A set of params requested by the client
|
7
7
|
#
|
8
8
|
# It's able to extract the relevant params from a Rack env of from an Hash.
|
@@ -126,7 +126,7 @@ module Hanami
|
|
126
126
|
#
|
127
127
|
# @since 0.7.0
|
128
128
|
#
|
129
|
-
# @see
|
129
|
+
# @see https://guides.hanamirb.org/validations/overview
|
130
130
|
#
|
131
131
|
# @example
|
132
132
|
# class Signup
|
@@ -202,7 +202,7 @@ module Hanami
|
|
202
202
|
error_set.each_with_object([]) do |(key, messages), result|
|
203
203
|
k = Utils::String.titleize(key)
|
204
204
|
|
205
|
-
_messages = if messages.is_a?(Hash)
|
205
|
+
_messages = if messages.is_a?(::Hash)
|
206
206
|
error_messages(messages)
|
207
207
|
else
|
208
208
|
messages.map { |message| "#{k} #{message}" }
|
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'rack/request'
|
2
|
+
require 'securerandom'
|
2
3
|
|
3
4
|
module Hanami
|
4
|
-
|
5
|
+
class Action
|
5
6
|
# An HTTP request based on top of Rack::Request.
|
6
7
|
# This guarantees backwards compatibility with with Rack.
|
7
8
|
#
|
@@ -9,36 +10,45 @@ module Hanami
|
|
9
10
|
#
|
10
11
|
# @see http://www.rubydoc.info/gems/rack/Rack/Request
|
11
12
|
class Request < ::Rack::Request
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
HTTP_ACCEPT = "HTTP_ACCEPT".freeze
|
14
|
+
REQUEST_ID = "hanami.request_id".freeze
|
15
|
+
DEFAULT_ACCEPT = "*/*".freeze
|
16
|
+
DEFAULT_ID_LENGTH = 16
|
17
|
+
|
18
|
+
attr_reader :params
|
19
|
+
|
20
|
+
def initialize(env, params)
|
21
|
+
super(env)
|
22
|
+
@params = params
|
18
23
|
end
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# @api private
|
24
|
-
def session
|
25
|
-
raise NotImplementedError, 'Please include Action::Session and use Action#session'
|
25
|
+
def id
|
26
|
+
# FIXME make this number configurable and document the probabilities of clashes
|
27
|
+
@id ||= @env[REQUEST_ID] = SecureRandom.hex(DEFAULT_ID_LENGTH)
|
26
28
|
end
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
def accept?(mime_type)
|
31
|
+
!!::Rack::Utils.q_values(accept).find do |mime, _|
|
32
|
+
::Rack::Mime.match?(mime_type, mime)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def accept_header?
|
37
|
+
accept != DEFAULT_ACCEPT
|
38
|
+
end
|
39
|
+
|
40
|
+
# @since 0.1.0
|
31
41
|
# @api private
|
32
|
-
def
|
33
|
-
|
42
|
+
def accept
|
43
|
+
@accept ||= @env[HTTP_ACCEPT] || DEFAULT_ACCEPT
|
34
44
|
end
|
35
45
|
|
36
46
|
# @raise [NotImplementedError]
|
37
47
|
#
|
38
48
|
# @since 0.3.1
|
39
49
|
# @api private
|
40
|
-
def
|
41
|
-
raise NotImplementedError, 'Please use
|
50
|
+
def content_type
|
51
|
+
raise NotImplementedError, 'Please use Action#content_type'
|
42
52
|
end
|
43
53
|
|
44
54
|
# @raise [NotImplementedError]
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'rack/response'
|
5
|
+
require 'hanami/utils/kernel'
|
6
|
+
require 'hanami/action/flash'
|
7
|
+
require 'hanami/action/halt'
|
8
|
+
require 'hanami/action/cookie_jar'
|
9
|
+
require 'hanami/action/cache/cache_control'
|
10
|
+
require 'hanami/action/cache/expires'
|
11
|
+
require 'hanami/action/cache/conditional_get'
|
12
|
+
|
13
|
+
module Hanami
|
14
|
+
class Action
|
15
|
+
class Response < ::Rack::Response
|
16
|
+
DEFAULT_VIEW_OPTIONS = -> * { {} }.freeze
|
17
|
+
|
18
|
+
REQUEST_METHOD = "REQUEST_METHOD"
|
19
|
+
HTTP_ACCEPT = "HTTP_ACCEPT"
|
20
|
+
SESSION_KEY = "rack.session"
|
21
|
+
REQUEST_ID = "hanami.request_id"
|
22
|
+
LOCATION = "Location"
|
23
|
+
|
24
|
+
X_CASCADE = "X-Cascade"
|
25
|
+
CONTENT_LENGTH = "Content-Length"
|
26
|
+
NOT_FOUND = 404
|
27
|
+
|
28
|
+
RACK_STATUS = 0
|
29
|
+
RACK_HEADERS = 1
|
30
|
+
RACK_BODY = 2
|
31
|
+
|
32
|
+
HEAD = "HEAD"
|
33
|
+
|
34
|
+
FLASH_SESSION_KEY = "_flash"
|
35
|
+
|
36
|
+
EMPTY_BODY = [].freeze
|
37
|
+
|
38
|
+
FILE_SYSTEM_ROOT = Pathname.new("/").freeze
|
39
|
+
|
40
|
+
attr_reader :request, :action, :exposures, :format, :env, :view_options
|
41
|
+
attr_accessor :charset
|
42
|
+
|
43
|
+
def self.build(status, env)
|
44
|
+
new(action: "", configuration: nil, content_type: Mime.best_q_match(env[HTTP_ACCEPT]), env: env).tap do |r|
|
45
|
+
r.status = status
|
46
|
+
r.body = Http::Status.message_for(status)
|
47
|
+
r.set_format(Mime.format_for(r.content_type))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(request:, action:, configuration:, content_type: nil, env: {}, headers: {}, view_options: nil)
|
52
|
+
super([], 200, headers.dup)
|
53
|
+
set_header("Content-Type", content_type)
|
54
|
+
|
55
|
+
@request = request
|
56
|
+
@action = action
|
57
|
+
@configuration = configuration
|
58
|
+
@charset = ::Rack::MediaType.params(content_type).fetch('charset', nil)
|
59
|
+
@exposures = {}
|
60
|
+
@env = env
|
61
|
+
@view_options = view_options || DEFAULT_VIEW_OPTIONS
|
62
|
+
|
63
|
+
@sending_file = false
|
64
|
+
end
|
65
|
+
|
66
|
+
def body=(str)
|
67
|
+
@length = 0
|
68
|
+
@body = EMPTY_BODY.dup
|
69
|
+
|
70
|
+
# FIXME: there could be a bug that prevents Content-Length to be sent for files
|
71
|
+
if str.is_a?(::Rack::File::Iterator)
|
72
|
+
@body = str
|
73
|
+
else
|
74
|
+
write(str) unless str.nil? || str == EMPTY_BODY
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def render(view, **options)
|
79
|
+
self.body = view.(**view_options.(request, self), **options).to_str
|
80
|
+
end
|
81
|
+
|
82
|
+
def format=(args)
|
83
|
+
@format, content_type = *args
|
84
|
+
content_type = Action::Mime.content_type_with_charset(content_type, charset)
|
85
|
+
set_header("Content-Type", content_type)
|
86
|
+
end
|
87
|
+
|
88
|
+
def [](key)
|
89
|
+
@exposures.fetch(key)
|
90
|
+
end
|
91
|
+
|
92
|
+
def []=(key, value)
|
93
|
+
@exposures[key] = value
|
94
|
+
end
|
95
|
+
|
96
|
+
def session
|
97
|
+
env[SESSION_KEY] ||= {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def cookies
|
101
|
+
@cookies ||= CookieJar.new(env.dup, headers, @configuration.cookies)
|
102
|
+
end
|
103
|
+
|
104
|
+
def flash
|
105
|
+
@flash ||= Flash.new(session[FLASH_SESSION_KEY])
|
106
|
+
end
|
107
|
+
|
108
|
+
def redirect_to(url, status: 302)
|
109
|
+
return unless renderable?
|
110
|
+
|
111
|
+
redirect(::String.new(url), status)
|
112
|
+
Halt.call(status)
|
113
|
+
end
|
114
|
+
|
115
|
+
def send_file(path)
|
116
|
+
_send_file(
|
117
|
+
Rack::File.new(path, @configuration.public_directory).call(env)
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def unsafe_send_file(path)
|
122
|
+
directory = if Pathname.new(path).relative?
|
123
|
+
@configuration.root_directory
|
124
|
+
else
|
125
|
+
FILE_SYSTEM_ROOT
|
126
|
+
end
|
127
|
+
|
128
|
+
_send_file(
|
129
|
+
Rack::File.new(path, directory).call(env)
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def cache_control(*values)
|
134
|
+
directives = Cache::CacheControl::Directives.new(*values)
|
135
|
+
headers.merge!(directives.headers)
|
136
|
+
end
|
137
|
+
|
138
|
+
def expires(amount, *values)
|
139
|
+
directives = Cache::Expires::Directives.new(amount, *values)
|
140
|
+
headers.merge!(directives.headers)
|
141
|
+
end
|
142
|
+
|
143
|
+
def fresh(options)
|
144
|
+
conditional_get = Cache::ConditionalGet.new(env, options)
|
145
|
+
|
146
|
+
headers.merge!(conditional_get.headers)
|
147
|
+
|
148
|
+
conditional_get.fresh? do
|
149
|
+
Halt.call(304)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# @api private
|
154
|
+
def request_id
|
155
|
+
env.fetch(REQUEST_ID) do
|
156
|
+
# FIXME: raise a meaningful error, by inviting devs to include Hanami::Action::Session
|
157
|
+
raise "Can't find request ID"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def set_format(value)
|
162
|
+
@format = value
|
163
|
+
end
|
164
|
+
|
165
|
+
def renderable?
|
166
|
+
return !head? && body.empty? if body.respond_to?(:empty?)
|
167
|
+
|
168
|
+
!@sending_file && !head?
|
169
|
+
end
|
170
|
+
|
171
|
+
alias to_ary to_a
|
172
|
+
|
173
|
+
def head?
|
174
|
+
env[REQUEST_METHOD] == HEAD
|
175
|
+
end
|
176
|
+
|
177
|
+
# @api private
|
178
|
+
def _send_file(send_file_response)
|
179
|
+
headers.merge!(send_file_response[RACK_HEADERS])
|
180
|
+
|
181
|
+
if send_file_response[RACK_STATUS] == NOT_FOUND
|
182
|
+
headers.delete(X_CASCADE)
|
183
|
+
headers.delete(CONTENT_LENGTH)
|
184
|
+
Halt.call(NOT_FOUND)
|
185
|
+
else
|
186
|
+
self.status = send_file_response[RACK_STATUS]
|
187
|
+
self.body = send_file_response[RACK_BODY]
|
188
|
+
@sending_file = true
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -1,143 +1,21 @@
|
|
1
1
|
require 'hanami/action/flash'
|
2
2
|
|
3
3
|
module Hanami
|
4
|
-
|
4
|
+
class Action
|
5
5
|
# Session API
|
6
6
|
#
|
7
7
|
# This module isn't included by default.
|
8
8
|
#
|
9
9
|
# @since 0.1.0
|
10
10
|
module Session
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# @api private
|
15
|
-
SESSION_KEY = 'rack.session'.freeze
|
16
|
-
|
17
|
-
# The key that is used by flash to transport errors
|
18
|
-
#
|
19
|
-
# @since 0.3.0
|
20
|
-
# @api private
|
21
|
-
ERRORS_KEY = :__errors
|
22
|
-
|
23
|
-
# Add session to default exposures
|
24
|
-
#
|
25
|
-
# @since 0.4.4
|
26
|
-
# @api private
|
27
|
-
def self.included(action)
|
28
|
-
action.class_eval do
|
29
|
-
_expose :session, :flash
|
11
|
+
def self.included(base)
|
12
|
+
base.class_eval do
|
13
|
+
before { |req, _| req.id }
|
30
14
|
end
|
31
15
|
end
|
32
16
|
|
33
|
-
# Gets the session from the request and expose it as an Hash.
|
34
|
-
#
|
35
|
-
# @return [Hash] the HTTP session from the request
|
36
|
-
#
|
37
|
-
# @since 0.1.0
|
38
|
-
#
|
39
|
-
# @example
|
40
|
-
# require 'hanami/controller'
|
41
|
-
# require 'hanami/action/session'
|
42
|
-
#
|
43
|
-
# class Show
|
44
|
-
# include Hanami::Action
|
45
|
-
# include Hanami::Action::Session
|
46
|
-
#
|
47
|
-
# def call(params)
|
48
|
-
# # ...
|
49
|
-
#
|
50
|
-
# # get a value
|
51
|
-
# session[:user_id] # => '23'
|
52
|
-
#
|
53
|
-
# # set a value
|
54
|
-
# session[:foo] = 'bar'
|
55
|
-
#
|
56
|
-
# # remove a value
|
57
|
-
# session[:bax] = nil
|
58
|
-
# end
|
59
|
-
# end
|
60
|
-
def session
|
61
|
-
@_env[SESSION_KEY] ||= {}
|
62
|
-
end
|
63
|
-
|
64
|
-
# Read errors from flash or delegate to the superclass
|
65
|
-
#
|
66
|
-
# @return [Hanami::Validations::Errors] A collection of validation errors
|
67
|
-
#
|
68
|
-
# @since 0.3.0
|
69
|
-
#
|
70
|
-
# @see Hanami::Action::Validatable
|
71
|
-
# @see Hanami::Action::Session#flash
|
72
|
-
def errors
|
73
|
-
flash[ERRORS_KEY] || params.respond_to?(:errors) && params.errors
|
74
|
-
end
|
75
|
-
|
76
17
|
private
|
77
18
|
|
78
|
-
# Container useful to transport data with the HTTP session
|
79
|
-
#
|
80
|
-
# @return [Hanami::Action::Flash] a Flash instance
|
81
|
-
#
|
82
|
-
# @since 0.3.0
|
83
|
-
#
|
84
|
-
# @see Hanami::Action::Flash
|
85
|
-
def flash
|
86
|
-
@flash ||= Flash.new(session)
|
87
|
-
end
|
88
|
-
|
89
|
-
# In case of validations errors, preserve those informations after a
|
90
|
-
# redirect.
|
91
|
-
#
|
92
|
-
# @return [void]
|
93
|
-
#
|
94
|
-
# @since 0.3.0
|
95
|
-
# @api private
|
96
|
-
#
|
97
|
-
# @see Hanami::Action::Redirect#redirect_to
|
98
|
-
#
|
99
|
-
# @example
|
100
|
-
# require 'hanami/controller'
|
101
|
-
#
|
102
|
-
# module Comments
|
103
|
-
# class Index
|
104
|
-
# include Hanami::Action
|
105
|
-
# include Hanami::Action::Session
|
106
|
-
#
|
107
|
-
# expose :comments
|
108
|
-
#
|
109
|
-
# def call(params)
|
110
|
-
# @comments = CommentRepository.all
|
111
|
-
# end
|
112
|
-
# end
|
113
|
-
#
|
114
|
-
# class Create
|
115
|
-
# include Hanami::Action
|
116
|
-
# include Hanami::Action::Session
|
117
|
-
#
|
118
|
-
# params do
|
119
|
-
# param :text, type: String, presence: true
|
120
|
-
# end
|
121
|
-
#
|
122
|
-
# def call(params)
|
123
|
-
# comment = Comment.new(params)
|
124
|
-
# CommentRepository.create(comment) if params.valid?
|
125
|
-
#
|
126
|
-
# redirect_to '/comments'
|
127
|
-
# end
|
128
|
-
# end
|
129
|
-
# end
|
130
|
-
#
|
131
|
-
# # The validation errors caused by Comments::Create are available
|
132
|
-
# # **after the redirect** in the context of Comments::Index.
|
133
|
-
def redirect_to(*args)
|
134
|
-
if params.respond_to?(:valid?)
|
135
|
-
flash[ERRORS_KEY] = errors.to_a unless params.valid?
|
136
|
-
end
|
137
|
-
flash.keep!
|
138
|
-
super
|
139
|
-
end
|
140
|
-
|
141
19
|
# Finalize the response
|
142
20
|
#
|
143
21
|
# @return [void]
|
@@ -146,9 +24,14 @@ module Hanami
|
|
146
24
|
# @api private
|
147
25
|
#
|
148
26
|
# @see Hanami::Action#finish
|
149
|
-
def finish
|
27
|
+
def finish(req, res, *)
|
28
|
+
if (next_flash = res.flash.next).any?
|
29
|
+
res.session['_flash'] = next_flash
|
30
|
+
else
|
31
|
+
res.session.delete('_flash')
|
32
|
+
end
|
33
|
+
|
150
34
|
super
|
151
|
-
flash.clear
|
152
35
|
end
|
153
36
|
end
|
154
37
|
end
|