hanami-controller 1.3.3 → 2.0.0.alpha1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -7
  3. data/README.md +295 -537
  4. data/hanami-controller.gemspec +3 -3
  5. data/lib/hanami/action.rb +653 -38
  6. data/lib/hanami/action/base_params.rb +2 -2
  7. data/lib/hanami/action/cache.rb +1 -139
  8. data/lib/hanami/action/cache/cache_control.rb +4 -4
  9. data/lib/hanami/action/cache/conditional_get.rb +4 -5
  10. data/lib/hanami/action/cache/directives.rb +1 -1
  11. data/lib/hanami/action/cache/expires.rb +3 -3
  12. data/lib/hanami/action/cookie_jar.rb +3 -3
  13. data/lib/hanami/action/cookies.rb +3 -62
  14. data/lib/hanami/action/flash.rb +2 -2
  15. data/lib/hanami/action/glue.rb +5 -31
  16. data/lib/hanami/action/halt.rb +12 -0
  17. data/lib/hanami/action/mime.rb +77 -491
  18. data/lib/hanami/action/params.rb +3 -3
  19. data/lib/hanami/action/rack/file.rb +1 -1
  20. data/lib/hanami/action/request.rb +30 -20
  21. data/lib/hanami/action/response.rb +174 -0
  22. data/lib/hanami/action/session.rb +8 -117
  23. data/lib/hanami/action/validatable.rb +2 -2
  24. data/lib/hanami/controller.rb +0 -210
  25. data/lib/hanami/controller/configuration.rb +51 -506
  26. data/lib/hanami/controller/version.rb +1 -1
  27. metadata +12 -21
  28. data/lib/hanami/action/callable.rb +0 -92
  29. data/lib/hanami/action/callbacks.rb +0 -214
  30. data/lib/hanami/action/configurable.rb +0 -50
  31. data/lib/hanami/action/exposable.rb +0 -126
  32. data/lib/hanami/action/exposable/guard.rb +0 -104
  33. data/lib/hanami/action/head.rb +0 -121
  34. data/lib/hanami/action/rack.rb +0 -411
  35. data/lib/hanami/action/rack/callable.rb +0 -47
  36. data/lib/hanami/action/rack/errors.rb +0 -53
  37. data/lib/hanami/action/redirect.rb +0 -59
  38. data/lib/hanami/action/throwable.rb +0 -169
