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
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationConfiguration
6
+ # Wrapper for application-level configuration of HTTP cookies for Hanami actions.
7
+ # This decorates the hash of cookie options that is otherwise directly configurable
8
+ # on actions, and adds the `enabled?` method to allow `ApplicationAction` to
9
+ # determine whether to include the `Action::Cookies` module.
10
+ #
11
+ # @since 2.0.0
12
+ class Cookies
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def enabled?
20
+ !options.nil?
21
+ end
22
+
23
+ def to_h
24
+ options.to_h
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/constants"
4
+ require "hanami/utils/string"
5
+ require "hanami/utils/class"
6
+
7
+ module Hanami
8
+ class Action
9
+ class ApplicationConfiguration
10
+ # Configuration for HTTP sessions in Hanami actions
11
+ #
12
+ # @since 2.0.0
13
+ class Sessions
14
+ attr_reader :storage, :options
15
+
16
+ def initialize(storage = nil, *options)
17
+ @storage = storage
18
+ @options = options
19
+ end
20
+
21
+ def enabled?
22
+ !storage.nil?
23
+ end
24
+
25
+ def middleware
26
+ return [] if !enabled?
27
+
28
+ [[storage_middleware, options]]
29
+ end
30
+
31
+ private
32
+
33
+ def storage_middleware
34
+ require_storage
35
+
36
+ name = Utils::String.classify(storage)
37
+ Utils::Class.load!(name, ::Rack::Session)
38
+ end
39
+
40
+ def require_storage
41
+ require "rack/session/#{storage}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,7 +2,7 @@ require 'rack/request'
2
2
  require 'hanami/utils/hash'
3
3
 
4
4
  module Hanami
5
- module Action
5
+ class Action
6
6
  class BaseParams
7
7
  # The key that returns raw input from the Rack env
8
8
  #
@@ -173,7 +173,7 @@ module Hanami
173
173
  def _router_params(fallback = {})
174
174
  env.fetch(ROUTER_PARAMS) do
175
175
  if session = fallback.delete(RACK_SESSION) # rubocop:disable Lint/AssignmentInCondition
176
- fallback[RACK_SESSION] = Utils::Hash.new(session).symbolize!.to_hash
176
+ fallback[RACK_SESSION] = Utils::Hash.deep_symbolize(session)
177
177
  end
178
178
 
179
179
  fallback
@@ -3,7 +3,7 @@ require 'hanami/action/cache/expires'
3
3
  require 'hanami/action/cache/conditional_get'
4
4
 
5
5
  module Hanami
6
- module Action
6
+ class Action
7
7
  # Cache type API
8
8
  #
9
9
  # @since 0.3.0
@@ -26,144 +26,6 @@ module Hanami
26
26
  include CacheControl, Expires
27
27
  end
28
28
  end
