hanami-controller 2.0.0.alpha8 → 2.0.0.beta1

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.
@@ -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