hanami-controller 2.0.0.alpha8 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,7 @@
1
- require 'hanami/action/base_params'
2
- require 'hanami/validations/form'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/action/base_params"
4
+ require "hanami/validations/form"
3
5
 
4
6
  module Hanami
5
7
  class Action
@@ -48,45 +50,41 @@ module Hanami
48
50
  # @example Basic usage
49
51
  # require "hanami/controller"
50
52
  #
51
- # class MyAction
52
- # include Hanami::Action
53
- #
53
+ # class MyAction < Hanami::Action
54
54
  # params do
55
55
  # required(:book).schema do
56
56
  # required(:isbn).filled(:str?)
57
57
  # end
58
58
  # end
59
59
  #
60
- # def call(params)
60
+ # def handle(req, res)
61
61
  # # 1. Don't try to save the record if the params aren't valid
62
- # return unless params.valid?
62
+ # return unless req.params.valid?
63
63
  #
64
- # BookRepository.new.create(params[:book])
64
+ # BookRepository.new.create(req.params[:book])
65
65
  # rescue Hanami::Model::UniqueConstraintViolationError
66
66
  # # 2. Add an error in case the record wasn't unique
67
- # params.errors.add(:book, :isbn, "is not unique")
67
+ # req.params.errors.add(:book, :isbn, "is not unique")
68
68
  # end
69
69
  # end
70
70
  #
71
71
  # @example Invalid argument
72
72
  # require "hanami/controller"
73
73
  #
74
- # class MyAction
75
- # include Hanami::Action
76
- #
74
+ # class MyAction < Hanami::Action
77
75
  # params do
78
76
  # required(:book).schema do
79
77
  # required(:title).filled(:str?)
80
78
  # end
81
79
  # end
82
80
  #
83
- # def call(params)
84
- # puts params.to_h # => {}
85
- # puts params.valid? # => false
86
- # puts params.error_messages # => ["Book is missing"]
87
- # puts params.errors # => {:book=>["is missing"]}
81
+ # def handle(req, *)
82
+ # puts req.params.to_h # => {}
83
+ # puts req.params.valid? # => false
84
+ # puts req.params.error_messages # => ["Book is missing"]
85
+ # puts req.params.errors # => {:book=>["is missing"]}
88
86
  #
89
- # params.errors.add(:book, :isbn, "is not unique") # => ArgumentError
87
+ # req.params.errors.add(:book, :isbn, "is not unique") # => ArgumentError
90
88
  # end
91
89
  # end
92
90
  def add(*args)
@@ -129,9 +127,8 @@ module Hanami
129
127
  # @see https://guides.hanamirb.org/validations/overview
130
128
  #
131
129
  # @example
132
- # class Signup
130
+ # class Signup < Hanami::Action
133
131
  # MEGABYTE = 1024 ** 2
134
- # include Hanami::Action
135
132
  #
136
133
  # params do
137
134
  # required(:first_name).filled(:str?)
@@ -143,13 +140,13 @@ module Hanami
143
140
  # optional(:avatar).filled(size?: 1..(MEGABYTE * 3))
144
141
  # end
145
142
  #
146
- # def call(params)
147
- # halt 400 unless params.valid?
143
+ # def handle(req, *)
144
+ # halt 400 unless req.params.valid?
148
145
  # # ...
149
146
  # end
150
147
  # end
151
148
  def self.params(&blk)
152
- validations(&blk || ->() {})
149
+ validations(&blk || -> {})
153
150
  end
154
151
 
155
152
  # Initialize the params and freeze them.
@@ -186,7 +183,13 @@ module Hanami
186
183
  #
187
184
  # @example
188
185
  # params.errors
189
- # # => {:email=>["is missing", "is in invalid format"], :name=>["is missing"], :tos=>["is missing"], :age=>["is missing"], :address=>["is missing"]}
186
+ # # => {
187
+ # :email=>["is missing", "is in invalid format"],
188
+ # :name=>["is missing"],
189
+ # :tos=>["is missing"],
190
+ # :age=>["is missing"],
191
+ # :address=>["is missing"]
192
+ # }
190
193
  attr_reader :errors
191
194
 
192
195
  # Returns flat collection of full error messages
@@ -197,18 +200,25 @@ module Hanami
197
200
  #
198
201
  # @example
199
202
  # params.error_messages
200
- # # => ["Email is missing", "Email is in invalid format", "Name is missing", "Tos is missing", "Age is missing", "Address is missing"]
203
+ # # => [
204
+ # "Email is missing",
205
+ # "Email is in invalid format",
206
+ # "Name is missing",
207
+ # "Tos is missing",
208
+ # "Age is missing",
209
+ # "Address is missing"
210
+ # ]
201
211
  def error_messages(error_set = errors)
