rage-rb 0.6.0 → 1.0.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 +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
|