rage-rb 1.17.1 → 1.19.0

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.
@@ -4,262 +4,363 @@ require "yaml"
4
4
  require "erb"
5
5
 
6
6
  ##
7
- # `Rage.configure` can be used to adjust the behavior of your Rage application:
7
+ # Configuration class for Rage framework.
8
8
  #
9
+ # Use {Rage.configure Rage.configure} to access and modify the configuration.
10
+ #
11
+ # **Example:**
9
12
  # ```ruby
10
13
  # Rage.configure do
11
- # config.logger = Rage::Logger.new(STDOUT)
12
- # config.server.workers_count = 2
13
- # end
14
- # ```
15
- #
16
- # # General Configuration
17
- #
18
- # • _config.logger_
19
- #
20
- # > The logger that will be used for `Rage.logger` and any related `Rage` logging. Custom loggers should implement Ruby's {https://ruby-doc.org/3.2.2/stdlibs/logger/Logger.html#class-Logger-label-Entries Logger} interface.
21
- #
22
- # • _config.log_formatter_
23
- #
24
- # > The formatter of the Rage logger. Built in options include `Rage::TextFormatter` and `Rage::JSONFormatter`. Defaults to an instance of `Rage::TextFormatter`.
25
- #
26
- # • _config.log_level_
27
- #
28
- # > Defines the verbosity of the Rage logger. This option defaults to `:debug` for all environments except production, where it defaults to `:info`. The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`.
29
- #
30
- # • _config.secret_key_base_
31
- #
32
- # > The `secret_key_base` is used as the input secret to the application's key generator, which is used to encrypt cookies. Rage will fall back to the `SECRET_KEY_BASE` environment variable if this is not set.
33
- #
34
- # • _config.fallback_secret_key_base_
35
- #
36
- # > Defines one or several old secrets that need to be rotated. Can accept a single key or an array of keys. Rage will fall back to the `FALLBACK_SECRET_KEY_BASE` environment variable if this is not set.
37
- #
38
- # • _config.after_initialize_
39
- #
40
- # > Schedule a block of code to run after Rage has finished loading the application code. Use this to reference application-level constants during the initialization process.
41
- # > ```
42
- # Rage.config.after_initialize do
43
- # SUPER_USER = User.find_by!(super: true)
44
- # end
45
- # > ```
46
- #
47
- # # Middleware Configuration
48
- #
49
- # • _config.middleware.use_
50
- #
51
- # > Adds a middleware to the top of the middleware stack. **This is the recommended way of adding a middleware.**
52
- # > ```
53
- # config.middleware.use Rack::Cors do
54
- # allow do
55
- # origins "*"
56
- # resource "*", headers: :any
57
- # end
58
- # end
59
- # > ```
60
- #
61
- # • _config.middleware.insert_before_
62
- #
63
- # > Adds middleware at a specified position before another middleware. The position can be either an index or another middleware.
64
- #
65
- # > **_❗️Heads up:_** By default, Rage always uses the `Rage::FiberWrapper` middleware, which wraps every request in a separate fiber. Make sure to always have this middleware in the top of the stack. Placing other middlewares in front may lead to undefined behavior.
66
- #
67
- # > ```
68
- # config.middleware.insert_before Rack::Head, Magical::Unicorns
69
- # config.middleware.insert_before 0, Magical::Unicorns
70
- # > ```
71
- #
72
- # • _config.middleware.insert_after_
73
- #
74
- # > Adds middleware at a specified position after another middleware. The position can be either an index or another middleware.
75
- #
76
- # > ```
77
- # config.middleware.insert_after Rack::Head, Magical::Unicorns
78
- # > ```
79
- #
80
- # # Server Configuration
81
- #
82
- # _• config.server.max_clients_
83
- #
84
- # > Limits the number of simultaneous connections the server can accept. Defaults to the maximum number of open files.
85
- #
86
- # > **_❗️Heads up:_** Decreasing this number is almost never a good idea. Depending on your application specifics, you are encouraged to use other methods to limit the number of concurrent connections:
87
- #
88
- # > 1. If your application is exposed to the public, you may want to use a cloud rate limiter, like {https://developers.cloudflare.com/waf Cloudflare WAF} or {https://docs.fastly.com/en/ngwaf Fastly WAF}.
89
- # > 2. Otherwise, consider using tools like {https://github.com/rack/rack-attack Rack::Attack} or {https://github.com/mperham/connection_pool connection_pool}.
90
- #
91
- # > ```
92
- # # Limit the amount of connections your application can accept
93
- # config.middleware.use Rack::Attack
94
- # Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |req|
95
- # req.ip
96
- # end
97
- # #
98
- # # Limit the amount of connections to a specific resource
99
- # HTTP = ConnectionPool.new(size: 5, timeout: 5) { Net::HTTP }
100
- # HTTP.with do |conn|
101
- # conn.get("/my-resource")
102
- # end
103
- # > ```
104
- #
105
- # • _config.server.port_
106
- #
107
- # > Specifies what port the server will listen on.
108
- #
109
- # • _config.server.workers_count_
110
- #
111
- # > Specifies the number of server processes to run. Defaults to 1 in development and to the number of available CPU cores in other environments.
112
- #
113
- # • _config.server.timeout_
114
- #
115
- # > Specifies connection timeout.
116
- #
117
- # # Static file server
118
- #
119
- # • _config.public_file_server.enabled_
120
- #
121
- # > Configures whether Rage should serve static files from the public directory. Defaults to `false`.
122
- #
123
- # # Cable Configuration
124
- #
125
- # • _config.cable.protocol_
126
- #
127
- # > Specifies the protocol the server will use. Supported values include {Rage::Cable::Protocols::ActioncableV1Json :actioncable_v1_json} and {Rage::Cable::Protocols::RawWebSocketJson :raw_websocket_json}. Defaults to {Rage::Cable::Protocols::ActioncableV1Json :actioncable_v1_json}.
128
- #
129
- # • _config.cable.allowed_request_origins_
130
- #
131
- # > Restricts the server to only accept requests from specified origins. The origins can be instances of strings or regular expressions, against which a check for the match will be performed.
132
- #
133
- # • _config.cable.disable_request_forgery_protection_
134
- #
135
- # > Allows requests from any origin.
136
- #
137
- # # OpenAPI Configuration
138
- # • _config.openapi.tag_resolver_
139
- #
140
- # > Specifies the proc to build tags for API operations. The proc accepts the controller class, the symbol name of the action, and the default tag built by Rage.
141
- #
142
- # > ```ruby
143
- # config.openapi.tag_resolver = proc do |controller, action, default_tag|
144
- # # ...
14
+ # config.log_level = :warn
15
+ # config.server.port = 8080
145
16
  # end
