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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa6f9f4b7b4e1f422fd778585be775834fbe1e8e30c523e2cac096bab71d0046
4
- data.tar.gz: 776090fd9779c994c95bf4ec368cb4562c544b8fc2d99043fa0c2cb5c91903af
3
+ metadata.gz: 847e3debe188b3b34ffdd6b7306edeca0a053b82c0e5106b3faa37a6e43f20bf
4
+ data.tar.gz: 202db65e0a17d27ae94178d93ddfca940a95237450095b06c0fa4e080a6fd09a
5
5
  SHA512:
6
- metadata.gz: c5bbbfc62d7da523ae2f7e1d9b3be2acaa67072fd34fe464f23ab909324c46c707983e828c451969c46cb019f11c6ca0abc026d641b44c4df5340add13c830dd
7
- data.tar.gz: 814cebf4625dc114d07de548981e0a593b78f2d55dd514ca9824d68296cfabd99f79e838b2580fdc07480ed906c8081585a38d6361f33bd64c3a06a070155bed
6
+ metadata.gz: 74610482cca2fd1394e3ffd4bd2cf577ed78a980140e39bf359ece4050bed7497dc1845a472a3c403a6fb0cbd48c1124a1714b8e7c8352b0ed720287177418c9
7
+ data.tar.gz: cd7cec704b0019012d1a08aea63b97dd83e0a6ee17155228d9517575e7882482c58b4b227d6663b053cc77a2edc551e34da53d0bf9bc0463e254ca917f9e8af4
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,47 @@
1
+ ### Table of Contents
2
+
3
+ [API Workflow](#api-workflow)<br>
4
+ [Executing Controller Actions](#executing-controller-actions)<br>
5
+ [Cable Workflow](#cable-workflow)<br>
6
+ [OpenAPI Workflow](#openapi-workflow)<br>
7
+ [Design Principles](#design-principles)<br>
8
+
9
+ ### API Workflow
10
+
11
+ The following diagram describes some of Rage's internal components and the way they interact with each other:
12
+
13
+ ![overview](https://github.com/rage-rb/rage/assets/2270393/0d45bbe3-622c-4b17-b8d8-552c567fecb3)
14
+
15
+ ### Executing Controller Actions
16
+
17
+ To maximize runtime performance, Rage pre-compiles controller actions when the application boots. For each action, it resolves the full chain of callbacks and exception handlers, building a single, optimized procedure.
18
+
19
+ When a request comes in, Rage executes this pre-compiled procedure directly, avoiding the overhead of resolving callbacks and exception handlers on every request. All of this happens at boot time to ensure the request-response cycle is as fast as possible.
20
+
21
+ ### Cable Workflow
22
+
23
+ `Rage::Cable` provides a component for handling real-time communication over WebSockets. The workflow involves authenticating connections and subscribing them to channels for bidirectional messaging.
24
+
25
+ The following diagram describes the components of a `Rage::Cable` application:
26
+
27
+ ![cable](https://github.com/user-attachments/assets/86db2091-f93a-44f8-9512-c4701770d09e)
28
+
29
+ ### OpenAPI Workflow
30
+
31
+ `Rage::OpenAPI` generates OpenAPI 3.0 specifications by parsing comments in controller files. This process happens at boot time, building the specification and storing it in memory to be served for API documentation.
32
+
33
+ The following diagram describes the flow of `Rage::OpenAPI`:
34
+
35
+ <img width="800" src="https://github.com/user-attachments/assets/b4a87b1e-9a0f-4432-a3e9-0106ff546f3f" />
36
+
37
+ ### Design Principles
38
+
39
+ * **Lean Happy Path:** We try to execute as many operations as possible during server initialization to minimize workload during request processing. Additionally, new features should be designed to avoid impacting the framework performance for users who do not utilize those features.
40
+
41
+ * **Performance Over Code Style:** We recognize that framework and application code have different requirements. While testability and readability are crucial for application code, framework code prioritizes performance and careful abstraction. This allows for future modifications while maintaining backward compatibility, though readability remains important.
42
+
43
+ * **Rails Compatibility:** A key objective is to provide a familiar experience for Rails developers, with the Controller and Cable APIs being largely compatible. However, Rage is not a reimplementation of Rails. Instead, it provides a familiar foundation and builds upon it with its own unique features.
44
+
45
+ * **Idiomatic Ruby:** We prioritize idiomatic Ruby, avoiding unnecessary abstractions. User-level code is expected to embrace standard Ruby syntax, approaches, and patterns, as this is preferable to a framework-level abstraction that accomplishes the same task.
46
+
47
+ * **Single-Threaded Fiber-Based Approach:** Each request is processed in a separate, isolated execution context (Fiber), pausing whenever it encounters blocking I/O. The single-threaded approach eliminates thread synchronization overhead, leading to enhanced performance and simplified code.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.19.0] - 2025-12-03
4
+
5
+ ### Added
6
+
7
+ - Add ability to specify external loggers (#178).
8
+ - Pass all of log data to deferred tasks (#173).
9
+ - Add the `Request#route_uri_pattern` method (#175).
10
+ - Support global log tags and context (#171, #177).
11
+
12
+ ### Fixed
13
+
14
+ - Fix reloading in dev with user-level fibers (#170).
15
+
16
+ ## [1.18.0] - 2025-10-29
17
+
18
+ ### Added
19
+
20
+ - Add `Rage::Events` (#167).
21
+
22
+ ### Fixed
23
+
24
+ - Fix sequential `Fiber.await` calls (#168).
25
+
3
26
  ## [1.17.1] - 2025-08-21
4
27
 
5
28
  ### Fixed
data/lib/rage/all.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "uploaded_file"
12
12
  require_relative "errors"
13
13
  require_relative "params_parser"
14
14
  require_relative "code_loader"
15
+ require_relative "log_processor"
15
16
 
16
17
  require_relative "router/strategies/host"
17
18
  require_relative "router/backend"
@@ -4,10 +4,11 @@ class Rage::Application
4
4
  def initialize(router)
5
5
  @router = router
6
6
  @exception_app = build_exception_app
7
+ @log_processor = Rage.__log_processor
7
8
  end
8
9
 
9
10
  def call(env)
10
- init_logger(env)
11
+ @log_processor.init_request_logger(env)
11
12
 
12
13
  handler = @router.lookup(env)
13
14
 
@@ -25,34 +26,11 @@ class Rage::Application
25
26
  response = @exception_app.call(500, e)
26
27
 
27
28
  ensure
28
- finalize_logger(env, response, params)
29
+ @log_processor.finalize_request_logger(env, response, params)
29
30
  end
30
31
 
31
32
  private
32
33
 
33
- DEFAULT_LOG_CONTEXT = {}.freeze
34
- private_constant :DEFAULT_LOG_CONTEXT
35
-
36
- def init_logger(env)
37
- Thread.current[:rage_logger] = {
38
- tags: [(env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag)],
39
- context: DEFAULT_LOG_CONTEXT,
40
- request_start: Process.clock_gettime(Process::CLOCK_MONOTONIC)
41
- }
42
- end
43
-
44
- def finalize_logger(env, response, params)
45
- logger = Thread.current[:rage_logger]
46
-
47
- duration = (
48
- (Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger[:request_start]) * 1000
49
- ).round(2)
50
-
51
- logger[:final] = { env:, params:, response:, duration: }
52
- Rage.logger.info("")
53
- logger[:final] = nil
54
- end
55
-
56
34
  def build_exception_app
57
35
  if Rage.env.development?
58
36
  ->(status, e) do
@@ -52,11 +52,10 @@ module Rage::Cable
52
52
  end
53
53
 
54
54
  @protocol = protocol
55
- @default_log_context = {}.freeze
55
+ @log_processor = Rage.__log_processor
56
56
  end
57
57
 
58
58
  def on_open(connection)
59
- connection.env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag
60
59
  schedule_fiber(connection) { @protocol.on_open(connection) }
61
60
  end
62
61
 
@@ -83,7 +82,7 @@ module Rage::Cable
83
82
 
84
83
  def schedule_fiber(connection)
85
84
  Fiber.schedule do
86
- Thread.current[:rage_logger] = { tags: [connection.env["rage.request_id"]], context: @default_log_context }
85
+ @log_processor.init_request_logger(connection.env)
87
86
  yield
88
87
  rescue => e
89
88
  log_error(e)
data/lib/rage/cli.rb CHANGED
@@ -12,7 +12,7 @@ module Rage
12
12
  File.expand_path("templates", __dir__)
13
13
  end
14
14
 
15
- desc "migration NAME", "Generate a new migration."
15
+ desc "migration NAME", "Generate a new migration"
16
16
  def migration(name = nil)
17
17
  return help("migration") if name.nil?
18
18
 
@@ -20,7 +20,7 @@ module Rage
20
20
  Rake::Task["db:new_migration"].invoke(name)
21
21
  end
22
22
 
23
- desc "model NAME", "Generate a new model."
23
+ desc "model NAME", "Generate a new model"
24
24
  def model(name = nil)
25
25
  return help("model") if name.nil?
26
26
 
@@ -30,7 +30,7 @@ module Rage
30
30
  template("model-template/model.rb", "app/models/#{name.singularize.underscore}.rb")
31
31
  end
32
32
 
33
- desc "controller NAME", "Generate a new controller."
33
+ desc "controller NAME", "Generate a new controller"
34
34
  def controller(name = nil)
35
35
  return help("controller") if name.nil?
36
36
 
@@ -65,9 +65,9 @@ module Rage
65
65
  true
66
66
  end
67
67
 
68
- desc "new PATH", "Create a new application."
69
- option :database, aliases: "-d", desc: "Preconfigure for selected database.", enum: %w(mysql trilogy postgresql sqlite3)
70
- option :help, aliases: "-h", desc: "Show this message."
68
+ desc "new PATH", "Create a new application"
69
+ option :database, aliases: "-d", desc: "Preconfigure for selected database", enum: %w(mysql trilogy postgresql sqlite3)
70
+ option :help, aliases: "-h", desc: "Show this message"
71
71
  def new(path = nil)
72
72
  return help("new") if options.help? || path.nil?
73
73
 
@@ -75,12 +75,12 @@ module Rage
75
75
  CLINewAppGenerator.start([path, options[:database]])
76
76
  end
77
77
 
78
- desc "s", "Start the app server."
79
- option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000."
80
- option :environment, aliases: "-e", desc: "Specifies the environment to run this server under (test/development/production)."
81
- option :binding, aliases: "-b", desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments."
82
- option :config, aliases: "-c", desc: "Uses a custom rack configuration."
83
- option :help, aliases: "-h", desc: "Show this message."
78
+ desc "s", "Start the app server"
79
+ option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000"
80
+ option :environment, aliases: "-e", desc: "Specifies the environment to run this server under (test/development/production)"
81
+ option :binding, aliases: "-b", desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments"
82
+ option :config, aliases: "-c", desc: "Uses a custom rack configuration"
83
+ option :help, aliases: "-h", desc: "Show this message"
84
84
  def server
85
85
  return help("server") if options.help?
86
86
 
@@ -104,9 +104,9 @@ module Rage
104
104
  ::Iodine.start
105
105
  end
106
106
 
107
- desc "routes", "List all routes."
107
+ desc "routes", "List all routes"
108
108
  option :grep, aliases: "-g", desc: "Filter routes by pattern"
109
- option :help, aliases: "-h", desc: "Show this message."
109
+ option :help, aliases: "-h", desc: "Show this message"
110
110
  def routes
111
111
  return help("routes") if options.help?
112
112
  # the result would be something like this:
@@ -162,8 +162,8 @@ module Rage
162
162
  end
163
163
  end
164
164
 
165
- desc "c", "Start the app console."
166
- option :help, aliases: "-h", desc: "Show this message."
165
+ desc "c", "Start the app console"
166
+ option :help, aliases: "-h", desc: "Show this message"
167
167
  def console
168
168
  return help("console") if options.help?
169
169
 
@@ -185,17 +185,49 @@ module Rage
185
185
  end
186
186
  end
187
187
 
188
+ desc "events [EVENT1, EVENT2]", "List all registered events and their subscribers"
189
+ option :help, aliases: "-h", desc: "Show this message"
190
+ def events(*event_class_names)
191
+ return help("events") if options.help?
192
+
193
+ environment
194
+ Rage::Events.__eager_load_subscribers if Rage.env.development?
195
+
196
+ event_classes = if event_class_names.any?
197
+ event_class_names.flat_map { |name| name.split(",") }.map do |event_class_name|
198
+ @last_event_class_name = event_class_name
199
+ Object.const_get(event_class_name)
200
+ end
201
+ else
202
+ registered_events = Rage::Events.__registered_subscribers.keys
203
+ registered_events.reject do |event_class|
204
+ registered_events.any? { |e| e.ancestors.include?(event_class) && e.ancestors.index(event_class) != 0 }
205
+ end
206
+ end
207
+
208
+ event_classes.each { |event_class| print_event_subscribers_tree(event_class) }
209
+
210
+ rescue NameError
211
+ if @last_event_class_name
212
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: Rage::Events.__registered_subscribers.keys)
213
+ suggestion = DidYouMean.formatter.message_for(spell_checker.correct(@last_event_class_name))
214
+ puts "Could not find the `#{@last_event_class_name}` event. #{suggestion}"
215
+ else
216
+ raise
217
+ end
218
+ end
219
+
188
220
  desc "version", "Return the current version of the framework"
189
221
  def version
190
222
  puts Rage::VERSION
191
223
  end
192
224
 
193
225
  map "generate" => :g
194
- desc "g TYPE", "Generate new code."
226
+ desc "g TYPE", "Generate new code"
195
227
  subcommand "g", CLICodeGenerator
196
228
 
197
229
  map "--tasks" => :tasks
198
- desc "--tasks", "See the list of available tasks."
230
+ desc "--tasks", "See the list of available tasks"
199
231
  def tasks
200
232
  require "io/console"
201
233
 
@@ -275,6 +307,57 @@ module Rage
275
307
  end
276
308
  end
277
309
  end
310
+
311
+ def print_event_subscribers_tree(event_class)
312
+ subscribers = Rage::Events.__get_subscribers(event_class)
313
+
314
+ event_ancestors = event_class.ancestors.take_while { |klass| klass != Struct && klass != Data && klass != Object }
315
+
316
+ # build a tree of all events and their subscribers
317
+ tree = event_ancestors.each_with_object({}) do |ancestor, memo|
318
+ level = event_class.ancestors.count { |klass| klass.ancestors.include?(ancestor) } - 1
319
+ filtered_subscribers = subscribers.select { |subscriber| subscriber.__event_classes.include?(ancestor) }
320
+
321
+ memo[ancestor] = { level:, subscribers: filtered_subscribers }
322
+ end
323
+
324
+ # reject events without subscribers located on the last levels
325
+ i = 0
326
+ tree = tree.reject do |_, node|
327
+ level, subscribers = node[:level], node[:subscribers]
328
+ next_level = tree.values.dig(i + 1, :level)
329
+ i += 1
330
+
331
+ (next_level.nil? || next_level < level) && subscribers.empty?
332
+ end
333
+
334
+ # indentation for each level
335
+ padding = " " * 3
336
+
337
+ # print the tree
338
+ tree.each_with_index do |(event_ancestor, node), i|
339
+ level, subscribers = node[:level], node[:subscribers]
340
+ next_level = tree.values.dig(i + 1, :level)
341
+
342
+ prefix = if i > 0 && next_level != level
343
+ "└─"
344
+ else
345
+ "├─"
346
+ end
347
+
348
+ event_class_line = "#{padding * level}#{prefix} \e[90m#{event_ancestor}\e[0m"
349
+ if level == 0
350
+ puts event_class_line
351
+ else
352
+ puts "|#{event_class_line}"
353
+ end
354
+
355
+ subscribers.each do |subscriber|
356
+ prefix = subscriber == subscribers.last && next_level != level + 1 ? "└─" : "├─"
357
+ puts "│#{padding * (level + 1)}#{prefix} \e[1m#{subscriber}\e[0m"
358
+ end
359
+ end
360
+ end
278
361
  end
279
362
 
280
363
  class CLINewAppGenerator < Thor::Group
@@ -64,6 +64,10 @@ class Rage::CodeLoader
64
64
  @last_watched, @last_update_at = current_watched, current_update_at
65
65
  end
66
66
 
67
+ def load_file(path)
68
+ @loader.load_file(path)
69
+ end
70
+
67
71
  private
68
72
 
69
73
  def configure_components
@@ -83,5 +87,9 @@ class Rage::CodeLoader
83
87
  unless Rage.autoload?(:OpenAPI) # the `OpenAPI` component is loaded
84
88
  Rage::OpenAPI.__reset_data_cache
85
89
  end
90
+
91
+ unless Rage.autoload?(:Events) # the `Events` component is loaded
92
+ Rage::Events.__reset_subscribers
93
+ end
86
94
  end
87
95
  end