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.
@@ -30,6 +30,14 @@ class Rage::Deferred::Metadata
30
30
  !!task.__next_retry_in(attempts, nil)
31
31
  end
32
32
 
33
+ # Returns the number of seconds until the next retry, or `nil` if no retry will occur.
34
+ # The result is memoized per attempt so that the value reported here matches what the queue uses to schedule the retry.
35
+ # @return [Numeric, nil] retry delay in seconds, or `nil` if the task won't be retried
36
+ def will_retry_in
37
+ task = Rage::Deferred::Context.get_task(context)
38
+ task.__next_retry_in(attempts, nil)
39
+ end
40
+
33
41
  private
34
42
 
35
43
  def context
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ class Rage::Deferred::Scheduler
5
+ def self.start(tasks)
6
+ return if tasks.empty?
7
+
8
+ Rage::Internal.pick_a_worker(purpose: "deferred-scheduler") do
9
+ puts("INFO: #{Process.pid} is managing scheduled tasks.") if Rage.logger.info?
10
+ register_timers(tasks)
11
+ end
12
+ end
13
+
14
+ def self.register_timers(tasks)
15
+ tasks.each do |entry|
16
+ interval = (entry[:interval] * 1000).to_i
17
+
18
+ if Rage.env.development?
19
+ Iodine.run_every(interval) { Object.const_get(entry[:task].name).enqueue }
20
+ else
21
+ Iodine.run_every(interval) { entry[:task].enqueue }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -31,15 +31,16 @@
31
31
  # ```
32
32
  #
33
33
  module Rage::Deferred::Task
34
- MAX_ATTEMPTS = 5
34
+ MAX_ATTEMPTS = 20
35
35
  private_constant :MAX_ATTEMPTS
36
36
 
37
- BACKOFF_INTERVAL = 5
38
- private_constant :BACKOFF_INTERVAL
39
-
40
37
  # @private
41
38
  CONTEXT_KEY = :__rage_deferred_execution_context
42
39
 
40
+ # @private
41
+ RETRY_IN_CACHE_VAR = :@__rage_deferred_retry_in
42
+ private_constant :RETRY_IN_CACHE_VAR
43
+
43
44
  def perform
44
45
  end
45
46
 
@@ -80,6 +81,8 @@ module Rage::Deferred::Task
80
81
 
81
82
  true
82
83
  rescue Exception => e
84
+ Rage::Errors.report(e)
85
+
83
86
  unless respond_to?(:__deferred_suppress_exception_logging?, true) && __deferred_suppress_exception_logging?
84
87
  Rage.logger.with_context(task_log_context) do