202
212
  error_set.each_with_object([]) do |(key, messages), result|
203
213
  k = Utils::String.titleize(key)
204
214
 
205
- _messages = if messages.is_a?(::Hash)
206
- error_messages(messages)
207
- else
208
- messages.map { |message| "#{k} #{message}" }
209
- end
215
+ msgs = if messages.is_a?(::Hash)
216
+ error_messages(messages)
217
+ else
218
+ messages.map { |message| "#{k} #{message}" }
219
+ end
210
220
 
211
- result.concat(_messages)
221
+ result.concat(msgs)
212
222
  end
213
223
  end
214
224
 
@@ -1,4 +1,6 @@
1
- require 'rack/file'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/file"
2
4
 
3
5
  module Hanami
4
6
  class Action
@@ -10,12 +12,6 @@ module Hanami
10
12
  #
11
13
  # @see Hanami::Action::Rack#send_file
12
14
  class File
13
- # The key that returns path info from the Rack env
14
- #
15
- # @since 1.0.0
16
- # @api private
17
- PATH_INFO = "PATH_INFO".freeze
18
-
19
15
  # @param path [String,Pathname] file path
20
16
  #
21
17
  # @since 0.4.3
@@ -29,11 +25,11 @@ module Hanami
29
25
  # @api private
30
26
  def call(env)
31
27
  env = env.dup
32
- env[PATH_INFO] = @path
28
+ env[Action::PATH_INFO] = @path
33
29
 
34
30
  @file.get(env)
35
31
  rescue Errno::ENOENT
36
- [404, {}, nil]
32
+ [Action::NOT_FOUND, {}, nil]
37
33
  end
38
34
  end
39
35
  end
@@ -1,5 +1,9 @@
1
- require 'rack/request'
2
- require 'securerandom'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+ require "rack/mime"
5
+ require "rack/request"
6
+ require "securerandom"
3
7
 
4
8
  module Hanami
5
9
  class Action
@@ -10,11 +14,6 @@ module Hanami
10
14
  #
11
15
  # @see http://www.rubydoc.info/gems/rack/Rack/Request
12
16
  class Request < ::Rack::Request
13
- HTTP_ACCEPT = "HTTP_ACCEPT".freeze
14
- REQUEST_ID = "hanami.request_id".freeze
15
- DEFAULT_ACCEPT = "*/*".freeze
16
- DEFAULT_ID_LENGTH = 16
17
-
18
17
  attr_reader :params
19
18
 
20
19
  def initialize(env, params)
@@ -23,8 +22,8 @@ module Hanami
23
22
  end
24
23
 
25
24
  def id
26
- # FIXME make this number configurable and document the probabilities of clashes
27
- @id ||= @env[REQUEST_ID] = SecureRandom.hex(DEFAULT_ID_LENGTH)
25
+ # FIXME: make this number configurable and document the probabilities of clashes
26
+ @id ||= @env[Action::REQUEST_ID] = SecureRandom.hex(Action::DEFAULT_ID_LENGTH)
28
27
  end
29
28
 
30
29
  def accept?(mime_type)
@@ -34,61 +33,13 @@ module Hanami
34
33
  end
35
34
 
36
35
  def accept_header?
37
- accept != DEFAULT_ACCEPT
36
+ accept != Action::DEFAULT_ACCEPT
38
37
  end
39
38
 
40
39
  # @since 0.1.0
41
40
  # @api private
42
41
  def accept
43
- @accept ||= @env[HTTP_ACCEPT] || DEFAULT_ACCEPT
44
- end
45
-
46
- # @raise [NotImplementedError]
47
- #
48
- # @since 0.3.1
49
- # @api private
50
- def content_type
51
- raise NotImplementedError, 'Please use Action#content_type'
52
- end
53
-
54
- # @raise [NotImplementedError]
55
- #
56
- # @since 0.3.1
57
- # @api private
58
- def update_param(*)
59
- raise NotImplementedError, 'Please use params passed to Action#call'
60
- end
61
-
62
- # @raise [NotImplementedError]
63
- #
64
- # @since 0.3.1
65
- # @api private
66
- def delete_param(*)
67
- raise NotImplementedError, 'Please use params passed to Action#call'
68
- end
69
-
70
- # @raise [NotImplementedError]
71
- #
72
- # @since 0.3.1
73
- # @api private
74
- def [](*)
75
- raise NotImplementedError, 'Please use params passed to Action#call'
76
- end
77
-
78
- # @raise [NotImplementedError]
79
- #
80
- # @since 0.3.1
81
- # @api private
82
- def []=(*)
83
- raise NotImplementedError, 'Please use params passed to Action#call'
84
- end
85
-
86
- # @raise [NotImplementedError]
87
- #
88
- # @since 0.3.1
89
- # @api private
90
- def values_at(*)
91
- raise NotImplementedError, 'Please use params passed to Action#call'
42
+ @accept ||= @env[Action::HTTP_ACCEPT] || Action::DEFAULT_ACCEPT
92
43
  end