146
- # > ```
147
- #
148
- # # Deferred Configuration
149
- # • _config.deferred.backend_
150
- #
151
- # > Specifies the backend for deferred tasks. Supported values are `:disk`, which uses disk storage, or `nil`, which disables persistence of deferred tasks.
152
- # > The `:disk` backend accepts the following options:
153
- # >
154
- # > - `:path` - the path to the directory where deferred tasks will be stored. Defaults to `storage`.
155
- # > - `:prefix` - the prefix for the deferred task files. Defaults to `deferred-`.
156
- # > - `:fsync_frequency` - the frequency of `fsync` calls in seconds. Defaults to `0.5`.
157
- #
158
- # > ```ruby
159
- # config.deferred.backend = :disk, { path: "storage" }
160
- # > ```
161
- #
162
- # • _config.deferred.backpressure_
163
- #
164
- # > Enables the backpressure for deferred tasks. The backpressure is used to limit the number of pending tasks in the queue. It accepts a hash with the following options:
165
- # >
166
- # > - `:high_water_mark` - the maximum number of pending tasks in the queue. Defaults to `1000`.
167
- # > - `:low_water_mark` - the minimum number of pending tasks in the queue before the backpressure is released. Defaults to `high_water_mark * 0.8`.
168
- # > - `:timeout` - the timeout for the backpressure in seconds. Defaults to `2`.
169
- #
170
- # > ```ruby
171
- # config.deferred.backpressure = { high_water_mark: 1000, low_water_mark: 800, timeout: 2 }
172
- # > ```
173
- #
174
- # > Additionally, you can set the backpressure value to `true` to use the default values:
175
- #
176
- # > ```ruby
177
- # config.deferred.backpressure = true
178
17
  # ```
179
18
  #
180
- # # Transient Settings
19
+ # ## Transient Settings
181
20
  #
182
21
  # The settings described in this section should be configured using **environment variables** and are either temporary or will become the default in the future.
183
22
  #
184
- # _RAGE_DISABLE_IO_WRITE_
185
- #
186
- # > Disables the `io_write` hook to fix the ["zero-length iov"](https://bugs.ruby-lang.org/issues/19640) error on Ruby < 3.3.
187
- #
188
- # • _RAGE_DISABLE_AR_POOL_PATCH_
189
- #
190
- # > Disables the `ActiveRecord::ConnectionPool` patch and makes Rage use the original ActiveRecord implementation.
191
- #
192
- # • _RAGE_DISABLE_AR_WEAK_CONNECTIONS_
193
- #
194
- # > Instructs Rage to not reuse Active Record connections between different fibers.
23
+ # - _RAGE_DISABLE_IO_WRITE_ - disables the `io_write` hook to fix the ["zero-length iov"](https://bugs.ruby-lang.org/issues/19640) error on Ruby < 3.3.
24
+ # - _RAGE_DISABLE_AR_POOL_PATCH_ - disables the `ActiveRecord::ConnectionPool` patch and makes Rage use the original ActiveRecord implementation.
25
+ # - _RAGE_DISABLE_AR_WEAK_CONNECTIONS_ - instructs Rage to not reuse Active Record connections between different fibers. Only applies to Active Record < 7.2.
195
26
  #
196
27
  class Rage::Configuration
28
+ # @private
197
29
  include Hooks
198
30
 
199
- attr_accessor :logger
200
- attr_reader :log_formatter, :log_level
201
- attr_writer :secret_key_base, :fallback_secret_key_base
202
-
31
+ # @private
203
32
  # used in DSL
204
33
  def config = self
205
34
 
