rage-rb 1.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c42516bec6fabfb0b06d9f11a5156298474a48f7988edba746122ae4ec172dc
4
- data.tar.gz: 3b6b5b7692bb693ba53137d11bc1d7883ace69e496b5a99ec367da9a78ec4675
3
+ metadata.gz: d2f73c4770587ebabc7d8fa0c4793d53742196b928f21cc362799cb068849982
4
+ data.tar.gz: aecaa4ae2848bc1b15a38da29d3030d718d74697faaf8525e60e82d10b24396c
5
5
  SHA512:
6
- metadata.gz: b72d88f12c3608006637d822c7d3f955ef549e61ee644fc3d7748f507381cfe4917d3eea7f6cb75cf49f053f7ba34ce407a8ed06033a06e89e95208a15b6144e
7
- data.tar.gz: f60d1950bce78665acfe6af71aeff1694692b6063cbf76e6c39a0af5dbcf8a79672b8603c0a39ce5d2b0beb0ecf61edfba310d6140fb94b899d85dbad0dd2c21
6
+ metadata.gz: bb31ef275be5dc22322af8c9d563befc000ad2d15bb22454f2d3664736e6e7881ccdac365e6687fc123c23da1a06ee3cd4d63d08b8b5123c1fee67ebb97fbc57
7
+ data.tar.gz: 7debadefde85efa180f73e963b252cb1065a6b6f1bf4b8ca6e64e19d82a0ed838d3b321ddf9222006204ab59c3d90213e1a4eb6e1ff04d1d45572c1e166b3d77
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
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
+
3
16
  ## [1.12.0] - 2025-01-21
4
17
 
5
18
  ### Added
data/Gemfile CHANGED
@@ -15,6 +15,7 @@ group :test do
15
15
  gem "http"
16
16
  gem "pg"
17
17
  gem "mysql2"
18
+ gem "bigdecimal"
18
19
  gem "connection_pool", "~> 2.0"
19
20
  gem "rbnacl"
20
21
  gem "domain_name"
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"
@@ -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
  }
@@ -14,6 +14,7 @@ end
14
14
  class Rage::Cable::Adapters::Redis < Rage::Cable::Adapters::Base
15
15
  REDIS_STREAM_NAME = "rage:cable:messages"
16
16
  DEFAULT_REDIS_OPTIONS = { reconnect_attempts: [0.05, 0.1, 0.5] }
17
+ REDIS_MIN_VERSION_SUPPORTED = Gem::Version.create(6)
17
18
 
18
19
  def initialize(config)
19
20
  @redis_stream = if (prefix = config.delete(:channel_prefix))
@@ -26,8 +27,8 @@ class Rage::Cable::Adapters::Redis < Rage::Cable::Adapters::Base
26
27
  @server_uuid = SecureRandom.uuid
27
28
 
28
29
  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}."
30
+ if redis_version < REDIS_MIN_VERSION_SUPPORTED
31
+ raise "Redis adapter only supports Redis 6+. Detected Redis version: #{redis_version}."
31
32
  end
32
33
 
33
34
  @trimming_strategy = redis_version < Gem::Version.create("6.2.0") ? :maxlen : :minid
@@ -73,7 +74,7 @@ class Rage::Cable::Adapters::Redis < Rage::Cable::Adapters::Base
73
74
  rescue RedisClient::Error => e
74
75
  puts "FATAL: Couldn't connect to Redis - all broadcasts will be limited to the current server."
75
76
  puts e.backtrace.join("\n")
76
- Gem::Version.create(5)
77
+ REDIS_MIN_VERSION_SUPPORTED
77
78
 
78
79
  ensure
79
80
  service_redis.close
@@ -51,33 +51,22 @@ module Rage::Cable
51
51
  end
52
52
 
53
53
  @protocol = protocol
54
+ @default_log_context = {}.freeze
54
55
  end
55
56
 
56
57
  def on_open(connection)
57
- Fiber.schedule do
58
- @protocol.on_open(connection)
59
- rescue => e
60
- log_error(e)
61
- end
58
+ connection.env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag
59
+ schedule_fiber(connection) { @protocol.on_open(connection) }
62
60
  end
63
61
 
64
62
  def on_message(connection, data)
65
- Fiber.schedule do
66
- @protocol.on_message(connection, data)
67
- rescue => e
68
- log_error(e)
69
- end
63
+ schedule_fiber(connection) { @protocol.on_message(connection, data) }
70
64
  end
71
65
 
72
66
  if protocol.respond_to?(:on_close)
73
67
  def on_close(connection)