29
-
30
- protected
31
-
32
- # Specify response freshness policy for HTTP caches (Cache-Control header).
33
- # Any number of non-value directives (:public, :private, :no_cache,
34
- # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
35
- # a Hash of value directives (:max_age, :min_stale, :s_max_age).
36
- #
37
- # See RFC 2616 / 14.9 for more on standard cache control directives:
38
- # http://tools.ietf.org/html/rfc2616#section-14.9.1
39
- #
40
- # @param values [Array<Symbols, Hash>] mapped to cache_control directives
41
- # @option values [Symbol] :public
42
- # @option values [Symbol] :private
43
- # @option values [Symbol] :no_cache
44
- # @option values [Symbol] :no_store
45
- # @option values [Symbol] :must_validate
46
- # @option values [Symbol] :proxy_revalidate
47
- # @option values [Hash] :max_age
48
- # @option values [Hash] :min_stale
49
- # @option values [Hash] :s_max_age
50
- #
51
- # @return void
52
- #
53
- # @since 0.3.0
54
- #
55
- # @example
56
- # require 'hanami/controller'
57
- # require 'hanami/action/cache'
58
- #
59
- # class Show
60
- # include Hanami::Action
61
- # include Hanami::Action::Cache
62
- #
63
- # def call(params)
64
- # # ...
65
- #
66
- # # set Cache-Control directives
67
- # cache_control :public, max_age: 900, s_maxage: 86400
68
- #
69
- # # overwrite previous Cache-Control directives
70
- # cache_control :private, :no_cache, :no_store
71
- #
72
- # => Cache-Control: private, no-store, max-age=900
73
- #
74
- # end
75
- # end
76
- def cache_control(*values)
77
- cache_control = CacheControl::Directives.new(*values)
78
- headers.merge!(cache_control.headers)
79
- end
80
-
81
- # Set the Expires header and Cache-Control/max-age directive. Amount
82
- # can be an integer number of seconds in the future or a Time object
83
- # indicating when the response should be considered "stale". The remaining
84
- # "values" arguments are passed to the #cache_control helper:
85
- #
86
- # @param amount [Integer,Time] number of seconds or point in time
87
- # @param values [Array<Symbols>] mapped to cache_control directives
88
- #
89
- # @return void
90
- #
91
- # @since 0.3.0
92
- #
93
- # @example
94
- # require 'hanami/controller'
95
- # require 'hanami/action/cache'
96
- #
97
- # class Show
98
- # include Hanami::Action
99
- # include Hanami::Action::Cache
100
- #
101
- # def call(params)
102
- # # ...
103
- #
104
- # # set Cache-Control directives and Expires
105
- # expires 900, :public
106
- #
107
- # # overwrite Cache-Control directives and Expires
108
- # expires 300, :private, :no_cache, :no_store
109
- #
110
- # => Expires: Thu, 26 Jun 2014 12:00:00 GMT
111
- # => Cache-Control: private, no-cache, no-store max-age=300
112
- #
113
- # end
114
- # end
115
- def expires(amount, *values)
116
- expires = Expires::Directives.new(amount, *values)
117
- headers.merge!(expires.headers)
118
- end
119
-
120
- # Set the etag, last_modified, or both headers on the response
121
- # and halts a 304 Not Modified if the request is still fresh
122
- # respecting IfNoneMatch and IfModifiedSince request headers
123
- #
124
- # @param options [Hash]
125
- # @option options [Integer] :etag for testing IfNoneMatch conditions
126
- # @option options [Date] :last_modified for testing IfModifiedSince conditions
127
- #
128
- # @return void
129
- #
130
- # @since 0.3.0
131
- #
132
- # @example
133
- # require 'hanami/controller'
134
- # require 'hanami/action/cache'
135
- #
136
- # class Show
137
- # include Hanami::Action
138
- # include Hanami::Action::Cache
139
- #
140
- # def call(params)
141
- # # ...
142
- #
143
- # # set etag response header and halt 304
144
- # # if request matches IF_NONE_MATCH header
145
- # fresh etag: @resource.updated_at.to_i
146
- #
147
- # # set last_modified response header and halt 304
148
- # # if request matches IF_MODIFIED_SINCE
149
- # fresh last_modified: @resource.updated_at
150
- #
151
- # # set etag and last_modified response header,
152
- # # halt 304 if request matches IF_MODIFIED_SINCE
153
- # # and IF_NONE_MATCH
154
- # fresh last_modified: @resource.updated_at
155
- #
156
- # end
157
- # end
158
- def fresh(options)
159
- conditional_get = ConditionalGet.new(@_env, options)
160
-
161
- headers.merge!(conditional_get.headers)
162
-
163
- conditional_get.fresh? do
164
- halt 304
165
- end
166
- end
167
29
  end
168
30
  end
169
31
  end
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/cache/directives'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Cache
6
6
  # Module with Cache-Control logic
7
7
  #
@@ -37,7 +37,7 @@ module Hanami
37
37
  def cache_control_directives