35
+ # @!group General Configuration
36
+
37
+ # Returns the logger used by Rage.
38
+ # @return [Rage::Logger, nil]
39
+ def logger
40
+ @logger
41
+ end
42
+
43
+ # Set the logger used by Rage.
44
+ # Accepts a logger object that implements the `#debug`, `#info`, `#warn`, `#error`, `#fatal`, and `#unknown` methods, or `nil`. If set to `nil`, logging will be disabled.
45
+ # `Rage.logger` always returns an instance of {Rage::Logger Rage::Logger}, but if you provide a custom object, it will be used internally by `Rage.logger`.
46
+ #
47
+ # @overload logger=(logger)
48
+ # Set a standard logger
49
+ # @param logger [#debug, #info, #warn, #error, #fatal, #unknown]
50
+ # @example
51
+ # config.logger = Rage::Logger.new(STDOUT)
52
+ # @overload logger=(callable)
53
+ # Set an external logger. This allows you to send Rage's raw structured logging data directly to external observability platforms without serializing it to text first.
54
+ #
55
+ # The external logger receives pre-parsed structured data (severity, tags, context) rather than formatted strings. This differs from `config.log_formatter` in that formatters control how logs are formatted (text vs JSON), while the external logger controls where logs are sent and how they integrate with external platforms.
56
+ # @param callable [ExternalLoggerInterface]
57
+ # @example
58
+ # config.logger = proc do |severity:, tags:, context:, message:, request_info:|
59
+ # # Custom logging logic here
60
+ # end
61
+ # @overload logger=(nil)
62
+ # Disable logging
63
+ # @example
64
+ # config.logger = nil
65
+ def logger=(logger)
66
+ @logger = if logger.nil? || logger.is_a?(Rage::Logger)
67
+ logger
68
+ elsif Rage::Logger::METHODS_MAP.keys.all? { |method| logger.respond_to?(method) }
69
+ Rage::Logger.new(Rage::Logger::External::Static[logger])
70
+ elsif logger.respond_to?(:call)
71
+ Rage::Logger.new(Rage::Logger::External::Dynamic[logger])
72
+ else
73
+ raise ArgumentError, "Invalid logger: must be an instance of `Rage::Logger`, respond to `#call`, or implement all standard Ruby Logger methods (`#debug`, `#info`, `#warn`, `#error`, `#fatal`, `#unknown`)"
74
+ end
75
+ end
76
+
77
+ # Returns the log formatter used by Rage.
78
+ # @return [#call, nil]
79
+ def log_formatter
80
+ @log_formatter
81
+ end
82
+
83
+ # Set the log formatter used by Rage.
84
+ # Built in options include {Rage::TextFormatter Rage::TextFormatter} and {Rage::JSONFormatter Rage::JSONFormatter}.
85
+ #
86
+ # @param formatter [#call] a callable object that formats log messages
87
+ # @example
88
+ # config.log_formatter = proc do |severity, datetime, progname, msg|
89
+ # "[#{datetime}] #{severity} -- #{progname}: #{msg}\n"
90
+ # end
206
91
  def log_formatter=(formatter)
207
92
  raise ArgumentError, "Custom log formatter should respond to `#call`" unless formatter.respond_to?(:call)
208
93
  @log_formatter = formatter
209
94
  end
210
95
 
96
+ # Returns the log level used by Rage.
97
+ # @return [Integer, nil]
98
+ def log_level
99
+ @log_level
100
+ end
101
+
102
+ # Set the log level used by Rage.
103
+ # @param level [:debug, :info, :warn, :error, :fatal, :unknown, Integer] the log level
104
+ # @example
105
+ # config.log_level = :info
211
106
  def log_level=(level)
212
107
  @log_level = level.is_a?(Symbol) ? Logger.const_get(level.to_s.upcase) : level
213
108
  end
214
109
 
110
+ # The secret key base is used as the input secret to the application's key generator, which is used to encrypt cookies. Rage will fall back to the `SECRET_KEY_BASE` environment variable if this is not set.
111
+ # @param key [String] the secret key base
112
+ def secret_key_base=(key)
113
+ @secret_key_base = key
114
+ end
115
+
116
+ # Returns the secret key base used for encrypting cookies.
117
+ # @return [String, nil]
215
118
  def secret_key_base
216
119
  @secret_key_base || ENV["SECRET_KEY_BASE"]
217
120
  end
218
121
 
122
+ # Set one or several old secrets that need to be rotated. Can accept a single key or an array of keys. Rage will fall back to the `FALLBACK_SECRET_KEY_BASE` environment variable if this is not set.
123
+ # @param key [String, Array<String>] the fallback secret key base(s)
124
+ def fallback_secret_key_base=(key)
125
+ @fallback_secret_key_base = key
126
+ end
127
+
128
+ # Returns the fallback secret key base(s) used for decrypting cookies encrypted with old secrets.
129
+ # @return [Array<String>]
219
130
  def fallback_secret_key_base
220
131
  Array(@fallback_secret_key_base || ENV["FALLBACK_SECRET_KEY_BASE"])
221
132
  end
222
133
 