93
44
  end
94
45
  end
@@ -1,61 +1,58 @@
1
1
  # frozen_string_literal: true
2
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'
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
12
 
13
13
  module Hanami
14
14
  class Action
15
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"
16
+ # @since 2.0.0
17
+ # @api private
18
+ DEFAULT_VIEW_OPTIONS = -> (*) { {} }.freeze
35
19
 
20
+ # @since 2.0.0
21
+ # @api private
36
22
  EMPTY_BODY = [].freeze
37
23
 
24
+ # @since 2.0.0
25
+ # @api private
38
26
  FILE_SYSTEM_ROOT = Pathname.new("/").freeze
39
27
 
28
+ # @since 2.0.0
29
+ # @api private
40
30
  attr_reader :request, :action, :exposures, :format, :env, :view_options
31
+
32
+ # @since 2.0.0
33
+ # @api private
41
34
  attr_accessor :charset
42
35
 
36
+ # @since 2.0.0
37
+ # @api private
43
38
  def self.build(status, env)
44
- new(action: "", configuration: nil, content_type: Mime.best_q_match(env[HTTP_ACCEPT]), env: env).tap do |r|
39
+ new(action: "", configuration: nil, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r| # rubocop:disable Layout/LineLength
45
40
  r.status = status
46
41
  r.body = Http::Status.message_for(status)
47
42
  r.set_format(Mime.format_for(r.content_type))
48
43
  end
49
44
  end
50
45
 
51
- def initialize(request:, action:, configuration:, content_type: nil, env: {}, headers: {}, view_options: nil)
46
+ # @since 2.0.0
47
+ # @api private
48
+ def initialize(request:, action:, configuration:, content_type: nil, env: {}, headers: {}, view_options: nil) # rubocop:disable Metrics/ParameterLists
52
49
  super([], 200, headers.dup)
53
- set_header("Content-Type", content_type)
50
+ set_header(Action::CONTENT_TYPE, content_type)
54
51
 
55
52
  @request = request
56
53
  @action = action
57
54
  @configuration = configuration
58
- @charset = ::Rack::MediaType.params(content_type).fetch('charset', nil)
55
+ @charset = ::Rack::MediaType.params(content_type).fetch("charset", nil)
59
56
  @exposures = {}
60
57
  @env = env
61
58
  @view_options = view_options || DEFAULT_VIEW_OPTIONS
@@ -63,6 +60,8 @@ module Hanami
63
60
  @sending_file = false
64
61
  end
65
62
 
63
+ # @since 2.0.0
64
+ # @api public
66
65
  def body=(str)
67
66
  @length = 0
68
67
  @body = EMPTY_BODY.dup
@@ -75,49 +74,69 @@ module Hanami
75
74
  end
76
75
  end
77
76
 
77
+ # @since 2.0.0
78
+ # @api public
78
79
  def render(view, **options)
79
80
  self.body = view.(**view_options.(request, self), **exposures.merge(options)).to_str
80
81
  end
81
82
 
83
+ # @since 2.0.0
84
+ # @api public
82
85
  def format=(args)
83
86
  @format, content_type = *args
84
87
  content_type = Action::Mime.content_type_with_charset(content_type, charset)
85
88
  set_header("Content-Type", content_type)
86
89
  end
87
90
 
91
+ # @since 2.0.0
92
+ # @api public
88
93
  def [](key)
89
94
  @exposures.fetch(key)
90
95
  end
91
96
 
97
+ # @since 2.0.0
98
+ # @api public
92
99
  def []=(key, value)
93
100
  @exposures[key] = value
94
101
  end
95
102
 
103
+ # @since 2.0.0
104
+ # @api public
96
105
  def session
97
- env[SESSION_KEY] ||= {}
106
+ env[Action::RACK_SESSION] ||= {}
98
107
  end
99
108
 
109
+ # @since 2.0.0
110
+ # @api public
100
111
  def cookies
101
112
  @cookies ||= CookieJar.new(env.dup, headers, @configuration.cookies)
102
113
  end
103
114
 
115
+ # @since 2.0.0
116
+ # @api public
104
117
  def flash
105
- @flash ||= Flash.new(session[FLASH_SESSION_KEY])
118
+ @flash ||= Flash.new(session[Flash::KEY])
106
119
  end
107
120
 
121
+ # @since 2.0.0
122
+ # @api public
108
123
  def redirect_to(url, status: 302)
109
- return unless renderable?
124
+ return unless allow_redirect?
110
125
 
111
126
  redirect(::String.new(url), status)
112
127
  Halt.call(status)
113
128
  end
114
129
 
130
+ # @since 2.0.0
131
+ # @api public
115
132
  def send_file(path)
116
133
  _send_file(
117
134
  Rack::File.new(path, @configuration.public_directory).call(env)
118
135
  )
119
136
  end
120
137
 
138
+ # @since 2.0.0
139
+ # @api public
121
140
  def unsafe_send_file(path)
122
141
  directory = if Pathname.new(path).relative?
123
142
  @configuration.root_directory
@@ -130,16 +149,22 @@ module Hanami
130
149
  )