@@ -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 https://guides.hanamirb.org/validations/overview
129
+ # @see http://hanamirb.org/guides/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,174 @@
1
+ require 'rack'
2
+ require 'rack/response'
3
+ require 'hanami/utils/kernel'
4
+ require 'hanami/action/flash'
5
+ require 'hanami/action/halt'
6
+ require 'hanami/action/cookie_jar'
7
+ require 'hanami/action/cache/cache_control'
8
+ require 'hanami/action/cache/expires'
9
+ require 'hanami/action/cache/conditional_get'
10
+
11
+ module Hanami
12
+ class Action
13
+ class Response < ::Rack::Response
14
+ REQUEST_METHOD = "REQUEST_METHOD".freeze
15
+ HTTP_ACCEPT = "HTTP_ACCEPT".freeze
16
+ SESSION_KEY = "rack.session".freeze
17
+ REQUEST_ID = "hanami.request_id".freeze
18
+ LOCATION = "Location".freeze
19
+
20
+ X_CASCADE = "X-Cascade".freeze
21
+ CONTENT_LENGTH = "Content-Length".freeze
22
+ NOT_FOUND = 404
23
+
24
+ RACK_STATUS = 0
25
+ RACK_HEADERS = 1
26
+ RACK_BODY = 2
27
+
28
+ HEAD = "HEAD".freeze
29
+
30
+ EMPTY_BODY = [].freeze
31
+
32
+ attr_reader :action, :exposures, :format, :env
33
+ attr_accessor :charset
34
+
35
+ def self.build(response, env)
36
+ new(action: "", configuration: nil, content_type: Mime.best_q_match(env[HTTP_ACCEPT]), env: env, header: response[RACK_HEADERS]).tap do |r|
37
+ r.status = response[RACK_STATUS]
38
+ r.body = response[RACK_BODY].first
39
+ r.set_format(Mime.format_for(r.content_type))
40
+ end
41
+ end
42
+
43
+ def initialize(action:, configuration:, content_type: nil, env: {}, header: {})
44
+ super([], 200, header.dup)
45
+ set_header("Content-Type", content_type)
46
+
47
+ @action = action
48
+ @configuration = configuration
49
+ @charset = ::Rack::MediaType.params(content_type).fetch('charset', nil)
50
+ @exposures = {}
51
+ @env = env
52
+
53
+ @sending_file = false
54
+ end
55
+
56
+ def body=(str)
57
+ @length = 0
58
+ @body = EMPTY_BODY.dup
59
+
60
+ # FIXME: there could be a bug that prevents Content-Length to be sent for files
61
+ if str.is_a?(::Rack::File::Iterator)
62
+ @body = str
63
+ else
64
+ write(str) unless str.nil? || str == EMPTY_BODY
65
+ end
66
+ end
67
+
68
+ def format=(args)
69
+ @format, content_type = *args
70
+ content_type = Action::Mime.content_type_with_charset(content_type, charset)
71
+ set_header("Content-Type", content_type)
72
+ end
73
+
74
+ def [](key)
75
+ @exposures.fetch(key)
76
+ end
77
+
78
+ def []=(key, value)
79
+ @exposures[key] = value
80
+ end
81
+
82
+ def session
83
+ env[SESSION_KEY] ||= {}
84
+ end
85
+
86
+ def cookies
87
+ @cookies ||= CookieJar.new(env.dup, headers, @configuration.cookies)
88
+ end
89
+
90
+ def flash
91
+ @flash ||= Flash.new(session)
92
+ end
93
+
94
+ def redirect_to(url, status: 302)
95
+ return unless renderable?
96
+
97
+ # This trick avoids to instantiate `flash` if it wasn't already.
98
+ flash.keep! if defined?(@flash)
99
+
100
+ redirect(::String.new(url), status)
101
+ Halt.call(status)
102
+ end
103
+
104
+ def send_file(path)
105
+ _send_file(
106
+ Rack::File.new(path, @configuration.public_directory).call(env)
107
+ )
108
+ end
109
+
110
+ def unsafe_send_file(path)
111
+ directory = @configuration.root_directory if Pathname.new(path).relative?
112
+
113
+ _send_file(
114
+ Rack::File.new(path, directory).call(env)
115
+ )
116
+ end
117
+
118
+ def cache_control(*values)
119
+ directives = Cache::CacheControl::Directives.new(*values)
120
+ headers.merge!(directives.headers)
121
+ end
122
+
123
+ def expires(amount, *values)
124
+ directives = Cache::Expires::Directives.new(amount, *values)
125
+ headers.merge!(directives.headers)
126
+ end
127
+
128
+ def fresh(options)
129
+ conditional_get = Cache::ConditionalGet.new(env, options)
130
+
131
+ headers.merge!(conditional_get.headers)
132
+
133
+ conditional_get.fresh? do
134
+ Halt.call(304)
135
+ end
136
+ end
137
+
138
+ # @api private
139
+ def request_id
140
+ env.fetch(REQUEST_ID) do
141
+ # FIXME: raise a meaningful error, by inviting devs to include Hanami::Action::Session
142
+ raise "Can't find request ID"
143
+ end
144
+ end
145
+
146
+ def set_format(value)
147
+ @format = value
148
+ end
149
+
150
+ def renderable?
151
+ !@sending_file && !head?
152
+ end
153
+
154
+ def head?
155
+ env[REQUEST_METHOD] == HEAD
156
+ end
157
+
158
+ # @api private
159
+ def _send_file(send_file_response)
160
+ headers.merge!(send_file_response[RACK_HEADERS])
161
+
162
+ if send_file_response[RACK_STATUS] == NOT_FOUND
163
+ headers.delete(X_CASCADE)
164
+ headers.delete(CONTENT_LENGTH)
165
+ Halt.call(NOT_FOUND)
166
+ else
167
+ self.status = send_file_response[RACK_STATUS]
168
+ self.body = send_file_response[RACK_BODY]
169
+ @sending_file = true
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,78 +1,19 @@
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
19
  # Container useful to transport data with the HTTP session
@@ -86,58 +27,6 @@ module Hanami
86
27
  @flash ||= Flash.new(session)
87
28
  end
88
29
 
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
30
  # Finalize the response
142
31
  #
143
32
  # @return [void]
@@ -146,9 +35,11 @@ module Hanami
146
35
  # @api private
147
36
  #
148
37
  # @see Hanami::Action#finish
149
- def finish
38
+ def finish(req, res, *)
39
+ res.flash.clear
40
+ res[:session] = res.session
41
+ res[:flash] = res.flash
150
42
  super
151
- flash.clear
152
43
  end
153
44
  end
154
45
  end
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/params'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Validatable
6
6
  # Defines the class name for anonymous params
7
7
  #
@@ -50,7 +50,7 @@ module Hanami
50
50
  # @since 0.3.0
51
51
  #
52
52
  # @see Hanami::Action::Params
53
- # @see https://guides.hanamirb.org//validations/overview
53
+ # @see http://hanamirb.org/guides/validations/overview/
54
54
  #
55
55
  # @example Anonymous Block
56
56
  # require 'hanami/controller'