223
- def server
224
- @server ||= Server.new
134
+ # Schedule a block of code to run after Rage has finished loading the application code. Use this to reference application-level constants during the initialization process.
135
+ # @example
136
+ # Rage.config.after_initialize do
137
+ # SUPER_USER = User.find_by!(super: true)
138
+ # end
139
+ def after_initialize(&block)
140
+ push_hook(block, :after_initialize)
225
141
  end
142
+ # @!endgroup
226
143
 
144
+ # @!group Middleware Configuration
145
+ # Allows configuring the middleware stack used by Rage.
146
+ # @return [Rage::Configuration::Middleware]
227
147
  def middleware
228
148
  @middleware ||= Middleware.new
229
149
  end
150
+ # @!endgroup
230
151
 
231
- def cable
232
- @cable ||= Cable.new
152
+ # @!group Server Configuration
153
+ # Allows configuring the built-in Rage server.
154
+ # @return [Rage::Configuration::Server]
155
+ def server
156
+ @server ||= Server.new
233
157
  end
158
+ # @!endgroup
234
159
 
160
+ # @!group Static File Server
161
+ # Allows configuring the static file server used by Rage.
162
+ # @return [Rage::Configuration::PublicFileServer]
235
163
  def public_file_server
236
164
  @public_file_server ||= PublicFileServer.new
237
165
  end
166
+ # @!endgroup
167
+
168
+ # @!group Cable Configuration
169
+ # Allows configuring Cable settings.
170
+ # @return [Rage::Configuration::Cable]
171
+ def cable
172
+ @cable ||= Cable.new
173
+ end
174
+ # @!endgroup
238
175
 
176
+ # @!group OpenAPI Configuration
177
+ # Allows configuring OpenAPI settings.
178
+ # @return [Rage::Configuration::OpenAPI]
239
179
  def openapi
240
180
  @openapi ||= OpenAPI.new
241
181
  end
182
+ # @!endgroup
242
183
 
184
+ # @!group Deferred Configuration
185
+ # Allows configuring Deferred settings.
186
+ # @return [Rage::Configuration::Deferred]
243
187
  def deferred
244
188
  @deferred ||= Deferred.new
245
189
  end
190
+ # @!endgroup
246
191
 
247
- def internal
248
- @internal ||= Internal.new
192
+ # @!group Logging Context and Tags Configuration
193
+ # Allows configuring custom log context objects that will be included in every log entry.
194
+ # @return [Rage::Configuration::LogContext]
195
+ def log_context
196
+ @log_context ||= LogContext.new
249
197
  end
250
198
 
251
- def after_initialize(&block)
252
- push_hook(block, :after_initialize)
199
+ # Allows configuring custom log tags that will be included in every log entry.
200
+ # @return [Rage::Configuration::LogTags]
201
+ def log_tags
202
+ @log_tags ||= LogTags.new
203
+ end
204
+ # @!endgroup
205
+
206
+ # @private
207
+ def internal
208
+ @internal ||= Internal.new
253
209
  end
254
210
 
211
+ # @private
255
212
  def run_after_initialize!
256
213
  run_hooks_for!(:after_initialize, self)
257
214
  end
258
215
 
216
+ class LogContext
217
+ # @private
218
+ def initialize
219
+ @objects = []
220
+ end
221
+
222
+ # @private
223
+ def objects
224
+ @objects.dup
225
+ end
226
+
227
+ # Add a new custom log context object. Each context object is evaluated independently and the results are merged into the final log entry.
228
+ # @overload <<(hash)
229
+ # Add a static log context entry.
230
+ # @param hash [Hash] a hash representing the log context
231
+ # @example
232
+ # Rage.configure do
233
+ # config.log_context << { version: ENV["APP_VERSION"] }
234
+ # end
235
+ # @overload <<(callable)
236
+ # Add a dynamic log context entry. Dynamic context entries are executed on every log call to capture dynamic state like changing span IDs during request processing.
237
+ # @param callable [#call] a callable object that returns a hash representing the log context or nil
238
+ # @example
239
+ # Rage.configure do
240
+ # config.log_context << proc { { trace_id: MyObservabilitySDK.trace_id } if MyObservabilitySDK.active? }
241
+ # end
242
+ # @note Exceptions from dynamic context callables will cause the entire request to fail. Make sure to handle exceptions inside the callable if necessary.
243
+ def <<(block_or_hash)
244
+ validate_input!(block_or_hash)
245
+ @objects << block_or_hash
246
+ @objects.tap(&:flatten!).tap(&:uniq!)
247
+
248
+ self
249
+ end
250
+
251
+ alias_method :push, :<<
252
+
253
+ # Remove a custom log context object.
254
+ # @param block_or_hash [Hash, #call] the context object to remove
255
+ # @example
256
+ # Rage.configure do
257
+ # config.log_context.delete(MyObservabilitySDK::LOG_CONTEXT)
258
+ # end
259
+ def delete(block_or_hash)
260
+ @objects.delete(block_or_hash)
261
+ end
262
+
263
+ private
264
+
265
+ def validate_input!(obj)
266
+ if obj.is_a?(Array)
267
+ obj.each { |item| validate_input!(item) }
268
+ elsif !obj.is_a?(Hash) && !obj.respond_to?(:call)
269
+ raise ArgumentError, "custom log context has to be a hash, an array of hashes, or respond to `#call`"
270
+ end
271
+ end
272
+ end
273
+
274
+ class LogTags < LogContext
275
+ # @!method <<(block_or_string)
276
+ # Add a new custom log tag. Each tag is evaluated independently and the results are merged into the final log entry.
277
+ # @overload <<(string)
278
+ # Add a static log tag.
279
+ # @param string [String] the log tag
280
+ # @example
281
+ # Rage.configure do
282
+ # config.log_tags << Rage.env
283
+ # end
284
+ # @overload <<(callable)
285
+ # Add a dynamic log tag. Dynamic tags are executed on every log call.
286
+ # @param callable [#call] a callable object that returns a string representing the log tag, an array of log tags, or nil
287
+ # @example
288
+ # Rage.configure do
289
+ # config.log_tags << proc { Current.tenant.slug }
290
+ # end
291
+ # @note Exceptions from dynamic tag callables will cause the entire request to fail. Make sure to handle exceptions inside the callable if necessary.
292
+
293
+ # @!method delete(block_or_string)
294
+ # Remove a custom log tag object.
295
+ # @param block_or_string [String, #call] the tag object to remove
296
+ # @example
297
+ # Rage.configure do
298
+ # config.log_tags.delete(MyObservabilitySDK::LOG_TAGS)
299
+ # end
300
+
301
+ # @private
302
+ private
303
+
304
+ def validate_input!(obj)
305
+ if obj.is_a?(Array)
306
+ obj.each { |item| validate_input!(item) }
307
+ elsif !obj.respond_to?(:to_str) && !obj.respond_to?(:call)
308
+ raise ArgumentError, "custom log tag has to be a string, an array of strings, or respond to `#call`"
309
+ end
310
+ end
311
+ end
312
+
259
313
  class Server
