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
|
@@ -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
|
data/lib/rage/deferred/task.rb
CHANGED
|
@@ -31,15 +31,16 @@
|
|
|
31
31
|
# ```
|
|
32
32
|
#
|
|
33
33
|
module Rage::Deferred::Task
|
|
34
|
-
MAX_ATTEMPTS =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
worker_lock = File.new(lock_path)
|
|
76
|
+
lock_file = File.open(lock_path, File::CREAT | File::WRONLY)
|
|
75
77
|
|
|
76
|
-
if
|
|
77
|
-
|
|
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
|
data/lib/rage/openapi/builder.rb
CHANGED
|
@@ -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>]
|
data/lib/rage/openapi/openapi.rb
CHANGED
|
@@ -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]
|
data/lib/rage/openapi/parser.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
data/lib/rage/params_parser.rb
CHANGED
|
@@ -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)
|
data/lib/rage/router/dsl.rb
CHANGED
|
@@ -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(
|
|
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
|
data/lib/rage/sse/application.rb
CHANGED