rage-rb 1.11.0 → 1.13.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 +25 -0
- data/Gemfile +2 -0
- data/README.md +1 -2
- data/lib/rage/all.rb +1 -0
- data/lib/rage/application.rb +3 -3
- data/lib/rage/cable/adapters/base.rb +16 -0
- data/lib/rage/cable/adapters/redis.rb +128 -0
- data/lib/rage/cable/cable.rb +37 -21
- data/lib/rage/cable/channel.rb +1 -1
- data/lib/rage/cable/protocol/actioncable_v1_json.rb +36 -14
- data/lib/rage/cli.rb +1 -1
- data/lib/rage/configuration.rb +27 -0
- data/lib/rage/controller/api.rb +102 -26
- data/lib/rage/cookies.rb +1 -1
- data/lib/rage/fiber_scheduler.rb +2 -2
- data/lib/rage/middleware/request_id.rb +42 -0
- data/lib/rage/openapi/builder.rb +1 -0
- data/lib/rage/openapi/collector.rb +1 -0
- data/lib/rage/openapi/converter.rb +5 -2
- data/lib/rage/openapi/nodes/method.rb +3 -0
- data/lib/rage/openapi/nodes/parent.rb +4 -1
- data/lib/rage/openapi/nodes/root.rb +7 -0
- data/lib/rage/openapi/openapi.rb +1 -1
- data/lib/rage/openapi/parser.rb +54 -36
- data/lib/rage/openapi/parsers/ext/alba.rb +4 -0
- data/lib/rage/request.rb +8 -0
- data/lib/rage/templates/Gemfile +1 -1
- data/lib/rage/version.rb +1 -1
- data/rage.gemspec +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2f73c4770587ebabc7d8fa0c4793d53742196b928f21cc362799cb068849982
|
4
|
+
data.tar.gz: aecaa4ae2848bc1b15a38da29d3030d718d74697faaf8525e60e82d10b24396c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb31ef275be5dc22322af8c9d563befc000ad2d15bb22454f2d3664736e6e7881ccdac365e6687fc123c23da1a06ee3cd4d63d08b8b5123c1fee67ebb97fbc57
|
7
|
+
data.tar.gz: 7debadefde85efa180f73e963b252cb1065a6b6f1bf4b8ca6e64e19d82a0ed838d3b321ddf9222006204ab59c3d90213e1a4eb6e1ff04d1d45572c1e166b3d77
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,30 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.13.0] - 2025-02-12
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- [CLI] Support the PORT ENV variable by [@TheBlackArroVV](https://github.com/TheBlackArroVV) (#124).
|
8
|
+
- Add the `RequestId` middleware (#127).
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
|
12
|
+
- Correctly process persistent HTTP connections (#128).
|
13
|
+
- [OpenAPI] Ignore empty comments (#126).
|
14
|
+
- [Cable] Improve the time to connect (#129).
|
15
|
+
|
16
|
+
## [1.12.0] - 2025-01-21
|
17
|
+
|
18
|
+
### Added
|
19
|
+
|
20
|
+
- Add Redis adapter (#114).
|
21
|
+
- Add global response tags (#110).
|
22
|
+
- Implement around_action callbacks (#107).
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
|
26
|
+
- Support date types in Alba serializers (#112).
|
27
|
+
|
3
28
|
## [1.11.0] - 2024-12-18
|
4
29
|
|
5
30
|
### Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -6,8 +6,7 @@
|
|
6
6
|

|
7
7
|

|
8
8
|
|
9
|
-
|
10
|
-
Inspired by [Deno](https://deno.com) and built on top of [Iodine](https://github.com/rage-rb/iodine), this is a Ruby web framework that is based on the following design principles:
|
9
|
+
Rage is a high-performance framework compatible with Rails, featuring [WebSocket](https://github.com/rage-rb/rage/wiki/WebSockets-guide) support and automatic generation of [OpenAPI](https://github.com/rage-rb/rage/wiki/OpenAPI-Guide) documentation for your APIs. The framework is built on top of [Iodine](https://github.com/rage-rb/iodine) and is based on the following design principles:
|
11
10
|
|
12
11
|
* **Rails compatible API** - Rails' API is clean, straightforward, and simply makes sense. It was one of the reasons why Rails was so successful in the past.
|
13
12
|
|
data/lib/rage/all.rb
CHANGED
@@ -30,6 +30,7 @@ require_relative "middleware/origin_validator"
|
|
30
30
|
require_relative "middleware/fiber_wrapper"
|
31
31
|
require_relative "middleware/cors"
|
32
32
|
require_relative "middleware/reloader"
|
33
|
+
require_relative "middleware/request_id"
|
33
34
|
|
34
35
|
if defined?(Sidekiq)
|
35
36
|
require_relative "sidekiq_session"
|
data/lib/rage/application.rb
CHANGED
@@ -7,7 +7,7 @@ class Rage::Application
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def call(env)
|
10
|
-
init_logger
|
10
|
+
init_logger(env)
|
11
11
|
|
12
12
|
handler = @router.lookup(env)
|
13
13
|
|
@@ -33,9 +33,9 @@ class Rage::Application
|
|
33
33
|
DEFAULT_LOG_CONTEXT = {}.freeze
|
34
34
|
private_constant :DEFAULT_LOG_CONTEXT
|
35
35
|
|
36
|
-
def init_logger
|
36
|
+
def init_logger(env)
|
37
37
|
Thread.current[:rage_logger] = {
|
38
|
-
tags: [Iodine::Rack::Utils.gen_request_tag],
|
38
|
+
tags: [(env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag)],
|
39
39
|
context: DEFAULT_LOG_CONTEXT,
|
40
40
|
request_start: Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
41
41
|
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Cable::Adapters::Base
|
4
|
+
def pick_a_worker(&block)
|
5
|
+
_lock, lock_path = Tempfile.new.yield_self { |file| [file, file.path] }
|
6
|
+
|
7
|
+
Iodine.on_state(:on_start) do
|
8
|
+
if File.new(lock_path).flock(File::LOCK_EX | File::LOCK_NB)
|
9
|
+
if Rage.logger.debug?
|
10
|
+
puts "INFO: #{Process.pid} is managing #{self.class.name.split("::").last} subscriptions."
|
11
|
+
end
|
12
|
+
block.call
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
if !defined?(RedisClient)
|
6
|
+
fail <<~ERR
|
7
|
+
|
8
|
+
Redis adapter depends on the `redis-client` gem. Add the following line to your Gemfile:
|
9
|
+
gem "redis-client"
|
10
|
+
|
11
|
+
ERR
|
12
|
+
end
|
13
|
+
|
14
|
+
class Rage::Cable::Adapters::Redis < Rage::Cable::Adapters::Base
|
15
|
+
REDIS_STREAM_NAME = "rage:cable:messages"
|
16
|
+
DEFAULT_REDIS_OPTIONS = { reconnect_attempts: [0.05, 0.1, 0.5] }
|
17
|
+
REDIS_MIN_VERSION_SUPPORTED = Gem::Version.create(6)
|
18
|
+
|
19
|
+
def initialize(config)
|
20
|
+
@redis_stream = if (prefix = config.delete(:channel_prefix))
|
21
|
+
"#{prefix}:#{REDIS_STREAM_NAME}"
|
22
|
+
else
|
23
|
+
REDIS_STREAM_NAME
|
24
|
+
end
|
25
|
+
|
26
|
+
@redis_config = RedisClient.config(**DEFAULT_REDIS_OPTIONS.merge(config))
|
27
|
+
@server_uuid = SecureRandom.uuid
|
28
|
+
|
29
|
+
redis_version = get_redis_version
|
30
|
+
if redis_version < REDIS_MIN_VERSION_SUPPORTED
|
31
|
+
raise "Redis adapter only supports Redis 6+. Detected Redis version: #{redis_version}."
|
32
|
+
end
|
33
|
+
|
34
|
+
@trimming_strategy = redis_version < Gem::Version.create("6.2.0") ? :maxlen : :minid
|
35
|
+
|
36
|
+
pick_a_worker { poll }
|
37
|
+
end
|
38
|
+
|
39
|
+
def publish(stream_name, data)
|
40
|
+
message_uuid = SecureRandom.uuid
|
41
|
+
|
42
|
+
publish_redis.call(
|
43
|
+
"XADD",
|
44
|
+
@redis_stream,
|
45
|
+
trimming_method, "~", trimming_value,
|
46
|
+
"*",
|
47
|
+
"1", stream_name,
|
48
|
+
"2", data.to_json,
|
49
|
+
"3", @server_uuid,
|
50
|
+
"4", message_uuid
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def publish_redis
|
57
|
+
@publish_redis ||= @redis_config.new_client
|
58
|
+
end
|
59
|
+
|
60
|
+
def trimming_method
|
61
|
+
@trimming_strategy == :maxlen ? "MAXLEN" : "MINID"
|
62
|
+
end
|
63
|
+
|
64
|
+
def trimming_value
|
65
|
+
@trimming_strategy == :maxlen ? "10000" : ((Time.now.to_f - 5 * 60) * 1000).to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_redis_version
|
69
|
+
service_redis = @redis_config.new_client
|
70
|
+
version = service_redis.call("INFO").match(/redis_version:([[:graph:]]+)/)[1]
|
71
|
+
|
72
|
+
Gem::Version.create(version)
|
73
|
+
|
74
|
+
rescue RedisClient::Error => e
|
75
|
+
puts "FATAL: Couldn't connect to Redis - all broadcasts will be limited to the current server."
|
76
|
+
puts e.backtrace.join("\n")
|
77
|
+
REDIS_MIN_VERSION_SUPPORTED
|
78
|
+
|
79
|
+
ensure
|
80
|
+
service_redis.close
|
81
|
+
end
|
82
|
+
|
83
|
+
def error_backoff_intervals
|
84
|
+
@error_backoff_intervals ||= Enumerator.new do |y|
|
85
|
+
y << 0.2 << 0.5 << 1 << 2 << 5
|
86
|
+
loop { y << 10 }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def poll
|
91
|
+
unless Fiber.scheduler
|
92
|
+
Fiber.set_scheduler(Rage::FiberScheduler.new)
|
93
|
+
end
|
94
|
+
|
95
|
+
Iodine.on_state(:start_shutdown) do
|
96
|
+
@stopping = true
|
97
|
+
end
|
98
|
+
|
99
|
+
Fiber.schedule do
|
100
|
+
read_redis = @redis_config.new_client
|
101
|
+
last_id = (Time.now.to_f * 1000).to_i
|
102
|
+
last_message_uuid = nil
|
103
|
+
|
104
|
+
loop do
|
105
|
+
data = read_redis.blocking_call(5, "XREAD", "COUNT", "100", "BLOCK", "5000", "STREAMS", @redis_stream, last_id)
|
106
|
+
|
107
|
+
if data
|
108
|
+
data[@redis_stream].each do |id, (_, stream_name, _, serialized_data, _, broadcaster_uuid, _, message_uuid)|
|
109
|
+
if broadcaster_uuid != @server_uuid && message_uuid != last_message_uuid
|
110
|
+
Rage.config.cable.protocol.broadcast(stream_name, JSON.parse(serialized_data))
|
111
|
+
end
|
112
|
+
|
113
|
+
last_id = id
|
114
|
+
last_message_uuid = message_uuid
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
rescue RedisClient::Error => e
|
119
|
+
Rage.logger.error("Subscriber error: #{e.message} (#{e.class})")
|
120
|
+
sleep error_backoff_intervals.next
|
121
|
+
rescue => e
|
122
|
+
@stopping ? break : raise(e)
|
123
|
+
else
|
124
|
+
error_backoff_intervals.rewind
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/rage/cable/cable.rb
CHANGED
@@ -8,11 +8,11 @@ module Rage::Cable
|
|
8
8
|
# run Rage.cable.application
|
9
9
|
# end
|
10
10
|
def self.application
|
11
|
-
|
12
|
-
|
11
|
+
# explicitly initialize the adapter
|
12
|
+
__adapter
|
13
13
|
|
14
|
-
handler = __build_handler(
|
15
|
-
accept_response = [0,
|
14
|
+
handler = __build_handler(__protocol)
|
15
|
+
accept_response = [0, __protocol.protocol_definition, []]
|
16
16
|
|
17
17
|
application = ->(env) do
|
18
18
|
if env["rack.upgrade?"] == :websocket
|
@@ -31,6 +31,15 @@ module Rage::Cable
|
|
31
31
|
@__router ||= Router.new
|
32
32
|
end
|
33
33
|
|
34
|
+
# @private
|
35
|
+
def self.__protocol
|
36
|
+
@__protocol ||= Rage.config.cable.protocol.tap { |protocol| protocol.init(__router) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.__adapter
|
40
|
+
@__adapter ||= Rage.config.cable.adapter
|
41
|
+
end
|
42
|
+
|
34
43
|
# @private
|
35
44
|
def self.__build_handler(protocol)
|
36
45
|
klass = Class.new do
|
@@ -42,33 +51,22 @@ module Rage::Cable
|
|
42
51
|
end
|
43
52
|
|
44
53
|
@protocol = protocol
|
54
|
+
@default_log_context = {}.freeze
|
45
55
|
end
|
46
56
|
|
47
57
|
def on_open(connection)
|
48
|
-
|
49
|
-
|
50
|
-
rescue => e
|
51
|
-
log_error(e)
|
52
|
-
end
|
58
|
+
connection.env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag
|
59
|
+
schedule_fiber(connection) { @protocol.on_open(connection) }
|
53
60
|
end
|
54
61
|
|
55
62
|
def on_message(connection, data)
|
56
|
-
|
57
|
-
@protocol.on_message(connection, data)
|
58
|
-
rescue => e
|
59
|
-
log_error(e)
|
60
|
-
end
|
63
|
+
schedule_fiber(connection) { @protocol.on_message(connection, data) }
|
61
64
|
end
|
62
65
|
|
63
66
|
if protocol.respond_to?(:on_close)
|
64
67
|
def on_close(connection)
|
65
68
|
return unless ::Iodine.running?
|
66
|
-
|
67
|
-
Fiber.schedule do
|
68
|
-
@protocol.on_close(connection)
|
69
|
-
rescue => e
|
70
|
-
log_error(e)
|
71
|
-
end
|
69
|
+
schedule_fiber(connection) { @protocol.on_close(connection) }
|
72
70
|
end
|
73
71
|
end
|
74
72
|
|
@@ -82,6 +80,15 @@ module Rage::Cable
|
|
82
80
|
|
83
81
|
private
|
84
82
|
|
83
|
+
def schedule_fiber(connection)
|
84
|
+
Fiber.schedule do
|
85
|
+
Thread.current[:rage_logger] = { tags: [connection.env["rage.request_id"]], context: @default_log_context }
|
86
|
+
yield
|
87
|
+
rescue => e
|
88
|
+
log_error(e)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
85
92
|
def log_error(e)
|
86
93
|
Rage.logger.error("Unhandled exception has occured - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
87
94
|
end
|
@@ -94,10 +101,14 @@ module Rage::Cable
|
|
94
101
|
#
|
95
102
|
# @param stream [String] the name of the stream
|
96
103
|
# @param data [Object] the object to send to the clients. This will later be encoded according to the protocol used.
|
104
|
+
# @return [true]
|
97
105
|
# @example
|
98
106
|
# Rage.cable.broadcast("chat", { message: "A new member has joined!" })
|
99
107
|
def self.broadcast(stream, data)
|
100
|
-
|
108
|
+
__protocol.broadcast(stream, data)
|
109
|
+
__adapter&.publish(stream, data)
|
110
|
+
|
111
|
+
true
|
101
112
|
end
|
102
113
|
|
103
114
|
# @!parse [ruby]
|
@@ -120,6 +131,11 @@ module Rage::Cable
|
|
120
131
|
# end
|
121
132
|
# end
|
122
133
|
|
134
|
+
module Adapters
|
135
|
+
autoload :Base, "rage/cable/adapters/base"
|
136
|
+
autoload :Redis, "rage/cable/adapters/redis"
|
137
|
+
end
|
138
|
+
|
123
139
|
module Protocol
|
124
140
|
end
|
125
141
|
end
|
data/lib/rage/cable/channel.rb
CHANGED
@@ -418,7 +418,7 @@ class Rage::Cable::Channel
|
|
418
418
|
# broadcast("notifications", { message: "A new member has joined!" })
|
419
419
|
# end
|
420
420
|
def broadcast(stream, data)
|
421
|
-
Rage.
|
421
|
+
Rage.cable.broadcast(stream, data)
|
422
422
|
end
|
423
423
|
|
424
424
|
# Transmit data to the current client.
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "zlib"
|
4
|
+
require "set"
|
5
|
+
|
3
6
|
##
|
4
7
|
# A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
|
5
8
|
# The class that defines a protocol should respond to the following methods:
|
@@ -17,6 +20,9 @@
|
|
17
20
|
# * `on_shutdown`
|
18
21
|
# * `on_close`
|
19
22
|
#
|
23
|
+
# It is likely that all logic around `@subscription_identifiers` has nothing to do with the protocol itself and
|
24
|
+
# should be extracted into another class. We'll refactor this once we start working on a new protocol.
|
25
|
+
#
|
20
26
|
class Rage::Cable::Protocol::ActioncableV1Json
|
21
27
|
module TYPE
|
22
28
|
WELCOME = "welcome"
|
@@ -55,14 +61,30 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
55
61
|
def self.init(router)
|
56
62
|
@router = router
|
57
63
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
64
|
+
Iodine.on_state(:on_start) do
|
65
|
+
ping_counter = Time.now.to_i
|
66
|
+
|
67
|
+
Iodine.run_every(3000) do
|
68
|
+
ping_counter += 1
|
69
|
+
Iodine.publish("cable:ping", { type: TYPE::PING, message: ping_counter }.to_json, Iodine::PubSub::PROCESS)
|
70
|
+
end
|
62
71
|
end
|
63
72
|
|
64
|
-
# Hash<String(stream name) =>
|
65
|
-
@subscription_identifiers = Hash.new { |hash, key| hash[key] =
|
73
|
+
# Hash<String(stream name) => Set<Hash>(subscription params)>
|
74
|
+
@subscription_identifiers = Hash.new { |hash, key| hash[key] = Set.new }
|
75
|
+
|
76
|
+
# this is a fallback to synchronize subscription identifiers across different worker processes;
|
77
|
+
# we expect connections to be distributed among all workers, so this code will almost never be called;
|
78
|
+
# we also synchronize subscriptions with the master process so that the forks that are spun up instead
|
79
|
+
# of the crashed ones also had access to the identifiers;
|
80
|
+
Iodine.subscribe("cable:synchronize") do |_, subscription_msg|
|
81
|
+
stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
|
82
|
+
@subscription_identifiers[stream_name] << params
|
83
|
+
end
|
84
|
+
|
85
|
+
Iodine.on_state(:on_finish) do
|
86
|
+
Iodine.unsubscribe("cable:synchronize")
|
87
|
+
end
|
66
88
|
end
|
67
89
|
|
68
90
|
# The method is called any time a new WebSocket connection is established.
|
@@ -147,8 +169,12 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
147
169
|
# @param name [String] the stream name
|
148
170
|
# @param params [Hash] parameters associated with the client
|
149
171
|
def self.subscribe(connection, name, params)
|
150
|
-
connection.subscribe("cable:#{name}:#{params.
|
151
|
-
|
172
|
+
connection.subscribe("cable:#{name}:#{Zlib.crc32(params.to_s)}")
|
173
|
+
|
174
|
+
unless @subscription_identifiers[name].include?(params)
|
175
|
+
@subscription_identifiers[name] << params
|
176
|
+
::Iodine.publish("cable:synchronize", [name, params].to_json)
|
177
|
+
end
|
152
178
|
end
|
153
179
|
|
154
180
|
# Broadcast data to all clients connected to a stream.
|
@@ -156,12 +182,8 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
156
182
|
# @param name [String] the stream name
|
157
183
|
# @param data [Object] the data to send
|
158
184
|
def self.broadcast(name, data)
|
159
|
-
|
160
|
-
|
161
|
-
while i < identifiers.length
|
162
|
-
params = identifiers[i]
|
163
|
-
::Iodine.publish("cable:#{name}:#{params.hash}", serialize(params, data))
|
164
|
-
i += 1
|
185
|
+
@subscription_identifiers[name].each do |params|
|
186
|
+
::Iodine.publish("cable:#{name}:#{Zlib.crc32(params.to_s)}", serialize(params, data))
|
165
187
|
end
|
166
188
|
end
|
167
189
|
end
|
data/lib/rage/cli.rb
CHANGED
@@ -71,7 +71,7 @@ module Rage
|
|
71
71
|
|
72
72
|
server_options = { service: :http, handler: app }
|
73
73
|
|
74
|
-
server_options[:port] = options[:port] || Rage.config.server.port
|
74
|
+
server_options[:port] = options[:port] || ENV["PORT"] || Rage.config.server.port
|
75
75
|
server_options[:address] = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost")
|
76
76
|
server_options[:timeout] = Rage.config.server.timeout
|
77
77
|
server_options[:max_clients] = Rage.config.server.max_clients
|
data/lib/rage/configuration.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "yaml"
|
4
|
+
require "erb"
|
5
|
+
|
3
6
|
##
|
4
7
|
# `Rage.configure` can be used to adjust the behavior of your Rage application:
|
5
8
|
#
|
@@ -277,6 +280,30 @@ class Rage::Configuration
|
|
277
280
|
end
|
278
281
|
end
|
279
282
|
end
|
283
|
+
|
284
|
+
def config
|
285
|
+
@config ||= begin
|
286
|
+
config_file = Rage.root.join("config/cable.yml")
|
287
|
+
|
288
|
+
if config_file.exist?
|
289
|
+
yaml = ERB.new(config_file.read).result
|
290
|
+
YAML.safe_load(yaml, aliases: true, symbolize_names: true)[Rage.env.to_sym] || {}
|
291
|
+
else
|
292
|
+
{}
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def adapter_config
|
298
|
+
config.except(:adapter)
|
299
|
+
end
|
300
|
+
|
301
|
+
def adapter
|
302
|
+
case config[:adapter]
|
303
|
+
when "redis"
|
304
|
+
Rage::Cable::Adapters::Redis.new(adapter_config)
|
305
|
+
end
|
306
|
+
end
|
280
307
|
end
|
281
308
|
|
282
309
|
class PublicFileServer
|
data/lib/rage/controller/api.rb
CHANGED
@@ -11,6 +11,8 @@ class RageController::API
|
|
11
11
|
def __register_action(action)
|
12
12
|
raise Rage::Errors::RouterError, "The action '#{action}' could not be found for #{self}" unless method_defined?(action)
|
13
13
|
|
14
|
+
around_actions_total = 0
|
15
|
+
|
14
16
|
before_actions_chunk = if @__before_actions
|
15
17
|
lines = __before_actions_for(action).map do |h|
|
16
18
|
condition = if h[:if] && h[:unless]
|
@@ -21,10 +23,30 @@ class RageController::API
|
|
21
23
|
"unless #{h[:unless]}"
|
22
24
|
end
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
if h[:around]
|
27
|
+
around_actions_total += 1
|
28
|
+
|
29
|
+
if condition
|
30
|
+
<<~RUBY
|
31
|
+
__should_apply_around_action = #{condition}
|
32
|
+
!@__before_callback_rendered
|
33
|
+
end
|
34
|
+
#{h[:wrapper]}(__should_apply_around_action) do
|
35
|
+
RUBY
|
36
|
+
else
|
37
|
+
<<~RUBY
|
38
|
+
__should_apply_around_action = !@__before_callback_rendered
|
39
|
+
#{h[:wrapper]}(__should_apply_around_action) do
|
40
|
+
RUBY
|
41
|
+
end
|
42
|
+
else
|
43
|
+
<<~RUBY
|
44
|
+
unless @__before_callback_rendered
|
45
|
+
#{h[:name]} #{condition}
|
46
|
+
@__before_callback_rendered = true if @__rendered
|
47
|
+
end
|
48
|
+
RUBY
|
49
|
+
end
|
28
50
|
end
|
29
51
|
|
30
52
|
lines.join("\n")
|
@@ -32,6 +54,8 @@ class RageController::API
|
|
32
54
|
""
|
33
55
|
end
|
34
56
|
|
57
|
+
around_actions_end_chunk = around_actions_total.times.reduce("") { |memo| memo + "end\n" }
|
58
|
+
|
35
59
|
after_actions_chunk = if @__after_actions
|
36
60
|
lines = __after_actions_for(action).map do |h|
|
37
61
|
condition = if h[:if] && h[:unless]
|
@@ -96,12 +120,15 @@ class RageController::API
|
|
96
120
|
|
97
121
|
#{wrap_parameters_chunk}
|
98
122
|
#{before_actions_chunk}
|
99
|
-
#{action}
|
123
|
+
#{action} unless @__before_callback_rendered
|
124
|
+
#{around_actions_end_chunk}
|
100
125
|
|
101
126
|
#{if !after_actions_chunk.empty?
|
102
127
|
<<~RUBY
|
103
|
-
@
|
104
|
-
|
128
|
+
unless @__before_callback_rendered
|
129
|
+
@__rendered = true
|
130
|
+
#{after_actions_chunk}
|
131
|
+
end
|
105
132
|
RUBY
|
106
133
|
end}
|
107
134
|
|
@@ -151,13 +178,29 @@ class RageController::API
|
|
151
178
|
end
|
152
179
|
|
153
180
|
# @private
|
154
|
-
@@
|
181
|
+
@@__dynamic_name_seed = ("a".."i").to_a.permutation
|
182
|
+
|
183
|
+
# @private
|
184
|
+
# define a method based on a block
|
185
|
+
def define_dynamic_method(block)
|
186
|
+
name = @@__dynamic_name_seed.next.join
|
187
|
+
define_method("__rage_dynamic_#{name}", block)
|
188
|
+
end
|
155
189
|
|
156
190
|
# @private
|
157
|
-
# define
|
158
|
-
def
|
159
|
-
name = @@
|
160
|
-
|
191
|
+
# define a method that will call a specified method if a condition is `true` or yield if `false`
|
192
|
+
def define_maybe_yield(method_name)
|
193
|
+
name = @@__dynamic_name_seed.next.join
|
194
|
+
|
195
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
196
|
+
def __rage_dynamic_#{name}(condition)
|
197
|
+
if condition
|
198
|
+
#{method_name} { yield }
|
199
|
+
else
|
200
|
+
yield
|
201
|
+
end
|
202
|
+
end
|
203
|
+
RUBY
|
161
204
|
end
|
162
205
|
|
163
206
|
############
|
@@ -183,7 +226,7 @@ class RageController::API
|
|
183
226
|
def rescue_from(*klasses, with: nil, &block)
|
184
227
|
unless with
|
185
228
|
if block_given?
|
186
|
-
with =
|
229
|
+
with = define_dynamic_method(block)
|
187
230
|
else
|
188
231
|
raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
|
189
232
|
end
|
@@ -239,6 +282,39 @@ class RageController::API
|
|
239
282
|
end
|
240
283
|
end
|
241
284
|
|
285
|
+
# Register a new `around_action` hook. Calls with the same `action_name` will overwrite the previous ones.
|
286
|
+
#
|
287
|
+
# @param action_name [Symbol, nil] the name of the callback to add
|
288
|
+
# @param [Hash] opts action options
|
289
|
+
# @option opts [Symbol, Array<Symbol>] :only restrict the callback to run only for specific actions
|
290
|
+
# @option opts [Symbol, Array<Symbol>] :except restrict the callback to run for all actions except specified
|
291
|
+
# @option opts [Symbol, Proc] :if only run the callback if the condition is true
|
292
|
+
# @option opts [Symbol, Proc] :unless only run the callback if the condition is false
|
293
|
+
# @example
|
294
|
+
# around_action :wrap_in_transaction
|
295
|
+
#
|
296
|
+
# def wrap_in_transaction
|
297
|
+
# ActiveRecord::Base.transaction do
|
298
|
+
# yield
|
299
|
+
# end
|
300
|
+
# end
|
301
|
+
def around_action(action_name = nil, **opts, &block)
|
302
|
+
action = prepare_action_params(action_name, **opts, &block)
|
303
|
+
action.merge!(around: true, wrapper: define_maybe_yield(action[:name]))
|
304
|
+
|
305
|
+
if @__before_actions && @__before_actions.frozen?
|
306
|
+
@__before_actions = @__before_actions.dup
|
307
|
+
end
|
308
|
+
|
309
|
+
if @__before_actions.nil?
|
310
|
+
@__before_actions = [action]
|
311
|
+
elsif (i = @__before_actions.find_index { |a| a[:name] == action_name })
|
312
|
+
@__before_actions[i] = action
|
313
|
+
else
|
314
|
+
@__before_actions << action
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
242
318
|
# Register a new `after_action` hook. Calls with the same `action_name` will overwrite the previous ones.
|
243
319
|
#
|
244
320
|
# @param action_name [Symbol, nil] the name of the callback to add
|
@@ -273,7 +349,7 @@ class RageController::API
|
|
273
349
|
# @example
|
274
350
|
# skip_before_action :find_photo, only: :create
|
275
351
|
def skip_before_action(action_name, only: nil, except: nil)
|
276
|
-
i = @__before_actions&.find_index { |a| a[:name] == action_name }
|
352
|
+
i = @__before_actions&.find_index { |a| a[:name] == action_name && !a[:around] }
|
277
353
|
raise ArgumentError, "The following action was specified to be skipped but couldn't be found: #{self}##{action_name}" unless i
|
278
354
|
|
279
355
|
@__before_actions = @__before_actions.dup if @__before_actions.frozen?
|
@@ -339,7 +415,7 @@ class RageController::API
|
|
339
415
|
# used by `before_action` and `after_action`
|
340
416
|
def prepare_action_params(action_name = nil, **opts, &block)
|
341
417
|
if block_given?
|
342
|
-
action_name =
|
418
|
+
action_name = define_dynamic_method(block)
|
343
419
|
elsif action_name.nil?
|
344
420
|
raise ArgumentError, "No handler provided. Pass the `action_name` parameter or provide a block."
|
345
421
|
end
|
@@ -354,8 +430,8 @@ class RageController::API
|
|
354
430
|
unless: _unless
|
355
431
|
}
|
356
432
|
|
357
|
-
action[:if] =
|
358
|
-
action[:unless] =
|
433
|
+
action[:if] = define_dynamic_method(action[:if]) if action[:if].is_a?(Proc)
|
434
|
+
action[:unless] = define_dynamic_method(action[:unless]) if action[:unless].is_a?(Proc)
|
359
435
|
|
360
436
|
action
|
361
437
|
end
|
@@ -509,27 +585,27 @@ class RageController::API
|
|
509
585
|
|
510
586
|
if !defined?(::ActionController::Parameters)
|
511
587
|
# Get the request data. The keys inside the hash are symbols, so `params.keys` returns an array of `Symbol`.<br>
|
512
|
-
# You can also load Strong
|
588
|
+
# You can also load Strong Parameters to have Rage automatically wrap `params` in an instance of `ActionController::Parameters`.<br>
|
513
589
|
# At the same time, if you are not implementing complex filtering rules or working with nested structures, consider using native `Hash#fetch` and `Hash#slice` instead.
|
514
590
|
#
|
515
591
|
# For multipart file uploads, the uploaded files are represented by an instance of {Rage::UploadedFile}.
|
516
592
|
#
|
517
593
|
# @return [Hash{Symbol=>String,Array,Hash,Numeric,NilClass,TrueClass,FalseClass}]
|
518
|
-
# @example
|
519
|
-
# #
|
520
|
-
# require "active_support/all"
|
521
|
-
# require "action_controller/metal/strong_parameters"
|
594
|
+
# @example With Strong Parameters
|
595
|
+
# # in the Gemfile:
|
596
|
+
# gem "activesupport", require: "active_support/all"
|
597
|
+
# gem "actionpack", require: "action_controller/metal/strong_parameters"
|
522
598
|
#
|
523
|
-
#
|
524
|
-
#
|
525
|
-
#
|
599
|
+
# # in the controller:
|
600
|
+
# params.require(:user).permit(:full_name, :dob)
|
601
|
+
# @example Without Strong Parameters
|
526
602
|
# params.fetch(:user).slice(:full_name, :dob)
|
527
603
|
def params
|
528
604
|
@__params
|
529
605
|
end
|
530
606
|
else
|
531
607
|
def params
|
532
|
-
@
|
608
|
+
@__params__ ||= ActionController::Parameters.new(@__params)
|
533
609
|
end
|
534
610
|
end
|
535
611
|
|
data/lib/rage/cookies.rb
CHANGED
data/lib/rage/fiber_scheduler.rb
CHANGED
@@ -12,10 +12,10 @@ class Rage::FiberScheduler
|
|
12
12
|
|
13
13
|
def io_wait(io, events, timeout = nil)
|
14
14
|
f = Fiber.current
|
15
|
-
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil
|
15
|
+
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil) { |err| f.resume(err) }
|
16
16
|
|
17
17
|
err = Fiber.defer(io.fileno)
|
18
|
-
if err && err < 0
|
18
|
+
if err == false || (err && err < 0)
|
19
19
|
err
|
20
20
|
else
|
21
21
|
events
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# The middleware establishes a connection between the `X-Request-Id` header (typically generated by a firewall, load balancer, or web server) and
|
5
|
+
# Rage's internal logging system. It ensures that:
|
6
|
+
#
|
7
|
+
# 1. All logs produced during the request are tagged with the value submitted in the `X-Request-Id` header.
|
8
|
+
# 2. The request ID is added back to the response in the `X-Request-Id` header. If no `X-Request-Id` header was provided in the request, the middleware adds an internally generated ID to the response.
|
9
|
+
#
|
10
|
+
# Additionally, the `X-Request-Id` header value is sanitized to a maximum of 255 characters, allowing only alphanumeric characters and dashes.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# Rage.configure do
|
14
|
+
# config.middleware.use Rage::RequestId
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
class Rage::RequestId
|
18
|
+
BLACKLISTED_CHARACTERS = /[^\w\-@]/
|
19
|
+
|
20
|
+
def initialize(app)
|
21
|
+
@app = app
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
env["rage.request_id"] = validate_external_request_id(env["HTTP_X_REQUEST_ID"])
|
26
|
+
response = @app.call(env)
|
27
|
+
response[1]["X-Request-Id"] = env["rage.request_id"]
|
28
|
+
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def validate_external_request_id(request_id)
|
35
|
+
if request_id && !request_id.empty?
|
36
|
+
request_id = request_id[0...255] if request_id.size > 255
|
37
|
+
request_id = request_id.gsub(BLACKLISTED_CHARACTERS, "") if request_id =~ BLACKLISTED_CHARACTERS
|
38
|
+
|
39
|
+
request_id
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/rage/openapi/builder.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Rage::OpenAPI::Converter
|
4
|
+
# @param nodes [Rage::OpenAPI::Nodes::Root]
|
4
5
|
def initialize(nodes)
|
5
6
|
@nodes = nodes
|
6
7
|
@used_tags = Set.new
|
@@ -51,8 +52,10 @@ class Rage::OpenAPI::Converter
|
|
51
52
|
"tags" => build_tags(node)
|
52
53
|
}
|
53
54
|
|
54
|
-
|
55
|
-
|
55
|
+
responses = node.parents.reverse.map(&:responses).reduce(&:merge).merge(node.responses)
|
56
|
+
|
57
|
+
memo[path][method]["responses"] = if responses.any?
|
58
|
+
responses.each_with_object({}) do |(status, response), memo|
|
56
59
|
memo[status] = if response.nil?
|
57
60
|
{ "description" => "" }
|
58
61
|
elsif response.key?("$ref") && response["$ref"].start_with?("#/components/responses")
|
@@ -5,6 +5,9 @@ class Rage::OpenAPI::Nodes::Method
|
|
5
5
|
attr_accessor :http_method, :http_path, :summary, :tag, :deprecated, :private, :description,
|
6
6
|
:request, :responses, :parameters
|
7
7
|
|
8
|
+
# @param controller [RageController::API]
|
9
|
+
# @param action [String]
|
10
|
+
# @param parents [Array<Rage::OpenAPI::Nodes::Parent>]
|
8
11
|
def initialize(controller, action, parents)
|
9
12
|
@controller = controller
|
10
13
|
@action = action
|
@@ -2,12 +2,15 @@
|
|
2
2
|
|
3
3
|
class Rage::OpenAPI::Nodes::Parent
|
4
4
|
attr_reader :root, :controller
|
5
|
-
attr_accessor :deprecated, :private, :auth
|
5
|
+
attr_accessor :deprecated, :private, :auth, :responses
|
6
6
|
|
7
|
+
# @param root [Rage::OpenAPI::Nodes::Root]
|
8
|
+
# @param controller [RageController::API]
|
7
9
|
def initialize(root, controller)
|
8
10
|
@root = root
|
9
11
|
@controller = controller
|
10
12
|
|
11
13
|
@auth = []
|
14
|
+
@responses = {}
|
12
15
|
end
|
13
16
|
end
|
@@ -28,10 +28,15 @@ class Rage::OpenAPI::Nodes::Root
|
|
28
28
|
@leaves = []
|
29
29
|
end
|
30
30
|
|
31
|
+
# @return [Array<Rage::OpenAPI::Nodes::Parent>]
|
31
32
|
def parent_nodes
|
32
33
|
@parent_nodes_cache.values
|
33
34
|
end
|
34
35
|
|
36
|
+
# @param controller [RageController::API]
|
37
|
+
# @param action [String]
|
38
|
+
# @param parent_nodes [Array<Rage::OpenAPI::Nodes::Parent>]
|
39
|
+
# @return [Rage::OpenAPI::Nodes::Method]
|
35
40
|
def new_method_node(controller, action, parent_nodes)
|
36
41
|
node = Rage::OpenAPI::Nodes::Method.new(controller, action, parent_nodes)
|
37
42
|
@leaves << node
|
@@ -39,6 +44,8 @@ class Rage::OpenAPI::Nodes::Root
|
|
39
44
|
node
|
40
45
|
end
|
41
46
|
|
47
|
+
# @param controller [RageController::API]
|
48
|
+
# @return [Rage::OpenAPI::Nodes::Parent]
|
42
49
|
def new_parent_node(controller)
|
43
50
|
@parent_nodes_cache[controller] ||= begin
|
44
51
|
node = Rage::OpenAPI::Nodes::Parent.new(self, controller)
|
data/lib/rage/openapi/openapi.rb
CHANGED
data/lib/rage/openapi/parser.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Rage::OpenAPI::Parser
|
4
|
+
# @param node [Rage::OpenAPI::Nodes::Parent]
|
5
|
+
# @param comments [Array<Prism::InlineComment>]
|
4
6
|
def parse_dangling_comments(node, comments)
|
5
7
|
i = 0
|
6
8
|
|
@@ -14,7 +16,7 @@ class Rage::OpenAPI::Parser
|
|
14
16
|
else
|
15
17
|
node.deprecated = true
|
16
18
|
end
|
17
|
-
children = find_children(comments[i + 1..])
|
19
|
+
children = find_children(comments[i + 1..], node)
|
18
20
|
|
19
21
|
elsif expression =~ /@private\b/
|
20
22
|
if node.private
|
@@ -22,7 +24,7 @@ class Rage::OpenAPI::Parser
|
|
22
24
|
else
|
23
25
|
node.private = true
|
24
26
|
end
|
25
|
-
children = find_children(comments[i + 1..])
|
27
|
+
children = find_children(comments[i + 1..], node)
|
26
28
|
|
27
29
|
elsif expression =~ /@version\s/
|
28
30
|
if node.root.version
|
@@ -38,9 +40,12 @@ class Rage::OpenAPI::Parser
|
|
38
40
|
node.root.title = expression[7..]
|
39
41
|
end
|
40
42
|
|
43
|
+
elsif expression =~ /@response\s/
|
44
|
+
parse_response_tag(expression, node, comments[i])
|
45
|
+
|
41
46
|
elsif expression =~ /@auth\s/
|
42
47
|
method, name, tail_name = expression[6..].split(" ", 3)
|
43
|
-
children = find_children(comments[i + 1..])
|
48
|
+
children = find_children(comments[i + 1..], node)
|
44
49
|
|
45
50
|
if tail_name
|
46
51
|
Rage::OpenAPI.__log_warn "incorrect `@auth` name detected at #{location_msg(comments[i])}; security scheme name cannot contain spaces"
|
@@ -69,6 +74,8 @@ class Rage::OpenAPI::Parser
|
|
69
74
|
end
|
70
75
|
end
|
71
76
|
|
77
|
+
# @param node [Rage::OpenAPI::Nodes::Method]
|
78
|
+
# @param comments [Array<Prism::InlineComment>]
|
72
79
|
def parse_method_comments(node, comments)
|
73
80
|
i = 0
|
74
81
|
|
@@ -76,7 +83,9 @@ class Rage::OpenAPI::Parser
|
|
76
83
|
children = nil
|
77
84
|
expression = comments[i].slice.delete_prefix("#").strip
|
78
85
|
|
79
|
-
if
|
86
|
+
if expression.empty?
|
87
|
+
# no-op
|
88
|
+
elsif !expression.start_with?("@")
|
80
89
|
if node.summary
|
81
90
|
Rage::OpenAPI.__log_warn "invalid summary entry detected at #{location_msg(comments[i])}; summary should only be one line"
|
82
91
|
else
|
@@ -89,7 +98,7 @@ class Rage::OpenAPI::Parser
|
|
89
98
|
else
|
90
99
|
node.deprecated = true
|
91
100
|
end
|
92
|
-
children = find_children(comments[i + 1..])
|
101
|
+
children = find_children(comments[i + 1..], node)
|
93
102
|
|
94
103
|
elsif expression =~ /@private\b/
|
95
104
|
if node.parents.any?(&:private)
|
@@ -97,38 +106,14 @@ class Rage::OpenAPI::Parser
|
|
97
106
|
else
|
98
107
|
node.private = true
|
99
108
|
end
|
100
|
-
children = find_children(comments[i + 1..])
|
109
|
+
children = find_children(comments[i + 1..], node)
|
101
110
|
|
102
111
|
elsif expression =~ /@description\s/
|
103
|
-
children = find_children(comments[i + 1..])
|
112
|
+
children = find_children(comments[i + 1..], node)
|
104
113
|
node.description = [expression[13..]] + children
|
105
114
|
|
106
115
|
elsif expression =~ /@response\s/
|
107
|
-
|
108
|
-
status, response_data = if response =~ /^\d{3}$/
|
109
|
-
[response, nil]
|
110
|
-
elsif response =~ /^\d{3}/
|
111
|
-
response.split(" ", 2)
|
112
|
-
else
|
113
|
-
["200", response]
|
114
|
-
end
|
115
|
-
|
116
|
-
if node.responses.has_key?(status)
|
117
|
-
Rage::OpenAPI.__log_warn "duplicate `@response` tag detected at #{location_msg(comments[i])}"
|
118
|
-
elsif response_data.nil?
|
119
|
-
node.responses[status] = nil
|
120
|
-
else
|
121
|
-
parsed = Rage::OpenAPI::Parsers::Response.parse(
|
122
|
-
response_data,
|
123
|
-
namespace: Rage::OpenAPI.__module_parent(node.controller)
|
124
|
-
)
|
125
|
-
|
126
|
-
if parsed
|
127
|
-
node.responses[status] = parsed
|
128
|
-
else
|
129
|
-
Rage::OpenAPI.__log_warn "unrecognized `@response` tag detected at #{location_msg(comments[i])}"
|
130
|
-
end
|
131
|
-
end
|
116
|
+
parse_response_tag(expression, node, comments[i])
|
132
117
|
|
133
118
|
elsif expression =~ /@request\s/
|
134
119
|
request = expression[9..]
|
@@ -149,7 +134,7 @@ class Rage::OpenAPI::Parser
|
|
149
134
|
|
150
135
|
elsif expression =~ /@internal\b/
|
151
136
|
# no-op
|
152
|
-
children = find_children(comments[i + 1..])
|
137
|
+
children = find_children(comments[i + 1..], node)
|
153
138
|
|
154
139
|
else
|
155
140
|
Rage::OpenAPI.__log_warn "unrecognized `#{expression.split(" ")[0]}` tag detected at #{location_msg(comments[i])}"
|
@@ -165,16 +150,21 @@ class Rage::OpenAPI::Parser
|
|
165
150
|
|
166
151
|
private
|
167
152
|
|
168
|
-
def find_children(comments)
|
153
|
+
def find_children(comments, node)
|
169
154
|
children = []
|
170
155
|
|
171
156
|
comments.each do |comment|
|
172
|
-
expression = comment.slice.sub(/^#\s
|
157
|
+
expression = comment.slice.sub(/^#\s?/, "")
|
173
158
|
|
174
|
-
if expression.
|
159
|
+
if expression.empty?
|
160
|
+
# no-op
|
161
|
+
elsif expression.start_with?(/\s{2}/)
|
175
162
|
children << expression.strip
|
176
163
|
elsif expression.start_with?("@")
|
177
164
|
break
|
165
|
+
elsif !node.summary
|
166
|
+
# no-op - this is likely the summary entry
|
167
|
+
break
|
178
168
|
else
|
179
169
|
Rage::OpenAPI.__log_warn "unrecognized expression detected at #{location_msg(comment)}; use two spaces to mark multi-line expressions"
|
180
170
|
break
|
@@ -190,4 +180,32 @@ class Rage::OpenAPI::Parser
|
|
190
180
|
|
191
181
|
"#{relative_path}:#{location.start_line}"
|
192
182
|
end
|
183
|
+
|
184
|
+
def parse_response_tag(expression, node, comment)
|
185
|
+
response = expression[10..].strip
|
186
|
+
status, response_data = if response =~ /^\d{3}$/
|
187
|
+
[response, nil]
|
188
|
+
elsif response =~ /^\d{3}/
|
189
|
+
response.split(" ", 2)
|
190
|
+
else
|
191
|
+
["200", response]
|
192
|
+
end
|
193
|
+
|
194
|
+
if node.responses.has_key?(status)
|
195
|
+
Rage::OpenAPI.__log_warn "duplicate `@response` tag detected at #{location_msg(comment)}"
|
196
|
+
elsif response_data.nil?
|
197
|
+
node.responses[status] = nil
|
198
|
+
else
|
199
|
+
parsed = Rage::OpenAPI::Parsers::Response.parse(
|
200
|
+
response_data,
|
201
|
+
namespace: Rage::OpenAPI.__module_parent(node.controller)
|
202
|
+
)
|
203
|
+
|
204
|
+
if parsed
|
205
|
+
node.responses[status] = parsed
|
206
|
+
else
|
207
|
+
Rage::OpenAPI.__log_warn "unrecognized `@response` tag detected at #{location_msg(comment)}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
193
211
|
end
|
@@ -273,6 +273,10 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
273
273
|
{ "type" => "number" }
|
274
274
|
when "Float"
|
275
275
|
{ "type" => "number", "format" => "float" }
|
276
|
+
when "Date"
|
277
|
+
{ "type" => "string", "format" => "date" }
|
278
|
+
when "DateTime", "Time"
|
279
|
+
{ "type" => "string", "format" => "date-time" }
|
276
280
|
else
|
277
281
|
{ "type" => "string" }
|
278
282
|
end
|
data/lib/rage/request.rb
CHANGED
@@ -76,6 +76,14 @@ class Rage::Request
|
|
76
76
|
@env["HTTP_USER_AGENT"]
|
77
77
|
end
|
78
78
|
|
79
|
+
# Returns the unique request ID. By default, this ID is internally generated, and all log entries created during the request
|
80
|
+
# are tagged with it. Alternatively, you can use the {Rage::RequestId} middleware to derive the ID from the `X-Request-Id` header.
|
81
|
+
def request_id
|
82
|
+
@env["rage.request_id"]
|
83
|
+
end
|
84
|
+
|
85
|
+
alias_method :uuid, :request_id
|
86
|
+
|
79
87
|
private
|
80
88
|
|
81
89
|
def if_none_match
|
data/lib/rage/templates/Gemfile
CHANGED
data/lib/rage/version.rb
CHANGED
data/rage.gemspec
CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
|
30
30
|
spec.add_dependency "thor", "~> 1.0"
|
31
31
|
spec.add_dependency "rack", "~> 2.0"
|
32
|
-
spec.add_dependency "rage-iodine", "~> 4.
|
32
|
+
spec.add_dependency "rage-iodine", "~> 4.1"
|
33
33
|
spec.add_dependency "zeitwerk", "~> 2.6"
|
34
34
|
spec.add_dependency "rack-test", "~> 2.1"
|
35
35
|
spec.add_dependency "rake", ">= 12.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rage-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Samoilov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '4.
|
47
|
+
version: '4.1'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '4.
|
54
|
+
version: '4.1'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: zeitwerk
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -116,6 +116,8 @@ files:
|
|
116
116
|
- lib/rage.rb
|
117
117
|
- lib/rage/all.rb
|
118
118
|
- lib/rage/application.rb
|
119
|
+
- lib/rage/cable/adapters/base.rb
|
120
|
+
- lib/rage/cable/adapters/redis.rb
|
119
121
|
- lib/rage/cable/cable.rb
|
120
122
|
- lib/rage/cable/channel.rb
|
121
123
|
- lib/rage/cable/connection.rb
|
@@ -139,6 +141,7 @@ files:
|
|
139
141
|
- lib/rage/middleware/fiber_wrapper.rb
|
140
142
|
- lib/rage/middleware/origin_validator.rb
|
141
143
|
- lib/rage/middleware/reloader.rb
|
144
|
+
- lib/rage/middleware/request_id.rb
|
142
145
|
- lib/rage/openapi/builder.rb
|
143
146
|
- lib/rage/openapi/collector.rb
|
144
147
|
- lib/rage/openapi/converter.rb
|