74
68
  return unless ::Iodine.running?
75
-
76
- Fiber.schedule do
77
- @protocol.on_close(connection)
78
- rescue => e
79
- log_error(e)
80
- end
69
+ schedule_fiber(connection) { @protocol.on_close(connection) }
81
70
  end
82
71
  end
83
72
 
@@ -91,6 +80,15 @@ module Rage::Cable
91
80
 
92
81
  private
93
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
+
94
92
  def log_error(e)
95
93
  Rage.logger.error("Unhandled exception has occured - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
96
94
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zlib"
4
+ require "set"
4
5
 
5
6
  ##
6
7
  # A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
@@ -69,8 +70,8 @@ class Rage::Cable::Protocol::ActioncableV1Json
69
70
  end
70
71
  end
71
72
 
72
- # Hash<String(stream name) => Array<Hash>(subscription params)>
73
- @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 }
74
75
 
75
76
  # this is a fallback to synchronize subscription identifiers across different worker processes;
76
77
  # we expect connections to be distributed among all workers, so this code will almost never be called;
@@ -78,7 +79,7 @@ class Rage::Cable::Protocol::ActioncableV1Json
78
79
  # of the crashed ones also had access to the identifiers;
79
80
  Iodine.subscribe("cable:synchronize") do |_, subscription_msg|
80
81
  stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
81
- @subscription_identifiers[stream_name] << params unless @subscription_identifiers[stream_name].include?(params)
82
+ @subscription_identifiers[stream_name] << params
82
83
  end
83
84
 
84
85
  Iodine.on_state(:on_finish) do
@@ -181,12 +182,8 @@ class Rage::Cable::Protocol::ActioncableV1Json
181
182
  # @param name [String] the stream name
182
183
  # @param data [Object] the data to send
183
184
  def self.broadcast(name, data)
184
- i, identifiers = 0, @subscription_identifiers[name]
185
-
186
- while i < identifiers.length
187
- params = identifiers[i]
185
+ @subscription_identifiers[name].each do |params|
188
186
  ::Iodine.publish("cable:#{name}:#{Zlib.crc32(params.to_s)}", serialize(params, data))
189
- i += 1
190
187
  end
191
188
  end
192
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
@@ -585,27 +585,27 @@ class RageController::API
585
585
 
586
586
  if !defined?(::ActionController::Parameters)
587
587
  # Get the request data. The keys inside the hash are symbols, so `params.keys` returns an array of `Symbol`.<br>
588
- # You can also load Strong Params to have Rage automatically wrap `params` in an instance of `ActionController::Parameters`.<br>
588
+ # You can also load Strong Parameters to have Rage automatically wrap `params` in an instance of `ActionController::Parameters`.<br>
589
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.
590
590
  #
591
591
  # For multipart file uploads, the uploaded files are represented by an instance of {Rage::UploadedFile}.
592
592
  #
593
593
  # @return [Hash{Symbol=>String,Array,Hash,Numeric,NilClass,TrueClass,FalseClass}]
594
- # @example
595
- # # make sure to load strong params before the `require "rage/all"` call
596
- # require "active_support/all"
597
- # 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"
598
598
  #
599
- # params.permit(:user).require(:full_name, :dob)
600
- # @example
601
- # # without strong params
599
+ # # in the controller:
600
+ # params.require(:user).permit(:full_name, :dob)
601
+ # @example Without Strong Parameters
602
602
  # params.fetch(:user).slice(:full_name, :dob)
603
603
  def params
604
604
  @__params
605
605
  end
606
606
  else
607
607
  def params
608
- @params ||= ActionController::Parameters.new(@__params)
608
+ @__params__ ||= ActionController::Parameters.new(@__params)
609
609
  end
610
610
  end
611
611
 
@@ -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 || 0) { |err| f.resume(err) }
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
@@ -119,7 +119,7 @@ module Rage::OpenAPI
119
119
 
120
120
  # @private
121
121
  def self.__log_warn(log)
122
- puts "WARNING: #{log}"
122
+ puts "[OpenAPI] WARNING: #{log}"
123
123
  end
124
124
 
125
125
  module Nodes
@@ -16,7 +16,7 @@ class Rage::OpenAPI::Parser
16
16
  else
17
17
  node.deprecated = true
18
18
  end
19
- children = find_children(comments[i + 1..])
19
+ children = find_children(comments[i + 1..], node)
20
20
 
21
21
  elsif expression =~ /@private\b/
22
22
  if node.private
@@ -24,7 +24,7 @@ class Rage::OpenAPI::Parser
24
24
  else
25
25
  node.private = true
26
26
  end
