hanami-controller 1.3.3 → 2.0.0.alpha1

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