rage-rb 1.27.0 → 1.28.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.
data/lib/rage/cli.rb CHANGED
@@ -4,65 +4,14 @@ require "thor"
4
4
  require "rack"
5
5
  require "rage/version"
6
6
 
7
+ require "rage/cli/base"
7
8
  require "rage/cli/skills"
9
+ require "rage/cli/openapi"
10
+ require "rage/cli/code_generator"
11
+ require "rage/cli/new_app_generator"
8
12
 
9
- module Rage
10
- class CLICodeGenerator < Thor
11
- include Thor::Actions
12
-
13
- def self.source_root
14
- File.expand_path("templates", __dir__)
15
- end
16
-
17
- desc "migration NAME", "Generate a new migration"
18
- def migration(name = nil)
19
- return help("migration") if name.nil?
20
-
21
- setup
22
- Rake::Task["db:new_migration"].invoke(name)
23
- end
24
-
25
- desc "model NAME", "Generate a new model"
26
- def model(name = nil)
27
- return help("model") if name.nil?
28
-
29
- setup
30
- migration("create_#{name.pluralize}")
31
- @model_name = name.classify
32
- template("model-template/model.rb", "app/models/#{name.singularize.underscore}.rb")
33
- end
34
-
35
- desc "controller NAME", "Generate a new controller"
36
- def controller(name = nil)
37
- return help("controller") if name.nil?
38
-
39
- setup
40
- unless defined?(ActiveSupport::Inflector)
41
- raise LoadError, <<~ERR
42
- ActiveSupport::Inflector is required to run this command. Add the following line to your Gemfile:
43
- gem "activesupport", require: "active_support/inflector"
44
- ERR
45
- end
46
-
47
- # remove trailing Controller if already present
48
- normalized_name = name.sub(/_?controller$/i, "")
49
- @controller_name = "#{normalized_name.camelize}Controller"
50
- file_name = "#{normalized_name.underscore}_controller.rb"
51
-
52
- template("controller-template/controller.rb", "app/controllers/#{file_name}")
53
- end
54
-
55
- private
56
-
57
- def setup
58
- @setup ||= begin
59
- require "rake"
60
- load "Rakefile"
61
- end
62
- end
63
- end
64
-
65
- class CLI < Thor
13
+ module Rage::CLI
14
+ class App < Base
66
15
  def self.exit_on_failure?
67
16
  true
68
17
  end
@@ -74,7 +23,7 @@ module Rage
74
23
  return help("new") if options.help? || path.nil?
75
24
 
76
25
  require "rage/all"
77
- CLINewAppGenerator.start([path, options[:database]])
26
+ NewAppGenerator.start([path, options[:database]])
78
27
  end
79
28
 
80
29
  desc "s", "Start the app server"
@@ -232,11 +181,14 @@ module Rage
232
181
  end
233
182
 
234
183
  desc "skills", "Manage coding agent skills"
235
- subcommand "skills", CLISkills
184
+ subcommand "skills", Skills
185
+
186
+ desc "openapi", "OpenAPI validation tools"
187
+ subcommand "openapi", OpenAPI
236
188
 
237
189
  map "generate" => :g
238
190
  desc "g TYPE", "Generate new code"
239
- subcommand "g", CLICodeGenerator
191
+ subcommand "g", CodeGenerator
240
192
 
241
193
  map "--tasks" => :tasks
242
194
  desc "--tasks", "See the list of available tasks"
@@ -273,26 +225,6 @@ module Rage
273
225
 
274
226
  private
275
227
 
276
- def environment
277
- require File.expand_path("config/application.rb", Dir.pwd)
278
-
279
- if Rage.config.internal.rails_mode
280
- require File.expand_path("config/environment.rb", Dir.pwd)
281
- end
282
- end
283
-
284
- def set_env(options)
285
- if options[:environment]
286
- ENV["RAGE_ENV"] = ENV["RAILS_ENV"] = options[:environment]
287
- elsif ENV["RAGE_ENV"]
288
- ENV["RAILS_ENV"] = ENV["RAGE_ENV"]
289
- elsif ENV["RAILS_ENV"]
290
- ENV["RAGE_ENV"] = ENV["RAILS_ENV"]
291
- else
292
- ENV["RAGE_ENV"] = ENV["RAILS_ENV"] = "development"
293
- end
294
- end
295
-
296
228
  def linked_rake_tasks
297
229
  require "rake"
298
230
  Rake::TaskManager.record_task_metadata = true
@@ -371,67 +303,4 @@ module Rage
371
303
  end
372
304
  end
373
305
  end