314
+ # @!attribute port
315
+ # Specify the port the server will listen on.
316
+ # @return [Integer]
317
+ # @example Change the default port
318
+ # Rage.configure do
319
+ # config.server.port = 3001
320
+ # end
321
+ #
322
+ # @!attribute workers_count
323
+ # Specify the number of worker processes to spawn. Use `-1` to spawn one worker per CPU core.
324
+ # @return [Integer]
325
+ # @example Change the number of worker processes
326
+ # Rage.configure do
327
+ # config.server.workers_count = 4
328
+ # end
329
+ #
330
+ # @!attribute timeout
331
+ # Specify the connection timeout in seconds.
332
+ # @return [Integer]
333
+ # @example Change the connection timeout
334
+ # Rage.configure do
335
+ # config.server.timeout = 30
336
+ # end
337
+ #
338
+ # @!attribute max_clients
339
+ # Limit the number of simultaneous connections the server can accept. Defaults to the maximum number of open files.
340
+ # @return [Integer]
341
+ #
342
+ # @note Decreasing this number is almost never a good idea. Depending on your application specifics, you are encouraged to use other methods to limit the number of concurrent connections:
343
+ #
344
+ # - If your application is exposed to the public, you may want to use a cloud rate limiter, like {https://developers.cloudflare.com/waf Cloudflare WAF} or {https://docs.fastly.com/en/ngwaf Fastly WAF}.
345
+ # - Otherwise, consider using tools like {https://github.com/rack/rack-attack Rack::Attack} or {https://github.com/mperham/connection_pool connection_pool}.
346
+ # @example Limit the amount of connections your application can accept
347
+ # Rage.configure do
348
+ # config.middleware.use Rack::Attack
349
+ # Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |req|
350
+ # req.ip
351
+ # end
352
+ # end
353
+ # @example Limit the amount of connections to a specific resource
354
+ # HTTP = ConnectionPool.new(size: 5, timeout: 5) { Net::HTTP }
355
+ # HTTP.with do |conn|
356
+ # conn.get("/my-resource")
357
+ # end
260
358
  attr_accessor :port, :workers_count, :timeout, :max_clients
359
+
360
+ # @private
261
361
  attr_reader :threads_count
262
362
 
363
+ # @private
263
364
  def initialize
264
365
  @threads_count = 1
265
366
  @workers_count = Rage.env.development? ? 1 : -1
@@ -268,16 +369,47 @@ class Rage::Configuration
268
369
  end
269
370
 
270
371
  class Middleware
372
+ # @private
271
373
  attr_reader :middlewares
272
374
 
375
+ # @private
273
376
  def initialize
274
377
  @middlewares = [[Rage::FiberWrapper]]
275
378
  end
276
379
 
380
+ # Add a new middleware to the end of the stack.
381
+ # @note This is the recommended way of adding a middleware.
382
+ # @param new_middleware [Class] the middleware class
383
+ # @param args [Array] arguments passed to the middleware initializer
384
+ # @param block [Proc] an optional block passed to the middleware initializer
385
+ # @example
386
+ # Rage.configure do
387
+ # config.middleware.use Rack::Cors do
388
+ # allow do
389
+ # origins "*"
390
+ # resource "*", headers: :any
391
+ # end
392
+ # end
393
+ # end
277
394
  def use(new_middleware, *args, &block)
278
395
  insert_after(@middlewares.length - 1, new_middleware, *args, &block)
279
396
  end
