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.
@@ -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
- <<-RUBY
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
- <<-RUBY
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
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
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
- if block_given?
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
- # @private
214
- DEFAULT_HEADERS = { "content-type" => "application/json; charset=utf-8" }.freeze
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, DEFAULT_HEADERS, []
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
- yield auth_header[7..]
399
+ payload = if auth_header&.start_with?("Bearer")
400
+ auth_header[7..]
304
401
  elsif auth_header&.start_with?("Token")
305
- yield auth_header[6..]
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
@@ -1,4 +1,7 @@
1
1
  module Rage::Errors
2
2
  class BadRequest < StandardError
3
3
  end
4
+
5
+ class RouterError < StandardError
6
+ end
4
7
  end
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
@@ -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, "unblock:#{Fiber.current.object_id}"
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("unblock:#{fiber.object_id}", "")
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
@@ -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
- @logdev = Logger::LogDevice.new(log, shift_age:, shift_size:, shift_period_suffix:, binmode:)
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 IRB - don't use the formatter
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 and implements the custom defer protocol with Iodine.
5
- # Scheduling fibers in a middleware allows the framework to be compatibe with custom Rack middlewares.
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