rage-rb 1.23.0 → 1.24.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +240 -0
- data/README.md +1 -0
- data/lib/rage/application.rb +1 -0
- data/lib/rage/cable/cable.rb +20 -15
- data/lib/rage/cable/channel.rb +2 -1
- data/lib/rage/configuration.rb +122 -27
- data/lib/rage/controller/api.rb +5 -31
- data/lib/rage/deferred/deferred.rb +7 -0
- data/lib/rage/deferred/metadata.rb +8 -0
- data/lib/rage/deferred/scheduler.rb +25 -0
- data/lib/rage/deferred/task.rb +19 -5
- data/lib/rage/errors.rb +83 -0
- data/lib/rage/events/subscriber.rb +6 -1
- data/lib/rage/internal.rb +15 -6
- data/lib/rage/middleware/fiber_wrapper.rb +1 -0
- data/lib/rage/openapi/builder.rb +1 -1
- data/lib/rage/openapi/converter.rb +5 -1
- data/lib/rage/openapi/nodes/method.rb +2 -1
- data/lib/rage/openapi/nodes/root.rb +2 -1
- data/lib/rage/openapi/openapi.rb +1 -1
- data/lib/rage/openapi/parser.rb +73 -2
- data/lib/rage/openapi/parsers/ext/alba.rb +30 -2
- data/lib/rage/openapi/parsers/request.rb +2 -2
- data/lib/rage/openapi/parsers/response.rb +2 -2
- data/lib/rage/openapi/parsers/yaml.rb +27 -5
- data/lib/rage/params_parser.rb +2 -2
- data/lib/rage/pubsub/adapters/redis.rb +2 -1
- data/lib/rage/router/dsl.rb +7 -2
- data/lib/rage/sse/application.rb +1 -0
- data/lib/rage/telemetry/tracer.rb +1 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +6 -0
- metadata +3 -3
- data/lib/rage/cable/adapters/base.rb +0 -16
- data/lib/rage/cable/adapters/redis.rb +0 -128
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b488d2ab6a7ebcc33ae77a768f55e9277b796c39c5a7451e32a823f4e7a8a848
|
|
4
|
+
data.tar.gz: c1e55ecf8182587ced30f1c0af834136edbd4e06d0466dfaf6685387d0ebb3d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ed0b0e62b74d3847804613ec77da801ceb2784dda9dc81895c93c6438a07377a82a5ff3ac4099bee8d4d9698ef51e2ac12233067d0672f3b5bdf97374e2fbc6
|
|
7
|
+
data.tar.gz: 6b6b8d71317c165d7ff5d3a1942cd81aeeeb5978f3672fc1de32464f4d4e3ed8f10171345a11db2e3427aff4cd84868403922541701c5464ce4260d96690250b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.24.0] - 2026-05-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- [Deferred] Add tests for log context capture and backward-compatible restore by [@jsxs0](https://github.com/jsxs0) (#274).
|
|
8
|
+
- [OpenAPI] Add support for per-endpoint OAuth2/OpenID scopes via `@auth_scope` tag by [@Piyush-Goenka](https://github.com/Piyush-Goenka) (#272).
|
|
9
|
+
- Reuse `define_dynamic_method` and `define_maybe_yield` methods in `RageController::API` from `Rage::Internal` by [@numice](https://github.com/numice) (#273).
|
|
10
|
+
- Add the `form_actions` router configuration (#278).
|
|
11
|
+
- [Deferred] Add native periodic task scheduling with multi-process leader election via `File#flock` by [@Abishekcs](https://github.com/Abishekcs) (#233).
|
|
12
|
+
- [OpenAPI] Support optional attributes and `Array<>` syntax by [@ayushman1210](https://github.com/ayushman1210) (#228).
|
|
13
|
+
- [Errors] Add centralized error reporting interface via `Rage.errors` and `config.error_handlers` by [@Digvijay-x1](https://github.com/Digvijay-x1) (#275).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- [OpenAPI] Fix SystemStackError in Alba parser with circular associations (#268).
|
|
18
|
+
- Rewind `rack.input` when parsing request body (#279).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- [Deferred] Increase default retry limit to 20 and update default retry backoff to `(attempt**4) + 10 + (rand(15) * attempt)` by [@anuj-pal27](https://github.com/anuj-pal27) (#271).
|
|
23
|
+
- Update `Rage::Cable` to use the new `PubSub` module (#281).
|
|
24
|
+
|
|
3
25
|
## [1.23.0] - 2026-04-15
|
|
4
26
|
|
|
5
27
|
### Fixed
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Contributing to Rage
|
|
2
|
+
|
|
3
|
+
This guide is designed to help contributors understand the project's internals, design principles, and conventions. Whether you're fixing a bug, adding a feature, or participating in GSoC, this document will help you get started.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Design Principles](#design-principles)
|
|
8
|
+
- [Documentation Standards](#documentation-standards)
|
|
9
|
+
- [Dynamic Code Generation](#dynamic-code-generation)
|
|
10
|
+
- [Iodine Integration](#iodine-integration)
|
|
11
|
+
- [The Fiber Runtime](#the-fiber-runtime)
|
|
12
|
+
- [A Note on AI Usage](#a-note-on-ai-usage)
|
|
13
|
+
|
|
14
|
+
## Design Principles
|
|
15
|
+
|
|
16
|
+
### Performance Over Readability
|
|
17
|
+
|
|
18
|
+
Rage is a framework, not application code. While readability matters, performance takes priority when the two conflict. Framework code runs on every request, so micro-optimizations compound into significant gains.
|
|
19
|
+
|
|
20
|
+
This doesn't mean writing deliberately obscure code. It means accepting that some patterns which would be discouraged in application code are acceptable here when they improve performance.
|
|
21
|
+
|
|
22
|
+
### Lean Happy Path
|
|
23
|
+
|
|
24
|
+
The happy path should execute as little code as possible. We achieve this through:
|
|
25
|
+
|
|
26
|
+
1. **Boot-time computation**: Move work to server startup whenever possible. Pre-compile routes, resolve callback chains, and build method definitions during initialization rather than on each request.
|
|
27
|
+
|
|
28
|
+
2. **Feature isolation**: New features should not impact performance for users who don't use them. If a feature requires runtime checks, consider whether those checks can be eliminated through code generation or configuration.
|
|
29
|
+
|
|
30
|
+
### Duplication Over Premature Abstraction
|
|
31
|
+
|
|
32
|
+
Duplication is cheaper than unnecessary abstraction.
|
|
33
|
+
|
|
34
|
+
Abstractions should emerge from observed patterns, not anticipated ones. When you see similar code in two places, resist the urge to immediately extract a helper. Introducing new abstraction layers or deduplicating code should only happen after the duplication has naturally occurred and proven to be a burden.
|
|
35
|
+
|
|
36
|
+
A wrong abstraction is worse than duplicated code because:
|
|
37
|
+
- It's harder to understand (you must trace through multiple layers)
|
|
38
|
+
- It's harder to modify (changes affect all call sites)
|
|
39
|
+
- It's harder to remove (it becomes load-bearing)
|
|
40
|
+
|
|
41
|
+
When you do introduce an abstraction, make sure it pulls its weight.
|
|
42
|
+
|
|
43
|
+
## Documentation Standards
|
|
44
|
+
|
|
45
|
+
### YARD Documentation
|
|
46
|
+
|
|
47
|
+
All user-facing methods must be documented using [YARD](https://yardoc.org/). Documentation comments use Markdown formatting.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Publish an event to all registered subscribers.
|
|
51
|
+
#
|
|
52
|
+
# @param event [Object] the event instance to publish
|
|
53
|
+
# @param context [Hash] optional context to pass to subscribers
|
|
54
|
+
# @return [void]
|
|
55
|
+
#
|
|
56
|
+
# @example Publishing an event
|
|
57
|
+
# Rage::Events.publish(OrderCreated.new(order: order))
|
|
58
|
+
#
|
|
59
|
+
# @example Publishing with context
|
|
60
|
+
# Rage::Events.publish(OrderCreated.new(order: order), context: { user_id: current_user.id })
|
|
61
|
+
#
|
|
62
|
+
def publish(event, context: nil)
|
|
63
|
+
# ...
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### The `@private` Tag
|
|
68
|
+
|
|
69
|
+
Some methods cannot be marked `private` using Ruby's `private` keyword (e.g., they need to be called from other classes within the framework), but they are not part of the public API. These methods should be marked with the `@private` YARD tag:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# @private
|
|
73
|
+
# Used internally by the router to register controller actions.
|
|
74
|
+
def __register_action(action)
|
|
75
|
+
# ...
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `@private` tag signals to contributors:
|
|
80
|
+
- This method is not part of the user-facing API
|
|
81
|
+
- It can be modified or removed without deprecation
|
|
82
|
+
- It can be used freely within the framework codebase
|
|
83
|
+
|
|
84
|
+
## Dynamic Code Generation
|
|
85
|
+
|
|
86
|
+
Rage relies heavily on dynamic code generation for both performance and flexibility. Understanding this pattern is essential for contributing to the framework.
|
|
87
|
+
|
|
88
|
+
### Why Dynamic Code Generation?
|
|
89
|
+
|
|
90
|
+
1. **Performance**: Generated code avoids runtime conditionals. Instead of checking "does this controller have before actions?" on every request, we generate a method that either includes the before action calls or doesn't.
|
|
91
|
+
|
|
92
|
+
2. **Flexibility**: Generated code can adapt to user-defined signatures, allowing optional parameters without forcing users to accept arguments they don't need.
|
|
93
|
+
|
|
94
|
+
### Examples in the Codebase
|
|
95
|
+
|
|
96
|
+
#### Controller Action Registration
|
|
97
|
+
|
|
98
|
+
`RageController::API.__register_action` (in `lib/rage/controller/api.rb`) generates a method for each controller action at boot time:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
102
|
+
def __run_#{action}
|
|
103
|
+
#{before_actions_chunk}
|
|
104
|
+
#{action} unless @__before_callback_rendered
|
|
105
|
+
#{after_actions_chunk}
|
|
106
|
+
[@__status, @__headers, @__body]
|
|
107
|
+
#{rescue_handlers_chunk}
|
|
108
|
+
end
|
|
109
|
+
RUBY
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This generates a single method that includes only the callbacks and exception handlers relevant to that specific action. No runtime resolution required.
|
|
113
|
+
|
|
114
|
+
#### Logger Rebuilding
|
|
115
|
+
|
|
116
|
+
`Rage::Logger#rebuild!` (in `lib/rage/logger/logger.rb`) generates logging methods based on the configured log level:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
if level_val < @level
|
|
120
|
+
# Log level is filtered out - generate a no-op method
|
|
121
|
+
def info(msg = nil, context = nil)
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
# Generate a method that actually logs
|
|
126
|
+
def info(msg = nil, context = nil)
|
|
127
|
+
# ... logging implementation
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
When logging at a level is disabled, the method becomes a no-op with zero overhead.
|
|
133
|
+
|
|
134
|
+
#### Telemetry Tracer
|
|
135
|
+
|
|
136
|
+
`Rage::Telemetry::Tracer#setup` (in `lib/rage/telemetry/tracer.rb`) generates tracing methods that call only the handlers registered for each span. If no handlers are registered, it generates a pass-through method.
|
|
137
|
+
|
|
138
|
+
### Dynamic Keyword Arguments
|
|
139
|
+
|
|
140
|
+
One pattern Rage uses extensively is dynamic keyword arguments. This allows users to define methods that accept only the parameters they care about, without requiring `**` to absorb extras.
|
|
141
|
+
|
|
142
|
+
For example, an event subscriber can be defined either way:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# Subscriber that only cares about the event
|
|
146
|
+
def call(event)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Subscriber that also needs context
|
|
150
|
+
def call(event, context:)
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Both work regardless of whether the event was published with context. The framework inspects the method signature and generates a call that passes only the expected arguments.
|
|
155
|
+
|
|
156
|
+
The `Rage::Internal.build_arguments` method (in `lib/rage/internal.rb`) implements this pattern:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
def build_arguments(method, arguments)
|
|
160
|
+
expected_parameters = method.parameters
|
|
161
|
+
|
|
162
|
+
arguments.filter_map { |arg_name, arg_value|
|
|
163
|
+
if expected_parameters.any? { |param_type, param_name| param_name == arg_name || param_type == :keyrest }
|
|
164
|
+
"#{arg_name}: #{arg_value}"
|
|
165
|
+
end
|
|
166
|
+
}.join(", ")
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This inspects the target method's parameters and generates a string containing only the arguments that method expects. The generated string is then embedded into dynamically defined code.
|
|
171
|
+
|
|
172
|
+
This pattern appears in:
|
|
173
|
+
- Event subscribers (accepting event with optional context)
|
|
174
|
+
- Telemetry handlers (accepting various span attributes)
|
|
175
|
+
- External loggers (accepting severity, message, context, etc.)
|
|
176
|
+
|
|
177
|
+
## Iodine Integration
|
|
178
|
+
|
|
179
|
+
Rage consists of two components: the framework (this repository) and its server, [Iodine](https://github.com/rage-rb/iodine). Iodine is not an external dependency; it's part of the Rage runtime, and its methods can be used freely within the codebase.
|
|
180
|
+
|
|
181
|
+
### Useful Iodine Methods
|
|
182
|
+
|
|
183
|
+
**`Iodine.run_after(milliseconds) { ... }`**: Schedule a block to run after a delay.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
Iodine.run_after(5000) do
|
|
187
|
+
cleanup_expired_sessions
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**`Iodine.run_every(milliseconds) { ... }`**: Schedule a block to run at regular intervals.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
Iodine.run_every(60_000) do
|
|
195
|
+
report_metrics
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**`Iodine.publish(channel, message, engine)`**: Send a message to subscribers. This is used for inter-fiber and inter-process communication.
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# Notify within the current process
|
|
203
|
+
Iodine.publish("my_channel", "message", Iodine::PubSub::PROCESS)
|
|
204
|
+
|
|
205
|
+
# Notify across all processes in the cluster
|
|
206
|
+
Iodine.publish("my_channel", "message", Iodine::PubSub::CLUSTER)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**`Iodine.on_state(state) { ... }`**: Register callbacks for server lifecycle events.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
Iodine.on_state(:on_start) do
|
|
213
|
+
# Runs when the worker process starts
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## The Fiber Runtime
|
|
218
|
+
|
|
219
|
+
### Rage::FiberWrapper
|
|
220
|
+
|
|
221
|
+
`Rage::FiberWrapper` (in `lib/rage/middleware/fiber_wrapper.rb`) is the glue between the framework and the server. It sits at the top of the middleware stack and:
|
|
222
|
+
|
|
223
|
+
1. Wraps every request in a Fiber
|
|
224
|
+
2. Implements the defer protocol for pausing/resuming async requests
|
|
225
|
+
|
|
226
|
+
When a request encounters blocking I/O (database query, HTTP request, etc.), the fiber yields. `FiberWrapper` detects this (`fiber.alive?`) and returns a special `:__http_defer__` signal to Iodine, which pauses the connection.
|
|
227
|
+
|
|
228
|
+
When the I/O completes, the fiber resumes and publishes a message to notify Iodine that the response is ready. This is the mechanism that enables transparent, non-blocking concurrency.
|
|
229
|
+
|
|
230
|
+
## A Note on AI Usage
|
|
231
|
+
|
|
232
|
+
Contributors are free to use AI tools however they see fit.
|
|
233
|
+
|
|
234
|
+
One thing to keep in mind: when delegating development to AI, the friction this removes is the very friction that enables developers to understand the system, learn, and grow as professionals.
|
|
235
|
+
|
|
236
|
+
There's value in the struggle of tracing through code, understanding why something was designed a certain way, and building mental models of complex systems. Use AI to assist and accelerate, but ensure you are still engaging deeply with the architecture and the "why" behind the code you are committing.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
Questions? Open an issue or reach out to the maintainers.
|
data/README.md
CHANGED
|
@@ -206,6 +206,7 @@ Rage is a good fit if you:
|
|
|
206
206
|
- Documentation: [https://rage-rb.dev](https://rage-rb.dev/docs/intro)
|
|
207
207
|
- API Reference: [https://rage-rb.dev/api](https://rage-rb.dev/api)
|
|
208
208
|
- Architecture: [ARCHITECTURE.md](https://github.com/rage-rb/rage/blob/main/ARCHITECTURE.md)
|
|
209
|
+
- Contributing: [CONTRIBUTING.md](https://github.com/rage-rb/rage/blob/main/CONTRIBUTING.md)
|
|
209
210
|
|
|
210
211
|
Contributions and thoughtful feedback are welcome.
|
|
211
212
|
|
data/lib/rage/application.rb
CHANGED
data/lib/rage/cable/cable.rb
CHANGED
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
# ```
|
|
30
30
|
#
|
|
31
31
|
module Rage::Cable
|
|
32
|
+
PUBSUB_BROADCASTER_ID = "cable"
|
|
33
|
+
private_constant :PUBSUB_BROADCASTER_ID
|
|
34
|
+
|
|
32
35
|
# Create a new Cable application.
|
|
33
36
|
#
|
|
34
37
|
# @example
|
|
@@ -36,9 +39,6 @@ module Rage::Cable
|
|
|
36
39
|
# run Rage.cable.application
|
|
37
40
|
# end
|
|
38
41
|
def self.application
|
|
39
|
-
# explicitly initialize the adapter
|
|
40
|
-
__adapter
|
|
41
|
-
|
|
42
42
|
handler = __build_handler(__protocol)
|
|
43
43
|
accept_response = [0, __protocol.protocol_definition, []]
|
|
44
44
|
|
|
@@ -61,6 +61,14 @@ module Rage::Cable
|
|
|
61
61
|
chain
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# @private
|
|
65
|
+
def self.__initialize
|
|
66
|
+
if (adapter = Rage.config.pubsub.adapter)
|
|
67
|
+
adapter.add_broadcaster(PUBSUB_BROADCASTER_ID, __protocol)
|
|
68
|
+
@__adapter = adapter
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
64
72
|
# @private
|
|
65
73
|
def self.__router
|
|
66
74
|
@__router ||= Router.new
|
|
@@ -71,11 +79,6 @@ module Rage::Cable
|
|
|
71
79
|
@__protocol ||= Rage.config.cable.protocol.tap { |protocol| protocol.init(__router) }
|
|
72
80
|
end
|
|
73
81
|
|
|
74
|
-
# @private
|
|
75
|
-
def self.__adapter
|
|
76
|
-
@__adapter ||= Rage.config.cable.adapter
|
|
77
|
-
end
|
|
78
|
-
|
|
79
82
|
# @private
|
|
80
83
|
def self.__build_handler(protocol)
|
|
81
84
|
klass = Class.new do
|
|
@@ -125,7 +128,8 @@ module Rage::Cable
|
|
|
125
128
|
end
|
|
126
129
|
|
|
127
130
|
def log_error(e)
|
|
128
|
-
Rage.logger.error("Unhandled exception has
|
|
131
|
+
Rage.logger.error("Unhandled exception has occurred - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
132
|
+
Rage::Errors.report(e)
|
|
129
133
|
end
|
|
130
134
|
end
|
|
131
135
|
|
|
@@ -142,7 +146,7 @@ module Rage::Cable
|
|
|
142
146
|
def self.broadcast(stream, data)
|
|
143
147
|
Rage::Telemetry.tracer.span_cable_stream_broadcast(stream:) do
|
|
144
148
|
__protocol.broadcast(stream, data)
|
|
145
|
-
__adapter&.publish(stream, data)
|
|
149
|
+
@__adapter&.publish(PUBSUB_BROADCASTER_ID, stream, data)
|
|
146
150
|
end
|
|
147
151
|
|
|
148
152
|
true
|
|
@@ -174,11 +178,6 @@ module Rage::Cable
|
|
|
174
178
|
# end
|
|
175
179
|
# end
|
|
176
180
|
|
|
177
|
-
module Adapters
|
|
178
|
-
autoload :Base, "rage/cable/adapters/base"
|
|
179
|
-
autoload :Redis, "rage/cable/adapters/redis"
|
|
180
|
-
end
|
|
181
|
-
|
|
182
181
|
module Protocols
|
|
183
182
|
end
|
|
184
183
|
|
|
@@ -191,3 +190,9 @@ require_relative "protocols/raw_web_socket_json"
|
|
|
191
190
|
require_relative "channel"
|
|
192
191
|
require_relative "connection"
|
|
193
192
|
require_relative "router"
|
|
193
|
+
|
|
194
|
+
if Rage.config.internal.initialized?
|
|
195
|
+
Rage::Cable.__initialize
|
|
196
|
+
else
|
|
197
|
+
Rage.config.after_initialize { Rage::Cable.__initialize }
|
|
198
|
+
end
|
data/lib/rage/cable/channel.rb
CHANGED
|
@@ -331,7 +331,8 @@ class Rage::Cable::Channel
|
|
|
331
331
|
Fiber.schedule do
|
|
332
332
|
slice.each { |channel| callback.call(channel) }
|
|
333
333
|
rescue => e
|
|
334
|
-
Rage.logger.error("Unhandled exception has
|
|
334
|
+
Rage.logger.error("Unhandled exception has occurred - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
335
|
+
Rage::Errors.report(e)
|
|
335
336
|
end
|
|
336
337
|
end
|
|
337
338
|
end
|
data/lib/rage/configuration.rb
CHANGED
|
@@ -219,6 +219,14 @@ class Rage::Configuration
|
|
|
219
219
|
end
|
|
220
220
|
# @!endgroup
|
|
221
221
|
|
|
222
|
+
# @!group Error Reporter Configuration
|
|
223
|
+
# Allows configuring error reporters.
|
|
224
|
+
# @return [Rage::Configuration::ErrorReporters]
|
|
225
|
+
def error_reporters
|
|
226
|
+
@error_reporters ||= ErrorReporters.new
|
|
227
|
+
end
|
|
228
|
+
# @!endgroup
|
|
229
|
+
|
|
222
230
|
# @!group OpenAPI Configuration
|
|
223
231
|
# Allows configuring OpenAPI settings.
|
|
224
232
|
# @return [Rage::Configuration::OpenAPI]
|
|
@@ -265,6 +273,14 @@ class Rage::Configuration
|
|
|
265
273
|
end
|
|
266
274
|
# @!endgroup
|
|
267
275
|
|
|
276
|
+
# @!group Router Configuration
|
|
277
|
+
# Allows configuring router settings.
|
|
278
|
+
# @return [Rage::Configuration::Router]
|
|
279
|
+
def router
|
|
280
|
+
@router ||= Router.new
|
|
281
|
+
end
|
|
282
|
+
# @!endgroup
|
|
283
|
+
|
|
268
284
|
# @private
|
|
269
285
|
def pubsub
|
|
270
286
|
@pubsub ||= PubSub.new
|
|
@@ -378,6 +394,60 @@ class Rage::Configuration
|
|
|
378
394
|
end
|
|
379
395
|
end
|
|
380
396
|
|
|
397
|
+
class ErrorReporters
|
|
398
|
+
# @private
|
|
399
|
+
def initialize
|
|
400
|
+
@objects = []
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# @private
|
|
404
|
+
def objects
|
|
405
|
+
@objects.dup
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Add a new error reporter.
|
|
409
|
+
# Error reporters should respond to `#call` and accept one of:
|
|
410
|
+
# - `call(exception)`
|
|
411
|
+
# - `call(exception, context: {})`
|
|
412
|
+
#
|
|
413
|
+
# @param reporter [#call]
|
|
414
|
+
# @return [self]
|
|
415
|
+
# @example
|
|
416
|
+
# Rage.configure do
|
|
417
|
+
# config.error_reporters << SentryReporter.new
|
|
418
|
+
# end
|
|
419
|
+
def <<(reporter)
|
|
420
|
+
validate_input!(reporter)
|
|
421
|
+
return self if @objects.include?(reporter)
|
|
422
|
+
|
|
423
|
+
@objects << reporter
|
|
424
|
+
Rage::Errors.__send__(:__register_reporter, reporter)
|
|
425
|
+
|
|
426
|
+
self
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
alias_method :push, :<<
|
|
430
|
+
|
|
431
|
+
# Remove an error reporter.
|
|
432
|
+
# @param reporter [#call] the reporter to remove
|
|
433
|
+
# @example
|
|
434
|
+
# reporter = SentryReporter.new
|
|
435
|
+
# Rage.configure do
|
|
436
|
+
# config.error_reporters.delete(reporter)
|
|
437
|
+
# end
|
|
438
|
+
def delete(reporter)
|
|
439
|
+
deleted = @objects.delete(reporter)
|
|
440
|
+
Rage::Errors.__send__(:__unregister_reporter, reporter) if deleted
|
|
441
|
+
deleted
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private
|
|
445
|
+
|
|
446
|
+
def validate_input!(reporter)
|
|
447
|
+
raise ArgumentError, "error reporter must respond to #call" unless reporter.respond_to?(:call)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
381
451
|
class Server
|
|
382
452
|
# @!attribute port
|
|
383
453
|
# Specify the port the server will listen on.
|
|
@@ -632,33 +702,6 @@ class Rage::Configuration
|
|
|
632
702
|
end
|
|
633
703
|
end
|
|
634
704
|
end
|
|
635
|
-
|
|
636
|
-
# @private
|
|
637
|
-
def config
|
|
638
|
-
@config ||= begin
|
|
639
|
-
config_file = Rage.root.join("config/cable.yml")
|
|
640
|
-
|
|
641
|
-
if config_file.exist?
|
|
642
|
-
yaml = ERB.new(config_file.read).result
|
|
643
|
-
YAML.safe_load(yaml, aliases: true, symbolize_names: true)[Rage.env.to_sym] || {}
|
|
644
|
-
else
|
|
645
|
-
{}
|
|
646
|
-
end
|
|
647
|
-
end
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
# @private
|
|
651
|
-
def adapter_config
|
|
652
|
-
config.except(:adapter)
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
# @private
|
|
656
|
-
def adapter
|
|
657
|
-
case config[:adapter]
|
|
658
|
-
when "redis"
|
|
659
|
-
Rage::Cable::Adapters::Redis.new(adapter_config)
|
|
660
|
-
end
|
|
661
|
-
end
|
|
662
705
|
end
|
|
663
706
|
|
|
664
707
|
class PublicFileServer
|
|
@@ -707,6 +750,46 @@ class Rage::Configuration
|
|
|
707
750
|
# @private
|
|
708
751
|
def initialize
|
|
709
752
|
@configured = false
|
|
753
|
+
@schedule_blocks = []
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Schedule a periodic task to run at a fixed interval.
|
|
757
|
+
# @example
|
|
758
|
+
# Rage.configure do
|
|
759
|
+
# config.deferred.schedule do
|
|
760
|
+
# every 5.minutes, task: ClearCache
|
|
761
|
+
# end
|
|
762
|
+
# end
|
|
763
|
+
def schedule(&block)
|
|
764
|
+
@schedule_blocks << block
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# @private
|
|
768
|
+
# Evaluates all stored schedule blocks and returns the collected tasks.
|
|
769
|
+
# Called at boot time after all app constants are loaded.
|
|
770
|
+
def scheduled_tasks
|
|
771
|
+
@schedule_blocks.flat_map do |block|
|
|
772
|
+
dsl = ScheduleDSL.new
|
|
773
|
+
dsl.instance_eval(&block)
|
|
774
|
+
dsl.tasks
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# @private
|
|
779
|
+
class ScheduleDSL
|
|
780
|
+
attr_reader :tasks
|
|
781
|
+
|
|
782
|
+
def initialize
|
|
783
|
+
@tasks = []
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# Registers a task to run on a fixed interval (in seconds)
|
|
787
|
+
def every(interval, task:)
|
|
788
|
+
unless task.is_a?(Class) && task.include?(Rage::Deferred::Task)
|
|
789
|
+
raise ArgumentError, "#{task} must be a class that includes Rage::Deferred::Task"
|
|
790
|
+
end
|
|
791
|
+
@tasks << { interval:, task: }
|
|
792
|
+
end
|
|
710
793
|
end
|
|
711
794
|
|
|
712
795
|
# Returns the backend instance used by `Rage::Deferred`.
|
|
@@ -988,6 +1071,17 @@ class Rage::Configuration
|
|
|
988
1071
|
attr_accessor :key
|
|
989
1072
|
end
|
|
990
1073
|
|
|
1074
|
+
class Router
|
|
1075
|
+
# @!attribute form_actions
|
|
1076
|
+
# Enable the automatic generation of `new` and `edit` routes via resource helpers.
|
|
1077
|
+
# @return [Boolean]
|
|
1078
|
+
# @example Enable form actions
|
|
1079
|
+
# Rage.configure do
|
|
1080
|
+
# config.router.form_actions = true
|
|
1081
|
+
# end
|
|
1082
|
+
attr_accessor :form_actions
|
|
1083
|
+
end
|
|
1084
|
+
|
|
991
1085
|
# @private
|
|
992
1086
|
class PubSub
|
|
993
1087
|
attr_reader :adapter
|
|
@@ -1004,6 +1098,7 @@ class Rage::Configuration
|
|
|
1004
1098
|
def config
|
|
1005
1099
|
@config ||= begin
|
|
1006
1100
|
config_file = Rage.root.join("config/pubsub.yml")
|
|
1101
|
+
config_file = Rage.root.join("config/cable.yml") unless config_file.exist?
|
|
1007
1102
|
|
|
1008
1103
|
config = if config_file.exist?
|
|
1009
1104
|
yaml = ERB.new(config_file.read).result
|
data/lib/rage/controller/api.rb
CHANGED
|
@@ -185,32 +185,6 @@ class RageController::API
|
|
|
185
185
|
klass.__wrap_parameters_options = __wrap_parameters_options
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
-
# @private
|
|
189
|
-
@@__dynamic_name_seed = ("a".."i").to_a.permutation
|
|
190
|
-
|
|
191
|
-
# @private
|
|
192
|
-
# define a method based on a block
|
|
193
|
-
def define_dynamic_method(block)
|
|
194
|
-
name = @@__dynamic_name_seed.next.join
|
|
195
|
-
define_method("__rage_dynamic_#{name}", block)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# @private
|
|
199
|
-
# define a method that will call a specified method if a condition is `true` or yield if `false`
|
|
200
|
-
def define_maybe_yield(method_name)
|
|
201
|
-
name = @@__dynamic_name_seed.next.join
|
|
202
|
-
|
|
203
|
-
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
204
|
-
def __rage_dynamic_#{name}(condition)
|
|
205
|
-
if condition
|
|
206
|
-
#{method_name} { yield }
|
|
207
|
-
else
|
|
208
|
-
yield
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
RUBY
|
|
212
|
-
end
|
|
213
|
-
|
|
214
188
|
# @private
|
|
215
189
|
def __register_renderer(name, block)
|
|
216
190
|
prepend(RageController::Renderers) unless ancestors.include?(RageController::Renderers)
|
|
@@ -240,7 +214,7 @@ class RageController::API
|
|
|
240
214
|
def rescue_from(*klasses, with: nil, &block)
|
|
241
215
|
unless with
|
|
242
216
|
if block_given?
|
|
243
|
-
with = define_dynamic_method(block)
|
|
217
|
+
with = Rage::Internal.define_dynamic_method(self, block)
|
|
244
218
|
else
|
|
245
219
|
raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
|
|
246
220
|
end
|
|
@@ -314,7 +288,7 @@ class RageController::API
|
|
|
314
288
|
# end
|
|
315
289
|
def around_action(action_name = nil, **opts, &block)
|
|
316
290
|
action = prepare_action_params(action_name, **opts, &block)
|
|
317
|
-
action.merge!(around: true, wrapper: define_maybe_yield(action[:name]))
|
|
291
|
+
action.merge!(around: true, wrapper: Rage::Internal.define_maybe_yield(self, action[:name]))
|
|
318
292
|
|
|
319
293
|
if @__before_actions && @__before_actions.frozen?
|
|
320
294
|
@__before_actions = @__before_actions.dup
|
|
@@ -429,7 +403,7 @@ class RageController::API
|
|
|
429
403
|
# used by `before_action` and `after_action`
|
|
430
404
|
def prepare_action_params(action_name = nil, **opts, &block)
|
|
431
405
|
if block_given?
|
|
432
|
-
action_name = define_dynamic_method(block)
|
|
406
|
+
action_name = Rage::Internal.define_dynamic_method(self, block)
|
|
433
407
|
elsif action_name.nil?
|
|
434
408
|
raise ArgumentError, "No handler provided. Pass the `action_name` parameter or provide a block."
|
|
435
409
|
end
|
|
@@ -444,8 +418,8 @@ class RageController::API
|
|
|
444
418
|
unless: _unless
|
|
445
419
|
}
|
|
446
420
|
|
|
447
|
-
action[:if] = define_dynamic_method(action[:if]) if action[:if].is_a?(Proc)
|
|
448
|
-
action[:unless] = define_dynamic_method(action[:unless]) if action[:unless].is_a?(Proc)
|
|
421
|
+
action[:if] = Rage::Internal.define_dynamic_method(self, action[:if]) if action[:if].is_a?(Proc)
|
|
422
|
+
action[:unless] = Rage::Internal.define_dynamic_method(self, action[:unless]) if action[:unless].is_a?(Proc)
|
|
449
423
|
|
|
450
424
|
action
|
|
451
425
|
end
|
|
@@ -82,10 +82,16 @@ module Rage::Deferred
|
|
|
82
82
|
)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# @private
|
|
86
|
+
def self.__start_scheduler
|
|
87
|
+
Rage::Deferred::Scheduler.start(Rage.config.deferred.scheduled_tasks)
|
|
88
|
+
end
|
|
89
|
+
|
|
85
90
|
# @private
|
|
86
91
|
def self.__initialize
|
|
87
92
|
__middleware_chain
|
|
88
93
|
__load_tasks
|
|
94
|
+
__start_scheduler
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
module Backends
|
|
@@ -96,6 +102,7 @@ module Rage::Deferred
|
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
require_relative "task"
|
|
105
|
+
require_relative "scheduler"
|
|
99
106
|
require_relative "queue"
|
|
100
107
|
require_relative "proxy"
|
|
101
108
|
require_relative "context"
|