38
38
  @cache_control_directives || Object.new.tap do |null_object|
39
39
  def null_object.headers
40
- Hash.new
40
+ ::Hash.new
41
41
  end
42
42
  end
43
43
  end
@@ -49,9 +49,9 @@ module Hanami
49
49
  # @api private
50
50
  #
51
51
  # @see Hanami::Action#finish
52
- def finish
52
+ def finish(_, res, _)
53
+ res.headers.merge!(self.class.cache_control_directives.headers) unless res.headers.include? HEADER
53
54
  super
54
- headers.merge!(self.class.cache_control_directives.headers) unless headers.include? HEADER
55
55
  end
56
56
 
57
57
  # Class which stores CacheControl values
@@ -1,5 +1,7 @@
1
+ require "hanami/utils/blank"
2
+
1
3
  module Hanami
2
- module Action
4
+ class Action
3
5
  module Cache
4
6
  # @since 0.3.0
5
7
  # @api private
@@ -67,7 +69,10 @@ module Hanami
67
69
  # @since 0.3.0
68
70
  # @api private
69
71
  def fresh?
70
- !Hanami::Utils::Blank.blank?(modified_since) && Time.httpdate(modified_since).to_i >= @value.to_time.to_i
72
+ return false if Hanami::Utils::Blank.blank?(modified_since)
73
+ return false if Hanami::Utils::Blank.blank?(@value)
74
+
75
+ Time.httpdate(modified_since).to_i >= @value.to_time.to_i
71
76
  end
72
77
 
73
78
  # @since 0.3.0
@@ -1,5 +1,5 @@
1
1
  module Hanami
2
- module Action
2
+ class Action
3
3
  module Cache
4
4
  # Cache-Control directives which have values
5
5
  #
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/cache/cache_control'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Cache
6
6
  # Module with Expires logic
7
7
  #
@@ -49,9 +49,9 @@ module Hanami
49
49
  # @api private
50
50
  #
51
51
  # @see Hanami::Action#finish
52
- def finish
52
+ def finish(_, res, _)
53
+ res.headers.merge!(self.class.expires_directives.headers) unless res.headers.include? HEADER
53
54
  super
54
- headers.merge!(self.class.expires_directives.headers) unless headers.include? HEADER
55
55
  end
56
56
 
57
57
  # Class which stores Expires directives
