rage-rb 0.5.2 → 0.6.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: abee0787faa5947d8ecb03f9c5851cb669295bc2bd52abe5513e7b10940ddfff
4
- data.tar.gz: d167a4d6a48dbced809578f3c3a569c5ccdb913ed7bf51eba3e8d054b61e1c75
3
+ metadata.gz: 0eed317e69386a2c2cc765df7be687be754934801390e96013902e78670171eb
4
+ data.tar.gz: 44c899c5843d112ca222df4b863852df9e3728a63954ee3b5ab4ffd2cffd4e2e
5
5
  SHA512:
6
- metadata.gz: eea4d03aa25b675fdf1f19836037dceaeb7f38e2227af9cafc1609d59b8f322d3e3fa49da7905b4c0dd783615d16ce6e592aa11f2fa934056d711f03aaafbb43
7
- data.tar.gz: adf6eaaee9a88eef39631581ce52eaa311b5ac61797f481a95e28502ccabf166615a87e215b5f5b3260ebdfbb5c28f8c32e1e8342566d9d23836e56eb97b14d8
6
+ metadata.gz: 4e158fd8202e9c6e4f94a7b243383c34f2179306a10724978083b6599197197221915857540ae2beb5ea98a5f602c6507bec89d192a57706b3aae2203c75a8a3
7
+ data.tar.gz: e9723bb6a0ef7b1a67db08799e3429ae4c2e9f88894afb9e614645673df7cb4e019107f0ca1339eb521fa7958fd259ca032385f98fdbddacd59415d960f0c347
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2023-12-22
4
+
5
+ ### Added
6
+
7
+ - Implement after actions (#53).
8
+ - Zeitwerk autoloading and reloading by [@alex-rogachev](https://github.com/alex-rogachev) (#54).
9
+ - Support the `environment`, `binding`, `timeout`, and `max_clients` options when using `rage s` (#52).
10
+ - Add CORS middleware (#49).
11
+
12
+ ### Fixed
13
+
14
+ - Prevent `block` and `sleep` channels from conflicting (#51).
15
+
3
16
  ## [0.5.2] - 2023-12-11
4
17
 
5
18
  ### Added
data/CODE_OF_CONDUCT.md CHANGED
@@ -39,7 +39,7 @@ This Code of Conduct applies within all community spaces, and also applies when
39
39
 
40
40
  ## Enforcement
41
41
 
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at roman.samoilov@wework.com. All complaints will be reviewed and investigated promptly and fairly.
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at developers@rage-rb.dev. All complaints will be reviewed and investigated promptly and fairly.
43
43
 
44
44
  All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
45
 
data/README.md CHANGED
@@ -52,6 +52,7 @@ Check out in-depth API docs for more information:
52
52
  - [Routing API](https://rage-rb.pages.dev/Rage/Router/DSL/Handler)
53
53
  - [Fiber API](https://rage-rb.pages.dev/Fiber)
54
54
  - [Logger API](https://rage-rb.pages.dev/Rage/Logger)
55
+ - [Configuration API](https://rage-rb.pages.dev/Rage/Configuration)
55
56
 
56
57
  Also, see the [changelog](https://github.com/rage-rb/rage/blob/master/CHANGELOG.md) and [upcoming-releases](https://github.com/rage-rb/rage#upcoming-releases) for currently supported and planned features.
57
58
 
data/lib/rage/all.rb CHANGED
@@ -9,7 +9,7 @@ require_relative "request"
9
9
  require_relative "uploaded_file"
10
10
  require_relative "errors"
11
11
  require_relative "params_parser"
12
- require_relative "fiber_wrapper"
12
+ require_relative "code_loader"
13
13
 
14
14
  require_relative "router/strategies/host"
15
15
  require_relative "router/backend"
@@ -23,6 +23,10 @@ require_relative "controller/api"
23
23
  require_relative "logger/text_formatter"
24
24
  require_relative "logger/logger"
25
25
 
26
+ require_relative "middleware/fiber_wrapper"
27
+ require_relative "middleware/cors"
28
+ require_relative "middleware/reloader"
29
+
26
30
  if defined?(Sidekiq)
27
31
  require_relative "sidekiq_session"
28
32
  end
data/lib/rage/cli.rb CHANGED
@@ -16,18 +16,23 @@ module Rage
16
16
 
17
17
  desc "s", "Start the app server."
18
18
  option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000."
19
+ option :environment, aliases: "-e", desc: "Specifies the environment to run this server under (test/development/production)."
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 :help, aliases: "-h", desc: "Show this message."
19
22
  def server
23
+ return help("server") if options.help?
24
+
25
+ set_env(options)
26
+
20
27
  app = ::Rack::Builder.parse_file("config.ru")
21
28
  app = app[0] if app.is_a?(Array)
22
29
 
23
- unless app.is_a?(Rage::FiberWrapper)
24
- raise <<-ERR
25
- Couldn't find the default middleware. Make sure to add the following line to your config.ru file:
26
- Rage.load_middlewares(self)
27
- ERR
28
- end
30
+ port = options[:port] || Rage.config.server.port
31
+ address = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost")
32
+ timeout = Rage.config.server.timeout
33
+ max_clients = Rage.config.server.max_clients
29
34
 
30
- ::Iodine.listen service: :http, handler: app, port: options[:port] || Rage.config.server.port
35
+ ::Iodine.listen service: :http, handler: app, port: port, address: address, timeout: timeout, max_clients: max_clients
31
36
  ::Iodine.threads = Rage.config.server.threads_count
32
37
  ::Iodine.workers = Rage.config.server.workers_count
33
38
 
@@ -36,12 +41,15 @@ module Rage
36
41
 
37
42
  desc 'routes', 'List all routes.'
38
43
  option :grep, aliases: "-g", desc: "Filter routes by pattern"
44
+ option :help, aliases: "-h", desc: "Show this message."
39
45
  def routes
46
+ return help("routes") if options.help?
40
47
  # the result would be something like this:
41
48
  # Verb Path Controller#Action
42
49
  # GET / application#index
43
50
 
44
51
  # load config/application.rb
52
+ set_env(options)
45
53
  environment
46
54
 
47
55
  routes = Rage.__router.routes
@@ -90,7 +98,12 @@ module Rage
90
98
  end
91
99
 
92
100
  desc "c", "Start the app console."
101
+ option :help, aliases: "-h", desc: "Show this message."
93
102
  def console
103
+ return help("console") if options.help?
104
+
105
+ set_env(options)
106
+
94
107
  require "irb"
95
108
  environment
96
109
  ARGV.clear
@@ -102,6 +115,10 @@ module Rage
102
115
  def environment
103
116
  require File.expand_path("config/application.rb", Dir.pwd)
104
117
  end
118
+
119
+ def set_env(options)
120
+ ENV["RAGE_ENV"] = options[:environment] || ENV["RAGE_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
121
+ end
105
122
  end
106
123
 
107
124
  class NewAppGenerator < Thor::Group
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ class Rage::CodeLoader
6
+ def initialize
7
+ @loader = Zeitwerk::Loader.new
8
+ @reloading = false
9
+ end
10
+
11
+ def setup
12
+ autoload_path = "#{Rage.root}/app"
13
+ enable_reloading = Rage.env.development?
14
+ enable_eager_loading = !Rage.env.development? && !Rage.env.test?
15
+
16
+ @loader.push_dir(autoload_path)
17
+ # The first level of directories in app directory won't be treated as modules
18
+ # e.g. app/controllers/pages_controller.rb will be linked to PagesController class
19
+ # instead of Controllers::PagesController
20
+ @loader.collapse("#{Rage.root}/app/*")
21
+ @loader.enable_reloading if enable_reloading
22
+ @loader.setup
23
+ @loader.eager_load if enable_eager_loading
24
+ end
25
+
26
+ def reload
27
+ @reloading = true
28
+ @loader.reload
29
+ Rage.__router.reset_routes
30
+ load("#{Rage.root}/config/routes.rb")
31
+ end
32
+
33
+ def reloading?
34
+ @reloading
35
+ end
36
+ end
@@ -1,9 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # # General Configuration
5
+ #
6
+ # • _config.logger_
7
+ #
8
+ # > The logger that will be used for `Rage.logger` and any related `Rage` logging. Custom loggers should implement Ruby's {https://ruby-doc.org/3.2.2/stdlibs/logger/Logger.html#class-Logger-label-Entries Logger} interface.
9
+ #
10
+ # • _config.log_formatter_
11
+ #
12
+ # > The formatter of the Rage logger. Built in options include `Rage::TextFormatter` and `Rage::JSONFormatter`. Defaults to an instance of `Rage::TextFormatter`.
13
+ #
14
+ # • _config.log_level_
15
+ #
16
+ # > Defines the verbosity of the Rage logger. This option defaults to `:debug` for all environments except production, where it defaults to `:info`. The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`.
17
+ #
18
+ # # Middleware Configuration
19
+ #
20
+ # • _config.middleware.use_
21
+ #
22
+ # > Adds a middleware to the top of the middleware stack. **This is the preferred way of adding a middleware.**
23
+ # > ```
24
+ # config.middleware.use Rack::Cors do
25
+ # allow do
26
+ # origins "*"
27
+ # resource "*", headers: :any
28
+ # end
29
+ # end
30
+ # > ```
31
+ #
32
+ # • _config.middleware.insert_before_
33
+ #
34
+ # > Adds middleware at a specified position before another middleware. The position can be either an index or another middleware.
35
+ #
36
+ # > **_❗️Heads up:_** By default, Rage always uses the `Rage::FiberWrapper` middleware, which wraps every request in a separate fiber. Make sure to always have this middleware in the top of the stack. Placing other middlewares in front may lead to undefined behavior.
37
+ #
38
+ # > ```
39
+ # config.middleware.insert_before Rack::Head, Magical::Unicorns
40
+ # config.middleware.insert_before 0, Magical::Unicorns
41
+ # > ```
42
+ #
43
+ # • _config.middleware.insert_after_
44
+ #
45
+ # > Adds middleware at a specified position after another middleware. The position can be either an index or another middleware.
46
+ #
47
+ # > ```
48
+ # config.middleware.insert_after Rack::Head, Magical::Unicorns
49
+ # > ```
50
+ #
51
+ # # Server Configuration
52
+ #
53
+ # _• config.server.max_clients_
54
+ #
55
+ # > Limits the number of simultaneous connections the server can accept. Defaults to the maximum number of open files.
56
+ #
57
+ # > **_❗️Heads up:_** Decreasing this number is almost never a good idea. Depending on your application specifics, you are encouraged to use other methods to limit the number of concurrent connections:
58
+ #
59
+ # > 1. If your application is exposed to the public, you may want to use a cloud rate limiter, like {https://developers.cloudflare.com/waf Cloudflare WAF} or {https://docs.fastly.com/en/ngwaf Fastly WAF}.
60
+ # > 2. Otherwise, consider using tools like {https://github.com/rack/rack-attack Rack::Attack} or {https://github.com/mperham/connection_pool connection_pool}.
61
+ #
62
+ # > ```
63
+ # # Limit the amount of connections your application can accept
64
+ # config.middleware.use Rack::Attack
65
+ # Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |req|
66
+ # req.ip
67
+ # end
68
+ # #
69
+ # # Limit the amount of connections to a specific resource
70
+ # HTTP = ConnectionPool.new(size: 5, timeout: 5) { Net::HTTP }
71
+ # HTTP.with do |conn|
72
+ # conn.get("/my-resource")
73
+ # end
74
+ # > ```
75
+ #
76
+ # • _config.server.port_
77
+ #
78
+ # > Specifies what port the server will listen on.
79
+ #
80
+ # • _config.server.workers_count_
81
+ #
82
+ # > Specifies the number of server processes to run.
83
+ #
84
+ # • _config.server.timeout_
85
+ #
86
+ # > Specifies connection timeout.
87
+ #
1
88
  class Rage::Configuration
2
- attr_accessor :logger, :log_formatter, :log_level
89
+ attr_accessor :logger
90
+ attr_reader :log_formatter, :log_level
3
91
 
4
92
  # used in DSL
5
93
  def config = self
6
94
 
95
+ def log_formatter=(formatter)
96
+ raise "Custom log formatter should respond to `#call`" unless formatter.respond_to?(:call)
97
+ @log_formatter = formatter
98
+ end
99
+
100
+ def log_level=(level)
101
+ @log_level = level.is_a?(Symbol) ? Logger.const_get(level.to_s.upcase) : level
102
+ end
103
+
7
104
  def server
8
105
  @server ||= Server.new
9
106
  end
@@ -13,7 +110,7 @@ class Rage::Configuration
13
110
  end
14
111
 
15
112
  class Server
16
- attr_accessor :port, :workers_count
113
+ attr_accessor :port, :workers_count, :timeout, :max_clients
17
114
  attr_reader :threads_count
18
115
 
19
116
  def initialize
@@ -36,6 +133,9 @@ class Rage::Configuration
36
133
 
37
134
  def insert_before(existing_middleware, new_middleware, *args, &block)
38
135
  index = find_middleware_index(existing_middleware)
136
+ if index == 0 && @middlewares[0][0] == Rage::FiberWrapper
137
+ puts("Warning: inserting #{new_middleware} before Rage::FiberWrapper may lead to undefined behavior.")
138
+ end
39
139
  @middlewares = (@middlewares[0...index] + [[new_middleware, args, block]] + @middlewares[index..]).uniq(&:first)
40
140
  end
41
141
 
@@ -60,9 +160,13 @@ class Rage::Configuration
60
160
  end
61
161
  end
62
162
 
163
+ # @private
63
164
  def __finalize
64
- @logger ||= Rage::Logger.new(nil)
65
- @logger.formatter = @log_formatter if @logger && @log_formatter
66
- @logger.level = @log_level if @logger && @log_level
165
+ if @logger
166
+ @logger.formatter = @log_formatter if @log_formatter
167
+ @logger.level = @log_level if @log_level
168
+ else
169
+ @logger = Rage::Logger.new(nil)
170
+ end
67
171
  end
68
172
  end
@@ -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,11 +75,18 @@ class RageController::API
50
75
  ""
51
76
  end
52
77
 
53
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
78
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
54
79
  def __run_#{action}
55
80
  #{before_actions_chunk}
56
81
  #{action}
57
82
 
83
+ #{if !after_actions_chunk.empty?
84
+ <<~RUBY
85
+ @__rendered = true
86
+ #{after_actions_chunk}
87
+ RUBY
88
+ end}
89
+
58
90
  [@__status, @__headers, @__body]
59
91
 
60
92
  #{rescue_handlers_chunk}
@@ -63,13 +95,14 @@ class RageController::API
63
95
  end
64
96
 
65
97
  # @private
66
- attr_writer :__before_actions, :__rescue_handlers
98
+ attr_writer :__before_actions, :__after_actions, :__rescue_handlers
67
99
 
68
100
  # @private
69
101
  # pass the variable down to the child; the child will continue to use it until changes need to be made;
70
102
  # only then the object will be copied; the frozen state communicates that the object is shared with the parent;
71
103
  def inherited(klass)
72
104
  klass.__before_actions = @__before_actions.freeze
105
+ klass.__after_actions = @__after_actions.freeze
73
106
  klass.__rescue_handlers = @__rescue_handlers.freeze
74
107
  end
75
108
 
@@ -148,29 +181,12 @@ class RageController::API
148
181
  # end
149
182
  # @note The block form doesn't receive an argument and is executed on the controller level as if it was a regular method.
150
183
  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)
184
+ action = prepare_action_params(action_name, **opts, &block)
158
185
 
159
186
  if @__before_actions && @__before_actions.frozen?
160
187
  @__before_actions = @__before_actions.dup
161
188
  end
162
189
 
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
190
  if @__before_actions.nil?
175
191
  @__before_actions = [action]
176
192
  elsif i = @__before_actions.find_index { |a| a[:name] == action_name }
@@ -180,6 +196,22 @@ class RageController::API
180
196
  end
181
197
  end
182
198
 
199
+ def after_action(action_name = nil, **opts, &block)
200
+ action = prepare_action_params(action_name, **opts, &block)
201
+
202
+ if @__after_actions && @__after_actions.frozen?
203
+ @__after_actions = @__after_actions.dup
204
+ end
205
+
206
+ if @__after_actions.nil?
207
+ @__after_actions = [action]
208
+ elsif i = @__after_actions.find_index { |a| a[:name] == action_name }
209
+ @__after_actions[i] = action
210
+ else
211
+ @__after_actions << action
212
+ end
213
+ end
214
+
183
215
  # Prevent a `before_action` hook from running.
184
216
  #
185
217
  # @param action_name [String] the name of the callback to skip
@@ -208,6 +240,32 @@ class RageController::API
208
240
 
209
241
  @__before_actions[i] = action
210
242
  end
243
+
244
+ private
245
+
246
+ # used by `before_action` and `after_action`
247
+ def prepare_action_params(action_name = nil, **opts, &block)
248
+ if block_given?
249
+ action_name = define_tmp_method(block)
250
+ elsif action_name.nil?
251
+ raise "No handler provided. Pass the `action_name` parameter or provide a block."
252
+ end
253
+
254
+ _only, _except, _if, _unless = opts.values_at(:only, :except, :if, :unless)
255
+
256
+ action = {
257
+ name: action_name,
258
+ only: _only && Array(_only),
259
+ except: _except && Array(_except),
260
+ if: _if,
261
+ unless: _unless
262
+ }
263
+
264
+ action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
265
+ action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
266
+
267
+ action
268
+ end
211
269
  end # class << self
212
270
 
213
271
  # @private
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)
@@ -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
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::Cors
4
+ def initialize(app, *, &)
5
+ @app = app
6
+ instance_eval(&)
7
+ end
8
+
9
+ def call(env)
10
+ if env["REQUEST_METHOD"] == "OPTIONS"
11
+ return (response = @cors_response)
12
+ end
13
+
14
+ response = @app.call(env)
15
+ response[1]["Access-Control-Allow-Credentials"] = @allow_credentials if @allow_credentials
16
+ response[1]["Access-Control-Expose-Headers"] = @expose_headers if @expose_headers
17
+
18
+ response
19
+ ensure
20
+ if origin = @cors_check.call(env)
21
+ headers = response[1]
22
+ headers["Access-Control-Allow-Origin"] = origin
23
+ if @origins != "*"
24
+ vary = headers["Vary"]
25
+ if vary.nil?
26
+ headers["Vary"] = "Origin"
27
+ elsif vary != "Origin"
28
+ headers["Vary"] += ", Origin"
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Set CORS rules for the application.
35
+ #
36
+ # @param origins [String, Regexp, "*"] origins allowed to access the application
37
+ # @param methods [Array<Symbol>, "*"] allowed methods when accessing the application
38
+ # @param allow_headers [Array<String>, "*"] indicate which HTTP headers can be used when making the actual request
39
+ # @param expose_headers [Array<String>, "*"] adds the specified headers to the allowlist that JavaScript in browsers is allowed to access
40
+ # @param max_age [Integer] indicate how long the results of a preflight request can be cached
41
+ # @param allow_credentials [Boolean] indicate whether or not the response to the request can be exposed when the `credentials` flag is `true`
42
+ # @example
43
+ # config.middleware.use Rage::Cors do
44
+ # allow "localhost:5173", "myhost.com"
45
+ # end
46
+ # @example
47
+ # config.middleware.use Rage::Cors do
48
+ # allow "*",
49
+ # methods: [:get, :post, :put],
50
+ # allow_headers: ["x-domain-token"],
51
+ # expose: ["Some-Custom-Response-Header"],
52
+ # max_age: 600
53
+ # end
54
+ # @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.
55
+ def allow(*origins, methods: "*", allow_headers: "*", expose_headers: nil, max_age: nil, allow_credentials: false)
56
+ @allow_headers = Array(allow_headers).join(", ") if allow_headers
57
+ @expose_headers = Array(expose_headers).join(", ") if expose_headers
58
+ @max_age = max_age.to_s if max_age
59
+ @allow_credentials = "true" if allow_credentials
60
+
61
+ @default_methods = %w(GET POST PUT PATCH DELETE HEAD OPTIONS)
62
+ @methods = if methods != "*"
63
+ methods.map! { |method| method.to_s.upcase }.tap { |m|
64
+ if (invalid_methods = m - @default_methods).any?
65
+ raise "Unsupported method passed to Rage::Cors: #{invalid_methods[0]}"
66
+ end
67
+ }.join(", ")
68
+ elsif @allow_credentials
69
+ @default_methods.join(", ")
70
+ else
71
+ "*"
72
+ end
73
+
74
+ if @allow_credentials
75
+ raise "Rage::Cors requires you to explicitly list allowed headers when using `allow_credentials: true`" if @allow_headers == "*"
76
+ raise "Rage::Cors requires you to explicitly list exposed headers when using `allow_credentials: true`" if @expose_headers == "*"
77
+ end
78
+
79
+ @origins = []
80
+ origins.each do |origin|
81
+ if origin == "*"
82
+ @origins = "*"
83
+ break
84
+ elsif origin.is_a?(Regexp) || origin =~ /^\S+:\/\//
85
+ @origins << origin
86
+ else
87
+ @origins << "https://#{origin}" << "http://#{origin}"
88
+ end
89
+ end
90
+
91
+ @cors_check = create_cors_proc
92
+ @cors_response = [204, create_headers, []]
93
+ end
94
+
95
+ private
96
+
97
+ def create_headers
98
+ headers = {
99
+ "Access-Control-Allow-Origin" => "",
100
+ "Access-Control-Allow-Methods" => @methods,
101
+ }
102
+
103
+ if @allow_headers
104
+ headers["Access-Control-Allow-Headers"] = @allow_headers
105
+ end
106
+ if @expose_headers
107
+ headers["Access-Control-Expose-Headers"] = @expose_headers
108
+ end
109
+ if @max_age
110
+ headers["Access-Control-Max-Age"] = @max_age
111
+ end
112
+ if @allow_credentials
113
+ headers["Access-Control-Allow-Credentials"] = @allow_credentials
114
+ end
115
+
116
+ headers
117
+ end
118
+
119
+ def create_cors_proc
120
+ if @origins == "*"
121
+ ->(env) { env["HTTP_ORIGIN"] }
122
+ else
123
+ origins_eval = @origins.map { |origin|
124
+ origin.is_a?(Regexp) ?
125
+ "origin =~ /#{origin.source}/.freeze" :
126
+ "origin == '#{origin}'.freeze"
127
+ }.join(" || ")
128
+
129
+ eval <<-RUBY
130
+ ->(env) do
131
+ origin = env["HTTP_ORIGIN".freeze]
132
+ origin if #{origins_eval}
133
+ end
134
+ RUBY
135
+ end
136
+ end
137
+ 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
@@ -14,6 +14,11 @@ class Rage::Router::Backend
14
14
  @constrainer = Rage::Router::Constrainer.new({})
15
15
  end
16
16
 
17
+ def reset_routes
18
+ @routes = []
19
+ @trees = {}
20
+ end
21
+
17
22
  def mount(path, handler, methods)
18
23
  raise "Mount handler should respond to `call`" unless handler.respond_to?(:call)
19
24
 
@@ -79,6 +84,9 @@ class Rage::Router::Backend
79
84
  end
80
85
 
81
86
  __on(method, path, handler, constraints, defaults, meta)
87
+
88
+ rescue Rage::Errors::RouterError => e
89
+ raise e unless Rage.code_loader.reloading?
82
90
  end
83
91
 
84
92
  def lookup(env)
@@ -280,7 +288,7 @@ class Rage::Router::Backend
280
288
  if Object.const_defined?(klass)
281
289
  Object.const_get(klass)
282
290
  else
283
- raise "Routing error: could not find the #{klass} class"
291
+ raise Rage::Errors::RouterError, "Routing error: could not find the #{klass} class"
284
292
  end
285
293
  end
286
294
  end
data/lib/rage/setup.rb CHANGED
@@ -2,22 +2,10 @@ Iodine.patch_rack
2
2
 
3
3
  require_relative "#{Rage.root}/config/environments/#{Rage.env}"
4
4
 
5
+ # Run application initializers
6
+ Dir["#{Rage.root}/config/initializers/**/*.rb"].each { |initializer| load(initializer) }
5
7
 
6
- # load application files
7
- app, bad = Dir["#{Rage.root}/app/**/*.rb"], []
8
-
9
- loop do
10
- path = app.shift
11
- break if path.nil?
12
-
13
- require_relative path
14
-
15
- # push the file to the end of the list in case it depends on another file that has not yet been required;
16
- # re-raise if only errored out files are left
17
- rescue NameError
18
- raise if (app - bad).empty?
19
- app << path
20
- bad << path
21
- end
8
+ # Load application classes
9
+ Rage.code_loader.setup
22
10
 
23
11
  require_relative "#{Rage.root}/config/routes"
@@ -7,4 +7,6 @@ Rage.configure do
7
7
 
8
8
  # Specify the logger
9
9
  config.logger = Rage::Logger.new(STDOUT)
10
+
11
+ config.middleware.use Rage::Reloader
10
12
  end
File without changes
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.5.2"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -28,7 +28,7 @@ module Rage
28
28
  end
29
29
 
30
30
  def self.env
31
- @__env ||= Rage::Env.new(ENV["RAGE_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
31
+ @__env ||= Rage::Env.new(ENV["RAGE_ENV"])
32
32
  end
33
33
 
34
34
  def self.groups
@@ -49,6 +49,10 @@ module Rage
49
49
  end
50
50
  end
51
51
 
52
+ def self.code_loader
53
+ @code_loader ||= Rage::CodeLoader.new
54
+ end
55
+
52
56
  module Router
53
57
  module Strategies
54
58
  end
data/rage.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency "thor", "~> 1.0"
31
31
  spec.add_dependency "rack", "~> 2.0"
32
32
  spec.add_dependency "rage-iodine", "~> 3.0"
33
+ spec.add_dependency "zeitwerk", "~> 2.6"
33
34
  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.5.2
4
+ version: 0.6.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-11 00:00:00.000000000 Z
11
+ date: 2023-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.6'
55
69
  description:
56
70
  email:
57
71
  - rsamoi@icloud.com
@@ -74,15 +88,18 @@ files:
74
88
  - lib/rage/all.rb
75
89
  - lib/rage/application.rb
76
90
  - lib/rage/cli.rb
91
+ - lib/rage/code_loader.rb
77
92
  - lib/rage/configuration.rb
78
93
  - lib/rage/controller/api.rb
79
94
  - lib/rage/env.rb
80
95
  - lib/rage/errors.rb
81
96
  - lib/rage/fiber.rb
82
97
  - lib/rage/fiber_scheduler.rb
83
- - lib/rage/fiber_wrapper.rb
84
98
  - lib/rage/logger/logger.rb
85
99
  - lib/rage/logger/text_formatter.rb
100
+ - lib/rage/middleware/cors.rb
101
+ - lib/rage/middleware/fiber_wrapper.rb
102
+ - lib/rage/middleware/reloader.rb
86
103
  - lib/rage/params_parser.rb
87
104
  - lib/rage/request.rb
88
105
  - lib/rage/router/README.md
@@ -101,6 +118,7 @@ files:
101
118
  - lib/rage/templates/config-environments-development.rb
102
119
  - lib/rage/templates/config-environments-production.rb
103
120
  - lib/rage/templates/config-environments-test.rb
121
+ - lib/rage/templates/config-initializers-.keep
104
122
  - lib/rage/templates/config-routes.rb
105
123
  - lib/rage/templates/config.ru
106
124
  - lib/rage/templates/lib-.keep