rage-rb 0.7.0 → 1.1.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/.yardopts +1 -1
- data/CHANGELOG.md +26 -0
- data/README.md +6 -2
- data/lib/rage/all.rb +1 -0
- data/lib/rage/configuration.rb +2 -2
- data/lib/rage/fiber.rb +66 -7
- data/lib/rage/fiber_scheduler.rb +20 -10
- data/lib/rage/logger/json_formatter.rb +5 -3
- data/lib/rage/logger/logger.rb +25 -14
- data/lib/rage/logger/text_formatter.rb +5 -3
- data/lib/rage/middleware/fiber_wrapper.rb +1 -1
- data/lib/rage/rails.rb +15 -6
- data/lib/rage/router/backend.rb +1 -19
- data/lib/rage/router/handler_storage.rb +5 -4
- data/lib/rage/router/util.rb +33 -0
- data/lib/rage/rspec.rb +178 -0
- data/lib/rage/version.rb +1 -1
- data/rage.gemspec +1 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd10469efc73f6134f72f79fe21871238b8860e958c085499c47b312adf91764
|
4
|
+
data.tar.gz: 34990a4885df4a4acd55fcbabf8d3a67e57e0609db406ee55d5f71cab0655ffb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc28afd194f122bf436c215cb6d88dfe3d923874f4e07c3c1db5dd3f33a4676354731fad3f6ffc29326423086b59dbb0d1860ecb5cb71cf4c7f0412a252af16f
|
7
|
+
data.tar.gz: 58cd0cea6daba63d67f9285d3b616980d2e69c778620c54b14c7c5cb1eccf093bfbc0644dec081c325d4cdc5f6c032b7f399ccb8ea5b052c1da401dcde08ed6e
|
data/.yardopts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
--exclude lib/rage/templates --markup markdown --no-private -o doc
|
1
|
+
--exclude lib/rage/templates --exclude lib/rage/rspec --exclude lib/rage/rails --markup markdown --no-private -o doc
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.1.0] - 2024-03-25
|
4
|
+
|
5
|
+
### Changed
|
6
|
+
|
7
|
+
- Change the way controller names are logged (#72).
|
8
|
+
- Use formatters in console (#71).
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
|
12
|
+
- Fix Fiber.await behavior in RSpec (#70).
|
13
|
+
|
14
|
+
## [1.0.0] - 2024-03-13
|
15
|
+
|
16
|
+
### Added
|
17
|
+
|
18
|
+
- RSpec integration (#60).
|
19
|
+
- Add DNS cache (#65).
|
20
|
+
- Allow to disable the `FiberScheduler#io_write` hook (#63).
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
|
24
|
+
- Preload fiber ID (#62).
|
25
|
+
- Release ActiveRecord connections on yield (#66).
|
26
|
+
- Logger fixes (#64).
|
27
|
+
- Fix publish calls in cluster mode (#67).
|
28
|
+
|
3
29
|
## [0.7.0] - 2024-01-09
|
4
30
|
|
5
31
|
- Add conditional GET using `stale?` by [@tonekk](https://github.com/tonekk) (#55).
|
data/README.md
CHANGED
@@ -53,8 +53,12 @@ Check out in-depth API docs for more information:
|
|
53
53
|
- [Fiber API](https://rage-rb.pages.dev/Fiber)
|
54
54
|
- [Logger API](https://rage-rb.pages.dev/Rage/Logger)
|
55
55
|
- [Configuration API](https://rage-rb.pages.dev/Rage/Configuration)
|
56
|
+
- [CORS middleware](https://rage-rb.pages.dev/Rage/Cors)
|
56
57
|
|
57
|
-
Also, see the
|
58
|
+
Also, see the following integration guides:
|
59
|
+
|
60
|
+
- [Rails integration](https://github.com/rage-rb/rage/wiki/Rails-integration)
|
61
|
+
- [RSpec integration](https://github.com/rage-rb/rage/wiki/RSpec-integration)
|
58
62
|
|
59
63
|
### Example
|
60
64
|
|
@@ -150,8 +154,8 @@ Status | Changes
|
|
150
154
|
:white_check_mark: | ~~Expose the `params` object.<br>Support header authentication with `authenticate_with_http_token`.<br>Router updates:<br> • add the `resources` route helper;<br> • add the `namespace` route helper;~~
|
151
155
|
:white_check_mark: | ~~Add request logging.~~
|
152
156
|
:white_check_mark: | ~~Automatic code reloading in development with Zeitwerk.~~
|
157
|
+
:white_check_mark: | ~~Support conditional get with `etag` and `last_modified`.~~
|
153
158
|
⏳ | Expose the `send_data` and `send_file` methods.
|
154
|
-
⏳ | Support conditional get with `etag` and `last_modified`.
|
155
159
|
⏳ | Expose the `cookies` and `session` objects.
|
156
160
|
⏳ | Implement Iodine-based equivalent of Action Cable.
|
157
161
|
|
data/lib/rage/all.rb
CHANGED
data/lib/rage/configuration.rb
CHANGED
@@ -19,7 +19,7 @@
|
|
19
19
|
#
|
20
20
|
# • _config.middleware.use_
|
21
21
|
#
|
22
|
-
# > Adds a middleware to the top of the middleware stack. **This is the
|
22
|
+
# > Adds a middleware to the top of the middleware stack. **This is the recommended way of adding a middleware.**
|
23
23
|
# > ```
|
24
24
|
# config.middleware.use Rack::Cors do
|
25
25
|
# allow do
|
@@ -166,7 +166,7 @@ class Rage::Configuration
|
|
166
166
|
|
167
167
|
# @private
|
168
168
|
class Internal
|
169
|
-
attr_accessor :rails_mode
|
169
|
+
attr_accessor :rails_mode
|
170
170
|
|
171
171
|
def inspect
|
172
172
|
"#<#{self.class.name}>"
|
data/lib/rage/fiber.rb
CHANGED
@@ -1,6 +1,46 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
##
|
4
|
+
# Rage provides a simple and efficient API to wait on several instances of IO at the same time - {Fiber.await}.
|
5
|
+
#
|
6
|
+
# Let's say we have the following controller:
|
7
|
+
# ```ruby
|
8
|
+
# class UsersController < RageController::API
|
9
|
+
# def show
|
10
|
+
# user = Net::HTTP.get(URI("http://users.service/users/#{params[:id]}"))
|
11
|
+
# bookings = Net::HTTP.get(URI("http://bookings.service/bookings?user_id=#{params[:id]}"))
|
12
|
+
# render json: { user: user, bookings: bookings }
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
# ```
|
16
|
+
# This code will fire two consecutive HTTP requests. If each request takes 1 second to execute, the total execution time will be 2 seconds.<br>
|
17
|
+
# With {Fiber.await}, we can significantly decrease the overall execution time by changing the code to fire the requests concurrently.
|
18
|
+
#
|
19
|
+
# To do this, we will need to:
|
20
|
+
#
|
21
|
+
# 1. Wrap every request in a separate fiber using {Fiber.schedule};
|
22
|
+
# 2. Pass newly created fibers into {Fiber.await};
|
23
|
+
#
|
24
|
+
# ```ruby
|
25
|
+
# class UsersController < RageController::API
|
26
|
+
# def show
|
27
|
+
# user, bookings = Fiber.await([
|
28
|
+
# Fiber.schedule { Net::HTTP.get(URI("http://users.service/users/#{params[:id]}")) },
|
29
|
+
# Fiber.schedule { Net::HTTP.get(URI("http://bookings.service/bookings?user_id=#{params[:id]}")) }
|
30
|
+
# ])
|
31
|
+
#
|
32
|
+
# render json: { user: user, bookings: bookings }
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
# ```
|
36
|
+
# With this change, if each request takes 1 second to execute, the total execution time will still be 1 second.
|
37
|
+
#
|
38
|
+
# ## Creating fibers
|
39
|
+
# Many developers see fibers as "lightweight threads" that should be used in conjunction with fiber pools, the same way we use thread pools for threads.<br>
|
40
|
+
# Instead, it makes sense to think of fibers as regular Ruby objects. We don't use a pool of arrays when we need to create an array - we create a new object and let Ruby and the GC do their job.<br>
|
41
|
+
# Same applies to fibers. Feel free to create as many fibers as you need on demand.
|
3
42
|
class Fiber
|
43
|
+
# @private
|
4
44
|
AWAIT_ERROR_MESSAGE = "err"
|
5
45
|
|
6
46
|
# @private
|
@@ -23,14 +63,14 @@ class Fiber
|
|
23
63
|
@__err
|
24
64
|
end
|
25
65
|
|
26
|
-
|
27
|
-
def
|
28
|
-
@__rage_id
|
66
|
+
# @private
|
67
|
+
def __set_id
|
68
|
+
@__rage_id = object_id.to_s
|
29
69
|
end
|
30
70
|
|
31
|
-
|
32
|
-
def
|
33
|
-
|
71
|
+
# @private
|
72
|
+
def __get_id
|
73
|
+
@__rage_id
|
34
74
|
end
|
35
75
|
|
36
76
|
# @private
|
@@ -49,8 +89,15 @@ class Fiber
|
|
49
89
|
Fiber.yield
|
50
90
|
end
|
51
91
|
|
92
|
+
# @private
|
93
|
+
# under normal circumstances, the method is a copy of `yield`, but it can be overriden to perform
|
94
|
+
# additional steps on yielding, e.g. releasing AR connections; see "lib/rage/rails.rb"
|
95
|
+
class << self
|
96
|
+
alias_method :defer, :yield
|
97
|
+
end
|
98
|
+
|
52
99
|
# Wait on several fibers at the same time. Calling this method will automatically pause the current fiber, allowing the
|
53
|
-
#
|
100
|
+
# server to process other requests. Once all fibers have completed, the current fiber will be automatically resumed.
|
54
101
|
#
|
55
102
|
# @param fibers [Fiber, Array<Fiber>] one or several fibers to wait on. The fibers must be created using the `Fiber.schedule` call.
|
56
103
|
# @example
|
@@ -101,4 +148,16 @@ class Fiber
|
|
101
148
|
fibers.map!(&:__get_result)
|
102
149
|
end
|
103
150
|
end
|
151
|
+
|
152
|
+
# @!method self.schedule(&block)
|
153
|
+
# Create a non-blocking fiber. Should mostly be used in conjunction with `Fiber.await`.
|
154
|
+
# @example
|
155
|
+
# Fiber.await([
|
156
|
+
# Fiber.schedule { request_1 },
|
157
|
+
# Fiber.schedule { request_2 }
|
158
|
+
# ])
|
159
|
+
# @example
|
160
|
+
# fiber_1 = Fiber.schedule { request_1 }
|
161
|
+
# fiber_2 = Fiber.schedule { request_2 }
|
162
|
+
# Fiber.await([fiber_1, fiber_2])
|
104
163
|
end
|
data/lib/rage/fiber_scheduler.rb
CHANGED
@@ -7,13 +7,14 @@ class Rage::FiberScheduler
|
|
7
7
|
|
8
8
|
def initialize
|
9
9
|
@root_fiber = Fiber.current
|
10
|
+
@dns_cache = {}
|
10
11
|
end
|
11
12
|
|
12
13
|
def io_wait(io, events, timeout = nil)
|
13
14
|
f = Fiber.current
|
14
15
|
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { |err| f.resume(err) }
|
15
16
|
|
16
|
-
err = Fiber.
|
17
|
+
err = Fiber.defer
|
17
18
|
if err == Errno::ETIMEDOUT::Errno
|
18
19
|
0
|
19
20
|
else
|
@@ -49,13 +50,15 @@ class Rage::FiberScheduler
|
|
49
50
|
end
|
50
51
|
end
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
53
|
+
unless ENV["RAGE_DISABLE_IO_WRITE"]
|
54
|
+
def io_write(io, buffer, length, offset = 0)
|
55
|
+
bytes_to_write = length
|
56
|
+
bytes_to_write = buffer.size if length == 0
|
55
57
|
|
56
|
-
|
58
|
+
::Iodine::Scheduler.write(io.fileno, buffer.get_string, bytes_to_write, offset)
|
57
59
|
|
58
|
-
|
60
|
+
bytes_to_write - offset
|
61
|
+
end
|
59
62
|
end
|
60
63
|
|
61
64
|
def kernel_sleep(duration = nil)
|
@@ -77,7 +80,13 @@ class Rage::FiberScheduler
|
|
77
80
|
# end
|
78
81
|
|
79
82
|
def address_resolve(hostname)
|
80
|
-
|
83
|
+
@dns_cache[hostname] ||= begin
|
84
|
+
::Iodine.run_after(60_000) do
|
85
|
+
@dns_cache[hostname] = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
Resolv.getaddresses(hostname)
|
89
|
+
end
|
81
90
|
end
|
82
91
|
|
83
92
|
def block(_blocker, timeout = nil)
|
@@ -100,7 +109,7 @@ class Rage::FiberScheduler
|
|
100
109
|
end
|
101
110
|
|
102
111
|
def unblock(_blocker, fiber)
|
103
|
-
::Iodine.publish(fiber.__block_channel, "")
|
112
|
+
::Iodine.publish(fiber.__block_channel, "", Iodine::PubSub::PROCESS)
|
104
113
|
end
|
105
114
|
|
106
115
|
def fiber(&block)
|
@@ -109,6 +118,7 @@ class Rage::FiberScheduler
|
|
109
118
|
fiber = if parent == @root_fiber
|
110
119
|
# the fiber to wrap a request in
|
111
120
|
Fiber.new(blocking: false) do
|
121
|
+
Fiber.current.__set_id
|
112
122
|
Fiber.current.__set_result(block.call)
|
113
123
|
end
|
114
124
|
else
|
@@ -119,10 +129,10 @@ class Rage::FiberScheduler
|
|
119
129
|
Thread.current[:rage_logger] = logger
|
120
130
|
Fiber.current.__set_result(block.call)
|
121
131
|
# send a message for `Fiber.await` to work
|
122
|
-
Iodine.publish("await:#{parent.object_id}", "") if parent.alive?
|
132
|
+
Iodine.publish("await:#{parent.object_id}", "", Iodine::PubSub::PROCESS) if parent.alive?
|
123
133
|
rescue Exception => e
|
124
134
|
Fiber.current.__set_err(e)
|
125
|
-
Iodine.publish("await:#{parent.object_id}", Fiber::AWAIT_ERROR_MESSAGE) if parent.alive?
|
135
|
+
Iodine.publish("await:#{parent.object_id}", Fiber::AWAIT_ERROR_MESSAGE, Iodine::PubSub::PROCESS) if parent.alive?
|
126
136
|
end
|
127
137
|
end
|
128
138
|
|
@@ -7,7 +7,7 @@ class Rage::JSONFormatter
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def call(severity, timestamp, _, message)
|
10
|
-
logger = Thread.current[:rage_logger]
|
10
|
+
logger = Thread.current[:rage_logger] || { tags: [], context: {} }
|
11
11
|
tags, context = logger[:tags], logger[:context]
|
12
12
|
|
13
13
|
if !context.empty?
|
@@ -17,8 +17,8 @@ class Rage::JSONFormatter
|
|
17
17
|
|
18
18
|
if final = logger[:final]
|
19
19
|
params, env = final[:params], final[:env]
|
20
|
-
if params
|
21
|
-
return "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"info\",\"method\":\"#{env["REQUEST_METHOD"]}\",\"path\":\"#{env["PATH_INFO"]}\",\"controller\":\"#{params[:controller]}\",\"action\":\"#{params[:action]}\",#{context_msg}\"status\":#{final[:response][0]},\"duration\":#{final[:duration]}}\n"
|
20
|
+
if params && params[:controller]
|
21
|
+
return "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"info\",\"method\":\"#{env["REQUEST_METHOD"]}\",\"path\":\"#{env["PATH_INFO"]}\",\"controller\":\"#{Rage::Router::Util.path_to_name(params[:controller])}\",\"action\":\"#{params[:action]}\",#{context_msg}\"status\":#{final[:response][0]},\"duration\":#{final[:duration]}}\n"
|
22
22
|
else
|
23
23
|
# no controller/action keys are written if there are no params
|
24
24
|
return "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"info\",\"method\":\"#{env["REQUEST_METHOD"]}\",\"path\":\"#{env["PATH_INFO"]}\",#{context_msg}\"status\":#{final[:response][0]},\"duration\":#{final[:duration]}}\n"
|
@@ -29,6 +29,8 @@ class Rage::JSONFormatter
|
|
29
29
|
tags_msg = "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
|
30
30
|
elsif tags.length == 2
|
31
31
|
tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
|
32
|
+
elsif tags.length == 0
|
33
|
+
tags_msg = "{\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
|
32
34
|
else
|
33
35
|
tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\""
|
34
36
|
i = 2
|
data/lib/rage/logger/logger.rb
CHANGED
@@ -33,6 +33,23 @@ require "logger"
|
|
33
33
|
# Rage.logger.info("Initializing")
|
34
34
|
# Rage.logger.debug { "This is a " + potentially + " expensive operation" }
|
35
35
|
# ```
|
36
|
+
#
|
37
|
+
# ## Using the logger
|
38
|
+
# The recommended approach to logging with Rage is to make sure your code always logs the same message no matter what the input is.
|
39
|
+
# You can achieve this by using the {with_context} and {tagged} methods. So, a code like this:
|
40
|
+
# ```ruby
|
41
|
+
# def process_purchase(user_id:, product_id:)
|
42
|
+
# Rage.logger.info "processing purchase with user_id = #{user_id}; product_id = #{product_id}"
|
43
|
+
# end
|
44
|
+
# ```
|
45
|
+
# turns into this:
|
46
|
+
# ```ruby
|
47
|
+
# def process_purchase(user_id:, product_id:)
|
48
|
+
# Rage.logger.with_context(user_id: user_id, product_id: product_id) do
|
49
|
+
# Rage.logger.info "processing purchase"
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
# ```
|
36
53
|
class Rage::Logger
|
37
54
|
METHODS_MAP = {
|
38
55
|
"debug" => Logger::DEBUG,
|
@@ -83,7 +100,7 @@ class Rage::Logger
|
|
83
100
|
# Rage.logger.info "cache miss"
|
84
101
|
# end
|
85
102
|
def with_context(context)
|
86
|
-
old_context = Thread.current[:rage_logger][:context]
|
103
|
+
old_context = (Thread.current[:rage_logger] ||= { tags: [], context: {} })[:context]
|
87
104
|
|
88
105
|
if old_context.empty? # there's nothing in the context yet
|
89
106
|
Thread.current[:rage_logger][:context] = context
|
@@ -92,8 +109,6 @@ class Rage::Logger
|
|
92
109
|
end
|
93
110
|
|
94
111
|
yield(self)
|
95
|
-
true
|
96
|
-
|
97
112
|
ensure
|
98
113
|
Thread.current[:rage_logger][:context] = old_context
|
99
114
|
end
|
@@ -106,17 +121,20 @@ class Rage::Logger
|
|
106
121
|
# Rage.logger.info "success"
|
107
122
|
# end
|
108
123
|
def tagged(tag)
|
109
|
-
Thread.current[:rage_logger][:tags] << tag
|
110
|
-
|
124
|
+
(Thread.current[:rage_logger] ||= { tags: [], context: {} })[:tags] << tag
|
111
125
|
yield(self)
|
112
|
-
true
|
113
|
-
|
114
126
|
ensure
|
115
127
|
Thread.current[:rage_logger][:tags].pop
|
116
128
|
end
|
117
129
|
|
118
130
|
alias_method :with_tag, :tagged
|
119
131
|
|
132
|
+
def debug? = @level <= Logger::DEBUG
|
133
|
+
def error? = @level <= Logger::ERROR
|
134
|
+
def fatal? = @level <= Logger::FATAL
|
135
|
+
def info? = @level <= Logger::INFO
|
136
|
+
def warn? = @level <= Logger::WARN
|
137
|
+
|
120
138
|
private
|
121
139
|
|
122
140
|
def define_log_methods
|
@@ -128,13 +146,6 @@ class Rage::Logger
|
|
128
146
|
false
|
129
147
|
end
|
130
148
|
RUBY
|
131
|
-
elsif (Rage.config.internal.rails_mode ? Rage.config.internal.rails_console : defined?(IRB))
|
132
|
-
# the call was made from the console - don't use the formatter
|
133
|
-
<<-RUBY
|
134
|
-
def #{level_name}(msg = nil)
|
135
|
-
@logdev.write((msg || yield) + "\n")
|
136
|
-
end
|
137
|
-
RUBY
|
138
149
|
elsif @formatter.class.name.start_with?("Rage::")
|
139
150
|
# the call was made from within the application and a built-in formatter is used;
|
140
151
|
# in such case we use the `gen_timestamp` method which is much faster than `Time.now.strftime`;
|
@@ -7,7 +7,7 @@ class Rage::TextFormatter
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def call(severity, timestamp, _, message)
|
10
|
-
logger = Thread.current[:rage_logger]
|
10
|
+
logger = Thread.current[:rage_logger] || { tags: [], context: {} }
|
11
11
|
tags, context = logger[:tags], logger[:context]
|
12
12
|
|
13
13
|
if !context.empty?
|
@@ -17,8 +17,8 @@ class Rage::TextFormatter
|
|
17
17
|
|
18
18
|
if final = logger[:final]
|
19
19
|
params, env = final[:params], final[:env]
|
20
|
-
if params
|
21
|
-
return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} controller=#{params[:controller]} action=#{params[:action]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
|
20
|
+
if params && params[:controller]
|
21
|
+
return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} controller=#{Rage::Router::Util.path_to_name(params[:controller])} action=#{params[:action]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
|
22
22
|
else
|
23
23
|
# no controller/action keys are written if there are no params
|
24
24
|
return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
|
@@ -29,6 +29,8 @@ class Rage::TextFormatter
|
|
29
29
|
tags_msg = "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
30
30
|
elsif tags.length == 2
|
31
31
|
tags_msg = "[#{tags[0]}][#{tags[1]}] timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
32
|
+
elsif tags.length == 0
|
33
|
+
tags_msg = "timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
32
34
|
else
|
33
35
|
tags_msg = "[#{tags[0]}][#{tags[1]}]"
|
34
36
|
i = 2
|
@@ -17,7 +17,7 @@ class Rage::FiberWrapper
|
|
17
17
|
@app.call(env)
|
18
18
|
ensure
|
19
19
|
# notify Iodine the request can now be resumed
|
20
|
-
Iodine.publish(Fiber.current.__get_id, "", Iodine::PubSub::PROCESS)
|
20
|
+
Iodine.publish(Fiber.current.__get_id, "", Iodine::PubSub::PROCESS)
|
21
21
|
end
|
22
22
|
|
23
23
|
# the fiber encountered blocking IO and yielded; instruct Iodine to pause the request
|
data/lib/rage/rails.rb
CHANGED
@@ -11,12 +11,6 @@ Iodine.patch_rack
|
|
11
11
|
# configure the framework
|
12
12
|
Rage.config.internal.rails_mode = true
|
13
13
|
|
14
|
-
# make sure log formatter is not used in console
|
15
|
-
Rails.application.console do
|
16
|
-
Rage.config.internal.rails_console = true
|
17
|
-
Rage.logger.level = Rage.logger.level if Rage.logger # trigger redefining log methods
|
18
|
-
end
|
19
|
-
|
20
14
|
# patch ActiveRecord's connection pool
|
21
15
|
if defined?(ActiveRecord)
|
22
16
|
Rails.configuration.after_initialize do
|
@@ -30,6 +24,21 @@ if defined?(ActiveRecord)
|
|
30
24
|
end
|
31
25
|
end
|
32
26
|
|
27
|
+
# release ActiveRecord connections on yield
|
28
|
+
if defined?(ActiveRecord)
|
29
|
+
class Fiber
|
30
|
+
def self.defer
|
31
|
+
res = Fiber.yield
|
32
|
+
|
33
|
+
if ActiveRecord::Base.connection_pool.active_connection?
|
34
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!
|
35
|
+
end
|
36
|
+
|
37
|
+
res
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
33
42
|
# plug into Rails' Zeitwerk instance to reload the code
|
34
43
|
Rails.autoloaders.main.on_setup do
|
35
44
|
if Iodine.running?
|
data/lib/rage/router/backend.rb
CHANGED
@@ -67,7 +67,7 @@ class Rage::Router::Backend
|
|
67
67
|
if handler.is_a?(String)
|
68
68
|
raise "Invalid route handler format, expected to match the 'controller#action' pattern" unless handler =~ STRING_HANDLER_REGEXP
|
69
69
|
|
70
|
-
controller, action =
|
70
|
+
controller, action = Rage::Router::Util.path_to_class($1), $2
|
71
71
|
run_action_method_name = controller.__register_action(action.to_sym)
|
72
72
|
|
73
73
|
meta[:controller] = $1
|
@@ -273,22 +273,4 @@ class Rage::Router::Backend
|
|
273
273
|
end
|
274
274
|
end
|
275
275
|
end
|
276
|
-
|
277
|
-
def to_controller_class(str)
|
278
|
-
str.capitalize!
|
279
|
-
str.gsub!(/([\/_])([a-zA-Z0-9]+)/) do
|
280
|
-
if $1 == "/"
|
281
|
-
"::#{$2.capitalize}"
|
282
|
-
else
|
283
|
-
$2.capitalize
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
klass = "#{str}Controller"
|
288
|
-
if Object.const_defined?(klass)
|
289
|
-
Object.const_get(klass)
|
290
|
-
else
|
291
|
-
raise Rage::Errors::RouterError, "Routing error: could not find the #{klass} class"
|
292
|
-
end
|
293
|
-
end
|
294
276
|
end
|
@@ -48,10 +48,11 @@ class Rage::Router::HandlerStorage
|
|
48
48
|
private
|
49
49
|
|
50
50
|
def compile_create_params_object(param_keys, defaults, meta)
|
51
|
-
lines = [
|
52
|
-
":controller => '#{meta[:controller]}'.freeze",
|
53
|
-
|
54
|
-
|
51
|
+
lines = if meta[:controller]
|
52
|
+
[":controller => '#{meta[:controller]}'.freeze", ":action => '#{meta[:action]}'.freeze"]
|
53
|
+
else
|
54
|
+
[]
|
55
|
+
end
|
55
56
|
|
56
57
|
param_keys.each_with_index do |key, i|
|
57
58
|
lines << ":#{key} => param_values[#{i}]"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Rage::Router::Util
|
2
|
+
class << self
|
3
|
+
# converts controller name in a path form into a class
|
4
|
+
# `api/v1/users` => `Api::V1::UsersController`
|
5
|
+
def path_to_class(str)
|
6
|
+
str = str.capitalize
|
7
|
+
str.gsub!(/([\/_])([a-zA-Z0-9]+)/) do
|
8
|
+
if $1 == "/"
|
9
|
+
"::#{$2.capitalize}"
|
10
|
+
else
|
11
|
+
$2.capitalize
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
klass = "#{str}Controller"
|
16
|
+
if Object.const_defined?(klass)
|
17
|
+
Object.const_get(klass)
|
18
|
+
else
|
19
|
+
raise Rage::Errors::RouterError, "Routing error: could not find the #{klass} class"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
@@names_map = {}
|
24
|
+
|
25
|
+
# converts controller name in a path form into a string representation of a class
|
26
|
+
# `api/v1/users` => `"Api::V1::UsersController"`
|
27
|
+
def path_to_name(str)
|
28
|
+
@@names_map[str] || begin
|
29
|
+
@@names_map[str] = path_to_class(str).name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/rage/rspec.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/test"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
# set up environment
|
7
|
+
ENV["RAGE_ENV"] ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test"
|
8
|
+
|
9
|
+
# load the app
|
10
|
+
require "bundler/setup"
|
11
|
+
require "rage"
|
12
|
+
require_relative "#{Rage.root}/config/application"
|
13
|
+
|
14
|
+
# verify the environment
|
15
|
+
abort("The test suite is running in #{Rage.env} mode instead of 'test'!") unless Rage.env.test?
|
16
|
+
|
17
|
+
# mock fiber methods as RSpec tests don't run concurrently
|
18
|
+
class Fiber
|
19
|
+
def self.schedule(&block)
|
20
|
+
fiber = Fiber.new(blocking: true) do
|
21
|
+
Fiber.current.__set_id
|
22
|
+
Fiber.current.__set_result(block.call)
|
23
|
+
end
|
24
|
+
fiber.resume
|
25
|
+
|
26
|
+
fiber
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.await(fibers)
|
30
|
+
Array(fibers).map(&:__get_result)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# define request helpers
|
35
|
+
module RageRequestHelpers
|
36
|
+
include Rack::Test::Methods
|
37
|
+
|
38
|
+
alias_method :response, :last_response
|
39
|
+
|
40
|
+
APP = Rack::Builder.parse_file("#{Rage.root}/config.ru").yield_self do |app|
|
41
|
+
app.is_a?(Array) ? app[0] : app
|
42
|
+
end
|
43
|
+
|
44
|
+
def app
|
45
|
+
APP
|
46
|
+
end
|
47
|
+
|
48
|
+
%w(get options head).each do |method_name|
|
49
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
50
|
+
def #{method_name}(path, params: {}, headers: {})
|
51
|
+
request("#{method_name.upcase}", path, params: params, headers: headers)
|
52
|
+
end
|
53
|
+
RUBY
|
54
|
+
end
|
55
|
+
|
56
|
+
%w(post put patch delete).each do |method_name|
|
57
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
58
|
+
def #{method_name}(path, params: {}, headers: {}, as: nil)
|
59
|
+
if as == :json
|
60
|
+
params = params.to_json
|
61
|
+
headers["content-type"] = "application/json"
|
62
|
+
end
|
63
|
+
|
64
|
+
request("#{method_name.upcase}", path, params: params, headers: headers.merge("IODINE_HAS_BODY" => !params.empty?))
|
65
|
+
end
|
66
|
+
RUBY
|
67
|
+
end
|
68
|
+
|
69
|
+
def request(method, path, params: {}, headers: {})
|
70
|
+
if headers.any?
|
71
|
+
headers = headers.transform_keys do |k|
|
72
|
+
if k.downcase == "content-type"
|
73
|
+
"CONTENT_TYPE"
|
74
|
+
elsif k.downcase == "content-length"
|
75
|
+
"CONTENT_LENGTH"
|
76
|
+
elsif k.upcase == k
|
77
|
+
k
|
78
|
+
else
|
79
|
+
"HTTP_#{k.tr("-", "_").upcase! || k}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
custom_request(method, path, params, headers)
|
85
|
+
end
|
86
|
+
|
87
|
+
def host!(host)
|
88
|
+
@__host = host
|
89
|
+
end
|
90
|
+
|
91
|
+
def default_host
|
92
|
+
@__host || "example.org"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# include request helpers
|
97
|
+
RSpec.configure do |config|
|
98
|
+
config.include(RageRequestHelpers, type: :request)
|
99
|
+
end
|
100
|
+
|
101
|
+
# patch MockResponse class
|
102
|
+
class Rack::MockResponse
|
103
|
+
def parsed_body
|
104
|
+
if headers["content-type"].start_with?("application/json")
|
105
|
+
JSON.parse(body)
|
106
|
+
else
|
107
|
+
body
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def code
|
112
|
+
status.to_s
|
113
|
+
end
|
114
|
+
|
115
|
+
alias_method :response_code, :status
|
116
|
+
end
|
117
|
+
|
118
|
+
# define http status matcher
|
119
|
+
RSpec::Matchers.matcher :have_http_status do |expected|
|
120
|
+
codes = Rack::Utils::SYMBOL_TO_STATUS_CODE
|
121
|
+
|
122
|
+
failure_message do |response|
|
123
|
+
actual = response.status
|
124
|
+
|
125
|
+
if expected.is_a?(Integer)
|
126
|
+
"expected the response to have status code #{expected} but it was #{actual}"
|
127
|
+
elsif expected == :success
|
128
|
+
"expected the response to have a success status code (2xx) but it was #{actual}"
|
129
|
+
elsif expected == :error
|
130
|
+
"expected the response to have an error status code (5xx) but it was #{actual}"
|
131
|
+
elsif expected == :missing
|
132
|
+
"expected the response to have a missing status code (404) but it was #{actual}"
|
133
|
+
else
|
134
|
+
"expected the response to have status code :#{expected} (#{codes[expected]}) but it was :#{codes.key(actual)} (#{actual})"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
failure_message_when_negated do |response|
|
139
|
+
actual = response.status
|
140
|
+
|
141
|
+
if expected.is_a?(Integer)
|
142
|
+
"expected the response not to have status code #{expected} but it was #{actual}"
|
143
|
+
elsif expected == :success
|
144
|
+
"expected the response not to have a success status code (2xx) but it was #{actual}"
|
145
|
+
elsif expected == :error
|
146
|
+
"expected the response not to have an error status code (5xx) but it was #{actual}"
|
147
|
+
elsif expected == :missing
|
148
|
+
"expected the response not to have a missing status code (404) but it was #{actual}"
|
149
|
+
else
|
150
|
+
"expected the response not to have status code :#{expected} (#{codes[expected]}) but it was :#{codes.key(actual)} (#{actual})"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
match do |response|
|
155
|
+
actual = response.status
|
156
|
+
|
157
|
+
case expected
|
158
|
+
when :success
|
159
|
+
actual >= 200 && actual < 300
|
160
|
+
when :error
|
161
|
+
actual >= 500
|
162
|
+
when :missing
|
163
|
+
actual == 404
|
164
|
+
when Symbol
|
165
|
+
actual == codes.fetch(expected)
|
166
|
+
else
|
167
|
+
actual == expected
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
if defined? RSpec::Rails::Matchers
|
173
|
+
module RSpec::Rails::Matchers
|
174
|
+
def have_http_status(_)
|
175
|
+
super
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/rage/version.rb
CHANGED
data/rage.gemspec
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:
|
4
|
+
version: 1.1.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: 2024-
|
11
|
+
date: 2024-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '2.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.1'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.1'
|
69
83
|
description:
|
70
84
|
email:
|
71
85
|
- rsamoi@icloud.com
|
@@ -112,6 +126,8 @@ files:
|
|
112
126
|
- lib/rage/router/handler_storage.rb
|
113
127
|
- lib/rage/router/node.rb
|
114
128
|
- lib/rage/router/strategies/host.rb
|
129
|
+
- lib/rage/router/util.rb
|
130
|
+
- lib/rage/rspec.rb
|
115
131
|
- lib/rage/setup.rb
|
116
132
|
- lib/rage/sidekiq_session.rb
|
117
133
|
- lib/rage/templates/Gemfile
|