280
397
 
398
+ # Insert a new middleware before an existing middleware in the stack.
399
+ # @note Rage always uses the `Rage::FiberWrapper` middleware, which wraps every request in a separate fiber. Make sure to always have this middleware in the top of the stack. Placing other middlewares in front may lead to undefined behavior.
400
+ # @param existing_middleware [Class, Integer] the existing middleware class or its index in the stack
401
+ # @param new_middleware [Class] the new middleware class
402
+ # @param args [Array] arguments passed to the middleware initializer
403
+ # @param block [Proc] an optional block passed to the middleware initializer
404
+ # @example
405
+ # Rage.configure do
406
+ # config.middleware.insert_before Rack::Runtime, Rack::Cors do
407
+ # allow do
408
+ # origins "*"
409
+ # resource "*", headers: :any
410
+ # end
411
+ # end
412
+ # end
281
413
  def insert_before(existing_middleware, new_middleware, *args, &block)
282
414
  index = find_middleware_index(existing_middleware)
283
415
  if index == 0 && @middlewares[0][0] == Rage::FiberWrapper
@@ -286,11 +418,28 @@ class Rage::Configuration
286
418
  @middlewares = (@middlewares[0...index] + [[new_middleware, args, block]] + @middlewares[index..]).uniq(&:first)
287
419
  end
288
420
 
421
+ # Insert a new middleware after an existing middleware in the stack.
422
+ # @param existing_middleware [Class, Integer] the existing middleware class or its index in the stack
423
+ # @param new_middleware [Class] the new middleware class
424
+ # @param args [Array] arguments passed to the middleware initializer
425
+ # @param block [Proc] an optional block passed to the middleware initializer
426
+ # @example
427
+ # Rage.configure do
428
+ # config.middleware.insert_after Rack::Runtime, Rack::Cors do
429
+ # allow do
430
+ # origins "*"
431
+ # resource "*", headers: :any
432
+ # end
433
+ # end
434
+ # end
289
435
  def insert_after(existing_middleware, new_middleware, *args, &block)
290
436
  index = find_middleware_index(existing_middleware)
291
437
  @middlewares = (@middlewares[0..index] + [[new_middleware, args, block]] + @middlewares[index + 1..]).uniq(&:first)
292
438
  end
293
439
 
440
+ # Check if a middleware is included in the stack.
441
+ # @param middleware [Class, Integer] the middleware class or its index in the stack
442
+ # @return [Boolean]
294
443
  def include?(middleware)
295
444
  !!find_middleware_index(middleware) rescue false
296
445
  end
@@ -312,9 +461,24 @@ class Rage::Configuration
312
461
  end
313
462
 
314
463
  class Cable
464
+ # @!attribute allowed_request_origins
465
+ # Restrict the server to only accept requests from specified origins. The origins can be strings or regular expressions. Defaults to `/localhost/` in development and test environments.
466
+ # @return [Array<Regexp>, Regexp, Array<String>, String, nil]
467
+ # @example
468
+ # Rage.configure do
469
+ # config.cable.allowed_request_origins = [/example\.com/, "myapp.com"]
470
+ # end
471
+ #
472
+ # @!attribute disable_request_forgery_protection
473
+ # Disable request forgery protection for WebSocket connections to allow requests from any origin.
474
+ # @return [Boolean]
475
+ # @example
476
+ # Rage.configure do
477
+ # config.cable.disable_request_forgery_protection = true
478
+ # end
315
479
  attr_accessor :allowed_request_origins, :disable_request_forgery_protection
316
- attr_reader :protocol
317
480
 
481
+ # @private
318
482
  def initialize
319
483
  @protocol = Rage::Cable::Protocols::ActioncableV1Json
320
484
  @allowed_request_origins = if Rage.env.development? || Rage.env.test?
@@ -322,6 +486,22 @@ class Rage::Configuration
322
486
  end
323
487
  end
324
488
 
489
+ # Returns the protocol the server will use.
490
+ # @return [Class] the protocol class
491
+ def protocol
492
+ @protocol
493
+ end
494
+
495
+ # Specify the protocol the server will use. Supported values include {Rage::Cable::Protocols::ActioncableV1Json :actioncable_v1_json} and {Rage::Cable::Protocols::RawWebSocketJson :raw_websocket_json}. Defaults to {Rage::Cable::Protocols::ActioncableV1Json :actioncable_v1_json}.
496
+ # @param protocol [:actioncable_v1_json, :raw_websocket_json] the protocol symbol
497
+ # @example Use the built-in ActionCable V1 JSON protocol
498
+ # Rage.configure do
499
+ # config.cable.protocol = :actioncable_v1_json
500
+ # end
501
+ # @example Use the built-in Raw WebSocket JSON protocol
502
+ # Rage.configure do
503
+ # config.cable.protocol = :raw_websocket_json
504
+ # end
325
505
  def protocol=(protocol)
326
506
  @protocol = case protocol
327
507
  when Class
@@ -350,6 +530,7 @@ class Rage::Configuration
350
530
  end
351
531
  end
352
532
 
533
+ # @private
353
534
  def config
354
535
  @config ||= begin
355
536
  config_file = Rage.root.join("config/cable.yml")
