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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0eed317e69386a2c2cc765df7be687be754934801390e96013902e78670171eb
4
- data.tar.gz: 44c899c5843d112ca222df4b863852df9e3728a63954ee3b5ab4ffd2cffd4e2e
3
+ metadata.gz: 1c3041038ae63ae245e261a7a2096195ab56291d236414e086646ae96a4a6d16
4
+ data.tar.gz: a64817ded16716fe714310c7318d8e1d318454615da43d37055b333e76778fa2
5
5
  SHA512:
6
- metadata.gz: 4e158fd8202e9c6e4f94a7b243383c34f2179306a10724978083b6599197197221915857540ae2beb5ea98a5f602c6507bec89d192a57706b3aae2203c75a8a3
7
- data.tar.gz: e9723bb6a0ef7b1a67db08799e3429ae4c2e9f88894afb9e614645673df7cb4e019107f0ca1339eb521fa7958fd259ca032385f98fdbddacd59415d960f0c347
6
+ metadata.gz: 350f5150012852a3ca2532c031ce8ef28338da6d985ef66dff59095dea6b3dbbc4498826996876722bc5b7991683fed82c2fd3854313f7ed59f9fff20aaf4315
7
+ data.tar.gz: 84cd798056a43c8eb0e94809f72f6b0ac602076aa904831a3a049a8ed13352cad9321329233063343b73e65c0fbf1521564b0cee6f3696c89877a0af15191c94
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2024-01-09
4
+
5
+ - Add conditional GET using `stale?` by [@tonekk](https://github.com/tonekk) (#55).
6
+ - Add Rails integration (#57).
7
+ - Add JSON log formatter (#59).
8
+
3
9
  ## [0.6.0] - 2023-12-22
4
10
 
5
11
  ### Added
data/README.md CHANGED
@@ -143,17 +143,17 @@ end
143
143
 
144
144
  ## Upcoming releases
145
145
 
146
- Version | Changes
147
- ------- |------------
148
- 0.2 :white_check_mark: | ~~Gem configuration by env.<br>Add `skip_before_action`.<br>Add `rescue_from`.<br>Router updates:<br>&emsp;• make the `root` helper work correctly with `scope`;<br>&emsp;• support the `defaults` option;~~
149
- 0.3 :white_check_mark: | ~~CLI updates:<br>&emsp;• `routes` task;<br>&emsp;• `console` task;<br>Support the `:if` and `:unless` options in `before_action`.<br>Allow to set response headers.~~
150
- 0.4 :white_check_mark: | ~~Expose the `params` object.<br>Support header authentication with `authenticate_with_http_token`.<br>Router updates:<br>&emsp;• add the `resources` route helper;<br>&emsp;• add the `namespace` route helper;~~
151
- 0.5 :white_check_mark: | ~~Add request logging.~~
152
- 0.6 | Automatic code reloading in development with Zeitwerk.
153
- 0.7 | Expose the `send_data` and `send_file` methods.
154
- 0.8 | Support conditional get with `etag` and `last_modified`.
155
- 0.9 | Expose the `cookies` and `session` objects.
156
- 1.0 | Implement Iodine-based equivalent of Action Cable.
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>&emsp;• make the `root` helper work correctly with `scope`;<br>&emsp;• support the `defaults` option;~~
149
+ :white_check_mark: | ~~CLI updates:<br>&emsp;• `routes` task;<br>&emsp;• `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>&emsp;• add the `resources` route helper;<br>&emsp;• 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"
@@ -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
- exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
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)
@@ -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
@@ -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
@@ -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, DEFAULT_HEADERS, []
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
- yield auth_header[7..]
399
+ payload = if auth_header&.start_with?("Bearer")
400
+ auth_header[7..]
362
401
  elsif auth_header&.start_with?("Token")
363
- yield auth_header[6..]
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
@@ -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
@@ -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
@@ -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&.include?(":")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
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.6.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: 2023-12-22 00:00:00.000000000 Z
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