374
-
375
- class CLINewAppGenerator < Thor::Group
376
- include Thor::Actions
377
- argument :path, type: :string
378
- argument :database, type: :string, required: false
379
-
380
- def self.source_root
381
- File.expand_path("templates", __dir__)
382
- end
383
-
384
- def setup
385
- @use_database = !database.nil?
386
- end
387
-
388
- def create_directory
389
- empty_directory(path)
390
- end
391
-
392
- def copy_files
393
- inject_templates
394
- end
395
-
396
- def install_database
397
- return unless @use_database
398
-
399
- @app_name = path.tr("-", "_").downcase
400
- append_to_file "#{path}/Gemfile", <<~RUBY
401
-
402
- gem "#{get_db_gem_name}"
403
- gem "activerecord"
404
- gem "standalone_migrations", require: false
405
- RUBY
406
-
407
- inject_templates("db-templates")
408
- inject_templates("db-templates/#{database}")
409
- end
410
-
411
- private
412
-
413
- def inject_templates(from = nil)
414
- root = "#{self.class.source_root}/#{from}"
415
-
416
- Dir.glob("*", base: root).each do |template|
417
- next if File.directory?("#{root}/#{template}")
418
-
419
- *template_path_parts, template_name = template.split("-")
420
- template("#{root}/#{template}", [path, *template_path_parts, template_name].join("/"))
421
- end
422
- end
423
-
424
- def get_db_gem_name
425
- case database
426
- when "mysql"
427
- "mysql2"
428
- when "trilogy"
429
- "trilogy"
430
- when "postgresql"
431
- "pg"
432
- when "sqlite3"
433
- "sqlite3"
434
- end
435
- end
436
- end
437
306
  end
@@ -37,6 +37,7 @@ class Rage::CodeLoader
37
37
  load("#{Rage.root}/config/routes.rb")
38
38
 
39
39
  reload_components
40
+ Rage.config.run_hooks_for(:after_reload)
40
41
  end
41
42
 
42
43
  # in Rails mode - reset the routes; everything else will be done by Rails
@@ -47,6 +48,7 @@ class Rage::CodeLoader
47
48
  Rage.__router.reset_routes
48
49
 
49
50
  reload_components
51
+ Rage.config.run_hooks_for(:after_reload)
50
52
  end
51
53
 
52
54
  def reloading?
@@ -54,7 +56,9 @@ class Rage::CodeLoader
54
56
  end
55
57
 
56
58
  def check_updated!
57
- current_watched = @autoload_path.glob("**/*.rb") + Rage.root.glob("config/routes.rb") + Rage.root.glob("config/openapi_components.*")
59
+ current_watched = @autoload_path.glob("**/*.{rb,erb}") + Rage.root.glob("config/routes.rb") + Rage.root.glob("config/openapi_components.*")
60
+ current_watched += Rage.config.code_loader.reload_paths.flat_map { |pattern| Rage.root.glob(pattern) }
61
+
58
62
  current_update_at = current_watched.max_by { |path| path.exist? ? path.mtime.to_f : 0 }&.mtime.to_f
59
63
  return false if !@last_watched && !@last_update_at
60
64
 
@@ -141,6 +141,15 @@ class Rage::Configuration
141
141
  push_hook(block, :after_initialize)
142
142
  end
143
143
 
144
+ # Schedule a block of code to run after Rage has reloaded the application code in development. Use this to reset state or re-initialize dependencies that cache application-level constants.
145
+ # @example
146
+ # Rage.config.after_reload do
147
+ # MyCache.clear
148
+ # end
149
+ def after_reload(&block)
150
+ push_hook(block, :after_reload)
151
+ end
152
+
144
153
  # Register a custom renderer that generates overloads `render` on all controllers.
145
154
  # The block receives the object passed to `render` together with any additional keyword arguments.
146
155
  # The code inside the block is executed in the context of the controller instance, so you can access all usual controller methods in it.
@@ -256,6 +265,29 @@ class Rage::Configuration
256
265
  def log_tags
257
266
  @log_tags ||= LogTags.new
258
267
  end
268
+
269
+ # Allows configuring case-insensitive partial matches for redacting structured log context keys.
270
+ # Matching keys will have their values replaced with `"[REDACTED]"` before the log entry is written.
271
+ #
272
+ # @param keys [String, Symbol, Array<String, Symbol>, nil] one or more keys to redact
273
+ # @example Redact common secrets from structured logs
274
+ # Rage.configure do
275
+ # config.log_redact_keys = [:password, :token, :secret]
276
+ # end
277
+ def log_redact_keys=(keys)
278
+ @log_redact_keys = Array(keys).filter_map { |key|
279
+ if !key.is_a?(String) && !key.is_a?(Symbol)
280
+ raise ArgumentError, "log redact keys have to be strings or symbols"
281
+ elsif !key.empty?
282
+ key.to_s
283
+ end
284
+ }.uniq
285
+ end
286
+
287
+ # @private
288
+ def log_redact_keys
289
+ @log_redact_keys || []
290
+ end
259
291
  # @!endgroup
260
292
 
261
293
  # @!group Telemetry Configuration
@@ -282,6 +314,14 @@ class Rage::Configuration
282
314
  end
283
315
  # @!endgroup
284
316
 
