rage-rb 0.6.0 → 1.0.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 +21 -0
- data/README.md +11 -11
- data/lib/rage/all.rb +2 -0
- data/lib/rage/application.rb +21 -3
- data/lib/rage/cli.rb +8 -1
- data/lib/rage/code_loader.rb +13 -1
- data/lib/rage/configuration.rb +13 -0
- data/lib/rage/controller/api.rb +97 -9
- data/lib/rage/fiber.rb +14 -6
- data/lib/rage/fiber_scheduler.rb +20 -10
- data/lib/rage/logger/json_formatter.rb +46 -0
- data/lib/rage/logger/logger.rb +10 -9
- data/lib/rage/logger/text_formatter.rb +11 -11
- data/lib/rage/middleware/cors.rb +3 -1
- data/lib/rage/middleware/fiber_wrapper.rb +1 -1
- data/lib/rage/rails.rb +77 -0
- data/lib/rage/request.rb +51 -0
- data/lib/rage/response.rb +21 -0
- data/lib/rage/router/dsl.rb +3 -1
- data/lib/rage/rspec.rb +178 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +18 -0
- data/rage.gemspec +1 -0
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9bb19c01aee898bea43a41450feeb655f94bde8277a4f9c18cab6b9d1accbb47
|
4
|
+
data.tar.gz: fe4806d8a2bfbb71496a720371cc8416b5283c9b8284c1a72ec46384ee234348
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43d06881f297512724588c06637a29eefcb8308fc1c086a09b013c4291d4ca58cf7b2ab482b1c1276a3e4a88544a71e95289268cc896d479cb92135f31555bf8
|
7
|
+
data.tar.gz: 9af4f8816208451c18ff3b31d7d5a7e230561af93753d1ee0f8177f515b92f3dd2c47007b7f65be823ceeb69b6338683a05c57eb1a13e17cfe769b87e4ef2540
|
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,26 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.0.0] - 2024-03-13
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- RSpec integration (#60).
|
8
|
+
- Add DNS cache (#65).
|
9
|
+
- Allow to disable the `FiberScheduler#io_write` hook (#63).
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- Preload fiber ID (#62).
|
14
|
+
- Release ActiveRecord connections on yield (#66).
|
15
|
+
- Logger fixes (#64).
|
16
|
+
- Fix publish calls in cluster mode (#67).
|
17
|
+
|
18
|
+
## [0.7.0] - 2024-01-09
|
19
|
+
|
20
|
+
- Add conditional GET using `stale?` by [@tonekk](https://github.com/tonekk) (#55).
|
21
|
+
- Add Rails integration (#57).
|
22
|
+
- Add JSON log formatter (#59).
|
23
|
+
|
3
24
|
## [0.6.0] - 2023-12-22
|
4
25
|
|
5
26
|
### Added
|
data/README.md
CHANGED
@@ -143,17 +143,17 @@ end
|
|
143
143
|
|
144
144
|
## Upcoming releases
|
145
145
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
146
|
+
Status | Changes
|
147
|
+
-- | ------------
|
148
|
+
:white_check_mark: | ~~Gem configuration by env.<br>Add `skip_before_action`.<br>Add `rescue_from`.<br>Router updates:<br> • make the `root` helper work correctly with `scope`;<br> • support the `defaults` option;~~
|
149
|
+
:white_check_mark: | ~~CLI updates:<br> • `routes` task;<br> • `console` task;<br>Support the `:if` and `:unless` options in `before_action`.<br>Allow to set response headers.~~
|
150
|
+
: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
|
+
:white_check_mark: | ~~Add request logging.~~
|
152
|
+
:white_check_mark: | ~~Automatic code reloading in development with Zeitwerk.~~
|
153
|
+
:white_check_mark: | ~~Support conditional get with `etag` and `last_modified`.~~
|
154
|
+
⏳ | Expose the `send_data` and `send_file` methods.
|
155
|
+
⏳ | Expose the `cookies` and `session` objects.
|
156
|
+
⏳ | Implement Iodine-based equivalent of Action Cable.
|
157
157
|
|
158
158
|
## Development
|
159
159
|
|
data/lib/rage/all.rb
CHANGED
@@ -6,6 +6,7 @@ require_relative "fiber"
|
|
6
6
|
require_relative "fiber_scheduler"
|
7
7
|
require_relative "configuration"
|
8
8
|
require_relative "request"
|
9
|
+
require_relative "response"
|
9
10
|
require_relative "uploaded_file"
|
10
11
|
require_relative "errors"
|
11
12
|
require_relative "params_parser"
|
@@ -21,6 +22,7 @@ require_relative "router/node"
|
|
21
22
|
require_relative "controller/api"
|
22
23
|
|
23
24
|
require_relative "logger/text_formatter"
|
25
|
+
require_relative "logger/json_formatter"
|
24
26
|
require_relative "logger/logger"
|
25
27
|
|
26
28
|
require_relative "middleware/fiber_wrapper"
|
data/lib/rage/application.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
class Rage::Application
|
4
4
|
def initialize(router)
|
5
5
|
@router = router
|
6
|
+
@exception_app = build_exception_app
|
6
7
|
end
|
7
8
|
|
8
9
|
def call(env)
|
@@ -17,10 +18,11 @@ class Rage::Application
|
|
17
18
|
[404, {}, ["Not Found"]]
|
18
19
|
end
|
19
20
|
|
21
|
+
rescue Rage::Errors::BadRequest => e
|
22
|
+
response = @exception_app.call(400, e)
|
23
|
+
|
20
24
|
rescue Exception => e
|
21
|
-
|
22
|
-
Rage.logger.error(exception_str)
|
23
|
-
response = [500, {}, [exception_str]]
|
25
|
+
response = @exception_app.call(500, e)
|
24
26
|
|
25
27
|
ensure
|
26
28
|
finalize_logger(env, response, params)
|
@@ -50,4 +52,20 @@ class Rage::Application
|
|
50
52
|
Rage.logger.info("")
|
51
53
|
logger[:final] = nil
|
52
54
|
end
|
55
|
+
|
56
|
+
def build_exception_app
|
57
|
+
if Rage.env.development?
|
58
|
+
->(status, e) do
|
59
|
+
exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
|
60
|
+
Rage.logger.error(exception_str)
|
61
|
+
[status, {}, [exception_str]]
|
62
|
+
end
|
63
|
+
else
|
64
|
+
->(status, e) do
|
65
|
+
exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
|
66
|
+
Rage.logger.error(exception_str)
|
67
|
+
[status, {}, []]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
53
71
|
end
|
data/lib/rage/cli.rb
CHANGED
@@ -18,13 +18,14 @@ module Rage
|
|
18
18
|
option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000."
|
19
19
|
option :environment, aliases: "-e", desc: "Specifies the environment to run this server under (test/development/production)."
|
20
20
|
option :binding, aliases: "-b", desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments."
|
21
|
+
option :config, aliases: "-c", desc: "Uses a custom rack configuration."
|
21
22
|
option :help, aliases: "-h", desc: "Show this message."
|
22
23
|
def server
|
23
24
|
return help("server") if options.help?
|
24
25
|
|
25
26
|
set_env(options)
|
26
27
|
|
27
|
-
app = ::Rack::Builder.parse_file("config.ru")
|
28
|
+
app = ::Rack::Builder.parse_file(options[:config] || "config.ru")
|
28
29
|
app = app[0] if app.is_a?(Array)
|
29
30
|
|
30
31
|
port = options[:port] || Rage.config.server.port
|
@@ -114,6 +115,12 @@ module Rage
|
|
114
115
|
|
115
116
|
def environment
|
116
117
|
require File.expand_path("config/application.rb", Dir.pwd)
|
118
|
+
|
119
|
+
# in Rails mode we delegate code loading to Rails, and thus need
|
120
|
+
# to manually load application code for CLI utilities to work
|
121
|
+
if Rage.config.internal.rails_mode
|
122
|
+
require "rage/setup"
|
123
|
+
end
|
117
124
|
end
|
118
125
|
|
119
126
|
def set_env(options)
|
data/lib/rage/code_loader.rb
CHANGED
@@ -4,11 +4,12 @@ require "zeitwerk"
|
|
4
4
|
|
5
5
|
class Rage::CodeLoader
|
6
6
|
def initialize
|
7
|
-
@loader = Zeitwerk::Loader.new
|
8
7
|
@reloading = false
|
9
8
|
end
|
10
9
|
|
11
10
|
def setup
|
11
|
+
@loader = Zeitwerk::Loader.new
|
12
|
+
|
12
13
|
autoload_path = "#{Rage.root}/app"
|
13
14
|
enable_reloading = Rage.env.development?
|
14
15
|
enable_eager_loading = !Rage.env.development? && !Rage.env.test?
|
@@ -23,13 +24,24 @@ class Rage::CodeLoader
|
|
23
24
|
@loader.eager_load if enable_eager_loading
|
24
25
|
end
|
25
26
|
|
27
|
+
# in standalone mode - reload the code and the routes
|
26
28
|
def reload
|
29
|
+
return unless @loader
|
30
|
+
|
27
31
|
@reloading = true
|
28
32
|
@loader.reload
|
29
33
|
Rage.__router.reset_routes
|
30
34
|
load("#{Rage.root}/config/routes.rb")
|
31
35
|
end
|
32
36
|
|
37
|
+
# in Rails mode - reset the routes; everything else will be done by Rails
|
38
|
+
def rails_mode_reload
|
39
|
+
return if @loader
|
40
|
+
|
41
|
+
@reloading = true
|
42
|
+
Rage.__router.reset_routes
|
43
|
+
end
|
44
|
+
|
33
45
|
def reloading?
|
34
46
|
@reloading
|
35
47
|
end
|
data/lib/rage/configuration.rb
CHANGED
@@ -109,6 +109,10 @@ class Rage::Configuration
|
|
109
109
|
@middleware ||= Middleware.new
|
110
110
|
end
|
111
111
|
|
112
|
+
def internal
|
113
|
+
@internal ||= Internal.new
|
114
|
+
end
|
115
|
+
|
112
116
|
class Server
|
113
117
|
attr_accessor :port, :workers_count, :timeout, :max_clients
|
114
118
|
attr_reader :threads_count
|
@@ -160,6 +164,15 @@ class Rage::Configuration
|
|
160
164
|
end
|
161
165
|
end
|
162
166
|
|
167
|
+
# @private
|
168
|
+
class Internal
|
169
|
+
attr_accessor :rails_mode, :rails_console
|
170
|
+
|
171
|
+
def inspect
|
172
|
+
"#<#{self.class.name}>"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
163
176
|
# @private
|
164
177
|
def __finalize
|
165
178
|
if @logger
|
data/lib/rage/controller/api.rb
CHANGED
@@ -75,8 +75,16 @@ class RageController::API
|
|
75
75
|
""
|
76
76
|
end
|
77
77
|
|
78
|
+
activerecord_loaded = Rage.config.internal.rails_mode && defined?(::ActiveRecord)
|
79
|
+
|
78
80
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
79
81
|
def __run_#{action}
|
82
|
+
#{if activerecord_loaded
|
83
|
+
<<~RUBY
|
84
|
+
ActiveRecord::Base.connection_pool.enable_query_cache!
|
85
|
+
RUBY
|
86
|
+
end}
|
87
|
+
|
80
88
|
#{before_actions_chunk}
|
81
89
|
#{action}
|
82
90
|
|
@@ -90,6 +98,24 @@ class RageController::API
|
|
90
98
|
[@__status, @__headers, @__body]
|
91
99
|
|
92
100
|
#{rescue_handlers_chunk}
|
101
|
+
|
102
|
+
ensure
|
103
|
+
#{if activerecord_loaded
|
104
|
+
<<~RUBY
|
105
|
+
ActiveRecord::Base.connection_pool.disable_query_cache!
|
106
|
+
if ActiveRecord::Base.connection_pool.active_connection?
|
107
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!
|
108
|
+
end
|
109
|
+
RUBY
|
110
|
+
end}
|
111
|
+
|
112
|
+
#{if method_defined?(:append_info_to_payload) || private_method_defined?(:append_info_to_payload)
|
113
|
+
<<~RUBY
|
114
|
+
context = {}
|
115
|
+
append_info_to_payload(context)
|
116
|
+
Thread.current[:rage_logger][:context] = context
|
117
|
+
RUBY
|
118
|
+
end}
|
93
119
|
end
|
94
120
|
RUBY
|
95
121
|
end
|
@@ -196,6 +222,16 @@ class RageController::API
|
|
196
222
|
end
|
197
223
|
end
|
198
224
|
|
225
|
+
# Register a new `after_action` hook. Calls with the same `action_name` will overwrite the previous ones.
|
226
|
+
#
|
227
|
+
# @param action_name [String, nil] the name of the callback to add
|
228
|
+
# @param [Hash] opts action options
|
229
|
+
# @option opts [Symbol, Array<Symbol>] :only restrict the callback to run only for specific actions
|
230
|
+
# @option opts [Symbol, Array<Symbol>] :except restrict the callback to run for all actions except specified
|
231
|
+
# @option opts [Symbol, Proc] :if only run the callback if the condition is true
|
232
|
+
# @option opts [Symbol, Proc] :unless only run the callback if the condition is false
|
233
|
+
# @example
|
234
|
+
# after_action :log_detailed_metrics, only: :create
|
199
235
|
def after_action(action_name = nil, **opts, &block)
|
200
236
|
action = prepare_action_params(action_name, **opts, &block)
|
201
237
|
|
@@ -268,22 +304,26 @@ class RageController::API
|
|
268
304
|
end
|
269
305
|
end # class << self
|
270
306
|
|
271
|
-
# @private
|
272
|
-
DEFAULT_HEADERS = { "content-type" => "application/json; charset=utf-8" }.freeze
|
273
|
-
|
274
307
|
# @private
|
275
308
|
def initialize(env, params)
|
276
309
|
@__env = env
|
277
310
|
@__params = params
|
278
|
-
@__status, @__headers, @__body = 204,
|
311
|
+
@__status, @__headers, @__body = 204, { "content-type" => "application/json; charset=utf-8" }, []
|
279
312
|
@__rendered = false
|
280
313
|
end
|
281
314
|
|
282
315
|
# Get the request object. See {Rage::Request}.
|
316
|
+
# @return [Rage::Request]
|
283
317
|
def request
|
284
318
|
@request ||= Rage::Request.new(@__env)
|
285
319
|
end
|
286
320
|
|
321
|
+
# Get the response object. See {Rage::Response}.
|
322
|
+
# @return [Rage::Response]
|
323
|
+
def response
|
324
|
+
@response ||= Rage::Response.new(@__headers, @__body)
|
325
|
+
end
|
326
|
+
|
287
327
|
# Send a response to the client.
|
288
328
|
#
|
289
329
|
# @param json [String, Object] send a json response to the client; objects like arrays will be serialized automatically
|
@@ -339,11 +379,10 @@ class RageController::API
|
|
339
379
|
|
340
380
|
# Set response headers.
|
341
381
|
#
|
382
|
+
# @return [Hash]
|
342
383
|
# @example
|
343
384
|
# headers["Content-Type"] = "application/pdf"
|
344
385
|
def headers
|
345
|
-
# copy-on-write implementation for the headers object
|
346
|
-
@__headers = {}.merge!(@__headers) if DEFAULT_HEADERS.equal?(@__headers)
|
347
386
|
@__headers
|
348
387
|
end
|
349
388
|
|
@@ -357,11 +396,24 @@ class RageController::API
|
|
357
396
|
def authenticate_with_http_token
|
358
397
|
auth_header = @__env["HTTP_AUTHORIZATION"]
|
359
398
|
|
360
|
-
if auth_header&.start_with?("Bearer")
|
361
|
-
|
399
|
+
payload = if auth_header&.start_with?("Bearer")
|
400
|
+
auth_header[7..]
|
362
401
|
elsif auth_header&.start_with?("Token")
|
363
|
-
|
402
|
+
auth_header[6..]
|
364
403
|
end
|
404
|
+
|
405
|
+
return unless payload
|
406
|
+
|
407
|
+
token = if payload.start_with?("token=")
|
408
|
+
payload[6..]
|
409
|
+
else
|
410
|
+
payload
|
411
|
+
end
|
412
|
+
|
413
|
+
token.delete_prefix!('"')
|
414
|
+
token.delete_suffix!('"')
|
415
|
+
|
416
|
+
yield token
|
365
417
|
end
|
366
418
|
|
367
419
|
if !defined?(::ActionController::Parameters)
|
@@ -389,4 +441,40 @@ class RageController::API
|
|
389
441
|
@params ||= ActionController::Parameters.new(@__params)
|
390
442
|
end
|
391
443
|
end
|
444
|
+
|
445
|
+
# Checks if the request is stale to decide if the action has to be rendered or the cached version is still valid. Use this method to implement conditional GET.
|
446
|
+
#
|
447
|
+
# @param etag [String] The etag of the requested resource.
|
448
|
+
# @param last_modified [Time] The last modified time of the requested resource.
|
449
|
+
# @return [Boolean] True if the response is stale, false otherwise.
|
450
|
+
# @example
|
451
|
+
# stale?(etag: "123", last_modified: Time.utc(2023, 12, 15))
|
452
|
+
# stale?(last_modified: Time.utc(2023, 12, 15))
|
453
|
+
# stale?(etag: "123")
|
454
|
+
# @note `stale?` will set the response status to 304 if the request is fresh. This side effect will cause a double render error, if `render` gets called after this method. Make sure to implement a proper conditional in your action to prevent this from happening:
|
455
|
+
# ```ruby
|
456
|
+
# if stale?(etag: "123")
|
457
|
+
# render json: { hello: "world" }
|
458
|
+
# end
|
459
|
+
# ```
|
460
|
+
def stale?(etag: nil, last_modified: nil)
|
461
|
+
still_fresh = request.fresh?(etag:, last_modified:)
|
462
|
+
|
463
|
+
head :not_modified if still_fresh
|
464
|
+
!still_fresh
|
465
|
+
end
|
466
|
+
|
467
|
+
# @private
|
468
|
+
# for comatibility with `Rails.application.routes.recognize_path`
|
469
|
+
def self.binary_params_for?(_)
|
470
|
+
false
|
471
|
+
end
|
472
|
+
|
473
|
+
# @!method append_info_to_payload(payload)
|
474
|
+
# Define this method to add more information to request logs.
|
475
|
+
# @param [Hash] payload the payload to add additional information to
|
476
|
+
# @example
|
477
|
+
# def append_info_to_payload(payload)
|
478
|
+
# payload[:response] = response.body
|
479
|
+
# end
|
392
480
|
end
|
data/lib/rage/fiber.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Fiber
|
4
|
+
# @private
|
4
5
|
AWAIT_ERROR_MESSAGE = "err"
|
5
6
|
|
6
7
|
# @private
|
@@ -23,14 +24,14 @@ class Fiber
|
|
23
24
|
@__err
|
24
25
|
end
|
25
26
|
|
26
|
-
|
27
|
-
def
|
28
|
-
@__rage_id
|
27
|
+
# @private
|
28
|
+
def __set_id
|
29
|
+
@__rage_id = object_id.to_s
|
29
30
|
end
|
30
31
|
|
31
|
-
|
32
|
-
def
|
33
|
-
|
32
|
+
# @private
|
33
|
+
def __get_id
|
34
|
+
@__rage_id
|
34
35
|
end
|
35
36
|
|
36
37
|
# @private
|
@@ -49,6 +50,13 @@ class Fiber
|
|
49
50
|
Fiber.yield
|
50
51
|
end
|
51
52
|
|
53
|
+
# @private
|
54
|
+
# under normal circumstances, the method is a copy of `yield`, but it can be overriden to perform
|
55
|
+
# additional steps on yielding, e.g. releasing AR connections; see "lib/rage/rails.rb"
|
56
|
+
class << self
|
57
|
+
alias_method :defer, :yield
|
58
|
+
end
|
59
|
+
|
52
60
|
# Wait on several fibers at the same time. Calling this method will automatically pause the current fiber, allowing the
|
53
61
|
# server to process other requests. Once all fibers have completed, the current fiber will be automatically resumed.
|
54
62
|
#
|
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
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Rage::JSONFormatter
|
2
|
+
def initialize
|
3
|
+
@pid = Process.pid.to_s
|
4
|
+
Iodine.on_state(:on_start) do
|
5
|
+
@pid = Process.pid.to_s
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(severity, timestamp, _, message)
|
10
|
+
logger = Thread.current[:rage_logger] || { tags: [], context: {} }
|
11
|
+
tags, context = logger[:tags], logger[:context]
|
12
|
+
|
13
|
+
if !context.empty?
|
14
|
+
context_msg = ""
|
15
|
+
context.each { |k, v| context_msg << "\"#{k}\":#{v.to_json}," }
|
16
|
+
end
|
17
|
+
|
18
|
+
if final = logger[:final]
|
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"
|
22
|
+
else
|
23
|
+
# no controller/action keys are written if there are no params
|
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"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if tags.length == 1
|
29
|
+
tags_msg = "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
|
30
|
+
elsif tags.length == 2
|
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}\""
|
34
|
+
else
|
35
|
+
tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\""
|
36
|
+
i = 2
|
37
|
+
while i < tags.length
|
38
|
+
tags_msg << ",\"#{tags[i]}\""
|
39
|
+
i += 1
|
40
|
+
end
|
41
|
+
tags_msg << "],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
|
42
|
+
end
|
43
|
+
|
44
|
+
"#{tags_msg},#{context_msg}\"message\":\"#{message}\"}\n"
|
45
|
+
end
|
46
|
+
end
|
data/lib/rage/logger/logger.rb
CHANGED
@@ -83,7 +83,7 @@ class Rage::Logger
|
|
83
83
|
# Rage.logger.info "cache miss"
|
84
84
|
# end
|
85
85
|
def with_context(context)
|
86
|
-
old_context = Thread.current[:rage_logger][:context]
|
86
|
+
old_context = (Thread.current[:rage_logger] ||= { tags: [], context: {} })[:context]
|
87
87
|
|
88
88
|
if old_context.empty? # there's nothing in the context yet
|
89
89
|
Thread.current[:rage_logger][:context] = context
|
@@ -92,8 +92,6 @@ class Rage::Logger
|
|
92
92
|
end
|
93
93
|
|
94
94
|
yield(self)
|
95
|
-
true
|
96
|
-
|
97
95
|
ensure
|
98
96
|
Thread.current[:rage_logger][:context] = old_context
|
99
97
|
end
|
@@ -106,17 +104,20 @@ class Rage::Logger
|
|
106
104
|
# Rage.logger.info "success"
|
107
105
|
# end
|
108
106
|
def tagged(tag)
|
109
|
-
Thread.current[:rage_logger][:tags] << tag
|
110
|
-
|
107
|
+
(Thread.current[:rage_logger] ||= { tags: [], context: {} })[:tags] << tag
|
111
108
|
yield(self)
|
112
|
-
true
|
113
|
-
|
114
109
|
ensure
|
115
110
|
Thread.current[:rage_logger][:tags].pop
|
116
111
|
end
|
117
112
|
|
118
113
|
alias_method :with_tag, :tagged
|
119
114
|
|
115
|
+
def debug? = @level <= Logger::DEBUG
|
116
|
+
def error? = @level <= Logger::ERROR
|
117
|
+
def fatal? = @level <= Logger::FATAL
|
118
|
+
def info? = @level <= Logger::INFO
|
119
|
+
def warn? = @level <= Logger::WARN
|
120
|
+
|
120
121
|
private
|
121
122
|
|
122
123
|
def define_log_methods
|
@@ -128,8 +129,8 @@ class Rage::Logger
|
|
128
129
|
false
|
129
130
|
end
|
130
131
|
RUBY
|
131
|
-
elsif defined?(IRB)
|
132
|
-
# the call was made from
|
132
|
+
elsif (Rage.config.internal.rails_mode ? Rage.config.internal.rails_console : defined?(IRB))
|
133
|
+
# the call was made from the console - don't use the formatter
|
133
134
|
<<-RUBY
|
134
135
|
def #{level_name}(msg = nil)
|
135
136
|
@logdev.write((msg || yield) + "\n")
|
@@ -7,16 +7,21 @@ class Rage::TextFormatter
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def call(severity, timestamp, _, message)
|
10
|
-
logger = Thread.current[:rage_logger]
|
11
|
-
tags = logger[:tags]
|
10
|
+
logger = Thread.current[:rage_logger] || { tags: [], context: {} }
|
11
|
+
tags, context = logger[:tags], logger[:context]
|
12
|
+
|
13
|
+
if !context.empty?
|
14
|
+
context_msg = ""
|
15
|
+
context.each { |k, v| context_msg << "#{k}=#{v} " }
|
16
|
+
end
|
12
17
|
|
13
18
|
if final = logger[:final]
|
14
19
|
params, env = final[:params], final[:env]
|
15
20
|
if params
|
16
|
-
return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} controller=#{params[:controller]} action=#{params[:action]} status=#{final[:response][0]} duration=#{final[:duration]}\n"
|
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"
|
17
22
|
else
|
18
23
|
# no controller/action keys are written if there are no params
|
19
|
-
return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} status=#{final[:response][0]} duration=#{final[:duration]}\n"
|
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"
|
20
25
|
end
|
21
26
|
end
|
22
27
|
|
@@ -24,6 +29,8 @@ class Rage::TextFormatter
|
|
24
29
|
tags_msg = "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
25
30
|
elsif tags.length == 2
|
26
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}"
|
27
34
|
else
|
28
35
|
tags_msg = "[#{tags[0]}][#{tags[1]}]"
|
29
36
|
i = 2
|
@@ -34,13 +41,6 @@ class Rage::TextFormatter
|
|
34
41
|
tags_msg << " timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
35
42
|
end
|
36
43
|
|
37
|
-
context = logger[:context]
|
38
|
-
|
39
|
-
if !context.empty?
|
40
|
-
context_msg = ""
|
41
|
-
context.each { |k, v| context_msg << "#{k}=#{v} " }
|
42
|
-
end
|
43
|
-
|
44
44
|
"#{tags_msg} #{context_msg}message=#{message}\n"
|
45
45
|
end
|
46
46
|
end
|
data/lib/rage/middleware/cors.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Rage::Cors
|
4
|
+
# @private
|
4
5
|
def initialize(app, *, &)
|
5
6
|
@app = app
|
6
7
|
instance_eval(&)
|
7
8
|
end
|
8
9
|
|
10
|
+
# @private
|
9
11
|
def call(env)
|
10
12
|
if env["REQUEST_METHOD"] == "OPTIONS"
|
11
13
|
return (response = @cors_response)
|
@@ -17,7 +19,7 @@ class Rage::Cors
|
|
17
19
|
|
18
20
|
response
|
19
21
|
ensure
|
20
|
-
if origin = @cors_check.call(env)
|
22
|
+
if !$! && origin = @cors_check.call(env)
|
21
23
|
headers = response[1]
|
22
24
|
headers["Access-Control-Allow-Origin"] = origin
|
23
25
|
if @origins != "*"
|
@@ -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
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
if Gem::Version.new(Rails.version) < Gem::Version.new(6)
|
2
|
+
fail "Rage is only compatible with Rails 6+. Detected Rails version: #{Rails.version}."
|
3
|
+
end
|
4
|
+
|
5
|
+
# load the framework
|
6
|
+
require "rage/all"
|
7
|
+
|
8
|
+
# patch Rack
|
9
|
+
Iodine.patch_rack
|
10
|
+
|
11
|
+
# configure the framework
|
12
|
+
Rage.config.internal.rails_mode = true
|
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
|
+
# patch ActiveRecord's connection pool
|
21
|
+
if defined?(ActiveRecord)
|
22
|
+
Rails.configuration.after_initialize do
|
23
|
+
module ActiveRecord::ConnectionAdapters
|
24
|
+
class ConnectionPool
|
25
|
+
def connection_cache_key(_)
|
26
|
+
Fiber.current
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# release ActiveRecord connections on yield
|
34
|
+
if defined?(ActiveRecord)
|
35
|
+
class Fiber
|
36
|
+
def self.defer
|
37
|
+
res = Fiber.yield
|
38
|
+
|
39
|
+
if ActiveRecord::Base.connection_pool.active_connection?
|
40
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!
|
41
|
+
end
|
42
|
+
|
43
|
+
res
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# plug into Rails' Zeitwerk instance to reload the code
|
49
|
+
Rails.autoloaders.main.on_setup do
|
50
|
+
if Iodine.running?
|
51
|
+
Rage.code_loader.rails_mode_reload
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# patch `ActionDispatch::Reloader` to synchronize `reload!` calls
|
56
|
+
Rails.configuration.after_initialize do
|
57
|
+
conditional_mutex = Module.new do
|
58
|
+
def call(env)
|
59
|
+
@mutex ||= Mutex.new
|
60
|
+
if Rails.application.reloader.check!
|
61
|
+
@mutex.synchronize { super }
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
ActionDispatch::Reloader.prepend(conditional_mutex)
|
69
|
+
end
|
70
|
+
|
71
|
+
# clone Rails logger
|
72
|
+
Rails.configuration.after_initialize do
|
73
|
+
if Rails.logger && !Rage.logger
|
74
|
+
rails_logdev = Rails.logger.instance_variable_get(:@logdev)
|
75
|
+
Rage.config.logger = Rage::Logger.new(rails_logdev) if rails_logdev.is_a?(Logger::LogDevice)
|
76
|
+
end
|
77
|
+
end
|
data/lib/rage/request.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "time"
|
4
|
+
|
3
5
|
class Rage::Request
|
4
6
|
# @private
|
5
7
|
def initialize(env)
|
@@ -14,6 +16,55 @@ class Rage::Request
|
|
14
16
|
@headers ||= Headers.new(@env)
|
15
17
|
end
|
16
18
|
|
19
|
+
# Check if the request is fresh.
|
20
|
+
# @param etag [String] The etag of the requested resource.
|
21
|
+
# @param last_modified [Time] The last modified time of the requested resource.
|
22
|
+
# @return [Boolean] True if the request is fresh, false otherwise.
|
23
|
+
# @example
|
24
|
+
# request.fresh?(etag: "123", last_modified: Time.utc(2023, 12, 15))
|
25
|
+
# request.fresh?(last_modified: Time.utc(2023, 12, 15))
|
26
|
+
# request.fresh?(etag: "123")
|
27
|
+
def fresh?(etag:, last_modified:)
|
28
|
+
# Always render response when no freshness information
|
29
|
+
# is provided in the request.
|
30
|
+
return false unless if_none_match || if_not_modified_since
|
31
|
+
|
32
|
+
etag_matches?(
|
33
|
+
requested_etags: if_none_match, response_etag: etag
|
34
|
+
) && not_modified?(
|
35
|
+
request_not_modified_since: if_not_modified_since,
|
36
|
+
response_last_modified: last_modified
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def if_none_match
|
43
|
+
headers["HTTP_IF_NONE_MATCH"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def if_not_modified_since
|
47
|
+
headers["HTTP_IF_MODIFIED_SINCE"] ? Time.httpdate(headers["HTTP_IF_MODIFIED_SINCE"]) : nil
|
48
|
+
rescue ArgumentError
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def etag_matches?(requested_etags:, response_etag:)
|
53
|
+
requested_etags = requested_etags ? requested_etags.split(",").each(&:strip!) : []
|
54
|
+
|
55
|
+
return true if requested_etags.empty?
|
56
|
+
return false if response_etag.nil?
|
57
|
+
|
58
|
+
requested_etags.include?(response_etag) || requested_etags.include?("*")
|
59
|
+
end
|
60
|
+
|
61
|
+
def not_modified?(request_not_modified_since:, response_last_modified:)
|
62
|
+
return true if request_not_modified_since.nil?
|
63
|
+
return false if response_last_modified.nil?
|
64
|
+
|
65
|
+
request_not_modified_since >= response_last_modified
|
66
|
+
end
|
67
|
+
|
17
68
|
# @private
|
18
69
|
class Headers
|
19
70
|
HTTP = "HTTP_"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Response
|
4
|
+
# @private
|
5
|
+
def initialize(headers, body)
|
6
|
+
@headers = headers
|
7
|
+
@body = body
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the content of the response as a string. This contains the contents of any calls to `render`.
|
11
|
+
# @return [String]
|
12
|
+
def body
|
13
|
+
@body[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the headers for the response.
|
17
|
+
# @return [Hash]
|
18
|
+
def headers
|
19
|
+
@headers
|
20
|
+
end
|
21
|
+
end
|
data/lib/rage/router/dsl.rb
CHANGED
@@ -7,6 +7,8 @@ class Rage::Router::DSL
|
|
7
7
|
|
8
8
|
def draw(&block)
|
9
9
|
Handler.new(@router).instance_eval(&block)
|
10
|
+
# propagate route definitions to Rails for `rails routes` to work
|
11
|
+
Rails.application.routes.draw(&block) if Rage.config.internal.rails_mode
|
10
12
|
end
|
11
13
|
|
12
14
|
##
|
@@ -283,7 +285,7 @@ class Rage::Router::DSL
|
|
283
285
|
end
|
284
286
|
|
285
287
|
_module, _path, _only, _except, _param = opts.values_at(:module, :path, :only, :except, :param)
|
286
|
-
raise ":param option can't contain colons" if _param
|
288
|
+
raise ":param option can't contain colons" if _param.to_s.include?(":")
|
287
289
|
|
288
290
|
_only = Array(_only) if _only
|
289
291
|
_except = Array(_except) if _except
|
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(_)
|
30
|
+
# no-op
|
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/lib/rage-rb.rb
CHANGED
@@ -45,6 +45,24 @@ module Rage
|
|
45
45
|
|
46
46
|
def self.load_middlewares(rack_builder)
|
47
47
|
config.middleware.middlewares.each do |middleware, args, block|
|
48
|
+
# in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
|
49
|
+
# if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`, but Rack
|
50
|
+
# expects the middleware to respond to `#new`, so we wrap the middleware into a helper module
|
51
|
+
if Rage.config.internal.rails_mode
|
52
|
+
rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
|
53
|
+
if rails_middleware
|
54
|
+
wrapper = Module.new do
|
55
|
+
extend self
|
56
|
+
attr_accessor :middleware
|
57
|
+
def new(app, *, &)
|
58
|
+
middleware.build(app)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
wrapper.middleware = rails_middleware
|
62
|
+
middleware = wrapper
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
48
66
|
rack_builder.use(middleware, *args, &block)
|
49
67
|
end
|
50
68
|
end
|
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: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Samoilov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-13 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
|
@@ -95,13 +109,16 @@ files:
|
|
95
109
|
- lib/rage/errors.rb
|
96
110
|
- lib/rage/fiber.rb
|
97
111
|
- lib/rage/fiber_scheduler.rb
|
112
|
+
- lib/rage/logger/json_formatter.rb
|
98
113
|
- lib/rage/logger/logger.rb
|
99
114
|
- lib/rage/logger/text_formatter.rb
|
100
115
|
- lib/rage/middleware/cors.rb
|
101
116
|
- lib/rage/middleware/fiber_wrapper.rb
|
102
117
|
- lib/rage/middleware/reloader.rb
|
103
118
|
- lib/rage/params_parser.rb
|
119
|
+
- lib/rage/rails.rb
|
104
120
|
- lib/rage/request.rb
|
121
|
+
- lib/rage/response.rb
|
105
122
|
- lib/rage/router/README.md
|
106
123
|
- lib/rage/router/backend.rb
|
107
124
|
- lib/rage/router/constrainer.rb
|
@@ -109,6 +126,7 @@ files:
|
|
109
126
|
- lib/rage/router/handler_storage.rb
|
110
127
|
- lib/rage/router/node.rb
|
111
128
|
- lib/rage/router/strategies/host.rb
|
129
|
+
- lib/rage/rspec.rb
|
112
130
|
- lib/rage/setup.rb
|
113
131
|
- lib/rage/sidekiq_session.rb
|
114
132
|
- lib/rage/templates/Gemfile
|