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
@@ -2,7 +2,7 @@ require 'hanami/action/base_params'
2
2
  require 'hanami/validations/form'
3
3
 
4
4
  module Hanami
5
- module Action
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 http://hanamirb.org/guides/validations/overview/
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,7 @@
1
1
  require 'rack/file'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Rack
6
6
  # File to be sent
7
7
  #
@@ -1,7 +1,8 @@
1
1
  require 'rack/request'
2
+ require 'securerandom'
2
3
 
3
4
  module Hanami
4
- module Action
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
- # @raise [NotImplementedError]
13
- #
14
- # @since 0.3.1
15
- # @api private
16
- def content_type
17
- raise NotImplementedError, 'Please use Action#content_type'
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
- # @raise [NotImplementedError]
21
- #
22
- # @since 0.3.1
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
- # @raise [NotImplementedError]
29
- #
30
- # @since 0.3.1
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 cookies
33
- raise NotImplementedError, 'Please include Action::Cookies and use Action#cookies'
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 params
41
- raise NotImplementedError, 'Please use params passed to Action#call'
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
- module Action
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
- # The key that returns raw session from the Rack env
12
- #
13
- # @since 0.1.0
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