rage-rb 0.5.2 → 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 +19 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +12 -11
- data/lib/rage/all.rb +7 -1
- data/lib/rage/application.rb +21 -3
- data/lib/rage/cli.rb +32 -8
- data/lib/rage/code_loader.rb +48 -0
- data/lib/rage/configuration.rb +122 -5
- data/lib/rage/controller/api.rb +178 -32
- data/lib/rage/errors.rb +3 -0
- data/lib/rage/fiber.rb +8 -0
- data/lib/rage/fiber_scheduler.rb +2 -2
- data/lib/rage/logger/json_formatter.rb +44 -0
- data/lib/rage/logger/logger.rb +4 -4
- data/lib/rage/logger/text_formatter.rb +8 -10
- data/lib/rage/middleware/cors.rb +139 -0
- data/lib/rage/{fiber_wrapper.rb → middleware/fiber_wrapper.rb} +4 -2
- data/lib/rage/middleware/reloader.rb +16 -0
- 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/backend.rb +9 -1
- data/lib/rage/router/dsl.rb +3 -1
- data/lib/rage/setup.rb +4 -16
- data/lib/rage/templates/config-environments-development.rb +2 -0
- data/lib/rage/templates/config-initializers-.keep +0 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +23 -1
- data/rage.gemspec +1 -0
- metadata +24 -3
data/lib/rage/controller/api.rb
CHANGED
@@ -8,7 +8,7 @@ class RageController::API
|
|
8
8
|
# sends a correct response down to the server;
|
9
9
|
# returns the name of the newly defined method;
|
10
10
|
def __register_action(action)
|
11
|
-
raise "The action '#{action}' could not be found for #{self}" unless method_defined?(action)
|
11
|
+
raise Rage::Errors::RouterError, "The action '#{action}' could not be found for #{self}" unless method_defined?(action)
|
12
12
|
|
13
13
|
before_actions_chunk = if @__before_actions
|
14
14
|
filtered_before_actions = @__before_actions.select do |h|
|
@@ -25,7 +25,7 @@ class RageController::API
|
|
25
25
|
"unless #{h[:unless]}"
|
26
26
|
end
|
27
27
|
|
28
|
-
|
28
|
+
<<~RUBY
|
29
29
|
#{h[:name]} #{condition}
|
30
30
|
return [@__status, @__headers, @__body] if @__rendered
|
31
31
|
RUBY
|
@@ -36,9 +36,34 @@ class RageController::API
|
|
36
36
|
""
|
37
37
|
end
|
38
38
|
|
39
|
+
after_actions_chunk = if @__after_actions
|
40
|
+
filtered_after_actions = @__after_actions.select do |h|
|
41
|
+
(!h[:only] || h[:only].include?(action)) &&
|
42
|
+
(!h[:except] || !h[:except].include?(action))
|
43
|
+
end
|
44
|
+
|
45
|
+
lines = filtered_after_actions.map! do |h|
|
46
|
+
condition = if h[:if] && h[:unless]
|
47
|
+
"if #{h[:if]} && !#{h[:unless]}"
|
48
|
+
elsif h[:if]
|
49
|
+
"if #{h[:if]}"
|
50
|
+
elsif h[:unless]
|
51
|
+
"unless #{h[:unless]}"
|
52
|
+
end
|
53
|
+
|
54
|
+
<<~RUBY
|
55
|
+
#{h[:name]} #{condition}
|
56
|
+
RUBY
|
57
|
+
end
|
58
|
+
|
59
|
+
lines.join("\n")
|
60
|
+
else
|
61
|
+
""
|
62
|
+
end
|
63
|
+
|
39
64
|
rescue_handlers_chunk = if @__rescue_handlers
|
40
65
|
lines = @__rescue_handlers.map do |klasses, handler|
|
41
|
-
|
66
|
+
<<~RUBY
|
42
67
|
rescue #{klasses.join(", ")} => __e
|
43
68
|
#{handler}(__e)
|
44
69
|
[@__status, @__headers, @__body]
|
@@ -50,26 +75,60 @@ class RageController::API
|
|
50
75
|
""
|
51
76
|
end
|
52
77
|
|
53
|
-
|
78
|
+
activerecord_loaded = Rage.config.internal.rails_mode && defined?(::ActiveRecord)
|
79
|
+
|
80
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
54
81
|
def __run_#{action}
|
82
|
+
#{if activerecord_loaded
|
83
|
+
<<~RUBY
|
84
|
+
ActiveRecord::Base.connection_pool.enable_query_cache!
|
85
|
+
RUBY
|
86
|
+
end}
|
87
|
+
|
55
88
|
#{before_actions_chunk}
|
56
89
|
#{action}
|
57
90
|
|
91
|
+
#{if !after_actions_chunk.empty?
|
92
|
+
<<~RUBY
|
93
|
+
@__rendered = true
|
94
|
+
#{after_actions_chunk}
|
95
|
+
RUBY
|
96
|
+
end}
|
97
|
+
|
58
98
|
[@__status, @__headers, @__body]
|
59
99
|
|
60
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}
|
61
119
|
end
|
62
120
|
RUBY
|
63
121
|
end
|
64
122
|
|
65
123
|
# @private
|
66
|
-
attr_writer :__before_actions, :__rescue_handlers
|
124
|
+
attr_writer :__before_actions, :__after_actions, :__rescue_handlers
|
67
125
|
|
68
126
|
# @private
|
69
127
|
# pass the variable down to the child; the child will continue to use it until changes need to be made;
|
70
128
|
# only then the object will be copied; the frozen state communicates that the object is shared with the parent;
|
71
129
|
def inherited(klass)
|
72
130
|
klass.__before_actions = @__before_actions.freeze
|
131
|
+
klass.__after_actions = @__after_actions.freeze
|
73
132
|
klass.__rescue_handlers = @__rescue_handlers.freeze
|
74
133
|
end
|
75
134
|
|
@@ -148,29 +207,12 @@ class RageController::API
|
|
148
207
|
# end
|
149
208
|
# @note The block form doesn't receive an argument and is executed on the controller level as if it was a regular method.
|
150
209
|
def before_action(action_name = nil, **opts, &block)
|
151
|
-
|
152
|
-
action_name = define_tmp_method(block)
|
153
|
-
elsif action_name.nil?
|
154
|
-
raise "No handler provided. Pass the `action_name` parameter or provide a block."
|
155
|
-
end
|
156
|
-
|
157
|
-
_only, _except, _if, _unless = opts.values_at(:only, :except, :if, :unless)
|
210
|
+
action = prepare_action_params(action_name, **opts, &block)
|
158
211
|
|
159
212
|
if @__before_actions && @__before_actions.frozen?
|
160
213
|
@__before_actions = @__before_actions.dup
|
161
214
|
end
|
162
215
|
|
163
|
-
action = {
|
164
|
-
name: action_name,
|
165
|
-
only: _only && Array(_only),
|
166
|
-
except: _except && Array(_except),
|
167
|
-
if: _if,
|
168
|
-
unless: _unless
|
169
|
-
}
|
170
|
-
|
171
|
-
action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
|
172
|
-
action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
|
173
|
-
|
174
216
|
if @__before_actions.nil?
|
175
217
|
@__before_actions = [action]
|
176
218
|
elsif i = @__before_actions.find_index { |a| a[:name] == action_name }
|
@@ -180,6 +222,32 @@ class RageController::API
|
|
180
222
|
end
|
181
223
|
end
|
182
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
|
235
|
+
def after_action(action_name = nil, **opts, &block)
|
236
|
+
action = prepare_action_params(action_name, **opts, &block)
|
237
|
+
|
238
|
+
if @__after_actions && @__after_actions.frozen?
|
239
|
+
@__after_actions = @__after_actions.dup
|
240
|
+
end
|
241
|
+
|
242
|
+
if @__after_actions.nil?
|
243
|
+
@__after_actions = [action]
|
244
|
+
elsif i = @__after_actions.find_index { |a| a[:name] == action_name }
|
245
|
+
@__after_actions[i] = action
|
246
|
+
else
|
247
|
+
@__after_actions << action
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
183
251
|
# Prevent a `before_action` hook from running.
|
184
252
|
#
|
185
253
|
# @param action_name [String] the name of the callback to skip
|
@@ -208,24 +276,54 @@ class RageController::API
|
|
208
276
|
|
209
277
|
@__before_actions[i] = action
|
210
278
|
end
|
211
|
-
end # class << self
|
212
279
|
|
213
|
-
|
214
|
-
|
280
|
+
private
|
281
|
+
|
282
|
+
# used by `before_action` and `after_action`
|
283
|
+
def prepare_action_params(action_name = nil, **opts, &block)
|
284
|
+
if block_given?
|
285
|
+
action_name = define_tmp_method(block)
|
286
|
+
elsif action_name.nil?
|
287
|
+
raise "No handler provided. Pass the `action_name` parameter or provide a block."
|
288
|
+
end
|
289
|
+
|
290
|
+
_only, _except, _if, _unless = opts.values_at(:only, :except, :if, :unless)
|
291
|
+
|
292
|
+
action = {
|
293
|
+
name: action_name,
|
294
|
+
only: _only && Array(_only),
|
295
|
+
except: _except && Array(_except),
|
296
|
+
if: _if,
|
297
|
+
unless: _unless
|
298
|
+
}
|
299
|
+
|
300
|
+
action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
|
301
|
+
action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
|
302
|
+
|
303
|
+
action
|
304
|
+
end
|
305
|
+
end # class << self
|
215
306
|
|
216
307
|
# @private
|
217
308
|
def initialize(env, params)
|
218
309
|
@__env = env
|
219
310
|
@__params = params
|
220
|
-
@__status, @__headers, @__body = 204,
|
311
|
+
@__status, @__headers, @__body = 204, { "content-type" => "application/json; charset=utf-8" }, []
|
221
312
|
@__rendered = false
|
222
313
|
end
|
223
314
|
|
224
315
|
# Get the request object. See {Rage::Request}.
|
316
|
+
# @return [Rage::Request]
|
225
317
|
def request
|
226
318
|
@request ||= Rage::Request.new(@__env)
|
227
319
|
end
|
228
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
|
+
|
229
327
|
# Send a response to the client.
|
230
328
|
#
|
231
329
|
# @param json [String, Object] send a json response to the client; objects like arrays will be serialized automatically
|
@@ -281,11 +379,10 @@ class RageController::API
|
|
281
379
|
|
282
380
|
# Set response headers.
|
283
381
|
#
|
382
|
+
# @return [Hash]
|
284
383
|
# @example
|
285
384
|
# headers["Content-Type"] = "application/pdf"
|
286
385
|
def headers
|
287
|
-
# copy-on-write implementation for the headers object
|
288
|
-
@__headers = {}.merge!(@__headers) if DEFAULT_HEADERS.equal?(@__headers)
|
289
386
|
@__headers
|
290
387
|
end
|
291
388
|
|
@@ -299,11 +396,24 @@ class RageController::API
|
|
299
396
|
def authenticate_with_http_token
|
300
397
|
auth_header = @__env["HTTP_AUTHORIZATION"]
|
301
398
|
|
302
|
-
if auth_header&.start_with?("Bearer")
|
303
|
-
|
399
|
+
payload = if auth_header&.start_with?("Bearer")
|
400
|
+
auth_header[7..]
|
304
401
|
elsif auth_header&.start_with?("Token")
|
305
|
-
|
402
|
+
auth_header[6..]
|
403
|
+
end
|
404
|
+
|
405
|
+
return unless payload
|
406
|
+
|
407
|
+
token = if payload.start_with?("token=")
|
408
|
+
payload[6..]
|
409
|
+
else
|
410
|
+
payload
|
306
411
|
end
|
412
|
+
|
413
|
+
token.delete_prefix!('"')
|
414
|
+
token.delete_suffix!('"')
|
415
|
+
|
416
|
+
yield token
|
307
417
|
end
|
308
418
|
|
309
419
|
if !defined?(::ActionController::Parameters)
|
@@ -331,4 +441,40 @@ class RageController::API
|
|
331
441
|
@params ||= ActionController::Parameters.new(@__params)
|
332
442
|
end
|
333
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
|
334
480
|
end
|
data/lib/rage/errors.rb
CHANGED
data/lib/rage/fiber.rb
CHANGED
@@ -33,6 +33,14 @@ class Fiber
|
|
33
33
|
!@__rage_id.nil?
|
34
34
|
end
|
35
35
|
|
36
|
+
# @private
|
37
|
+
def __block_channel(force = false)
|
38
|
+
@__block_channel_i ||= 0
|
39
|
+
@__block_channel_i += 1 if force
|
40
|
+
|
41
|
+
"block:#{object_id}:#{@__block_channel_i}"
|
42
|
+
end
|
43
|
+
|
36
44
|
# @private
|
37
45
|
# pause a fiber and resume in the next iteration of the event loop
|
38
46
|
def self.pause
|
data/lib/rage/fiber_scheduler.rb
CHANGED
@@ -81,7 +81,7 @@ class Rage::FiberScheduler
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def block(_blocker, timeout = nil)
|
84
|
-
f, fulfilled, channel = Fiber.current, false,
|
84
|
+
f, fulfilled, channel = Fiber.current, false, Fiber.current.__block_channel(true)
|
85
85
|
|
86
86
|
resume_fiber_block = proc do
|
87
87
|
unless fulfilled
|
@@ -100,7 +100,7 @@ class Rage::FiberScheduler
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def unblock(_blocker, fiber)
|
103
|
-
::Iodine.publish(
|
103
|
+
::Iodine.publish(fiber.__block_channel, "")
|
104
104
|
end
|
105
105
|
|
106
106
|
def fiber(&block)
|
@@ -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
@@ -56,8 +56,8 @@ class Rage::Logger
|
|
56
56
|
# @param shift_period_suffix [String] the log file suffix format for daily, weekly or monthly rotation
|
57
57
|
# @param binmode sets whether the logger writes in binary mode
|
58
58
|
def initialize(log, level: Logger::DEBUG, formatter: Rage::TextFormatter.new, shift_age: 0, shift_size: 104857600, shift_period_suffix: "%Y%m%d", binmode: false)
|
59
|
-
if log && log != File::NULL
|
60
|
-
|
59
|
+
@logdev = if log && log != File::NULL
|
60
|
+
Logger::LogDevice.new(log, shift_age:, shift_size:, shift_period_suffix:, binmode:)
|
61
61
|
end
|
62
62
|
|
63
63
|
@formatter = formatter
|
@@ -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
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Cors
|
4
|
+
# @private
|
5
|
+
def initialize(app, *, &)
|
6
|
+
@app = app
|
7
|
+
instance_eval(&)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @private
|
11
|
+
def call(env)
|
12
|
+
if env["REQUEST_METHOD"] == "OPTIONS"
|
13
|
+
return (response = @cors_response)
|
14
|
+
end
|
15
|
+
|
16
|
+
response = @app.call(env)
|
17
|
+
response[1]["Access-Control-Allow-Credentials"] = @allow_credentials if @allow_credentials
|
18
|
+
response[1]["Access-Control-Expose-Headers"] = @expose_headers if @expose_headers
|
19
|
+
|
20
|
+
response
|
21
|
+
ensure
|
22
|
+
if !$! && origin = @cors_check.call(env)
|
23
|
+
headers = response[1]
|
24
|
+
headers["Access-Control-Allow-Origin"] = origin
|
25
|
+
if @origins != "*"
|
26
|
+
vary = headers["Vary"]
|
27
|
+
if vary.nil?
|
28
|
+
headers["Vary"] = "Origin"
|
29
|
+
elsif vary != "Origin"
|
30
|
+
headers["Vary"] += ", Origin"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set CORS rules for the application.
|
37
|
+
#
|
38
|
+
# @param origins [String, Regexp, "*"] origins allowed to access the application
|
39
|
+
# @param methods [Array<Symbol>, "*"] allowed methods when accessing the application
|
40
|
+
# @param allow_headers [Array<String>, "*"] indicate which HTTP headers can be used when making the actual request
|
41
|
+
# @param expose_headers [Array<String>, "*"] adds the specified headers to the allowlist that JavaScript in browsers is allowed to access
|
42
|
+
# @param max_age [Integer] indicate how long the results of a preflight request can be cached
|
43
|
+
# @param allow_credentials [Boolean] indicate whether or not the response to the request can be exposed when the `credentials` flag is `true`
|
44
|
+
# @example
|
45
|
+
# config.middleware.use Rage::Cors do
|
46
|
+
# allow "localhost:5173", "myhost.com"
|
47
|
+
# end
|
48
|
+
# @example
|
49
|
+
# config.middleware.use Rage::Cors do
|
50
|
+
# allow "*",
|
51
|
+
# methods: [:get, :post, :put],
|
52
|
+
# allow_headers: ["x-domain-token"],
|
53
|
+
# expose: ["Some-Custom-Response-Header"],
|
54
|
+
# max_age: 600
|
55
|
+
# end
|
56
|
+
# @note The middleware only supports the basic case of allowing one or several origins for the whole application. Use {https://github.com/cyu/rack-cors Rack::Cors} if you are looking to specify more advanced rules.
|
57
|
+
def allow(*origins, methods: "*", allow_headers: "*", expose_headers: nil, max_age: nil, allow_credentials: false)
|
58
|
+
@allow_headers = Array(allow_headers).join(", ") if allow_headers
|
59
|
+
@expose_headers = Array(expose_headers).join(", ") if expose_headers
|
60
|
+
@max_age = max_age.to_s if max_age
|
61
|
+
@allow_credentials = "true" if allow_credentials
|
62
|
+
|
63
|
+
@default_methods = %w(GET POST PUT PATCH DELETE HEAD OPTIONS)
|
64
|
+
@methods = if methods != "*"
|
65
|
+
methods.map! { |method| method.to_s.upcase }.tap { |m|
|
66
|
+
if (invalid_methods = m - @default_methods).any?
|
67
|
+
raise "Unsupported method passed to Rage::Cors: #{invalid_methods[0]}"
|
68
|
+
end
|
69
|
+
}.join(", ")
|
70
|
+
elsif @allow_credentials
|
71
|
+
@default_methods.join(", ")
|
72
|
+
else
|
73
|
+
"*"
|
74
|
+
end
|
75
|
+
|
76
|
+
if @allow_credentials
|
77
|
+
raise "Rage::Cors requires you to explicitly list allowed headers when using `allow_credentials: true`" if @allow_headers == "*"
|
78
|
+
raise "Rage::Cors requires you to explicitly list exposed headers when using `allow_credentials: true`" if @expose_headers == "*"
|
79
|
+
end
|
80
|
+
|
81
|
+
@origins = []
|
82
|
+
origins.each do |origin|
|
83
|
+
if origin == "*"
|
84
|
+
@origins = "*"
|
85
|
+
break
|
86
|
+
elsif origin.is_a?(Regexp) || origin =~ /^\S+:\/\//
|
87
|
+
@origins << origin
|
88
|
+
else
|
89
|
+
@origins << "https://#{origin}" << "http://#{origin}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@cors_check = create_cors_proc
|
94
|
+
@cors_response = [204, create_headers, []]
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def create_headers
|
100
|
+
headers = {
|
101
|
+
"Access-Control-Allow-Origin" => "",
|
102
|
+
"Access-Control-Allow-Methods" => @methods,
|
103
|
+
}
|
104
|
+
|
105
|
+
if @allow_headers
|
106
|
+
headers["Access-Control-Allow-Headers"] = @allow_headers
|
107
|
+
end
|
108
|
+
if @expose_headers
|
109
|
+
headers["Access-Control-Expose-Headers"] = @expose_headers
|
110
|
+
end
|
111
|
+
if @max_age
|
112
|
+
headers["Access-Control-Max-Age"] = @max_age
|
113
|
+
end
|
114
|
+
if @allow_credentials
|
115
|
+
headers["Access-Control-Allow-Credentials"] = @allow_credentials
|
116
|
+
end
|
117
|
+
|
118
|
+
headers
|
119
|
+
end
|
120
|
+
|
121
|
+
def create_cors_proc
|
122
|
+
if @origins == "*"
|
123
|
+
->(env) { env["HTTP_ORIGIN"] }
|
124
|
+
else
|
125
|
+
origins_eval = @origins.map { |origin|
|
126
|
+
origin.is_a?(Regexp) ?
|
127
|
+
"origin =~ /#{origin.source}/.freeze" :
|
128
|
+
"origin == '#{origin}'.freeze"
|
129
|
+
}.join(" || ")
|
130
|
+
|
131
|
+
eval <<-RUBY
|
132
|
+
->(env) do
|
133
|
+
origin = env["HTTP_ORIGIN".freeze]
|
134
|
+
origin if #{origins_eval}
|
135
|
+
end
|
136
|
+
RUBY
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
##
|
4
|
-
# The middleware wraps every request in a Fiber
|
5
|
-
#
|
4
|
+
# The middleware wraps every request in a separate Fiber. It should always be on the top of the middleware stack,
|
5
|
+
# as it implements a custom defer protocol, which may break middlewares located above.
|
6
6
|
#
|
7
7
|
class Rage::FiberWrapper
|
8
8
|
def initialize(app)
|
@@ -16,9 +16,11 @@ class Rage::FiberWrapper
|
|
16
16
|
fiber = Fiber.schedule do
|
17
17
|
@app.call(env)
|
18
18
|
ensure
|
19
|
+
# notify Iodine the request can now be resumed
|
19
20
|
Iodine.publish(Fiber.current.__get_id, "", Iodine::PubSub::PROCESS) if Fiber.current.__yielded?
|
20
21
|
end
|
21
22
|
|
23
|
+
# the fiber encountered blocking IO and yielded; instruct Iodine to pause the request
|
22
24
|
if fiber.alive?
|
23
25
|
[:__http_defer__, fiber]
|
24
26
|
else
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Reloader
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
Rage.code_loader.reload
|
10
|
+
@app.call(env)
|
11
|
+
rescue Exception => e
|
12
|
+
exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
|
13
|
+
puts(exception_str)
|
14
|
+
[500, {}, [exception_str]]
|
15
|
+
end
|
16
|
+
end
|