@@ -363,10 +544,12 @@ class Rage::Configuration
363
544
  end
364
545
  end
365
546
 
547
+ # @private
366
548
  def adapter_config
367
549
  config.except(:adapter)
368
550
  end
369
551
 
552
+ # @private
370
553
  def adapter
371
554
  case config[:adapter]
372
555
  when "redis"
@@ -376,20 +559,54 @@ class Rage::Configuration
376
559
  end
377
560
 
378
561
  class PublicFileServer
562
+ # @!attribute enabled
563
+ # Configure whether Rage should serve static files from the `public` directory. Defaults to `false`.
564
+ # @return [Boolean] whether the static file server is enabled
565
+ # @example
566
+ # Rage.configure do
567
+ # config.public_file_server.enabled = true
568
+ # end
379
569
  attr_accessor :enabled
380
570
  end
381
571
 
382
572
  class OpenAPI
383
- attr_accessor :tag_resolver
573
+ # Specify the rules to customize how OpenAPI tags are generated for API operations.
574
+ # The method accepts a callable object that receives the controller class, the action name (as a symbol), and the original tag generated by Rage.
575
+ # The callable should return a string or an array of strings representing the tags to use for the API operation.
576
+ # This enables grouping endpoints in the OpenAPI documentation according to your application's needs.
577
+ # @param tag_resolver [#call] a callable object that resolves OpenAPI tags
578
+ # @example
579
+ # Rage.configure do
580
+ # config.openapi.tag_resolver = proc do |controller_class, action_name, default_tag|
581
+ # if controller_class.name.start_with?("Admin::")
582
+ # [default_tag, "Admin"]
583
+ # else
584
+ # [default_tag, "Public"]
585
+ # end
586
+ # end
587
+ # end
588
+ def tag_resolver=(tag_resolver)
589
+ unless tag_resolver.respond_to?(:call)
590
+ raise ArgumentError, "Custom tag resolver should respond to `#call`"
591
+ end
592
+
593
+ @tag_resolver = tag_resolver
594
+ end
595
+
596
+ # Returns the OpenAPI tag resolver used by Rage.
597
+ # @return [#call, nil]
598
+ def tag_resolver
599
+ @tag_resolver
600
+ end
384
601
  end
385
602
 
386
603
  class Deferred
387
- attr_reader :backpressure
388
-
604
+ # @private
389
605
  def initialize
390
606
  @configured = false
391
607
  end
392
608
 
609
+ # Returns the backend instance used by `Rage::Deferred`.
393
610
  def backend
394
611
  unless @backend_class
395
612
  @backend_class = Rage::Deferred::Backends::Disk
@@ -399,6 +616,27 @@ class Rage::Configuration
399
616
  @backend_class.new(**@backend_options)
400
617
  end
401
618
 
619
+ # Specify the backend used to persist deferred tasks. Supported values are `:disk`, which uses disk storage, or `nil`, which disables persistence of deferred tasks.
620
+ # @overload backend=(disk, options = {})
621
+ # Use the disk backend.
622
+ # @param options [Hash] additional backend options
623
+ # @option options [Pathname, String] :path the directory where deferred tasks will be stored. Defaults to `storage/`
624
+ # @option options [String] :prefix the prefix used for deferred task files. Defaults to `deferred-`
625
+ # @option options [Integer] :fsync_frequency the frequency of `fsync` calls in seconds. Defaults to `0.5`
626
+ # @example Use the disk backend with default options
627
+ # Rage.configure do
628
+ # config.deferred.backend = :disk
629
+ # end
630
+ # @example Use the disk backend with custom options
631
+ # Rage.configure do
632
+ # config.deferred.backend = :disk, path: "my_storage", fsync_frequency: 1000
633
+ # end
634
+ # @overload backend=(nil)
635
+ # Disable persistence of deferred tasks.
636
+ # @example
637
+ # Rage.configure do
638
+ # config.deferred.backend = nil
639
+ # end
402
640
  def backend=(config)
403
641
  @configured = true
404
642
 
@@ -422,6 +660,7 @@ class Rage::Configuration
422
660
  class Backpressure
423
661
  attr_reader :high_water_mark, :low_water_mark, :timeout, :sleep_interval, :timeout_iterations
424
662
 
663
+ # @private
425
664
  def initialize(high_water_mark = nil, low_water_mark = nil, timeout = nil)
426
665
  @high_water_mark = high_water_mark || 1_000
427
666
  @low_water_mark = low_water_mark || (@high_water_mark * 0.8).round
@@ -432,6 +671,38 @@ class Rage::Configuration
432
671
  end
433
672
  end
434
673
 