131
150
  end
132
151
 
152
+ # @since 2.0.0
153
+ # @api public
133
154
  def cache_control(*values)
134
155
  directives = Cache::CacheControl::Directives.new(*values)
135
156
  headers.merge!(directives.headers)
136
157
  end
137
158
 
159
+ # @since 2.0.0
160
+ # @api public
138
161
  def expires(amount, *values)
139
162
  directives = Cache::Expires::Directives.new(amount, *values)
140
163
  headers.merge!(directives.headers)
141
164
  end
142
165
 
166
+ # @since 2.0.0
167
+ # @api public
143
168
  def fresh(options)
144
169
  conditional_get = Cache::ConditionalGet.new(env, options)
145
170
 
@@ -150,41 +175,59 @@ module Hanami
150
175
  end
151
176
  end
152
177
 
178
+ # @since 2.0.0
153
179
  # @api private
154
180
  def request_id
155
- env.fetch(REQUEST_ID) do
181
+ env.fetch(Action::REQUEST_ID) do
156
182
  # FIXME: raise a meaningful error, by inviting devs to include Hanami::Action::Session
157
183
  raise "Can't find request ID"
158
184
  end
159
185
  end
160
186
 
161
- def set_format(value)
187
+ # @since 2.0.0
188
+ # @api public
189
+ def set_format(value) # rubocop:disable Naming/AccessorMethodName
162
190
  @format = value
163
191
  end
164
192
 
193
+ # @since 2.0.0
194
+ # @api private
165
195
  def renderable?
166
196
  return !head? && body.empty? if body.respond_to?(:empty?)
167
197
 
168
198
  !@sending_file && !head?
169
199
  end
170
200
 
171
- alias to_ary to_a
201
+ # @since 2.0.0
202
+ # @api private
203
+ def allow_redirect?
204
+ return body.empty? if body.respond_to?(:empty?)
205
+
206
+ !@sending_file
207
+ end
208
+
209
+ # @since 2.0.0
210
+ # @api private
211
+ alias_method :to_ary, :to_a
172
212
 
213
+ # @since 2.0.0
214
+ # @api public
173
215
  def head?
174
- env[REQUEST_METHOD] == HEAD
216
+ env[Action::REQUEST_METHOD] == Action::HEAD
175
217
  end
176
218
 
219
+ # @since 2.0.0
177
220
  # @api private
178
221
  def _send_file(send_file_response)
179
- headers.merge!(send_file_response[RACK_HEADERS])
222
+ headers.merge!(send_file_response[Action::RESPONSE_HEADERS])
180
223
 
181
- if send_file_response[RACK_STATUS] == NOT_FOUND
182
- headers.delete(X_CASCADE)
183
- headers.delete(CONTENT_LENGTH)
184
- Halt.call(NOT_FOUND)
224
+ if send_file_response[Action::RESPONSE_CODE] == Action::NOT_FOUND
225
+ headers.delete(Action::X_CASCADE)
226
+ headers.delete(Action::CONTENT_LENGTH)
227
+ Halt.call(Action::NOT_FOUND)
185
228
  else
186
- self.status = send_file_response[RACK_STATUS]
187
- self.body = send_file_response[RACK_BODY]
229
+ self.status = send_file_response[Action::RESPONSE_CODE]
230
+ self.body = send_file_response[Action::RESPONSE_BODY]
188
231
  @sending_file = true
189
232
  end
