rage-rb 0.7.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|