27
- children = find_children(comments[i + 1..])
27
+ children = find_children(comments[i + 1..], node)
28
28
 
29
29
  elsif expression =~ /@version\s/
30
30
  if node.root.version
@@ -45,7 +45,7 @@ class Rage::OpenAPI::Parser
45
45
 
46
46
  elsif expression =~ /@auth\s/
47
47
  method, name, tail_name = expression[6..].split(" ", 3)
48
- children = find_children(comments[i + 1..])
48
+ children = find_children(comments[i + 1..], node)
49
49
 
50
50
  if tail_name
51
51
  Rage::OpenAPI.__log_warn "incorrect `@auth` name detected at #{location_msg(comments[i])}; security scheme name cannot contain spaces"
@@ -83,7 +83,9 @@ class Rage::OpenAPI::Parser
83
83
  children = nil
84
84
  expression = comments[i].slice.delete_prefix("#").strip
85
85
 
86
- if !expression.start_with?("@")
86
+ if expression.empty?
87
+ # no-op
88
+ elsif !expression.start_with?("@")
87
89
  if node.summary
88
90
  Rage::OpenAPI.__log_warn "invalid summary entry detected at #{location_msg(comments[i])}; summary should only be one line"
89
91
  else
@@ -96,7 +98,7 @@ class Rage::OpenAPI::Parser
96
98
  else
97
99
  node.deprecated = true
98
100
  end
99
- children = find_children(comments[i + 1..])
101
+ children = find_children(comments[i + 1..], node)
100
102
 
101
103
  elsif expression =~ /@private\b/
102
104
  if node.parents.any?(&:private)
@@ -104,10 +106,10 @@ class Rage::OpenAPI::Parser
104
106
  else
105
107
  node.private = true
106
108
  end
107
- children = find_children(comments[i + 1..])
109
+ children = find_children(comments[i + 1..], node)
108
110
 
109
111
  elsif expression =~ /@description\s/
110
- children = find_children(comments[i + 1..])
112
+ children = find_children(comments[i + 1..], node)
111
113
  node.description = [expression[13..]] + children
112
114
 
113
115
  elsif expression =~ /@response\s/
@@ -132,7 +134,7 @@ class Rage::OpenAPI::Parser
132
134
 
133
135
  elsif expression =~ /@internal\b/
134
136
  # no-op
135
- children = find_children(comments[i + 1..])
137
+ children = find_children(comments[i + 1..], node)
136
138
 
137
139
  else
138
140
  Rage::OpenAPI.__log_warn "unrecognized `#{expression.split(" ")[0]}` tag detected at #{location_msg(comments[i])}"
@@ -148,16 +150,21 @@ class Rage::OpenAPI::Parser
148
150
 
149
151
  private
150
152
 
151
- def find_children(comments)
153
+ def find_children(comments, node)
152
154
  children = []
153
155
 
154
156
  comments.each do |comment|
155
- expression = comment.slice.sub(/^#\s/, "")
157
+ expression = comment.slice.sub(/^#\s?/, "")
156
158
 
157
- if expression.start_with?(/\s{2}/)
159
+ if expression.empty?
160
+ # no-op
161
+ elsif expression.start_with?(/\s{2}/)
158
162
  children << expression.strip
159
163
  elsif expression.start_with?("@")
160
164
  break
165
+ elsif !node.summary
166
+ # no-op - this is likely the summary entry
167
+ break
161
168
  else
162
169
  Rage::OpenAPI.__log_warn "unrecognized expression detected at #{location_msg(comment)}; use two spaces to mark multi-line expressions"
163
170
  break
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
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "rage-rb", "~> <%= Rage::VERSION[0..2] %>"
3
+ gem "rage-rb", "~> <%= Rage::VERSION.match(/\d+.\d+/).to_s %>"
4
4
 
5
5
  # Build JSON APIs with ease
6
6
  # gem "alba"
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "1.12.0"
4
+ VERSION = "1.13.0"
5
5
  end
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.0"
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.12.0
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: 2025-01-21 00:00:00.000000000 Z
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.0'
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.0'
54
+ version: '4.1'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: zeitwerk
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -141,6 +141,7 @@ files:
141
141
  - lib/rage/middleware/fiber_wrapper.rb
142
142
  - lib/rage/middleware/origin_validator.rb
143
143
  - lib/rage/middleware/reloader.rb
144
+ - lib/rage/middleware/request_id.rb
144
145
  - lib/rage/openapi/builder.rb
145
146
  - lib/rage/openapi/collector.rb
146
147
  - lib/rage/openapi/converter.rb