rage-rb 1.11.0 → 1.12.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 +12 -0
- data/Gemfile +1 -0
- data/README.md +1 -2
- data/lib/rage/cable/adapters/base.rb +16 -0
- data/lib/rage/cable/adapters/redis.rb +127 -0
- data/lib/rage/cable/cable.rb +23 -5
- data/lib/rage/cable/channel.rb +1 -1
- data/lib/rage/cable/protocol/actioncable_v1_json.rb +32 -7
- data/lib/rage/configuration.rb +27 -0
- data/lib/rage/controller/api.rb +93 -17
- data/lib/rage/cookies.rb +1 -1
- 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/parser.rb +36 -25
- data/lib/rage/openapi/parsers/ext/alba.rb +4 -0
- data/lib/rage/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c42516bec6fabfb0b06d9f11a5156298474a48f7988edba746122ae4ec172dc
|
4
|
+
data.tar.gz: 3b6b5b7692bb693ba53137d11bc1d7883ace69e496b5a99ec367da9a78ec4675
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b72d88f12c3608006637d822c7d3f955ef549e61ee644fc3d7748f507381cfe4917d3eea7f6cb75cf49f053f7ba34ce407a8ed06033a06e89e95208a15b6144e
|
7
|
+
data.tar.gz: f60d1950bce78665acfe6af71aeff1694692b6063cbf76e6c39a0af5dbcf8a79672b8603c0a39ce5d2b0beb0ecf61edfba310d6140fb94b899d85dbad0dd2c21
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.12.0] - 2025-01-21
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Add Redis adapter (#114).
|
8
|
+
- Add global response tags (#110).
|
9
|
+
- Implement around_action callbacks (#107).
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- Support date types in Alba serializers (#112).
|
14
|
+
|
3
15
|
## [1.11.0] - 2024-12-18
|
4
16
|
|
5
17
|
### 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
|
|
@@ -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,127 @@
|
|
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
|
+
|
18
|
+
def initialize(config)
|
19
|
+
@redis_stream = if (prefix = config.delete(:channel_prefix))
|
20
|
+
"#{prefix}:#{REDIS_STREAM_NAME}"
|
21
|
+
else
|
22
|
+
REDIS_STREAM_NAME
|
23
|
+
end
|
24
|
+
|
25
|
+
@redis_config = RedisClient.config(**DEFAULT_REDIS_OPTIONS.merge(config))
|
26
|
+
@server_uuid = SecureRandom.uuid
|
27
|
+
|
28
|
+
redis_version = get_redis_version
|
29
|
+
if redis_version < Gem::Version.create(5)
|
30
|
+
raise "Redis adapter only supports Redis 5+. Detected Redis version: #{redis_version}."
|
31
|
+
end
|
32
|
+
|
33
|
+
@trimming_strategy = redis_version < Gem::Version.create("6.2.0") ? :maxlen : :minid
|
34
|
+
|
35
|
+
pick_a_worker { poll }
|
36
|
+
end
|
37
|
+
|
38
|
+
def publish(stream_name, data)
|
39
|
+
message_uuid = SecureRandom.uuid
|
40
|
+
|
41
|
+
publish_redis.call(
|
42
|
+
"XADD",
|
43
|
+
@redis_stream,
|
44
|
+
trimming_method, "~", trimming_value,
|
45
|
+
"*",
|
46
|
+
"1", stream_name,
|
47
|
+
"2", data.to_json,
|
48
|
+
"3", @server_uuid,
|
49
|
+
"4", message_uuid
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def publish_redis
|
56
|
+
@publish_redis ||= @redis_config.new_client
|
57
|
+
end
|
58
|
+
|
59
|
+
def trimming_method
|
60
|
+
@trimming_strategy == :maxlen ? "MAXLEN" : "MINID"
|
61
|
+
end
|
62
|
+
|
63
|
+
def trimming_value
|
64
|
+
@trimming_strategy == :maxlen ? "10000" : ((Time.now.to_f - 5 * 60) * 1000).to_i
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_redis_version
|
68
|
+
service_redis = @redis_config.new_client
|
69
|
+
version = service_redis.call("INFO").match(/redis_version:([[:graph:]]+)/)[1]
|
70
|
+
|
71
|
+
Gem::Version.create(version)
|
72
|
+
|
73
|
+
rescue RedisClient::Error => e
|
74
|
+
puts "FATAL: Couldn't connect to Redis - all broadcasts will be limited to the current server."
|
75
|
+
puts e.backtrace.join("\n")
|
76
|
+
Gem::Version.create(5)
|
77
|
+
|
78
|
+
ensure
|
79
|
+
service_redis.close
|
80
|
+
end
|
81
|
+
|
82
|
+
def error_backoff_intervals
|
83
|
+
@error_backoff_intervals ||= Enumerator.new do |y|
|
84
|
+
y << 0.2 << 0.5 << 1 << 2 << 5
|
85
|
+
loop { y << 10 }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def poll
|
90
|
+
unless Fiber.scheduler
|
91
|
+
Fiber.set_scheduler(Rage::FiberScheduler.new)
|
92
|
+
end
|
93
|
+
|
94
|
+
Iodine.on_state(:start_shutdown) do
|
95
|
+
@stopping = true
|
96
|
+
end
|
97
|
+
|
98
|
+
Fiber.schedule do
|
99
|
+
read_redis = @redis_config.new_client
|
100
|
+
last_id = (Time.now.to_f * 1000).to_i
|
101
|
+
last_message_uuid = nil
|
102
|
+
|
103
|
+
loop do
|
104
|
+
data = read_redis.blocking_call(5, "XREAD", "COUNT", "100", "BLOCK", "5000", "STREAMS", @redis_stream, last_id)
|
105
|
+
|
106
|
+
if data
|
107
|
+
data[@redis_stream].each do |id, (_, stream_name, _, serialized_data, _, broadcaster_uuid, _, message_uuid)|
|
108
|
+
if broadcaster_uuid != @server_uuid && message_uuid != last_message_uuid
|
109
|
+
Rage.config.cable.protocol.broadcast(stream_name, JSON.parse(serialized_data))
|
110
|
+
end
|
111
|
+
|
112
|
+
last_id = id
|
113
|
+
last_message_uuid = message_uuid
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
rescue RedisClient::Error => e
|
118
|
+
Rage.logger.error("Subscriber error: #{e.message} (#{e.class})")
|
119
|
+
sleep error_backoff_intervals.next
|
120
|
+
rescue => e
|
121
|
+
@stopping ? break : raise(e)
|
122
|
+
else
|
123
|
+
error_backoff_intervals.rewind
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
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
|
@@ -94,10 +103,14 @@ module Rage::Cable
|
|
94
103
|
#
|
95
104
|
# @param stream [String] the name of the stream
|
96
105
|
# @param data [Object] the object to send to the clients. This will later be encoded according to the protocol used.
|
106
|
+
# @return [true]
|
97
107
|
# @example
|
98
108
|
# Rage.cable.broadcast("chat", { message: "A new member has joined!" })
|
99
109
|
def self.broadcast(stream, data)
|
100
|
-
|
110
|
+
__protocol.broadcast(stream, data)
|
111
|
+
__adapter&.publish(stream, data)
|
112
|
+
|
113
|
+
true
|
101
114
|
end
|
102
115
|
|
103
116
|
# @!parse [ruby]
|
@@ -120,6 +133,11 @@ module Rage::Cable
|
|
120
133
|
# end
|
121
134
|
# end
|
122
135
|
|
136
|
+
module Adapters
|
137
|
+
autoload :Base, "rage/cable/adapters/base"
|
138
|
+
autoload :Redis, "rage/cable/adapters/redis"
|
139
|
+
end
|
140
|
+
|
123
141
|
module Protocol
|
124
142
|
end
|
125
143
|
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,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "zlib"
|
4
|
+
|
3
5
|
##
|
4
6
|
# A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
|
5
7
|
# The class that defines a protocol should respond to the following methods:
|
@@ -17,6 +19,9 @@
|
|
17
19
|
# * `on_shutdown`
|
18
20
|
# * `on_close`
|
19
21
|
#
|
22
|
+
# It is likely that all logic around `@subscription_identifiers` has nothing to do with the protocol itself and
|
23
|
+
# should be extracted into another class. We'll refactor this once we start working on a new protocol.
|
24
|
+
#
|
20
25
|
class Rage::Cable::Protocol::ActioncableV1Json
|
21
26
|
module TYPE
|
22
27
|
WELCOME = "welcome"
|
@@ -55,14 +60,30 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
55
60
|
def self.init(router)
|
56
61
|
@router = router
|
57
62
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
63
|
+
Iodine.on_state(:on_start) do
|
64
|
+
ping_counter = Time.now.to_i
|
65
|
+
|
66
|
+
Iodine.run_every(3000) do
|
67
|
+
ping_counter += 1
|
68
|
+
Iodine.publish("cable:ping", { type: TYPE::PING, message: ping_counter }.to_json, Iodine::PubSub::PROCESS)
|
69
|
+
end
|
62
70
|
end
|
63
71
|
|
64
72
|
# Hash<String(stream name) => Array<Hash>(subscription params)>
|
65
73
|
@subscription_identifiers = Hash.new { |hash, key| hash[key] = [] }
|
74
|
+
|
75
|
+
# this is a fallback to synchronize subscription identifiers across different worker processes;
|
76
|
+
# we expect connections to be distributed among all workers, so this code will almost never be called;
|
77
|
+
# we also synchronize subscriptions with the master process so that the forks that are spun up instead
|
78
|
+
# of the crashed ones also had access to the identifiers;
|
79
|
+
Iodine.subscribe("cable:synchronize") do |_, subscription_msg|
|
80
|
+
stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
|
81
|
+
@subscription_identifiers[stream_name] << params unless @subscription_identifiers[stream_name].include?(params)
|
82
|
+
end
|
83
|
+
|
84
|
+
Iodine.on_state(:on_finish) do
|
85
|
+
Iodine.unsubscribe("cable:synchronize")
|
86
|
+
end
|
66
87
|
end
|
67
88
|
|
68
89
|
# The method is called any time a new WebSocket connection is established.
|
@@ -147,8 +168,12 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
147
168
|
# @param name [String] the stream name
|
148
169
|
# @param params [Hash] parameters associated with the client
|
149
170
|
def self.subscribe(connection, name, params)
|
150
|
-
connection.subscribe("cable:#{name}:#{params.
|
151
|
-
|
171
|
+
connection.subscribe("cable:#{name}:#{Zlib.crc32(params.to_s)}")
|
172
|
+
|
173
|
+
unless @subscription_identifiers[name].include?(params)
|
174
|
+
@subscription_identifiers[name] << params
|
175
|
+
::Iodine.publish("cable:synchronize", [name, params].to_json)
|
176
|
+
end
|
152
177
|
end
|
153
178
|
|
154
179
|
# Broadcast data to all clients connected to a stream.
|
@@ -160,7 +185,7 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
160
185
|
|
161
186
|
while i < identifiers.length
|
162
187
|
params = identifiers[i]
|
163
|
-
::Iodine.publish("cable:#{name}:#{params.
|
188
|
+
::Iodine.publish("cable:#{name}:#{Zlib.crc32(params.to_s)}", serialize(params, data))
|
164
189
|
i += 1
|
165
190
|
end
|
166
191
|
end
|
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
|
155
182
|
|
156
183
|
# @private
|
157
|
-
# define
|
158
|
-
def
|
159
|
-
name = @@
|
160
|
-
define_method("
|
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
|
189
|
+
|
190
|
+
# @private
|
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
|
data/lib/rage/cookies.rb
CHANGED
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/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
|
|
@@ -38,6 +40,9 @@ 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
48
|
children = find_children(comments[i + 1..])
|
@@ -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
|
|
@@ -104,31 +111,7 @@ class Rage::OpenAPI::Parser
|
|
104
111
|
node.description = [expression[13..]] + children
|
105
112
|
|
106
113
|
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
|
114
|
+
parse_response_tag(expression, node, comments[i])
|
132
115
|
|
133
116
|
elsif expression =~ /@request\s/
|
134
117
|
request = expression[9..]
|
@@ -190,4 +173,32 @@ class Rage::OpenAPI::Parser
|
|
190
173
|
|
191
174
|
"#{relative_path}:#{location.start_line}"
|
192
175
|
end
|
176
|
+
|
177
|
+
def parse_response_tag(expression, node, comment)
|
178
|
+
response = expression[10..].strip
|
179
|
+
status, response_data = if response =~ /^\d{3}$/
|
180
|
+
[response, nil]
|
181
|
+
elsif response =~ /^\d{3}/
|
182
|
+
response.split(" ", 2)
|
183
|
+
else
|
184
|
+
["200", response]
|
185
|
+
end
|
186
|
+
|
187
|
+
if node.responses.has_key?(status)
|
188
|
+
Rage::OpenAPI.__log_warn "duplicate `@response` tag detected at #{location_msg(comment)}"
|
189
|
+
elsif response_data.nil?
|
190
|
+
node.responses[status] = nil
|
191
|
+
else
|
192
|
+
parsed = Rage::OpenAPI::Parsers::Response.parse(
|
193
|
+
response_data,
|
194
|
+
namespace: Rage::OpenAPI.__module_parent(node.controller)
|
195
|
+
)
|
196
|
+
|
197
|
+
if parsed
|
198
|
+
node.responses[status] = parsed
|
199
|
+
else
|
200
|
+
Rage::OpenAPI.__log_warn "unrecognized `@response` tag detected at #{location_msg(comment)}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
193
204
|
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/version.rb
CHANGED
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.12.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-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -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
|