317
+ # @!group Code Loader Configuration
318
+ # Allows configuring code loader settings.
319
+ # @return [Rage::Configuration::CodeLoader]
320
+ def code_loader
321
+ @code_loader ||= CodeLoader.new
322
+ end
323
+ # @!endgroup
324
+
285
325
  # @!group Blocking Operation Pool Configuration
286
326
  # Allows configuring the thread pool for offloading native calls.
287
327
  # @return [Rage::Configuration::BlockingOperationPool]
@@ -435,7 +475,7 @@ class Rage::Configuration
435
475
  # end
436
476
  def <<(reporter)
437
477
  validate_input!(reporter)
438
- return self if @objects.include?(reporter)
478
+ raise ArgumentError, "#{reporter} is already registered" if @objects.include?(reporter)
439
479
 
440
480
  @objects << reporter
441
481
  Rage::Errors.__send__(:__register_reporter, reporter)
@@ -878,6 +918,7 @@ class Rage::Configuration
878
918
  @backend_options = parse_disk_backend_options(opts)
879
919
  Rage::Deferred::Backends::Disk
880
920
  when nil
921
+ @backend_options = {} if RUBY_VERSION.start_with?("3.3.")
881
922
  Rage::Deferred::Backends::Nil
882
923
  else
883
924
  raise ArgumentError, "unsupported backend value; supported keys are `:disk` and `nil`"
@@ -1123,6 +1164,29 @@ class Rage::Configuration
1123
1164
  attr_accessor :form_actions
1124
1165
  end
1125
1166
 
1167
+ class CodeLoader
1168
+ # @private
1169
+ def initialize
1170
+ @reload_paths = []
1171
+ end
1172
+
1173
+ # @private
1174
+ attr_reader :reload_paths
1175
+
1176
+ # Specify additional paths to watch for changes in development.
1177
+ #
1178
+ # @param paths [Array<Pathname, String>] glob patterns or directory paths to watch
1179
+ # @example Watch HAML templates
1180
+ # Rage.configure do
1181
+ # config.code_loader.reload_paths = ["app/views/**/*.haml"]
1182
+ # end
1183
+ def reload_paths=(paths)
1184
+ @reload_paths = Array(paths).map do |path|
1185
+ path.is_a?(Pathname) ? path : Pathname.new(path)
1186
+ end
1187
+ end
1188
+ end
1189
+
1126
1190
  class BlockingOperationPool
1127
1191
  # @!attribute enabled
1128
1192
  # Enable a background thread pool for offloading native calls that can be executed outside the GVL, freeing
@@ -1165,7 +1229,11 @@ class Rage::Configuration
1165
1229
  # end
1166
1230
  def <<(daemon)
1167
1231
  validate!(daemon)
1232
+ raise ArgumentError, "#{daemon} is already registered" if @klasses.include?(daemon)
1233
+
1168
1234
  @klasses << daemon
1235
+
1236
+ self
1169
1237
  end
1170
1238
 
1171
1239
  alias_method :push, :<<
@@ -1260,12 +1328,14 @@ class Rage::Configuration
1260
1328
  if @logger
1261
1329
  @logger.formatter = @log_formatter if @log_formatter
1262
1330
  @logger.level = @log_level if @log_level
1331
+ @logger.log_redact_keys = @log_redact_keys if @log_redact_keys
1263
1332
  else
1264
1333
  @logger = Rage::Logger.new(nil)
1265
1334
  end
1266
1335
 
1267
- if @log_formatter && @logger.external_logger.is_a?(Rage::Logger::External::Dynamic)
1336
+ if @log_formatter && @logger.external_logger.is_a?(Rage::Logger::External::Dynamic) && !@log_formatter_warning_shown
1268
1337
  puts "WARNING: changing the log formatter via `config.log_formatter=` has no effect when using a custom external logger."
1338
+ @log_formatter_warning_shown = true
1269
1339
  end
1270
1340
 
1271
1341
  if @log_context
data/lib/rage/daemon.rb CHANGED
@@ -174,7 +174,6 @@ class Rage::Daemon
174
174
  instance = new
175
175
  result = instance.perform
176
176
  break if result.equal?(Stop) || @__stopping
177
- Rage.logger.warn("Daemon exited, restarting...")
178
177
  rescue => e
179
178
  break if @__stopping
180
179
  Rage.logger.error("Daemon failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
@@ -185,7 +184,11 @@ class Rage::Daemon
185
184
 
186
185
  # reset backoff if ran successfully for a while
187
186
  backoff = INITIAL_BACKOFF if Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at > BACKOFF_RESET_INTERVAL
188
- sleep(backoff / 2 + rand * backoff / 2)
187
+
188
+ interval = (backoff / 2 + rand * backoff / 2).round(2)
189
+ Rage.logger.warn("Daemon exited, restarting in #{interval}s")
190
+ sleep(interval)
191
+
189
192
  backoff = (backoff * 2).clamp(INITIAL_BACKOFF, MAX_BACKOFF)
190
193
  end
191
194
  end