190
233
  end
@@ -1,4 +1,6 @@
1
- require 'hanami/action/flash'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/action/flash"
2
4
 
3
5
  module Hanami
4
6
  class Action
@@ -26,9 +28,9 @@ module Hanami
26
28
  # @see Hanami::Action#finish
27
29
  def finish(req, res, *)
28
30
  if (next_flash = res.flash.next).any?
29
- res.session['_flash'] = next_flash
31
+ res.session[Flash::KEY] = next_flash
30
32
  else
31
- res.session.delete('_flash')
33
+ res.session.delete(Flash::KEY)
32
34
  end
33
35
 
34
36
  super
@@ -1,4 +1,6 @@
1
- require 'hanami/action/params'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/action/params"
2
4
 
3
5
  module Hanami
4
6
  class Action
@@ -7,7 +9,7 @@ module Hanami
7
9
  #
8
10
  # @api private
9
11
  # @since 0.3.0
10
- PARAMS_CLASS_NAME = 'Params'.freeze
12
+ PARAMS_CLASS_NAME = "Params"
11
13
 
12
14
  # @api private
13
15
  # @since 0.1.0
@@ -53,28 +55,26 @@ module Hanami
53
55
  # @see https://guides.hanamirb.org//validations/overview
54
56
  #
55
57
  # @example Anonymous Block
56
- # require 'hanami/controller'
57
- #
58
- # class Signup
59
- # include Hanami::Action
58
+ # require "hanami/controller"
60
59
  #
60
+ # class Signup < Hanami::Action
61
61
  # params do
62
62
  # required(:first_name)
63
63
  # required(:last_name)
64
64
  # required(:email)
65
65
  # end
66
66
  #
67
- # def call(params)
68
- # puts params.class # => Signup::Params
69
- # puts params.class.superclass # => Hanami::Action::Params
67
+ # def handle(req, *)
68
+ # puts req.params.class # => Signup::Params
69
+ # puts req.params.class.superclass # => Hanami::Action::Params
70
70
  #
71
- # puts params[:first_name] # => "Luca"
72
- # puts params[:admin] # => nil
71
+ # puts req.params[:first_name] # => "Luca"
72
+ # puts req.params[:admin] # => nil
73
73
  # end
74
74
  # end
75
75
  #
76
76
  # @example Concrete class
77
- # require 'hanami/controller'
77
+ # require "hanami/controller"
78
78
  #
79
79
  # class SignupParams < Hanami::Action::Params
80
80
  # required(:first_name)
@@ -82,16 +82,15 @@ module Hanami
82
82
  # required(:email)
83
83
  # end
84
84
  #
85
- # class Signup
86
- # include Hanami::Action
85
+ # class Signup < Hanami::Action
87
86
  # params SignupParams
88
87
  #
89
- # def call(params)
90
- # puts params.class # => SignupParams
91
- # puts params.class.superclass # => Hanami::Action::Params
88
+ # def handle(req, *)
89
+ # puts req.params.class # => SignupParams
90
+ # puts req.params.class.superclass # => Hanami::Action::Params
92
91
  #
93
- # params[:first_name] # => "Luca"
94
- # params[:admin] # => nil
92
+ # req.params[:first_name] # => "Luca"
93
+ # req.params[:admin] # => nil
95
94
  # end
96
95
  # end
97
96
  def params(klass = nil, &blk)
@@ -2,13 +2,19 @@
2
2
 
3
3
  module Hanami
4
4
  class Action
5
+ # @since 2.0.0
6
+ # @api private
5
7
  class ViewNameInferrer
8
+ # @since 2.0.0
9
+ # @api private
6
10
  ALTERNATIVE_NAMES = {
7
11
  "create" => "new",
8
12
  "update" => "edit"
9
13
  }.freeze
10
14
 
11
15
  class << self
16
+ # @since 2.0.0
17
+ # @api private
12
18
  def call(action_name:, provider:)
13
19
  application = provider.respond_to?(:application) ? provider.application : Hanami.application
14
20
 
@@ -24,6 +30,8 @@ module Hanami
24
30
 
25
31
  private
26
32
 
33
+ # @since 2.0.0
34
+ # @api private
27
35
  def action_identifier_name(action_name, provider, name_base)
28
36
  provider
29
37
  .inflector
@@ -33,6 +41,8 @@ module Hanami
33
41
  .gsub("/", ".")
34
42
  end
35
43
 
44
+ # @since 2.0.0
45
+ # @api private
36
46
  def alternative_view_name(view_name)
37
47
  parts = view_name.split(".")
38
48