rage-rb 1.23.0 → 1.25.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 +38 -0
- data/CONTRIBUTING.md +240 -0
- data/README.md +66 -123
- 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 +166 -29
- data/lib/rage/controller/api.rb +10 -34
- data/lib/rage/cookies.rb +1 -1
- 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/fiber.rb +14 -23
- data/lib/rage/fiber_scheduler.rb +51 -6
- 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 +33 -1
- data/lib/rage/openapi/parser.rb +73 -2
- data/lib/rage/openapi/parsers/ext/alba.rb +30 -2
- data/lib/rage/openapi/parsers/ext/blueprinter.rb +110 -0
- data/lib/rage/openapi/parsers/request.rb +2 -2
- data/lib/rage/openapi/parsers/response.rb +3 -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/constrainer.rb +1 -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 +6 -4
- data/lib/rage/cable/adapters/base.rb +0 -16
- data/lib/rage/cable/adapters/redis.rb +0 -128
data/lib/rage/fiber_scheduler.rb
CHANGED
|
@@ -5,14 +5,20 @@ require "resolv"
|
|
|
5
5
|
class Rage::FiberScheduler
|
|
6
6
|
MAX_READ = 65536
|
|
7
7
|
|
|
8
|
+
# Initialize the scheduler, storing the root fiber and an empty DNS cache.
|
|
8
9
|
def initialize
|
|
9
10
|
@root_fiber = Fiber.current
|
|
10
11
|
@dns_cache = {}
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
# Wait for I/O events on a file descriptor, yielding the fiber until ready or timeout.
|
|
13
15
|
def io_wait(io, events, timeout = nil)
|
|
14
16
|
f = Fiber.current
|
|
15
|
-
|
|
17
|
+
gen = (f.__wait_generation += 1)
|
|
18
|
+
|
|
19
|
+
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil) do |err|
|
|
20
|
+
f.resume(err) if f.alive? && gen == f.__wait_generation
|
|
21
|
+
end
|
|
16
22
|
|
|
17
23
|
err = Fiber.defer(io.fileno)
|
|
18
24
|
if err == false || (err && err < 0)
|
|
@@ -22,6 +28,7 @@ class Rage::FiberScheduler
|
|
|
22
28
|
end
|
|
23
29
|
end
|
|
24
30
|
|
|
31
|
+
# Read data from an I/O object into a buffer, pausing the fiber between reads.
|
|
25
32
|
def io_read(io, buffer, length, offset = 0)
|
|
26
33
|
length_to_read = if length == 0
|
|
27
34
|
buffer.size > MAX_READ ? MAX_READ : buffer.size
|
|
@@ -51,6 +58,7 @@ class Rage::FiberScheduler
|
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
unless ENV["RAGE_DISABLE_IO_WRITE"]
|
|
61
|
+
# Write data from a buffer to an I/O object.
|
|
54
62
|
def io_write(io, buffer, length, offset = 0)
|
|
55
63
|
bytes_to_write = length
|
|
56
64
|
bytes_to_write = buffer.size if length == 0
|
|
@@ -61,6 +69,7 @@ class Rage::FiberScheduler
|
|
|
61
69
|
end
|
|
62
70
|
end
|
|
63
71
|
|
|
72
|
+
# Pause the current fiber for the specified duration.
|
|
64
73
|
def kernel_sleep(duration = nil)
|
|
65
74
|
block(nil, duration || 0)
|
|
66
75
|
Fiber.pause if duration.nil? || duration < 1
|
|
@@ -80,6 +89,7 @@ class Rage::FiberScheduler
|
|
|
80
89
|
# result
|
|
81
90
|
# end
|
|
82
91
|
|
|
92
|
+
# Resolve a hostname to IP addresses, caching results for 60 seconds.
|
|
83
93
|
def address_resolve(hostname)
|
|
84
94
|
@dns_cache[hostname] ||= begin
|
|
85
95
|
::Iodine.run_after(60_000) do
|
|
@@ -90,14 +100,18 @@ class Rage::FiberScheduler
|
|
|
90
100
|
end
|
|
91
101
|
end
|
|
92
102
|
|
|
103
|
+
# Block the current fiber until unblocked or timeout.
|
|
93
104
|
def block(_blocker, timeout = nil)
|
|
94
|
-
f, fulfilled
|
|
105
|
+
f, fulfilled = Fiber.current, false
|
|
106
|
+
|
|
107
|
+
gen = (f.__wait_generation += 1)
|
|
108
|
+
channel = f.__block_channel = "block:#{f.object_id}:#{gen}"
|
|
95
109
|
|
|
96
110
|
resume_fiber_block = proc do
|
|
97
111
|
unless fulfilled
|
|
98
112
|
fulfilled = true
|
|
99
113
|
::Iodine.defer { ::Iodine.unsubscribe(channel) }
|
|
100
|
-
f.resume if f.alive?
|
|
114
|
+
f.resume if f.alive? && gen == f.__wait_generation
|
|
101
115
|
end
|
|
102
116
|
end
|
|
103
117
|
|
|
@@ -109,10 +123,38 @@ class Rage::FiberScheduler
|
|
|
109
123
|
Fiber.yield
|
|
110
124
|
end
|
|
111
125
|
|
|
126
|
+
# Unblock a fiber by publishing to its block channel.
|
|
112
127
|
def unblock(_blocker, fiber)
|
|
113
|
-
::Iodine.publish(fiber.__block_channel, "", Iodine::PubSub::PROCESS)
|
|
128
|
+
::Iodine.publish(fiber.__block_channel, "", Iodine::PubSub::PROCESS) if fiber.__block_channel
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Interrupt a fiber by incrementing its generation and raising an exception.
|
|
132
|
+
def fiber_interrupt(fiber, exception)
|
|
133
|
+
fiber.__wait_generation += 1
|
|
134
|
+
fiber.raise(exception)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if defined?(Iodine::WorkerPool)
|
|
138
|
+
module BlockingOperationWait
|
|
139
|
+
# Offload a native call to the worker pool, yielding until complete.
|
|
140
|
+
def blocking_operation_wait(work)
|
|
141
|
+
f = Fiber.current
|
|
142
|
+
gen = (f.__wait_generation += 1)
|
|
143
|
+
|
|
144
|
+
worker_pool.enqueue(work) do
|
|
145
|
+
f.resume if f.alive? && gen == f.__wait_generation
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Fiber.yield
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private def worker_pool
|
|
152
|
+
@worker_pool ||= Iodine::WorkerPool.new(Rage.config.blocking_operation_pool.size)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
114
155
|
end
|
|
115
156
|
|
|
157
|
+
# Create and schedule a new non-blocking fiber, handling request and user-spawned fibers differently.
|
|
116
158
|
def fiber(&block)
|
|
117
159
|
parent = Fiber.current
|
|
118
160
|
|
|
@@ -131,19 +173,22 @@ class Rage::FiberScheduler
|
|
|
131
173
|
Fiber.current.__set_result(block.call)
|
|
132
174
|
end
|
|
133
175
|
# send a message for `Fiber.await` to work
|
|
134
|
-
Iodine.publish(parent.__await_channel, "", Iodine::PubSub::PROCESS) if parent.
|
|
176
|
+
Iodine.publish(parent.__await_channel, "", Iodine::PubSub::PROCESS) if parent.__await_channel
|
|
135
177
|
rescue Exception => e
|
|
136
178
|
Fiber.current.__set_err(e)
|
|
137
|
-
Iodine.publish(parent.__await_channel, Fiber::AWAIT_ERROR_MESSAGE, Iodine::PubSub::PROCESS) if parent.
|
|
179
|
+
Iodine.publish(parent.__await_channel, Fiber::AWAIT_ERROR_MESSAGE, Iodine::PubSub::PROCESS) if parent.__await_channel
|
|
138
180
|
end
|
|
139
181
|
end
|
|
140
182
|
|
|
183
|
+
fiber.__wait_generation = 0
|
|
141
184
|
fiber.resume
|
|
142
185
|
|
|
143
186
|
fiber
|
|
144
187
|
end
|
|
145
188
|
|
|
189
|
+
# Clean up by closing the worker pool and Iodine scheduler.
|
|
146
190
|
def close
|
|
191
|
+
@worker_pool&.close
|
|
147
192
|
::Iodine::Scheduler.close
|
|
148
193
|
end
|
|
149
194
|
end
|
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,13 +120,44 @@ 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]
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# @private
|
|
131
|
+
# @return [Array<Boolean, String, Hash>] a tuple of (is_collection, serializer, args)
|
|
132
|
+
def self.__parse_serializer_args(str)
|
|
133
|
+
is_collection, inner = __try_parse_collection(str)
|
|
134
|
+
|
|
135
|
+
if is_collection
|
|
136
|
+
# discard is_collection since we already know this is a collection from the outer call
|
|
137
|
+
_, clean_inner, args = __parse_serializer_args(inner)
|
|
138
|
+
if args.any?
|
|
139
|
+
[is_collection, clean_inner, args]
|
|
140
|
+
else
|
|
141
|
+
[is_collection, clean_inner, {}]
|
|
142
|
+
end
|
|
143
|
+
elsif str =~ /^([\w:]+)\(([^)]+)\)$/
|
|
144
|
+
[is_collection, $1, __parse_keywords($2)]
|
|
145
|
+
else
|
|
146
|
+
[is_collection, str, {}]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @private
|
|
151
|
+
def self.__parse_keywords(str)
|
|
152
|
+
return {} if str.nil? || str.empty?
|
|
153
|
+
|
|
154
|
+
str.split(",").each_with_object({}) do |part, hash|
|
|
155
|
+
option = YAML.load(part)
|
|
156
|
+
return nil unless option.is_a?(Hash)
|
|
157
|
+
hash.merge!(option.transform_keys!(&:to_sym))
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
130
161
|
# @private
|
|
131
162
|
def self.__module_parent(klass)
|
|
132
163
|
klass.name =~ /::[^:]+\z/ ? Object.const_get($`) : Object
|
|
@@ -191,6 +222,7 @@ require_relative "nodes/root"
|
|
|
191
222
|
require_relative "nodes/parent"
|
|
192
223
|
require_relative "nodes/method"
|
|
193
224
|
require_relative "parsers/ext/alba"
|
|
225
|
+
require_relative "parsers/ext/blueprinter"
|
|
194
226
|
require_relative "parsers/ext/active_record"
|
|
195
227
|
require_relative "parsers/yaml"
|
|
196
228
|
require_relative "parsers/shared_reference"
|
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
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Rage::OpenAPI::Parsers::Ext::Blueprinter
|
|
4
|
+
def initialize(namespace: Object, root: Rage::OpenAPI::Nodes::Root.new, **)
|
|
5
|
+
@namespace = namespace
|
|
6
|
+
@root = root
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def known_definition?(str)
|
|
10
|
+
_, str, _ = Rage::OpenAPI.__parse_serializer_args(str)
|
|
11
|
+
defined?(Blueprinter::Base) && @namespace.const_get(str).ancestors.include?(Blueprinter::Base)
|
|
12
|
+
rescue NameError
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse(klass_str)
|
|
17
|
+
visitor = __parse(klass_str)
|
|
18
|
+
visitor.build_schema
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def __parse(klass_str)
|
|
22
|
+
is_collection, klass_str, _ = Rage::OpenAPI.__parse_serializer_args(klass_str)
|
|
23
|
+
|
|
24
|
+
klass = @namespace.const_get(klass_str)
|
|
25
|
+
source_path, _ = Object.const_source_location(klass.name)
|
|
26
|
+
ast = Prism.parse_file(source_path)
|
|
27
|
+
|
|
28
|
+
visitor = Visitor.new(self, is_collection)
|
|
29
|
+
ast.value.accept(visitor)
|
|
30
|
+
|
|
31
|
+
visitor
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class VisitorContext
|
|
35
|
+
attr_accessor :symbols, :keywords, :strings
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@symbols = []
|
|
39
|
+
@strings = []
|
|
40
|
+
@keywords = {}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Visitor < Prism::Visitor
|
|
45
|
+
attr_accessor :schema
|
|
46
|
+
|
|
47
|
+
def initialize(parser, is_collection)
|
|
48
|
+
@parser = parser
|
|
49
|
+
@is_collection = is_collection
|
|
50
|
+
|
|
51
|
+
@context = nil
|
|
52
|
+
@schema = {}
|
|
53
|
+
@segment = @schema
|
|
54
|
+
@identifier = {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_schema
|
|
58
|
+
result = { "type" => "object" }
|
|
59
|
+
|
|
60
|
+
properties = {}
|
|
61
|
+
properties.merge!(@identifier)
|
|
62
|
+
properties.merge!(@schema.sort.to_h)
|
|
63
|
+
|
|
64
|
+
result["properties"] = properties if properties.any?
|
|
65
|
+
result = { "type" => "array", "items" => result } if @is_collection
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def visit_call_node(node)
|
|
70
|
+
case node.name
|
|
71
|
+
when :identifier
|
|
72
|
+
context = with_context { visit(node.arguments) }
|
|
73
|
+
@identifier[context.symbols.first] = { "type" => "string" }
|
|
74
|
+
|
|
75
|
+
when :fields, :field
|
|
76
|
+
context = with_context { visit(node.arguments) }
|
|
77
|
+
|
|
78
|
+
if context.keywords["name"]
|
|
79
|
+
@segment[context.keywords["name"]] = { "type" => "string" }
|
|
80
|
+
elsif node.block
|
|
81
|
+
@segment[context.symbols.first] = { "type" => "string" } if context.symbols.first
|
|
82
|
+
@segment[context.strings.first] = { "type" => "string" } if context.strings.first
|
|
83
|
+
else
|
|
84
|
+
context.symbols.each { |symbol| @segment[symbol] = { "type" => "string" } }
|
|
85
|
+
context.strings.each { |string| @segment[string] = { "type" => "string" } }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def visit_assoc_node(node)
|
|
91
|
+
@context.keywords[node.key.value] = node.value.unescaped
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def visit_symbol_node(node)
|
|
95
|
+
@context.symbols << node.value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def visit_string_node(node)
|
|
99
|
+
@context.strings << node.unescaped
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def with_context
|
|
105
|
+
@context = VisitorContext.new
|
|
106
|
+
yield
|
|
107
|
+
@context
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -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
|
|
|
@@ -5,12 +5,13 @@ class Rage::OpenAPI::Parsers::Response
|
|
|
5
5
|
Rage::OpenAPI::Parsers::SharedReference,
|
|
6
6
|
Rage::OpenAPI::Parsers::Ext::ActiveRecord,
|
|
7
7
|
Rage::OpenAPI::Parsers::Ext::Alba,
|
|
8
|
+
Rage::OpenAPI::Parsers::Ext::Blueprinter,
|
|
8
9
|
Rage::OpenAPI::Parsers::YAML
|
|
9
10
|
]
|
|
10
11
|
|
|
11
|
-
def self.parse(response_tag, namespace:)
|
|
12
|
+
def self.parse(response_tag, namespace:, root:)
|
|
12
13
|
parser = AVAILABLE_PARSERS.find do |parser_class|
|
|
13
|
-
parser = parser_class.new(namespace:)
|
|
14
|
+
parser = parser_class.new(namespace:, root:)
|
|
14
15
|
break parser if parser.known_definition?(response_tag)
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -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
|