674
+ # Returns the backpressure configuration used by `Rage::Deferred`.
675
+ # @return [Backpressure, nil]
676
+ def backpressure
677
+ @backpressure
678
+ end
679
+
680
+ # Configure backpressure settings for `Rage::Deferred`. Backpressure is used to limit the number of pending tasks in the queue and is disabled by default.
681
+ #
682
+ # @overload backpressure=(true)
683
+ # Enable backpressure with default settings.
684
+ # @example
685
+ # Rage.configure do
686
+ # config.deferred.backpressure = true
687
+ # end
688
+ #
689
+ # @overload backpressure=(false)
690
+ # Disable backpressure.
691
+ # @example
692
+ # Rage.configure do
693
+ # config.deferred.backpressure = false
694
+ # end
695
+ #
696
+ # @overload backpressure=(config)
697
+ # Enable backpressure with custom settings.
698
+ # @param config [Hash] backpressure configuration
699
+ # @option config [Integer] :high_water_mark the maximum number of deferred tasks allowed in the queue before applying backpressure. Defaults to `1000`.
700
+ # @option config [Integer] :low_water_mark the minimum number of deferred tasks in the queue at which backpressure is lifted. Defaults to `80%` of `:high_water_mark`.
701
+ # @option config [Integer] :timeout the maximum time in seconds to wait for the queue size to drop below `:low_water_mark` before raising the {Rage::Deferred::PushTimeout Rage::Deferred::PushTimeout} exception. Defaults to 2 seconds.
702
+ # @example
703
+ # Rage.configure do
704
+ # config.deferred.backpressure = { high_water_mark: 2000, low_water_mark: 1500, timeout: 5 }
705
+ # end
435
706
  def backpressure=(config)
436
707
  @configured = true
437
708
 
@@ -451,18 +722,22 @@ class Rage::Configuration
451
722
  @backpressure = Backpressure.new(high_water_mark, low_water_mark, timeout)
452
723
  end
453
724
 
725
+ # @private
454
726
  def default_disk_storage_path
455
727
  Pathname.new("storage")
456
728
  end
457
729
 
730
+ # @private
458
731
  def default_disk_storage_prefix
459
732
  "deferred-"
460
733
  end
461
734
 
735
+ # @private
462
736
  def has_default_disk_storage?
463
737
  default_disk_storage_path.glob("#{default_disk_storage_prefix}*").any?
464
738
  end
465
739
 
740
+ # @private
466
741
  def configured?
467
742
  @configured
468
743
  end
@@ -502,6 +777,14 @@ class Rage::Configuration
502
777
  class Internal
503
778
  attr_accessor :rails_mode
504
779
 
780
+ def initialized!
781
+ @initialized = true
782
+ end
783
+
784
+ def initialized?
785
+ !!@initialized
786
+ end
787
+
505
788
  def patch_ar_pool?
506
789
  !ENV["RAGE_DISABLE_AR_POOL_PATCH"] && !Rage.env.test?
507
790
  end
@@ -531,5 +814,52 @@ class Rage::Configuration
531
814
  else
532
815
  @logger = Rage::Logger.new(nil)
533
816
  end
817
+
818
+ if @log_formatter && @logger.external_logger.is_a?(Rage::Logger::External::Dynamic)
819
+ puts "WARNING: changing the log formatter via `config.log_formatter=` has no effect when using a custom external logger."
820
+ end
821
+
822
+ if @log_context
823
+ Rage.__log_processor.add_custom_context(@log_context.objects)
824
+ @logger.dynamic_context = Rage.__log_processor.dynamic_context
825
+ end
826
+
827
+ if @log_tags
828
+ Rage.__log_processor.add_custom_tags(@log_tags.objects)
829
+ @logger.dynamic_tags = Rage.__log_processor.dynamic_tags
830
+ end
534
831
  end
535
832
  end
833
+
834
+ # @!parse [ruby]
835
+ # # @note This class does not exist at runtime and is used for documentation purposes only. Do not inherit external loggers from it.
836
+ # class ExternalLoggerInterface
837
+ # # Called whenever a log entry is created.
838
+ # #
839
+ # # Rage automatically detects which parameters your external logger's `#call` method accepts, and only passes those parameters. You can omit any of the described parameters in your implementation.
840
+ # #
841
+ # # @param severity [:debug, :info, :warn, :error, :fatal, :unknown] the log severity
842
+ # # @param tags [Array] the log tags submitted via {Rage::Logger#tagged Rage::Logger#tagged}. The first tag is always the request ID
843
+ # # @param context [Hash] the log context submitted via {Rage::Logger#with_context Rage::Logger#with_context}
844
+ # # @param message [String, nil] the log message. For request logs generated by Rage, this is always `nil`
845
+ # # @param request_info [Hash, nil] request-specific information. The value is `nil` for non-request logs; for request logs, contains the following keys:
846
+ # # @option request_info [Hash] :env the Rack env object
847
+ # # @option request_info [Hash] :params the request parameters
848
+ # # @option request_info [Array] :response the Rack response object
849
+ # # @option request_info [Float] :duration the duration of the request in milliseconds
850
+ # # @example
851
+ # # Rage.configure do
852
+ # # config.logger = proc do |severity:, tags:, context:, message:, request_info:|
853
+ # # data = context.merge(tags:)
854
+ # #
855
+ # # if request_info
856
+ # # data[:path] = request_info[:env]["PATH_INFO"]
857
+ # # MyLoggingSDK.info("Request completed", data)
858
+ # # else
859
+ # # MyLoggingSDK.public_send(severity, message, data)
860
+ # # end
861
+ # # end
862
+ # # end
863
+ # def call(severity:, tags:, context:, message:, request_info:)
864
+ # end
865
+ # end