@@ -0,0 +1,430 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "hanami/utils/kernel"
5
+ require "pathname"
6
+ require_relative "mime"
7
+
8
+ module Hanami
9
+ class Action
10
+ class Configuration
11
+ include Dry::Configurable
12
+
13
+ # Initialize the Configuration
14
+ #
15
+ # @yield [config] the configuration object
16
+ #
17
+ # @return [Configuration]
18
+ #
19
+ # @since 2.0.0
20
+ # @api private
21
+ def initialize(*)
22
+ super
23
+ yield self if block_given?
24
+ end
25
+
26
+ # Returns the list of available settings
27
+ #
28
+ # @return [Set]
29
+ #
30
+ # @since 2.0.0
31
+ # @api private
32
+ def settings
33
+ self.class.settings
34
+ end
35
+
36
+ # @!method handled_exceptions=(exceptions)
37
+ #
38
+ # Specifies how to handle exceptions with an HTTP status
39
+ #
40
+ # Raised exceptions will return the corresponding HTTP status
41
+ #
42
+ # @param exceptions [Hash{Exception=>Integer}] exception classes as
43
+ # keys and HTTP statuses as values
44
+ #
45
+ # @return [void]
46
+ #
47
+ # @since 0.2.0
48
+ #
49
+ # @example
50
+ # configuration.handled_exceptions = {ArgumentError => 400}
51
+ #
52
+ # @!method handled_exceptions
53
+ #
54
+ # Returns the configured handled exceptions
55
+ #
56
+ # @return [Hash{Exception=>Integer}]
57
+ #
58
+ # @see handled_exceptions=
59
+ #
60
+ # @since 0.2.0
61
+ setting :handled_exceptions, {}
62
+
63
+ # Specifies how to handle exceptions with an HTTP status
64
+ #
65
+ # Raised exceptions will return the corresponding HTTP status
66
+ #
67
+ # The specified exceptions will be merged with any previously configured
68
+ # exceptions
69
+ #
70
+ # @param exceptions [Hash{Exception=>Integer}] exception classes as keys
71
+ # and HTTP statuses as values
72
+ #
73
+ # @return [void]
74
+ #
75
+ # @since 0.2.0
76
+ #
77
+ # @see handled_exceptions=
78
+ #
79
+ # @example
80
+ # configuration.handle_exceptions(ArgumentError => 400}
81
+ def handle_exception(exceptions)
82
+ self.handled_exceptions = handled_exceptions
83
+ .merge(exceptions)
84
+ .sort { |(ex1, _), (ex2, _)| ex1.ancestors.include?(ex2) ? -1 : 1 }
85
+ .to_h
86
+ end
87
+
88
+ # Default MIME type to format mapping
89
+ #
90
+ # @since 0.2.0
91
+ # @api private
92
+ DEFAULT_FORMATS = {
93
+ 'application/octet-stream' => :all,
94
+ '*/*' => :all,
95
+ 'text/html' => :html
96
+ }.freeze
97
+
98
+ # @!method formats=(formats)
99
+ #
100
+ # Specifies the MIME type to format mapping
101
+ #
102
+ # @param formats [Hash{String=>Symbol}] MIME type strings as keys and
103
+ # format symbols as values
104
+ #
105
+ # @return [void]
106
+ #
107
+ # @since 0.2.0
108
+ #
109
+ # @see format
110
+ # @see Hanami::Action::Mime
111
+ #
112
+ # @example
113
+ # configuration.formats = {"text/html" => :html}
114
+ #
115
+ # @!method formats
116
+ #
117
+ # Returns the configured MIME type to format mapping
118
+ #
119
+ # @return [Symbol,nil] the corresponding format, if present
120
+ #
121
+ # @see format
122
+ # @see formats=
123
+ #
124
+ # @since 0.2.0
125
+ setting :formats, DEFAULT_FORMATS.dup
126
+
127
+ # Registers a MIME type to format mapping
128
+ #
129
+ # @param hash [Hash{Symbol=>String}] format symbols as keys and the MIME
130
+ # type strings must as values
131
+ #
132
+ # @return [void]
133
+ #
134
+ # @since 0.2.0
135
+ #
136
+ # @see Hanami::Action::Mime
137
+ #
138
+ # @example configuration.format html: "text/html"
139
+ def format(hash)
140
+ symbol, mime_type = *Utils::Kernel.Array(hash)
141
+ formats[Utils::Kernel.String(mime_type)] = Utils::Kernel.Symbol(symbol)
142
+ end
143
+
144
+ # Returns the configured format for the given MIME type
145
+ #
146
+ # @param mime_type [#to_s,#to_str] A mime type
147
+ #
148
+ # @return [Symbol,nil] the corresponding format, nil if not found
149
+ #
150
+ # @see format
151
+ #
152
+ # @since 0.2.0
153
+ # @api private
154
+ def format_for(mime_type)
155
+ formats[mime_type]
156
+ end
157
+
158
+ # Returns the configured format's MIME types
159
+ #
160
+ # @return [Array<String>] the format's MIME types
161
+ #
162
+ # @see formats=
163
+ # @see format
164
+ #
165
+ # @since 0.8.0
166
+ #
167
+ # @api private
168
+ def mime_types
169
+ # FIXME: this isn't efficient. speed it up!
170
+ ((formats.keys - DEFAULT_FORMATS.keys) +
171
+ Hanami::Action::Mime::TYPES.values).freeze
172
+ end
173
+
174
+ # Returns a MIME type for the given format
175
+ #
176
+ # @param format [#to_sym] a format
177
+ #
178
+ # @return [String,nil] the corresponding MIME type, if present
179
+ #
180
+ # @since 0.2.0
181
+ # @api private
182
+ def mime_type_for(format)
183
+ formats.key(format)
184
+ end
185
+
186
+ # @!method default_request_format=(format)
187
+ #
188
+ # Sets a format as default fallback for all the requests without a strict
189
+ # requirement for the MIME type.
190
+ #
191
+ # The given format must be coercible to a symbol, and be a valid MIME
192
+ # type alias. If it isn't, at runtime the framework will raise an
193
+ # `Hanami::Controller::UnknownFormatError`.
194
+ #
195
+ # By default, this value is nil.
196
+ #
197
+ # @param format [Symbol]
198
+ #
199
+ # @return [void]
200
+ #
201
+ # @since 0.5.0
202
+ #
203
+ # @see Hanami::Action::Mime
204
+ #
205
+ # @!method default_request_format
206
+ #
207
+ # Returns the configured default request format
208
+ #
209
+ # @return [Symbol] format
210
+ #
211
+ # @see default_request_format=
212
+ #
213
+ # @since 0.5.0
214
+ setting :default_request_format do |format|
215
+ Utils::Kernel.Symbol(format) unless format.nil?
216
+ end
217
+
218
+ # @!method default_response_format=(format)
219
+ #
220
+ # Sets a format to be used for all responses regardless of the request
221
+ # type.
222
+ #
223
+ # The given format must be coercible to a symbol, and be a valid MIME
224
+ # type alias. If it isn't, at the runtime the framework will raise an
225
+ # `Hanami::Controller::UnknownFormatError`.
226
+ #
227
+ # By default, this value is nil.
228
+ #
229
+ # @param format [Symbol]
230
+ #
231
+ # @return [void]
232
+ #
233
+ # @since 0.5.0
234
+ #
235
+ # @see Hanami::Action::Mime
236
+ #
237
+ # @!method default_response_format
238
+ #
239
+ # Returns the configured default response format
240
+ #
241
+ # @return [Symbol] format
242
+ #
243
+ # @see default_request_format=
244
+ #
245
+ # @since 0.5.0
246
+ setting :default_response_format do |format|
247
+ Utils::Kernel.Symbol(format) unless format.nil?
248
+ end
249
+
250
+ # @!method default_charset=(charset)
251
+ #
252
+ # Sets a charset (character set) as default fallback for all the requests
253
+ # without a strict requirement for the charset.
254
+ #
255
+ # By default, this value is nil.
256
+ #
257
+ # @param charset [String]
258
+ #
259
+ # @return [void]
260
+ #
261
+ # @since 0.3.0
262
+ #
263
+ # @see Hanami::Action::Mime
264
+ #
265
+ # @!method default_charset
266
+ #
267
+ # Returns the configured default charset.
268
+ #
269
+ # @return [String,nil] the charset, if present
270
+ #
271
+ # @see default_charset=
272
+ #
273
+ # @since 0.3.0
274
+ setting :default_charset
275
+
276
+ # @!method default_headers=(headers)
277
+ #
278
+ # Sets default headers for all responses.
279
+ #
280
+ # By default, this is an empty hash.
281
+ #
282
+ # @param headers [Hash{String=>String}] the headers
283
+ #
284
+ # @return [void]
285
+ #
286
+ # @since 0.4.0
287
+ #
288
+ # @see default_headers
289
+ #
290
+ # @example
291
+ # configuration.default_headers = {'X-Frame-Options' => 'DENY'}
292
+ #
293
+ # @!method default_headers
294
+ #
295
+ # Returns the configured headers
296
+ #
297
+ # @return [Hash{String=>String}] the headers
298
+ #
299
+ # @since 0.4.0
300
+ #
301
+ # @see default_headers=
302
+ setting :default_headers, {} do |headers|
303
+ headers.compact
304
+ end
305
+
306
+ # @!method cookies=(cookie_options)
307
+ #
308
+ # Sets default cookie options for all responses.
309
+ #
310
+ # By default this, is an empty hash.
311
+ #
312
+ # @param cookie_options [Hash{Symbol=>String}] the cookie options
313
+ #
314
+ # @return [void]
315
+ #
316
+ # @since 0.4.0
317
+ #
318
+ # @example
319
+ # configuration.cookies = {
320
+ # domain: 'hanamirb.org',
321
+ # path: '/controller',
322
+ # secure: true,
323
+ # httponly: true
324
+ # }
325
+ #
326
+ # @!method cookies
327
+ #
328
+ # Returns the configured cookie options
329
+ #
330
+ # @return [Hash{Symbol=>String}]
331
+ #
332
+ # @since 0.4.0
333
+ #
334
+ # @see cookies=
335
+ setting :cookies, {} do |cookie_options|
336
+ # Call `to_h` here to permit `ApplicationConfiguration::Cookies` object to be
337
+ # provided when application actions are configured
338
+ cookie_options.to_h.compact
339
+ end
340
+
341
+ # @!method root_directory=(dir)
342
+ #
343
+ # Sets the the for the public directory, which is used for file downloads.
344
+ # This must be an existent directory.
345
+ #
346
+ # Defaults to the current working directory.
347
+ #
348
+ # @param dir [String] the directory path
349
+ #
350
+ # @return [void]
351
+ #
352
+ # @since 1.0.0
353
+ #
354
+ # @api private
355
+ #
356
+ # @!method root_directory
357
+ #
358
+ # Returns the configured root directory
359
+ #
360
+ # @return [String] the directory path
361
+ #
362
+ # @see root_directory=
363
+ #
364
+ # @since 1.0.0
365
+ #
366
+ # @api private
367
+ setting :root_directory do |dir|
368
+ dir ||= Dir.pwd
369
+
370
+ Pathname(dir).realpath
371
+ end
372
+
373
+ # Default public directory
374
+ #
375
+ # This serves as the root directory for file downloads
376
+ #
377
+ # @since 1.0.0
378
+ #
379
+ # @api private
380
+ DEFAULT_PUBLIC_DIRECTORY = 'public'.freeze
381
+
382
+ # @!method public_directory=(directory)
383
+ #
384
+ # Sets the path to public directory. This directory is used for file downloads.
385
+ #
386
+ # This given directory will be appended onto the root directory.
387
+ #
388
+ # By default, the public directory is "public".
389
+ #
390
+ # @param directory [String] the public directory path
391
+ #
392
+ # @return [void]
393
+ #
394
+ # @see root_directory
395
+ # @see public_directory
396
+ setting :public_directory, DEFAULT_PUBLIC_DIRECTORY
397
+
398
+ # Returns the configured public directory, appended onto the root directory.
399
+ #
400
+ # @return [String] the fill directory path
401
+ #
402
+ # @example
403
+ # configuration.public_directory = "public"
404
+ #
405
+ # configuration.public_directory
406
+ # # => "/path/to/root/public"
407
+ #
408
+ # @see public_directory=
409
+ # @see root_directory=
410
+ def public_directory
411
+ # This must be a string, for Rack compatibility
412
+ root_directory.join(super).to_s
413
+ end
414
+
415
+ private
416
+
417
+ def method_missing(name, *args, &block)
418
+ if config.respond_to?(name)
419
+ config.public_send(name, *args, &block)
420
+ else
421
+ super
422
+ end
423
+ end
424
+
425
+ def respond_to_missing?(name, _incude_all = false)
426
+ config.respond_to?(name) || super
427
+ end
428
+ end
429
+ end
430
+ end