rage-rb 0.6.0 → 0.7.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/CHANGELOG.md +6 -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/logger/json_formatter.rb +44 -0
- data/lib/rage/logger/logger.rb +2 -2
- data/lib/rage/logger/text_formatter.rb +8 -10
- data/lib/rage/middleware/cors.rb +3 -1
- data/lib/rage/rails.rb +62 -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/version.rb +1 -1
- data/lib/rage-rb.rb +18 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c3041038ae63ae245e261a7a2096195ab56291d236414e086646ae96a4a6d16
|
4
|
+
data.tar.gz: a64817ded16716fe714310c7318d8e1d318454615da43d37055b333e76778fa2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 350f5150012852a3ca2532c031ce8ef28338da6d985ef66dff59095dea6b3dbbc4498826996876722bc5b7991683fed82c2fd3854313f7ed59f9fff20aaf4315
|
7
|
+
data.tar.gz: 84cd798056a43c8eb0e94809f72f6b0ac602076aa904831a3a049a8ed13352cad9321329233063343b73e65c0fbf1521564b0cee6f3696c89877a0af15191c94
|
data/CHANGELOG.md
CHANGED
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
|
+
⏳ | Expose the `send_data` and `send_file` methods.
|
154
|
+
⏳ | Support conditional get with `etag` and `last_modified`.
|
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
|
@@ -0,0 +1,44 @@
|
|
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]
|
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
|
+
else
|
33
|
+
tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\""
|
34
|
+
i = 2
|
35
|
+
while i < tags.length
|
36
|
+
tags_msg << ",\"#{tags[i]}\""
|
37
|
+
i += 1
|
38
|
+
end
|
39
|
+
tags_msg << "],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
|
40
|
+
end
|
41
|
+
|
42
|
+
"#{tags_msg},#{context_msg}\"message\":\"#{message}\"}\n"
|
43
|
+
end
|
44
|
+
end
|
data/lib/rage/logger/logger.rb
CHANGED
@@ -128,8 +128,8 @@ class Rage::Logger
|
|
128
128
|
false
|
129
129
|
end
|
130
130
|
RUBY
|
131
|
-
elsif defined?(IRB)
|
132
|
-
# the call was made from
|
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
133
|
<<-RUBY
|
134
134
|
def #{level_name}(msg = nil)
|
135
135
|
@logdev.write((msg || yield) + "\n")
|
@@ -8,15 +8,20 @@ class Rage::TextFormatter
|
|
8
8
|
|
9
9
|
def call(severity, timestamp, _, message)
|
10
10
|
logger = Thread.current[:rage_logger]
|
11
|
-
tags = logger[:tags]
|
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
|
|
@@ -34,13 +39,6 @@ class Rage::TextFormatter
|
|
34
39
|
tags_msg << " timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
35
40
|
end
|
36
41
|
|
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
42
|
"#{tags_msg} #{context_msg}message=#{message}\n"
|
45
43
|
end
|
46
44
|
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 != "*"
|
data/lib/rage/rails.rb
ADDED
@@ -0,0 +1,62 @@
|
|
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
|
+
# plug into Rails' Zeitwerk instance to reload the code
|
34
|
+
Rails.autoloaders.main.on_setup do
|
35
|
+
if Iodine.running?
|
36
|
+
Rage.code_loader.rails_mode_reload
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# patch `ActionDispatch::Reloader` to synchronize `reload!` calls
|
41
|
+
Rails.configuration.after_initialize do
|
42
|
+
conditional_mutex = Module.new do
|
43
|
+
def call(env)
|
44
|
+
@mutex ||= Mutex.new
|
45
|
+
if Rails.application.reloader.check!
|
46
|
+
@mutex.synchronize { super }
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ActionDispatch::Reloader.prepend(conditional_mutex)
|
54
|
+
end
|
55
|
+
|
56
|
+
# clone Rails logger
|
57
|
+
Rails.configuration.after_initialize do
|
58
|
+
if Rails.logger && !Rage.logger
|
59
|
+
rails_logdev = Rails.logger.instance_variable_get(:@logdev)
|
60
|
+
Rage.config.logger = Rage::Logger.new(rails_logdev) if rails_logdev.is_a?(Logger::LogDevice)
|
61
|
+
end
|
62
|
+
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/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
|
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: 0.7.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-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -95,13 +95,16 @@ files:
|
|
95
95
|
- lib/rage/errors.rb
|
96
96
|
- lib/rage/fiber.rb
|
97
97
|
- lib/rage/fiber_scheduler.rb
|
98
|
+
- lib/rage/logger/json_formatter.rb
|
98
99
|
- lib/rage/logger/logger.rb
|
99
100
|
- lib/rage/logger/text_formatter.rb
|
100
101
|
- lib/rage/middleware/cors.rb
|
101
102
|
- lib/rage/middleware/fiber_wrapper.rb
|
102
103
|
- lib/rage/middleware/reloader.rb
|
103
104
|
- lib/rage/params_parser.rb
|
105
|
+
- lib/rage/rails.rb
|
104
106
|
- lib/rage/request.rb
|
107
|
+
- lib/rage/response.rb
|
105
108
|
- lib/rage/router/README.md
|
106
109
|
- lib/rage/router/backend.rb
|
107
110
|
- lib/rage/router/constrainer.rb
|