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