85
88
  Rage.logger.error("Deferred task failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
@@ -175,6 +178,17 @@ module Rage::Deferred::Task
175
178
 
176
179
  # @private
177
180
  def __next_retry_in(attempts, exception)
181
+ f = Fiber.current
182
+
183
+ if f.instance_variable_defined?(RETRY_IN_CACHE_VAR)
184
+ f.instance_variable_get(RETRY_IN_CACHE_VAR)
185
+ else
186
+ f.instance_variable_set(RETRY_IN_CACHE_VAR, __calculate_next_retry_in(attempts, exception))
187
+ end
188
+ end
189
+
190
+ # @private
191
+ def __calculate_next_retry_in(attempts, exception)
178
192
  max = @__max_retries || MAX_ATTEMPTS
179
193
  return if attempts > max
180
194
 
@@ -191,7 +205,7 @@ module Rage::Deferred::Task
191
205
 
192
206
  # @private
193
207
  def __default_backoff(attempt)
194
- rand(BACKOFF_INTERVAL * 2**attempt) + 1
208
+ (attempt**4) + 10 + (rand(15) * attempt)
195
209
  end
196
210
  end
197
211
  end
data/lib/rage/errors.rb CHANGED
@@ -1,4 +1,87 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rage::Errors
4
+ ReporterEntry = Struct.new(:reporter, :method_name)
5
+ private_constant :ReporterEntry
6
+
7
+ @reporters = []
8
+ @next_reporter_id = 0
9
+
10
+ class << self
11
+ # Forward an exception to all registered reporters.
12
+ #
13
+ # @param exception [Exception]
14
+ # @param context [Hash]
15
+ # @return [nil]
16
+ def report(exception, context: {})
17
+ return if @reporters.empty?
18
+ return if exception.instance_variable_defined?(:@_rage_error_reported)
19
+
20
+ ensure_backtrace(exception)
21
+
22
+ @reporters.each do |entry|
23
+ __send__(entry.method_name, entry.reporter, exception, context)
24
+ rescue => e
25
+ Rage.logger.error("Error reporter #{entry.reporter.class} failed while reporting #{exception.class}: #{e.class} (#{e.message})")
26
+ end
27
+
28
+ exception.instance_variable_set(:@_rage_error_reported, true) unless exception.frozen?
29
+
30
+ nil
31
+ end
32
+
33
+ # @private
34
+ def __register_reporter(reporter)
35
+ raise ArgumentError, "error reporter must respond to #call" unless reporter.respond_to?(:call)
36
+
37
+ reporter_id = @next_reporter_id
38
+ @next_reporter_id += 1
39
+ method_name = :"__report_#{reporter_id}"
40
+
41
+ arguments = Rage::Internal.build_arguments(
42
+ reporter.method(:call),
43
+ { context: "context" }
44
+ )
45
+ call_arguments = arguments.empty? ? "" : ", #{arguments}"
46
+
47
+ singleton_class.class_eval <<~RUBY, __FILE__, __LINE__ + 1
48
+ def #{method_name}(reporter, exception, context)
49
+ reporter.call(exception#{call_arguments})
50
+ end
51
+ RUBY
52
+
53
+ @reporters << ReporterEntry.new(reporter, method_name)
54
+
55
+ self
56
+ end
57
+
58
+ # @private
59
+ def __unregister_reporter(reporter)
60
+ @reporters.delete_if do |entry|
61
+ next false unless entry.reporter == reporter
62
+
63
+ singleton_class.remove_method(entry.method_name) if singleton_class.method_defined?(entry.method_name)
64
+ true
65
+ end
66
+
67
+ self
68
+ end
69
+
70
+ private
71
+
72
+ def ensure_backtrace(exception)
73
+ return if exception.frozen?
74
+ return unless exception.backtrace.nil?
75
+
76
+ begin
77
+ raise exception
78
+ rescue exception.class
79
+ end
80
+ end
81
+
82
+ private :__register_reporter, :__unregister_reporter
83
+ end
84
+
2
85
  class BadRequest < StandardError
3
86
  end
4
87
 
@@ -90,7 +90,12 @@ module Rage::Events::Subscriber
90
90
  Rage.logger.with_context(self.class.__log_context) do
91
91
  Rage.logger.error("Subscriber failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
92
92
  end
93
- raise e if self.class.__is_deferred
93
+
94
+ if self.class.__is_deferred
95
+ raise e
96
+ else
97
+ Rage::Errors.report(e)
98
+ end
94
99
  end
95
100
 
96
101
  private
data/lib/rage/internal.rb CHANGED
@@ -64,24 +64,33 @@ class Rage::Internal
64
64
  name_segments.join(":")
65
65
  end
66
66
 
67
+ LOCK_FILE_SUFFIX = rand(0x100000000).to_s(36)
68
+
67
69
  # Pick a worker process to execute a block of code.
68
70
  # This is useful for ensuring that certain code is only executed by a single worker in a multi-worker setup, e.g. for broadcasting messages to known streams or for running periodic tasks.
69
71
  # @yield The block of code to be executed by the picked worker
70
- def pick_a_worker(&block)
71
- @lock_file, lock_path = Tempfile.new.yield_self { |file| [file, file.path] }
72
+ def pick_a_worker(purpose:, &block)
73
+ attempt = proc do
74
+ lock_path = Pathname.new(Dir.tmpdir).join("rage-#{purpose}-lock-#{LOCK_FILE_SUFFIX}")
72
75
 
73
- Iodine.on_state(:on_start) do
74
- worker_lock = File.new(lock_path)
76
+ lock_file = File.open(lock_path, File::CREAT | File::WRONLY)
75
77
 
76
- if worker_lock.flock(File::LOCK_EX | File::LOCK_NB)
77
- @worker_lock = worker_lock
78
+ if lock_file.flock(File::LOCK_EX | File::LOCK_NB)
79
+ Iodine.on_state(:on_finish) { File.unlink(lock_file) if File.exist?(lock_file) }
80
+ worker_locks << lock_file
78
81
  block.call
79
82
  end
80
83
  end
84
+
85
+ Iodine.running? ? attempt.call : Iodine.on_state(:on_start) { attempt.call }
81
86
  end
82
87
 
83
88
  private
84
89
 
90
+ def worker_locks
91
+ @worker_locks ||= []
92
+ end
93
+
85
94
  def dynamic_name_seed
86
95
  @dynamic_name_seed ||= ("a".."j").to_a.permutation
87
96
  end
@@ -20,6 +20,7 @@ class Rage::FiberWrapper
20
20
  rescue Exception => e
21
21
  exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
22
22
  Rage.logger << exception_str
23
+ Rage::Errors.report(e)
23
24
  if Rage.env.development?
24
25
  [500, {}, [exception_str]]
25
26
  else
@@ -21,7 +21,7 @@ class Rage::OpenAPI::Builder
21
21
  end
22
22
 
23
23
  def run
24
- parser = Rage::OpenAPI::Parser.new
24
+ parser = Rage::OpenAPI::Parser.new(@nodes)
25
25
 
26
26
  @routes.each do |controller, routes|
27
27
  next if skip_controller?(controller)
@@ -137,6 +137,10 @@ class Rage::OpenAPI::Converter
137
137
  end
138
138
  end
139
139
 
140
+ if (dynamic_schemas = @nodes.schema_registry).any?
141
+ (@spec["components"]["schemas"] ||= {}).merge!(dynamic_schemas)
142
+ end
143
+
140
144
  @spec["tags"] = @used_tags.sort.map { |tag| { "name" => tag } }
141
145
 
142
146
  @spec
@@ -177,7 +181,7 @@ class Rage::OpenAPI::Converter
177
181
  @used_security_schemes << auth_entry.merge(name: auth_name)
178
182
  end
179
183
 
180
- { auth_name => [] }
184
+ { auth_name => node.auth_scopes.fetch(auth_name, []) }
181
185
  end
182
186
  end
183
187
  end
@@ -3,7 +3,7 @@
3
3
  class Rage::OpenAPI::Nodes::Method
4
4
  attr_reader :controller, :action, :parents
5
5
  attr_accessor :http_method, :http_path, :summary, :tag, :deprecated, :private, :description,
6
- :request, :responses, :parameters
6
+ :request, :responses, :parameters, :auth_scopes
7
7
 
8
8
  # @param controller [RageController::API]
9
9
  # @param action [String]
@@ -15,6 +15,7 @@ class Rage::OpenAPI::Nodes::Method
15
15
 
16
16
  @responses = {}
17
17
  @parameters = {}
18
+ @auth_scopes = {}
18
19
  end
19
20
 
20
21
  def root
@@ -20,12 +20,13 @@
20
20
  # Nodes::Method<index> Nodes::Method<show> Nodes::Method<show>
21
21
  #
22
22
  class Rage::OpenAPI::Nodes::Root
23
- attr_reader :leaves
23
+ attr_reader :leaves, :schema_registry
24
24
  attr_accessor :version, :title
25
25
 
26
26
  def initialize
27
27
  @parent_nodes_cache = {}
28
28
  @leaves = []
29
+ @schema_registry = {}
29
30
  end
30
31
 
31
32
  # @return [Array<Rage::OpenAPI::Nodes::Parent>]
@@ -120,7 +120,7 @@ module Rage::OpenAPI
120
120
 
121
121
  # @private
122
122
  def self.__try_parse_collection(str)
123
- if str =~ /^Array<([\w\s:\(\)]+)>$/ || str =~ /^\[([\w\s:\(\)]+)\]$/
123
+ if str =~ /^Array<([\w\s:\(\),]+)>$/ || str =~ /^\[([\w\s:\(\),]+)\]$/
124
124
  [true, $1]
125
125
  else
126
126
  [false, str]
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rage::OpenAPI::Parser
4
+ # @param root [Rage::OpenAPI::Nodes::Root]
5
+ def initialize(root)
6
+ @root = root
7
+ end
8
+
4
9
  # @param node [Rage::OpenAPI::Nodes::Parent]
5
10
  # @param comments [Array<Prism::InlineComment>]
6
11
  def parse_dangling_comments(node, comments)
@@ -125,7 +130,8 @@ class Rage::OpenAPI::Parser
125
130
  else
126
131
  parsed = Rage::OpenAPI::Parsers::Request.parse(
127
132
  request,
128
- namespace: Rage::OpenAPI.__module_parent(node.controller)
133
+ namespace: Rage::OpenAPI.__module_parent(node.controller),
134
+ root: @root
129
135
  )
130
136
 
131
137
  if parsed
@@ -138,6 +144,9 @@ class Rage::OpenAPI::Parser
138
144
  elsif expression =~ /@param\s/
139
145
  parse_param_tag(expression, node, comments[i])
140
146
 
147
+ elsif expression =~ /@auth_scope\s/
148
+ parse_auth_scope_tag(expression, node, comments[i])
149
+
141
150
  elsif expression =~ /@internal\b/
142
151
  # no-op
143
152
  children = find_children(comments[i + 1..], node)
@@ -203,7 +212,8 @@ class Rage::OpenAPI::Parser
203
212
  else
204
213
  parsed = Rage::OpenAPI::Parsers::Response.parse(
205
214
  response_data,
206
- namespace: Rage::OpenAPI.__module_parent(node.controller)
215
+ namespace: Rage::OpenAPI.__module_parent(node.controller),
216
+ root: @root
207
217
  )
208
218
 
209
219
  if parsed
@@ -256,6 +266,67 @@ class Rage::OpenAPI::Parser
256
266
  end
257
267
  end
258
268
 
269
+ def parse_auth_scope_tag(expression, node, comment)
270
+ content = expression.split(" ", 2)[1]
271
+
272
+ unless content
273
+ Rage::OpenAPI.__log_warn "invalid `@auth_scope` tag detected at #{location_msg(comment)}; expected [scope1, scope2] syntax"
274
+ return
275
+ end
276
+
277
+ parsed = YAML.safe_load(content)
278
+
279
+ if parsed.is_a?(Array)
280
+ scheme_name = nil
281
+ scopes = parsed.map(&:to_s)
282
+ elsif parsed.is_a?(String)
283
+ scheme_name = parsed.split(" ", 2)[0]
284
+ scopes_str = content.split(" ", 2)[1]
285
+
286
+ unless scopes_str
287
+ Rage::OpenAPI.__log_warn "invalid `@auth_scope` tag detected at #{location_msg(comment)}; expected [scope1, scope2] syntax"
288
+ return
289
+ end
290
+
291
+ scopes = YAML.safe_load(scopes_str)
292
+
293
+ unless scopes.is_a?(Array)
294
+ Rage::OpenAPI.__log_warn "invalid `@auth_scope` tag detected at #{location_msg(comment)}; expected [scope1, scope2] syntax"
295
+ return
296
+ end
297
+
298
+ scopes = scopes.map(&:to_s)
299
+ else
300
+ Rage::OpenAPI.__log_warn "invalid `@auth_scope` tag detected at #{location_msg(comment)}; expected [scope1, scope2] syntax"
301
+ return
302
+ end
303
+
304
+ if scheme_name.nil?
305
+ auth_entries = node.auth
306
+ if auth_entries.empty?
307
+ Rage::OpenAPI.__log_warn "no auth schemes found for `@auth_scope` shorthand at #{location_msg(comment)}; define an @auth tag on the controller first"
308
+ return
309
+ elsif auth_entries.length > 1
310
+ Rage::OpenAPI.__log_warn "ambiguous `@auth_scope` shorthand at #{location_msg(comment)}; multiple auth schemes found, specify the scheme name explicitly"
311
+ return
312
+ end
313
+ scheme_name = auth_entries[0][:name]
314
+ else
315
+ auth_names = node.auth.map { |e| e[:name] }
316
+ unless auth_names.include?(scheme_name)
317
+ Rage::OpenAPI.__log_warn "unknown scheme `#{scheme_name}` in `@auth_scope` tag at #{location_msg(comment)}; available schemes: #{auth_names.join(", ")}"
318
+ return
319
+ end
320
+ end
321
+
322
+ if node.auth_scopes.key?(scheme_name)
323
+ Rage::OpenAPI.__log_warn "duplicate `@auth_scope` tag for `#{scheme_name}` detected at #{location_msg(comment)}"
324
+ return
325
+ end
326
+
327
+ node.auth_scopes[scheme_name] = scopes
328
+ end
329
+
259
330
  def parse_param_tag(expression, node, comment)
260
331
  param = expression[7..].strip
261
332
 
@@ -3,8 +3,10 @@
3
3
  class Rage::OpenAPI::Parsers::Ext::Alba
4
4
  attr_reader :namespace
5
5
 
6
- def initialize(namespace: Object, **)
6
+ def initialize(namespace: Object, root: Rage::OpenAPI::Nodes::Root.new, **)
7
7
  @namespace = namespace
8
+ @root = root
9
+ @parsing_stack = Set.new
8
10
  end
9
11
 
10
12
  def known_definition?(str)
@@ -15,10 +17,27 @@ class Rage::OpenAPI::Parsers::Ext::Alba
15
17
  end
16
18
 
17
19
  def parse(klass_str)
18
- __parse(klass_str).build_schema
20
+ _, raw_klass_str = Rage::OpenAPI.__try_parse_collection(klass_str)
21
+ visitor = __parse(klass_str)
22
+
23
+ if @root.schema_registry.key?(raw_klass_str)
24
+ clean = { "type" => "object" }
25
+ clean["properties"] = visitor.schema if visitor.schema.any?
26
+ @root.schema_registry[raw_klass_str] = clean
27
+ end
28
+
29
+ visitor.build_schema
19
30
  end
20
31
 
21
32
  def __parse_nested(klass_str)
33
+ is_collection, raw_klass_str = Rage::OpenAPI.__try_parse_collection(klass_str)
34
+
35
+ if @parsing_stack.include?(raw_klass_str)
36
+ @root.schema_registry[raw_klass_str] ||= nil
37
+ ref = { "$ref" => "#/components/schemas/#{raw_klass_str}" }
38
+ return is_collection ? { "type" => "array", "items" => ref } : ref
39
+ end
40
+
22
41
  __parse(klass_str).tap { |visitor|
23
42
  visitor.root_key = visitor.root_key_for_collection = visitor.root_key_proc = visitor.key_transformer = nil
24
43
  }.build_schema
@@ -27,6 +46,13 @@ class Rage::OpenAPI::Parsers::Ext::Alba
27
46
  def __parse(klass_str)
28
47
  is_collection, klass_str = Rage::OpenAPI.__try_parse_collection(klass_str)
29
48
 
49
+ # return an empty visitor if we're already parsing this class;
50
+ # this serves as a recursion guard, specifically for the inheritance logic in `Visitor#visit_class_node`
51
+ if @parsing_stack.include?(klass_str)
52
+ return Visitor.new(self, is_collection)
53
+ end
54
+ @parsing_stack.add(klass_str)
55
+
30
56
  klass = @namespace.const_get(klass_str)
31
57
  source_path, _ = Object.const_source_location(klass.name)
32
58
  ast = Prism.parse_file(source_path)
@@ -34,6 +60,8 @@ class Rage::OpenAPI::Parsers::Ext::Alba
34
60
  visitor = Visitor.new(self, is_collection)
35
61
  ast.value.accept(visitor)
36
62
 
63
+ @parsing_stack.delete(klass_str)
64
+
37
65
  visitor
38
66
  end
39
67
 
@@ -7,9 +7,9 @@ class Rage::OpenAPI::Parsers::Request
7
7
  Rage::OpenAPI::Parsers::Ext::ActiveRecord
8
8
  ]
9
9
 
10
- def self.parse(request_tag, namespace:)
10
+ def self.parse(request_tag, namespace:, root:)
11
11
  parser = AVAILABLE_PARSERS.find do |parser_class|
12
- parser = parser_class.new(namespace:)
12
+ parser = parser_class.new(namespace:, root:)
13
13
  break parser if parser.known_definition?(request_tag)
14
14
  end
15
15
 
@@ -8,9 +8,9 @@ class Rage::OpenAPI::Parsers::Response
8
8
  Rage::OpenAPI::Parsers::YAML
9
9
  ]
10
10
 
11
- def self.parse(response_tag, namespace:)
11
+ def self.parse(response_tag, namespace:, root:)
12
12
  parser = AVAILABLE_PARSERS.find do |parser_class|
13
- parser = parser_class.new(namespace:)
13
+ parser = parser_class.new(namespace:, root:)
14
14
  break parser if parser.known_definition?(response_tag)
15
15
  end
16
16
 
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rage::OpenAPI::Parsers::YAML
4
+ # @private
5
+ class OptionalParam < String
6
+ end
7
+
4
8
  def initialize(**)
5
9
  end
6
10
 
7
11
  def known_definition?(yaml)
8
- object = YAML.safe_load(yaml) rescue nil
12
+ object = process_yaml(yaml) rescue nil
9
13
  !!object && object.is_a?(Enumerable)
10
14
  end
11
15
 
12
16
  def parse(yaml)
13
- __parse(YAML.safe_load(yaml))
17
+ __parse(process_yaml(yaml))
14
18
  end
15
19
 
16
20
  private
@@ -22,6 +26,8 @@ class Rage::OpenAPI::Parsers::YAML
22
26
  spec = { "type" => "object", "properties" => {} }
23
27
 
24
28
  object.each do |key, value|
29
+ key = OptionalParam.new(key[0...-1]) if key.end_with?("?")
30
+
25
31
  spec["properties"][key] = if value.is_a?(Enumerable)
26
32
  __parse(value)
27
33
  else
@@ -29,6 +35,8 @@ class Rage::OpenAPI::Parsers::YAML
29
35
  end
30
36
  end
31
37
 
38
+ spec["required"] = spec["properties"].keys.select { |k| !k.is_a?(OptionalParam) }
39
+
32
40
  elsif object.is_a?(Array) && object.length == 1
33
41
  spec = { "type" => "array", "items" => object[0].is_a?(Enumerable) ? __parse(object[0]) : type_to_spec(object[0]) }
34
42
 
@@ -39,9 +47,23 @@ class Rage::OpenAPI::Parsers::YAML
39
47
  spec
40
48
  end
41
49
 
42
- private
43
-
44
50
  def type_to_spec(type)
45
- Rage::OpenAPI.__type_to_spec(type) || { "type" => "string", "enum" => [type] }
51
+ is_collection, type_str = if type.is_a?(String)
52
+ Rage::OpenAPI.__try_parse_collection(type)
53
+ else
54
+ [false, type]
55
+ end
56
+
57
+ spec = Rage::OpenAPI.__type_to_spec(type_str) || { "type" => "string", "enum" => [type_str] }
58
+
59
+ if is_collection
60
+ { "type" => "array", "items" => spec }
61
+ else
62
+ spec
63
+ end
64
+ end
65
+
66
+ def process_yaml(str)
67
+ YAML.safe_load(str.gsub(/Array<([^>]+)>/, '[\1]'))
46
68
  end
47
69
  end
@@ -14,9 +14,9 @@ class Rage::ParamsParser
14
14
  end
15
15
 
16
16
  request_params = if content_type.start_with?("application/json")
17
- json_parse(env["rack.input"].read)
17
+ json_parse(env["rack.input"].tap { |io| io.rewind }.read)
18
18
  elsif content_type.start_with?("application/x-www-form-urlencoded")
19
- Iodine::Rack::Utils.parse_urlencoded_nested_query(env["rack.input"].read)
19
+ Iodine::Rack::Utils.parse_urlencoded_nested_query(env["rack.input"].tap { |io| io.rewind }.read)
20
20
  elsif content_type.start_with?("multipart/form-data")
21
21
  Iodine::Rack::Utils.parse_multipart(env["rack.input"], content_type)
22
22
  end
@@ -40,7 +40,7 @@ class Rage::PubSub::Adapters::Redis
40
40
 
41
41
  @trimming_strategy = redis_version < Gem::Version.create("6.2.0") ? :maxlen : :minid
42
42
 
43
- Rage::Internal.pick_a_worker do
43
+ Rage::Internal.pick_a_worker(purpose: "redis-pubsub") do
44
44
  puts("INFO: #{Process.pid} is managing Redis subscriptions.") if Rage.logger.info?
45
45
  poll
46
46
  end
@@ -136,6 +136,7 @@ class Rage::PubSub::Adapters::Redis
136
136
 
137
137
  rescue RedisClient::Error => e
138
138
  Rage.logger.error("Subscriber error: #{e.message} (#{e.class})")
139
+ Rage::Errors.report(e)
139
140
  sleep error_backoff_intervals.next
140
141
  rescue SystemCallError => e
141
142
  @stopping ? break : raise(e)
@@ -62,6 +62,8 @@ class Rage::Router::DSL
62
62
  @router = router
63
63
 
64
64
  @default_actions = %i(index create show update destroy)
65
+ @default_actions += %i(new edit) if Rage.config.router.form_actions
66
+
65
67
  @default_match_methods = %i(get post put patch delete head)
66
68
  @scope_opts = %i(module path controller)
67
69
 
@@ -359,6 +361,8 @@ class Rage::Router::DSL
359
361
  get("/:#{_param}", to: "#{resource}#show") if actions.include?(:show)
360
362
  patch("/:#{_param}", to: "#{resource}#update") if actions.include?(:update)
361
363
  put("/:#{_param}", to: "#{resource}#update") if actions.include?(:update)
364
+ get("/new", to: "#{resource}#new") if actions.include?(:new)
365
+ get("/:#{_param}/edit", to: "#{resource}#edit") if actions.include?(:edit)
362
366
  delete("/:#{_param}", to: "#{resource}#destroy") if actions.include?(:destroy)
363
367
 
364
368
  scope(path: ":#{to_singular(resource)}_#{_param}", controller: resource, &block) if block
@@ -379,7 +383,6 @@ class Rage::Router::DSL
379
383
  # # PATCH /photo => photos#update
380
384
  # # PUT /photo => photos#update
381
385
  # # DELETE /photo => photos#destroy
382
- # @note This helper doesn't generate the `new` and `edit` routes.
383
386
  # @note :param is not supported for singular resources.
384
387
  def resource(*_resources, **opts, &block)
385
388
  if _resources.length > 1
@@ -389,7 +392,7 @@ class Rage::Router::DSL
389
392
 
390
393
  _module, _path, _only, _except = opts.values_at(:module, :path, :only, :except)
391
394
 
392
- actions = __filter_actions(%i(create show update destroy), _only, _except)
395
+ actions = __filter_actions(@default_actions - [:index], _only, _except)
393
396
 
394
397
  resource_name = _resources[0].to_s
395
398
  controller_name = to_plural(resource_name)
@@ -398,6 +401,8 @@ class Rage::Router::DSL
398
401
  get("/", to: "#{controller_name}#show") if actions.include?(:show)
399
402
  patch("/", to: "#{controller_name}#update") if actions.include?(:update)
400
403
  put("/", to: "#{controller_name}#update") if actions.include?(:update)
404
+ get("/new", to: "#{controller_name}#new") if actions.include?(:new)
405
+ get("/edit", to: "#{controller_name}#edit") if actions.include?(:edit)
401
406
  delete("/", to: "#{controller_name}#destroy") if actions.include?(:destroy)
402
407
 
403
408
  scope(controller: controller_name, &block) if block
@@ -63,6 +63,7 @@ class Rage::SSE::Application
63
63
  end
64
64
  rescue => e
65
65
  Rage.logger.error("SSE stream failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
66
+ Rage::Errors.report(e)
66
67
  ensure
67
68
  Iodine.task_dec!
68
69
  end
@@ -74,6 +74,7 @@ class Rage::Telemetry::Tracer
74
74
  #{calls_chain}
75
75
  rescue Exception => e
76
76
  Rage.logger.error("Telemetry handler failed with error \#{e}:\\n\#{e.backtrace.join("\\n")}")
77
+ Rage::Errors.report(e)
77
78
  end
78
79
 
79
80
  unless yield_called