rage-rb 1.26.1 → 1.27.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: 1dab3bd5fbff6c838ff8bb686e7f81251f10a3734330f4788990791822484371
4
- data.tar.gz: c7d03259c16c307078ed3f01f70cf5505b7ce70a49726f52a109377ec9f3ee44
3
+ metadata.gz: 158f3d2cea7753f15be62035f7b196117284a4112159400ecf57759fb719ed08
4
+ data.tar.gz: 002ef2cc3b75084f0393b254f3c2b61e2e6be83d1a5cf0e64b7529aa3452e1a7
5
5
  SHA512:
6
- metadata.gz: 1d2d40b1bc876cfaf50fed91c34764798261f85c90e0ca8ffdd12f5039e4a4aa20b16e372be366ddbb9af07eb4de5a5a9b39c5bc603e604593255df347ef089f
7
- data.tar.gz: 952f48b476cc8dcd1c6b604b771e565f1298d14e5c2857f24881f2a22abb7b32bd67b7c3d75497516ca3b72ce6e61165086ac4979471bbcf0c1d7140fd6eb1ef
6
+ metadata.gz: 3f194f003221636c0fe3120a31d5b60a38d9a8a496fc873e13fdc6c2020f8a59f4348073a06a019f7e3de6609e4b73e2bb493ffce12bb63f8f317365a720fdcc
7
+ data.tar.gz: 3af06d4347e71056ca9bd26e9b0edfc84e554399ed2b37aedce6afbea6b11247091929874b76540a63b3041c8b1edd3f93cc197904b68d19e4ef81a94ccc2ffd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.27.0] - 2026-08-06
4
+
5
+ ### Added
6
+
7
+ - [OpenAPI] Add support for the `root:` option in Blueprinter response annotations (#343).
8
+ - Add `FiberScheduler#timeout_after` (#374).
9
+
10
+ ### Fixed
11
+
12
+ - [OpenAPI] Avoid caching a request-specific spec URL in the HTML docs page.
13
+ - [Router] Compose and restore `SCRIPT_NAME` correctly for mounted Rack apps.
14
+ - [OpenAPI] Fix schema registry key collisions for Blueprinter classes referenced under different views.
15
+ - Fixed RbNaCl dependency check to produce correct error message (#361).
16
+
3
17
  ## [1.26.1] - 2026-07-17
4
18
 
5
19
  ### Fixed
@@ -22,7 +36,6 @@
22
36
 
23
37
  ### Fixed
24
38
 
25
- - [CI] Check all changed PR file pages when requiring changelog updates.
26
39
  - [API] Ignore `If-Modified-Since` when `If-None-Match` is present.
27
40
  - [API] Use weak comparison for `If-None-Match` validation.
28
41
  - [Deferred] Ignore missing temp files during async disk storage cleanup.
data/CONTRIBUTING.md CHANGED
@@ -223,7 +223,7 @@ end
223
223
  1. Wraps every request in a Fiber
224
224
  2. Implements the defer protocol for pausing/resuming async requests
225
225
 
226
- When a request encounters blocking I/O (database query, HTTP request, etc.), the fiber yields. `FiberWrapper` detects this (`fiber.alive?`) and returns a special `:__http_defer__` signal to Iodine, which pauses the connection.
226
+ When a request encounters blocking I/O (database query, HTTP request, etc.), the fiber yields. `FiberWrapper` detects this (`fiber.alive?`) and returns the special `[:__http_defer__, fiber]` tuple to Iodine, which pauses the connection.
227
227
 
228
228
  When the I/O completes, the fiber resumes and publishes a message to notify Iodine that the response is ready. This is the mechanism that enables transparent, non-blocking concurrency.
229
229
 
data/README.md CHANGED
@@ -145,7 +145,7 @@ Rage prioritizes stable public APIs, long deprecation cycles, and minimal extern
145
145
 
146
146
  ## Learn More
147
147
 
148
- - Documentation: [https://rage-rb.dev](https://rage-rb.dev/docs/intro)
148
+ - Documentation: [https://rage-rb.dev/docs/intro](https://rage-rb.dev/docs/intro)
149
149
  - API Reference: [https://rage-rb.dev/api](https://rage-rb.dev/api)
150
150
  - Architecture: [ARCHITECTURE.md](https://github.com/rage-rb/rage/blob/main/ARCHITECTURE.md)
151
151
  - Contributing: [CONTRIBUTING.md](https://github.com/rage-rb/rage/blob/main/CONTRIBUTING.md)
@@ -23,6 +23,7 @@ require "erb"
23
23
  # - _RAGE_DISABLE_IO_WRITE_ - disables the `io_write` hook to fix the ["zero-length iov"](https://bugs.ruby-lang.org/issues/19640) error on Ruby < 3.3.
24
24
  # - _RAGE_DISABLE_AR_POOL_PATCH_ - disables the `ActiveRecord::ConnectionPool` patch and makes Rage use the original ActiveRecord implementation.
25
25
  # - _RAGE_DISABLE_AR_WEAK_CONNECTIONS_ - instructs Rage to not reuse Active Record connections between different fibers. Only applies to Active Record < 7.2.
26
+ # - _RAGE_ENABLE_NON_BLOCKING_TIMEOUT_ - enables the non-blocking `timeout_after` method on the fiber scheduler, which allows timeouts to work cooperatively with fiber-based concurrency.
26
27
  #
27
28
  class Rage::Configuration
28
29
  # @private
data/lib/rage/cookies.rb CHANGED
@@ -278,24 +278,22 @@ class Rage::Cookies
278
278
 
279
279
  private
280
280
 
281
- def ensure_rbnacl!(purpose:)
281
+ def ensure_rbnacl!
282
282
  return if defined?(RbNaCl) &&
283
283
  Gem::Version.create(RbNaCl::VERSION) >= RBNACL_MIN_VERSION &&
284
284
  Gem::Version.create(RbNaCl::VERSION) < RBNACL_MAX_VERSION
285
285
 
286
286
  fail <<~ERR
287
287
 
288
- Rage depends on `rbnacl` [>= #{RBNACL_MIN_VERSION}, < #{RBNACL_MAX_VERSION}] to support #{purpose}. Ensure the following line is added to your Gemfile:
288
+ Rage depends on `rbnacl` [>= #{RBNACL_MIN_VERSION}, < #{RBNACL_MAX_VERSION}] to support encrypted and signed cookies. Ensure the following line is added to your Gemfile:
289
289
  gem "rbnacl"
290
290
 
291
291
  ERR
292
292
  end
293
293
 
294
294
  def build_key(secret, purpose:)
295
- ensure_rbnacl!(purpose: purpose)
296
-
297
295
  if !secret
298
- raise "Rage.config.secret_key_base should be set to use #{purpose}"
296
+ raise "Rage.config.secret_key_base should be set to use encrypted or signed cookies"
299
297
  end
300
298
 
301
299
  RbNaCl::Hash.blake2b("", key: [secret].pack("H*"), digest_size: 32, personal: purpose)
@@ -337,7 +335,10 @@ class Rage::Cookies
337
335
  private
338
336
 
339
337
  def primary_box
340
- @primary_box ||= RbNaCl::SimpleBox.from_secret_key(build_key(Rage.config.secret_key_base, purpose: PURPOSE))
338
+ @primary_box ||= begin
339
+ ensure_rbnacl!
340
+ RbNaCl::SimpleBox.from_secret_key(build_key(Rage.config.secret_key_base, purpose: PURPOSE))
341
+ end
341
342
  end
342
343
 
343
344
  def fallback_boxes
@@ -401,9 +402,10 @@ class Rage::Cookies
401
402
  end
402
403
 
403
404
  def primary_signer
404
- @primary_signer ||= RbNaCl::HMAC::SHA512256.new(
405
- build_key(Rage.config.secret_key_base, purpose: PURPOSE)
406
- )
405
+ @primary_signer ||= begin
406
+ ensure_rbnacl!
407
+ RbNaCl::HMAC::SHA512256.new(build_key(Rage.config.secret_key_base, purpose: PURPOSE))
408
+ end
407
409
  end
408
410
 
409
411
  def fallback_signers
@@ -75,19 +75,23 @@ class Rage::FiberScheduler
75
75
  Fiber.pause if duration.nil? || duration < 1
76
76
  end
77
77
 
78
- # TODO: GC works a little strange with this closure;
79
- #
80
- # def timeout_after(duration, exception_class = Timeout::Error, *exception_arguments, &block)
81
- # fiber, block_status = Fiber.current, :running
82
- # ::Iodine.run_after((duration * 1000).to_i) do
83
- # fiber.raise(exception_class, exception_arguments) if block_status == :running
84
- # end
85
-
86
- # result = block.call
87
- # block_status = :finished
88
-
89
- # result
90
- # end
78
+ if ENV["RAGE_ENABLE_NON_BLOCKING_TIMEOUT"]
79
+ def timeout_after(duration, exception_class = Timeout::Error, *exception_arguments, &block)
80
+ f, fulfilled = Fiber.current, false
81
+
82
+ ::Iodine.run_after((duration * 1000).to_i) do
83
+ if !fulfilled && f.alive?
84
+ fulfilled = true
85
+ f.__wait_generation += 1
86
+ f.raise(exception_class, *exception_arguments)
87
+ end
88
+ end
89
+
90
+ block.call
91
+ ensure
92
+ fulfilled = true
93
+ end
94
+ end
91
95
 
92
96
  # Resolve a hostname to IP addresses, caching results for 60 seconds.
93
97
  def address_resolve(hostname)
@@ -13,7 +13,7 @@
13
13
  <script>
14
14
  window.onload = () => {
15
15
  window.ui = SwaggerUIBundle({
16
- url: '<%= spec_url %>',
16
+ url: window.location.pathname.replace(/\/?$/, "/json"),
17
17
  dom_id: '#swagger-ui',
18
18
  });
19
19
  };
@@ -42,11 +42,9 @@ module Rage::OpenAPI
42
42
  # run Rage.openapi.application(namespace: "Api::V2")
43
43
  # end
44
44
  def self.application(namespace: nil)
45
- html_app = ->(env) do
45
+ html_app = ->(_) do
46
46
  __data_cache[[:page, namespace]] ||= begin
47
- scheme, host, path = env["rack.url_scheme"], env["HTTP_HOST"], env["SCRIPT_NAME"]
48
- spec_url = "#{scheme}://#{host}#{path}/json"
49
- page = ERB.new(File.read("#{__dir__}/index.html.erb")).result(binding)
47
+ page = ERB.new(File.read("#{__dir__}/index.html.erb")).result
50
48
 
51
49
  [200, { "content-type" => "text/html; charset=UTF-8" }, [page]]
52
50
  end
@@ -120,7 +118,7 @@ module Rage::OpenAPI
120
118
 
121
119
  # @private
122
120
  def self.__try_parse_collection(str)
123
- if str =~ /^Array<([\w\s:\(\),]+)>$/ || str =~ /^\[([\w\s:\(\),]+)\]$/
121
+ if str =~ /^Array<([\w\s:\(\),'\"]+)>$/ || str =~ /^\[([\w\s:\(\),'\"]+)\]$/
124
122
  [true, $1]
125
123
  else
126
124
  [false, str]
@@ -20,9 +20,10 @@ class Rage::OpenAPI::Parsers::Ext::Blueprinter
20
20
  is_collection, raw_klass_str, serializer_options = Rage::OpenAPI.__parse_serializer_args(klass_str)
21
21
  klass = @namespace.const_get(raw_klass_str)
22
22
  schema = build_schema(klass, is_collection, serializer_options)
23
+ registry_key = "#{raw_klass_str}_#{serializer_options[:view] || :default}"
23
24
 
24
- if @root.schema_registry.key?(raw_klass_str)
25
- @root.schema_registry[raw_klass_str] = is_collection ? schema["items"] : schema
25
+ if @root.schema_registry.key?(registry_key)
26
+ @root.schema_registry[registry_key] = is_collection ? schema["items"] : schema
26
27
  end
27
28
 
28
29
  schema
@@ -32,26 +33,31 @@ class Rage::OpenAPI::Parsers::Ext::Blueprinter
32
33
 
33
34
  private
34
35
 
35
- def build_schema(klass, is_collection, serializer_options = nil)
36
- @parsing_stack.add(klass.name)
37
-
38
- view_name = serializer_options&.key?(:view) ? serializer_options[:view] : :default
36
+ def build_schema(klass, is_collection, serializer_options)
37
+ view_name = serializer_options&.key?(:view) ? serializer_options[:view].to_sym : :default
39
38
  reflections = klass.reflections
40
39
  view = reflections[view_name]
41
40
  raise InvalidViewError, "invalid view #{view_name}" unless view
42
41
 
42
+ @parsing_stack.add([klass.name, view_name])
43
+
43
44
  identifier_fields = extract_fields(reflections[:identifier])
44
45
  default_fields = extract_fields(view)
45
46
  association_fields = extract_associations(view)
46
47
 
47
- @parsing_stack.delete(klass.name)
48
+ @parsing_stack.delete([klass.name, view_name])
48
49
 
49
50
  schema = identifier_fields.merge(default_fields.merge(association_fields).sort.to_h)
50
51
 
51
52
  result = { "type" => "object" }
52
53
  result["properties"] = schema if schema.any?
53
54
  result = { "type" => "array", "items" => result } if is_collection
54
- result
55
+
56
+ if serializer_options&.key?(:root)
57
+ { "type" => "object", "properties" => { serializer_options[:root].to_s => result } }
58
+ else
59
+ result
60
+ end
55
61
  end
56
62
 
57
63
  def extract_fields(view)
@@ -65,14 +71,16 @@ class Rage::OpenAPI::Parsers::Ext::Blueprinter
65
71
  blueprint = association.blueprint
66
72
  name, display_name = association.name.to_s, association.display_name.to_s
67
73
  is_collection = collection_association?(name)
74
+ serializer_options = { view: association.view }
68
75
 
69
76
  item_schema = if blueprint.is_a?(Proc)
70
77
  { "type" => "object" }
71
- elsif @parsing_stack.include?(blueprint.name)
72
- @root.schema_registry[blueprint.name] ||= nil
73
- { "$ref" => "#/components/schemas/#{blueprint.name}" }
78
+ elsif @parsing_stack.include?([blueprint.name, association.view])
79
+ schema_key = "#{blueprint.name}_#{association.view}"
80
+ @root.schema_registry[schema_key] ||= nil
81
+ { "$ref" => "#/components/schemas/#{schema_key}" }
74
82
  else
75
- build_schema(blueprint, false)
83
+ build_schema(blueprint, false, serializer_options)
76
84
  end
77
85
 
78
86
  properties[display_name] = is_collection ? { "type" => "array", "items" => item_schema } : item_schema
@@ -1,6 +1,6 @@
1
1
  This is an almost complete rewrite of https://github.com/delvedor/find-my-way.
2
2
 
3
- Currently, the only constraint supported is the `host` constraint. Regexp constraints are likely to be added. Custom/lambda constraints are unlikely to be added.
3
+ Currently, the only constraint supported is the `host` constraint. The constraint value can be either a string or a regular expression. Custom/lambda constraints are unlikely to be added.
4
4
 
5
5
  Compared to the Rails router, the most notable difference except constraints is that a wildcard segment can only be in the last section of the path and cannot be named.
6
6
 
@@ -35,13 +35,17 @@ class Rage::Router::Backend
35
35
  # by the time the app is called, `rack.input` is already consumed in `Rage::ParamsParser`
36
36
  env["rack.input"].rewind
37
37
 
38
- env["SCRIPT_NAME"] = path
39
- sub_path = env["PATH_INFO"].delete_prefix!(path)
40
- env["PATH_INFO"] = "/" if sub_path == ""
38
+ script_name = env["SCRIPT_NAME"]
39
+ path_info = env["PATH_INFO"]
40
+
41
+ env["SCRIPT_NAME"] = "#{script_name}#{path}"
42
+ sub_path = path_info.delete_prefix(path)
43
+ env["PATH_INFO"] = sub_path == "" ? "/" : sub_path
41
44
 
42
45
  handler.call(env)
43
46
  ensure
44
- env["PATH_INFO"] = "#{env["SCRIPT_NAME"]}#{sub_path}"
47
+ env["SCRIPT_NAME"] = script_name
48
+ env["PATH_INFO"] = path_info
45
49
  end
46
50
 
47
51
  methods.each do |method|
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.26.1"
4
+ VERSION = "1.27.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.26.1
4
+ version: 1.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Samoilov