rage-rb 0.1.1 → 0.2.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: 88726c2fad086904077b7822723c8f840fee5beac920f21d08862c637c2c4b42
4
- data.tar.gz: 37520e426a1036244dab498bbc3bb7c4155c2886ad3ecc3293f376d4dbcd6390
3
+ metadata.gz: 4570bfdf4f86125be1d6c35b9ccba08a956fe8a20dac392e5ae914db347ad396
4
+ data.tar.gz: '019e919339f97e641e65f9aecc91c02740875ebcbe71edf14a07144950e47f07'
5
5
  SHA512:
6
- metadata.gz: 1d635e49fc41cc68889ff1605ee1055e5ef443987b310730d91c675ca96083115f8de871a7f6d738535ee98790d2bd8b540aa476f18676fcbf6592dc719e60db
7
- data.tar.gz: cdd975b3be0832d841455d736bdf1caa23d916e357ad8d20b641303b00d34dd2711fc8de34cb9a5488020ec59ec283aafc1aacb990119c075565b7c35dbfcfdd
6
+ metadata.gz: 9ad7b89eb46407831ae723c1c15b87ea12b898ae1784989374b07fcffcc3afcff2a237861c94565afe4c5f486b99edb15022b539ab9cad142778e4b2718b92ea
7
+ data.tar.gz: 471cb6bcbf294d9a4eb3e5fd69827f26661effafc1df20447b8e7eddd0816aff411266d22104988978c46318dc08c4fa753f1e1a4fa1327268a5f24d8851ac5a
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --exclude lib/rage/templates --markup markdown --no-private -o doc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2023-09-27
4
+
5
+ ### Added
6
+
7
+ - Gem configuration by env.
8
+ - Add `skip_before_action`.
9
+ - Add `rescue_from`.
10
+ - Add `Fiber.await`.
11
+ - Support the `defaults` route option.
12
+
13
+ ### Fixed
14
+
15
+ - Ignore trailing slashes in the URLs.
16
+ - Support constraints in routes with optional params.
17
+ - Make the `root` routes helper work correctly with scopes.
18
+ - Convert objects to string when rendering text.
19
+
3
20
  ## [0.1.0] - 2023-09-15
4
21
 
5
22
  - Initial release
data/Gemfile CHANGED
@@ -8,3 +8,9 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+
12
+ gem "pg"
13
+ gem "mysql2"
14
+
15
+ gem "benchmark-ips"
16
+ gem "mustache"
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <p align="center"><img height="200" src="https://github.com/rage-rb/rage/assets/2270393/9d06e0a4-5c20-49c7-b51d-e16ce8f1e1b7" /></p>
2
+
1
3
  # Rage
2
4
 
3
5
  [![Gem Version](https://badge.fury.io/rb/rage-rb.svg)](https://badge.fury.io/rb/rage-rb)
@@ -5,16 +7,14 @@
5
7
 
6
8
  Inspired by [Deno](https://deno.com) and built on top of [Iodine](https://github.com/rage-rb/iodine), this is a Ruby web framework that is based on the following design principles:
7
9
 
8
- * **Rails compatible API** - Rails' API is clean, straightforward, and simply makes sense. I believe it was one of the reasons why Rails was so successful in the past.
10
+ * **Rails compatible API** - Rails' API is clean, straightforward, and simply makes sense. It was one of the reasons why Rails was so successful in the past.
9
11
 
10
- * **High performance** - some think performance is not a major metric for a framework, but I don't believe it's true. Poor performance is a risk, and in today's world, companies refuse to use risky technologies.
12
+ * **High performance** - some think performance is not a major metric for a framework, but it's not true. Poor performance is a risk, and in today's world, companies refuse to use risky technologies.
11
13
 
12
- * **API-only** - the only technology we should be using to create web UI is JavaScript. I recommend checking out [Vite](https://vitejs.dev) if you don't know where to start.
14
+ * **API-only** - the only technology we should be using to create web UI is JavaScript. Check out [Vite](https://vitejs.dev) if you don't know where to start.
13
15
 
14
16
  * **Acceptance of modern Ruby** - the framework includes a fiber scheduler, which means your code never blocks while waiting on IO.
15
17
 
16
- This framework results from reflecting on [Ruby's declining popularity](https://survey.stackoverflow.co/2023/#most-popular-technologies-language) and attempting to answer why this is happening and what we, as a community, could be doing differently.
17
-
18
18
  ## Installation
19
19
 
20
20
  Install the gem:
@@ -40,6 +40,76 @@ $ rage s
40
40
 
41
41
  Start coding!
42
42
 
43
+ ## Getting Started
44
+
45
+ This gem is designed to be a drop-in replacement for Rails in API mode. Public API is mostly expected to match Rails, however, sometimes it's a little bit more strict.
46
+
47
+ Check out in-depth API docs for more information:
48
+
49
+ - [Controller API](https://rage-rb.github.io/rage/RageController/API.html)
50
+ - [Routing API](https://rage-rb.github.io/rage/Rage/Router/DSL/Handler.html)
51
+ - [Fiber API](https://rage-rb.github.io/rage/Fiber.html)
52
+
53
+ 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.
54
+
55
+ ### Example
56
+
57
+ A sample controller could look like this:
58
+
59
+ ```ruby
60
+ require "net/http"
61
+
62
+ class PagesController < RageController::API
63
+ rescue_from SocketError do |_|
64
+ render json: { message: "error" }, status: 500
65
+ end
66
+
67
+ before_action :set_metadata
68
+
69
+ def show
70
+ page = Net::HTTP.get(URI("https://httpbin.org/json"))
71
+ render json: { page: page, metadata: @metadata }
72
+ end
73
+
74
+ private
75
+
76
+ def set_metadata
77
+ @metadata = { format: "json", time: Time.now.to_i }
78
+ end
79
+ end
80
+ ```
81
+
82
+ Apart from `RageController::API` as a parent class, this is mostly a regular Rails controller. However, the main difference is under the hood - Rage runs every request in a separate fiber. During the call to `Net::HTTP.get`, the fiber is automatically paused, enabling the server to process other requests. Once the HTTP request is finished, the fiber will be resumed, potentially allowing to process hundreds of requests simultaneously.
83
+
84
+ To make this controller work, we would also need to update `config/routes.rb`. In this case, the file would look the following way:
85
+
86
+ ```ruby
87
+ Rage.routes.draw do
88
+ get "page", to: "pages#show"
89
+ end
90
+ ```
91
+
92
+ :information_source: **Note**: Rage will automatically pause a fiber and continue to process other fibers on HTTP, PostgreSQL, and MySQL calls. Calls to `Thread.join` and `Ractor.join` will also automatically pause the current fiber.
93
+
94
+ Additionally, `Fiber.await` can be used to run several requests in parallel:
95
+
96
+ ```ruby
97
+ require "net/http"
98
+
99
+ class PagesController < RageController::API
100
+ def index
101
+ pages = Fiber.await(
102
+ Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/json")) },
103
+ Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/html")) },
104
+ )
105
+
106
+ render json: { pages: pages }
107
+ end
108
+ end
109
+ ```
110
+
111
+ :information_source: **Note**: When using `Fiber.await`, it is important to wrap any instance of IO into a fiber using `Fiber.schedule`.
112
+
43
113
  ## Benchmarks
44
114
 
45
115
  #### hello world
@@ -51,7 +121,7 @@ class ArticlesController < ApplicationController
51
121
  end
52
122
  end
53
123
  ```
54
- ![Requests per second](https://github.com/rage-rb/rage/assets/2270393/7d9f408c-7cec-4cc0-a509-66c9dedc1d0a)
124
+ ![Requests per second](https://github.com/rage-rb/rage/assets/2270393/6c221903-e265-4c94-80e1-041f266c8f47)
55
125
 
56
126
  #### waiting on IO
57
127
 
@@ -71,7 +141,7 @@ end
71
141
 
72
142
  Version | Changes
73
143
  ------- |------------
74
- 0.2 | 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;
144
+ 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;~~
75
145
  0.3 | 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.
76
146
  0.4 | 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;<br>&emsp;• support regexp constraints;
77
147
  0.5 | Implement Iodine-based equivalent of `ActionController::Live`.<br>Use `ActionDispatch::RemoteIp`.
@@ -2,7 +2,9 @@
2
2
 
3
3
  class Rage::Application
4
4
  def initialize(router)
5
- Fiber.set_scheduler(Rage::FiberScheduler.new)
5
+ Iodine.on_state(:on_start) do
6
+ Fiber.set_scheduler(Rage::FiberScheduler.new)
7
+ end
6
8
  @router = router
7
9
  end
8
10
 
@@ -17,7 +19,7 @@ class Rage::Application
17
19
  end
18
20
 
19
21
  rescue => e
20
- [500, {}, ["#{e.class}:#{e.message}\n\n#{e.backtrace.join("\n")}"]]
22
+ [500, {}, ["#{e.class}:#{e.message}\n\n#{e.backtrace.join("\n")}"]] # TODO: check Rage.env
21
23
 
22
24
  ensure
23
25
  # notify Iodine the request can now be served
data/lib/rage/cli.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require "iodine"
5
- require "rack"
4
+ require "rage"
6
5
 
7
6
  module Rage
8
7
  class CLI < Thor
@@ -20,8 +19,10 @@ module Rage
20
19
  app = ::Rack::Builder.parse_file("config.ru")
21
20
  app = app[0] if app.is_a?(Array)
22
21
 
23
- ::Iodine.listen service: :http, handler: app
24
- ::Iodine.threads = 1
22
+ ::Iodine.listen service: :http, handler: app, port: Rage.config.port
23
+ ::Iodine.threads = Rage.config.threads_count
24
+ ::Iodine.workers = Rage.config.workers_count
25
+
25
26
  ::Iodine.start
26
27
  end
27
28
  end
@@ -0,0 +1,10 @@
1
+ class Rage::Configuration
2
+ attr_accessor :port, :workers_count
3
+ attr_reader :threads_count
4
+
5
+ def initialize
6
+ @threads_count = 1
7
+ @workers_count = -1
8
+ @port = 3000
9
+ end
10
+ end
@@ -2,6 +2,11 @@
2
2
 
3
3
  class RageController::API
4
4
  class << self
5
+ # @private
6
+ # used by the router to register a new action;
7
+ # registering means defining a new method which calls the action, makes additional calls (e.g. before actions) and
8
+ # sends a correct response down to the server;
9
+ # returns the name of the newly defined method;
5
10
  def __register_action(action)
6
11
  raise "The action '#{action}' could not be found for #{self}" unless method_defined?(action)
7
12
 
@@ -23,37 +28,143 @@ class RageController::API
23
28
  ""
24
29
  end
25
30
 
31
+ rescue_handlers_chunk = if @__rescue_handlers
32
+ lines = @__rescue_handlers.map do |klasses, handler|
33
+ <<-RUBY
34
+ rescue #{klasses.join(", ")} => __e
35
+ #{handler}(__e)
36
+ [@__status, @__headers, @__body]
37
+ RUBY
38
+ end
39
+
40
+ lines.join("\n")
41
+ else
42
+ ""
43
+ end
44
+
26
45
  class_eval <<-RUBY
27
46
  def __run_#{action}
28
47
  #{before_actions_chunk}
29
48
  #{action}
30
49
 
31
50
  [@__status, @__headers, @__body]
51
+
52
+ #{rescue_handlers_chunk}
32
53
  end
33
54
  RUBY
34
55
  end
35
56
 
36
- # Register a new `before_action` hook.
57
+ # @private
58
+ attr_writer :__before_actions, :__rescue_handlers
59
+
60
+ # @private
61
+ # pass the variable down to the child; the child will continue to use it until changes need to be made;
62
+ # only then the object will be copied; the frozen state communicates that the object is shared with the parent;
63
+ def inherited(klass)
64
+ klass.__before_actions = @__before_actions.freeze
65
+ klass.__rescue_handlers = @__rescue_handlers.freeze
66
+ end
67
+
68
+ ############
69
+ #
70
+ # PUBLIC API
71
+ #
72
+ ############
73
+
74
+ # Register a global exception handler. Handlers are inherited and matched from bottom to top.
75
+ #
76
+ # @param klasses [Class, Array<Class>] exception classes to watch on
77
+ # @param with [Symbol] the name of a handler method. The method must take one argument, which is the raised exception. Alternatively, you can pass a block, which must also take one argument.
78
+ # @example
79
+ # rescue_from User::NotAuthorized, with: :deny_access
80
+ #
81
+ # def deny_access(exception)
82
+ # head :forbidden
83
+ # end
84
+ # @example
85
+ # rescue_from User::NotAuthorized do |_|
86
+ # head :forbidden
87
+ # end
88
+ # @note Unlike Rails, the handler must always take an argument. Use `_` if you don't care about the actual exception.
89
+ def rescue_from(*klasses, with: nil, &block)
90
+ unless with
91
+ if block_given?
92
+ name = ("a".."z").to_a.sample(15).join
93
+ with = define_method("__#{name}", &block)
94
+ else
95
+ raise "No handler provided. Pass the `with` keyword argument or provide a block."
96
+ end
97
+ end
98
+
99
+ if @__rescue_handlers.nil?
100
+ @__rescue_handlers = []
101
+ elsif @__rescue_handlers.frozen?
102
+ @__rescue_handlers = @__rescue_handlers.dup
103
+ end
104
+
105
+ @__rescue_handlers.unshift([klasses, with])
106
+ end
107
+
108
+ # Register a new `before_action` hook. Calls with the same `action_name` will overwrite the previous ones.
37
109
  #
38
110
  # @param action_name [String] the name of the callback to add
39
111
  # @param only [Symbol, Array<Symbol>] restrict the callback to run only for specific actions
40
112
  # @param except [Symbol, Array<Symbol>] restrict the callback to run for all actions except specified
41
113
  # @example
42
114
  # before_action :find_photo, only: :show
115
+ #
43
116
  # def find_photo
44
- # ...
117
+ # Photo.first
45
118
  # end
46
119
  def before_action(action_name, only: nil, except: nil)
47
- (@__before_actions ||= []) << {
48
- name: action_name,
49
- only: only && Array(only),
50
- except: except && Array(except)
51
- }
120
+ if @__before_actions && @__before_actions.frozen?
121
+ @__before_actions = @__before_actions.dup
122
+ end
123
+
124
+ action = { name: action_name, only: only && Array(only), except: except && Array(except) }
125
+ if @__before_actions.nil?
126
+ @__before_actions = [action]
127
+ elsif i = @__before_actions.find_index { |a| a[:name] == action_name }
128
+ @__before_actions[i] = action
129
+ else
130
+ @__before_actions << action
131
+ end
132
+ end
133
+
134
+ # Prevent a `before_action` hook from running.
135
+ #
136
+ # @param action_name [String] the name of the callback to skip
137
+ # @param only [Symbol, Array<Symbol>] restrict the callback to be skipped only for specific actions
138
+ # @param except [Symbol, Array<Symbol>] restrict the callback to be skipped for all actions except specified
139
+ # @example
140
+ # skip_before_action :find_photo, only: :create
141
+ def skip_before_action(action_name, only: nil, except: nil)
142
+ i = @__before_actions&.find_index { |a| a[:name] == action_name }
143
+ raise "The following action was specified to be skipped but cannot be found: #{self}##{action_name}" unless i
144
+
145
+ @__before_actions = @__before_actions.dup if @__before_actions.frozen?
146
+
147
+ if only.nil? && except.nil?
148
+ @__before_actions.delete_at(i)
149
+ return
150
+ end
151
+
152
+ action = @__before_actions[i].dup
153
+ if only
154
+ action[:except] ? action[:except] |= Array(only) : action[:except] = Array(only)
155
+ end
156
+ if except
157
+ action[:only] = Array(except)
158
+ end
159
+
160
+ @__before_actions[i] = action
52
161
  end
53
162
  end # class << self
54
163
 
164
+ # @private
55
165
  DEFAULT_HEADERS = { "content-type" => "application/json; charset=utf-8" }.freeze
56
166
 
167
+ # @private
57
168
  def initialize(env, params)
58
169
  @__env = env
59
170
  @__params = params
@@ -72,7 +183,7 @@ class RageController::API
72
183
  # render status: :ok
73
184
  # @example
74
185
  # render plain: "hello world", status: 201
75
- # @note `render` doesn't terminate execution of the action, so if you want to exit an action after rendering, you need to do something like 'render(...) and return'
186
+ # @note `render` doesn't terminate execution of the action, so if you want to exit an action after rendering, you need to do something like `render(...) and return`.
76
187
  def render(json: nil, plain: nil, status: nil)
77
188
  raise "Render was called multiple times in this action" if @__rendered
78
189
  @__rendered = true
@@ -81,8 +192,8 @@ class RageController::API
81
192
  @__body << if json
82
193
  json.is_a?(String) ? json : json.to_json
83
194
  else
84
- set_header("content-type", "text/plain; charset=utf-8")
85
- plain
195
+ __set_header("content-type", "text/plain; charset=utf-8")
196
+ plain.to_s
86
197
  end
87
198
 
88
199
  @__status = 200
@@ -116,7 +227,8 @@ class RageController::API
116
227
 
117
228
  private
118
229
 
119
- def set_header(key, value)
230
+ # copy-on-write implementation for the headers object
231
+ def __set_header(key, value)
120
232
  @__headers = @__headers.dup if DEFAULT_HEADERS.equal?(@__headers)
121
233
  @__headers[key] = value
122
234
  end
data/lib/rage/fiber.rb CHANGED
@@ -1,9 +1,38 @@
1
1
  class Fiber
2
+ # @private
2
3
  def __set_result(result)
3
4
  @__result = result
4
5
  end
5
6
 
7
+ # @private
6
8
  def __get_result
7
9
  @__result
8
10
  end
11
+
12
+ # Wait on several fibers at the same time. Calling this method will automatically pause the current fiber, allowing the
13
+ # server to process other requests. Once all fibers have completed, the current fiber will be automatically resumed.
14
+ #
15
+ # @param fibers [Fiber, Array<Fiber>] one or several fibers to wait on. The fibers must be created using the `Fiber.schedule` call.
16
+ # @example
17
+ # Fiber.await(
18
+ # Fiber.schedule { request_1 },
19
+ # Fiber.schedule { request_2 },
20
+ # )
21
+ # @note This method should only be used when multiple fibers have to be processed in parallel. There's no need to use `Fiber.await` for single IO calls.
22
+ def self.await(*fibers)
23
+ f = Fiber.current
24
+
25
+ num_wait_for = fibers.count(&:alive?)
26
+ return fibers.map(&:__get_result) if num_wait_for == 0
27
+
28
+ Iodine.subscribe("await:#{f.object_id}") do
29
+ num_wait_for -= 1
30
+ f.resume if num_wait_for == 0
31
+ end
32
+
33
+ Fiber.yield
34
+ Iodine.defer { Iodine.unsubscribe("await:#{f.object_id}") }
35
+
36
+ fibers.map(&:__get_result)
37
+ end
9
38
  end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "resolv"
4
+
3
5
  class Rage::FiberScheduler
6
+ def initialize
7
+ @root_fiber = Fiber.current
8
+ end
9
+
4
10
  def io_wait(io, events, timeout = nil)
5
11
  f = Fiber.current
6
12
  ::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { f.resume }
@@ -84,8 +90,14 @@ class Rage::FiberScheduler
84
90
  end
85
91
 
86
92
  def fiber(&block)
93
+ f = Fiber.current
94
+ inner_schedule = f != @root_fiber
95
+
87
96
  fiber = Fiber.new(blocking: false) do
88
97
  Fiber.current.__set_result(block.call)
98
+ ensure
99
+ # send a message for `Fiber.await` to work
100
+ Iodine.publish("await:#{f.object_id}", "") if inner_schedule
89
101
  end
90
102
  fiber.resume
91
103
 
@@ -12,7 +12,7 @@ class Rage::Router::Backend
12
12
  @constrainer = Rage::Router::Constrainer.new({})
13
13
  end
14
14
 
15
- def on(method, path, handler, constraints: {})
15
+ def on(method, path, handler, constraints: {}, defaults: nil)
16
16
  raise "Path could not be empty" if path&.empty?
17
17
 
18
18
  if match_index = (path =~ OPTIONAL_PARAM_REGEXP)
@@ -21,8 +21,8 @@ class Rage::Router::Backend
21
21
  path_full = path.sub(OPTIONAL_PARAM_REGEXP, "/#{$1}")
22
22
  path_optional = path.sub(OPTIONAL_PARAM_REGEXP, "")
23
23
 
24
- on(method, path_full, handler)
25
- on(method, path_optional, handler)
24
+ on(method, path_full, handler, constraints: constraints, defaults: defaults)
25
+ on(method, path_optional, handler, constraints: constraints, defaults: defaults)
26
26
  return
27
27
  end
28
28
 
@@ -42,7 +42,7 @@ class Rage::Router::Backend
42
42
  handler = ->(env, _params) { orig_handler.call(env) }
43
43
  end
44
44
 
45
- __on(method, path, handler, constraints)
45
+ __on(method, path, handler, constraints, defaults)
46
46
  end
47
47
 
48
48
  def lookup(env)
@@ -52,7 +52,7 @@ class Rage::Router::Backend
52
52
 
53
53
  private
54
54
 
55
- def __on(method, path, handler, constraints)
55
+ def __on(method, path, handler, constraints, defaults)
56
56
  @constrainer.validate_constraints(constraints)
57
57
  # Let the constrainer know if any constraints are being used now
58
58
  @constrainer.note_usage(constraints)
@@ -159,13 +159,14 @@ class Rage::Router::Backend
159
159
  end
160
160
  end
161
161
 
162
- route = { method: method, path: path, pattern: pattern, params: params, constraints: constraints, handler: handler }
162
+ route = { method: method, path: path, pattern: pattern, params: params, constraints: constraints, handler: handler, defaults: defaults }
163
163
  @routes << route
164
164
  current_node.add_route(route, @constrainer)
165
165
  end
166
166
 
167
167
  def find(env, derived_constraints)
168
168
  method, path = env["REQUEST_METHOD"], env["PATH_INFO"]
169
+ path.delete_suffix!("/") if path.end_with?("/") && path.length > 1
169
170
 
170
171
  current_node = @trees[method]
171
172
  return nil unless current_node
@@ -10,11 +10,13 @@ class Rage::Router::DSL
10
10
  end
11
11
 
12
12
  class Handler
13
+ # @private
13
14
  def initialize(router)
14
15
  @router = router
15
16
 
16
17
  @path_prefixes = []
17
18
  @module_prefixes = []
19
+ @defaults = []
18
20
  end
19
21
 
20
22
  # Register a new GET route.
@@ -22,10 +24,13 @@ class Rage::Router::DSL
22
24
  # @param path [String] the path for the route handler
23
25
  # @param to [String] the route handler in the format of "controller#action"
24
26
  # @param constraints [Hash] a hash of constraints for the route
27
+ # @param defaults [Hash] a hash of default parameters for the route
25
28
  # @example
26
29
  # get "/photos/:id", to: "photos#show", constraints: { host: /myhost/ }
27
- def get(path, to:, constraints: nil)
28
- __on("GET", path, to, constraints)
30
+ # @example
31
+ # get "/photos(/:id)", to: "photos#show", defaults: { id: "-1" }
32
+ def get(path, to:, constraints: nil, defaults: nil)
33
+ __on("GET", path, to, constraints, defaults)
29
34
  end
30
35
 
31
36
  # Register a new POST route.
@@ -33,10 +38,13 @@ class Rage::Router::DSL
33
38
  # @param path [String] the path for the route handler
34
39
  # @param to [String] the route handler in the format of "controller#action"
35
40
  # @param constraints [Hash] a hash of constraints for the route
41
+ # @param defaults [Hash] a hash of default parameters for the route
36
42
  # @example
37
43
  # post "/photos", to: "photos#create", constraints: { host: /myhost/ }
38
- def post(path, to:, constraints: nil)
39
- __on("POST", path, to, constraints)
44
+ # @example
45
+ # post "/photos", to: "photos#create", defaults: { format: "jpg" }
46
+ def post(path, to:, constraints: nil, defaults: nil)
47
+ __on("POST", path, to, constraints, defaults)
40
48
  end
41
49
 
42
50
  # Register a new PUT route.
@@ -44,10 +52,13 @@ class Rage::Router::DSL
44
52
  # @param path [String] the path for the route handler
45
53
  # @param to [String] the route handler in the format of "controller#action"
46
54
  # @param constraints [Hash] a hash of constraints for the route
55
+ # @param defaults [Hash] a hash of default parameters for the route
47
56
  # @example
48
57
  # put "/photos/:id", to: "photos#update", constraints: { host: /myhost/ }
49
- def put(path, to:, constraints: nil)
50
- __on("PUT", path, to, constraints)
58
+ # @example
59
+ # put "/photos(/:id)", to: "photos#update", defaults: { id: "-1" }
60
+ def put(path, to:, constraints: nil, defaults: nil)
61
+ __on("PUT", path, to, constraints, defaults)
51
62
  end
52
63
 
53
64
  # Register a new PATCH route.
@@ -55,10 +66,13 @@ class Rage::Router::DSL
55
66
  # @param path [String] the path for the route handler
56
67
  # @param to [String] the route handler in the format of "controller#action"
57
68
  # @param constraints [Hash] a hash of constraints for the route
69
+ # @param defaults [Hash] a hash of default parameters for the route
58
70
  # @example
59
71
  # patch "/photos/:id", to: "photos#update", constraints: { host: /myhost/ }
60
- def patch(path, to:, constraints: nil)
61
- __on("PATCH", path, to, constraints)
72
+ # @example
73
+ # patch "/photos(/:id)", to: "photos#update", defaults: { id: "-1" }
74
+ def patch(path, to:, constraints: nil, defaults: nil)
75
+ __on("PATCH", path, to, constraints, defaults)
62
76
  end
63
77
 
64
78
  # Register a new DELETE route.
@@ -66,10 +80,13 @@ class Rage::Router::DSL
66
80
  # @param path [String] the path for the route handler
67
81
  # @param to [String] the route handler in the format of "controller#action"
68
82
  # @param constraints [Hash] a hash of constraints for the route
83
+ # @param defaults [Hash] a hash of default parameters for the route
69
84
  # @example
70
85
  # delete "/photos/:id", to: "photos#destroy", constraints: { host: /myhost/ }
71
- def delete(path, to:, constraints: nil)
72
- __on("DELETE", path, to, constraints)
86
+ # @example
87
+ # delete "/photos(/:id)", to: "photos#destroy", defaults: { id: "-1" }
88
+ def delete(path, to:, constraints: nil, defaults: nil)
89
+ __on("DELETE", path, to, constraints, defaults)
73
90
  end
74
91
 
75
92
  # Register a new route pointing to '/'.
@@ -78,7 +95,7 @@ class Rage::Router::DSL
78
95
  # @example
79
96
  # root to: "photos#index"
80
97
  def root(to:)
81
- __on("GET", "/", to, nil)
98
+ __on("GET", "/", to, nil, nil)
82
99
  end
83
100
 
84
101
  # Scopes a set of routes to the given default options.
@@ -114,21 +131,39 @@ class Rage::Router::DSL
114
131
  @module_prefixes.pop if opts[:module]
115
132
  end
116
133
 
134
+ # Specify default parameters for a set of routes.
135
+ #
136
+ # @param defaults [Hash] a hash of default parameters
137
+ # @example
138
+ # defaults id: "-1", format: "jpg" do
139
+ # get "photos/(:id)", to: "photos#index"
140
+ # end
141
+ def defaults(defaults, &block)
142
+ @defaults << defaults
143
+ instance_eval &block
144
+ @defaults.pop
145
+ end
146
+
117
147
  private
118
148
 
119
- def __on(method, path, to, constraints)
149
+ def __on(method, path, to, constraints, defaults)
120
150
  if path != "/"
121
151
  path = "/#{path}" unless path.start_with?("/")
122
152
  path = path.delete_suffix("/") if path.end_with?("/")
123
153
  end
124
154
 
155
+ if path == "/" && @path_prefixes.any?
156
+ path = ""
157
+ end
158
+
125
159
  path_prefix = @path_prefixes.any? ? "/#{@path_prefixes.join("/")}" : nil
126
160
  module_prefix = @module_prefixes.any? ? "#{@module_prefixes.join("/")}/" : nil
161
+ defaults = (defaults ? @defaults + [defaults] : @defaults).reduce(&:merge)
127
162
 
128
163
  if to.is_a?(String)
129
- @router.on(method, "#{path_prefix}#{path}", "#{module_prefix}#{to}", constraints: constraints || {})
164
+ @router.on(method, "#{path_prefix}#{path}", "#{module_prefix}#{to}", constraints: constraints || {}, defaults: defaults)
130
165
  else
131
- @router.on(method, "#{path_prefix}#{path}", to, constraints: constraints || {})
166
+ @router.on(method, "#{path_prefix}#{path}", to, constraints: constraints || {}, defaults: defaults)
132
167
  end
133
168
  end
134
169
  end
@@ -22,7 +22,7 @@ class Rage::Router::HandlerStorage
22
22
  params: params,
23
23
  constraints: constraints,
24
24
  handler: route[:handler],
25
- create_params_object: compile_create_params_object(params)
25
+ create_params_object: compile_create_params_object(params, route[:defaults])
26
26
  }
27
27
 
28
28
  constraints_keys = constraints.keys
@@ -47,13 +47,19 @@ class Rage::Router::HandlerStorage
47
47
 
48
48
  private
49
49
 
50
- def compile_create_params_object(param_keys)
50
+ def compile_create_params_object(param_keys, defaults)
51
51
  lines = []
52
52
 
53
53
  param_keys.each_with_index do |key, i|
54
54
  lines << "'#{key}' => param_values[#{i}]"
55
55
  end
56
56
 
57
+ if defaults
58
+ defaults.except(*param_keys.map(&:to_sym)).each do |key, value|
59
+ lines << "'#{key}' => '#{value}'"
60
+ end
61
+ end
62
+
57
63
  eval "->(param_values) { { #{lines.join(',')} } }"
58
64
  end
59
65
 
data/lib/rage/setup.rb CHANGED
@@ -2,5 +2,6 @@ Iodine.patch_rack
2
2
 
3
3
  project_root = Pathname.new(".").expand_path
4
4
 
5
+ require_relative "#{project_root}/config/environments/#{Rage.env}"
5
6
  Dir["#{project_root}/app/**/*.rb"].each { |path| require_relative path }
6
7
  require_relative "#{project_root}/config/routes"
@@ -1,4 +1,6 @@
1
1
  require "bundler/setup"
2
- Bundler.require(:default)
2
+
3
+ require "rage"
4
+ Bundler.require(*Rage.groups)
3
5
 
4
6
  require "rage/setup"
@@ -0,0 +1,7 @@
1
+ Rage.configure do |config|
2
+ # Specify the number of server processes to run. Defaults to number of CPU cores.
3
+ config.workers_count = 1
4
+
5
+ # Specify the port the server will listen on.
6
+ config.port = 3000
7
+ end
@@ -0,0 +1,7 @@
1
+ Rage.configure do |config|
2
+ # Specify the number of server processes to run. Defaults to number of CPU cores.
3
+ # config.workers_count = ENV.fetch("WEB_CONCURRENCY", 1)
4
+
5
+ # Specify the port the server will listen on.
6
+ config.port = 3000
7
+ end
@@ -0,0 +1,7 @@
1
+ Rage.configure do |config|
2
+ # Specify the number of server processes to run. Defaults to number of CPU cores.
3
+ config.workers_count = 1
4
+
5
+ # Specify the port the server will listen on.
6
+ config.port = 3000
7
+ end
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.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -17,6 +17,22 @@ module Rage
17
17
  @__router ||= Rage::Router::Backend.new
18
18
  end
19
19
 
20
+ def self.config
21
+ @config ||= Rage::Configuration.new
22
+ end
23
+
24
+ def self.configure
25
+ yield(config)
26
+ end
27
+
28
+ def self.env
29
+ @__env ||= ENV["RAGE_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
30
+ end
31
+
32
+ def self.groups
33
+ [:default, Rage.env.to_sym]
34
+ end
35
+
20
36
  module Router
21
37
  module Strategies
22
38
  end
@@ -29,6 +45,7 @@ end
29
45
  require_relative "rage/application"
30
46
  require_relative "rage/fiber"
31
47
  require_relative "rage/fiber_scheduler"
48
+ require_relative "rage/configuration"
32
49
 
33
50
  require_relative "rage/router/strategies/host"
34
51
  require_relative "rage/router/backend"
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.1.1
4
+ version: 0.2.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-09-21 00:00:00.000000000 Z
11
+ date: 2023-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -61,6 +61,7 @@ extensions: []
61
61
  extra_rdoc_files: []
62
62
  files:
63
63
  - ".rspec"
64
+ - ".yardopts"
64
65
  - CHANGELOG.md
65
66
  - CODE_OF_CONDUCT.md
66
67
  - Gemfile
@@ -72,6 +73,7 @@ files:
72
73
  - lib/rage.rb
73
74
  - lib/rage/application.rb
74
75
  - lib/rage/cli.rb
76
+ - lib/rage/configuration.rb
75
77
  - lib/rage/controller/api.rb
76
78
  - lib/rage/fiber.rb
77
79
  - lib/rage/fiber_scheduler.rb
@@ -86,6 +88,9 @@ files:
86
88
  - lib/rage/templates/Gemfile
87
89
  - lib/rage/templates/app-controllers-application_controller.rb
88
90
  - lib/rage/templates/config-application.rb
91
+ - lib/rage/templates/config-environments-development.rb
92
+ - lib/rage/templates/config-environments-production.rb
93
+ - lib/rage/templates/config-environments-test.rb
89
94
  - lib/rage/templates/config-routes.rb
90
95
  - lib/rage/templates/config.ru
91
96
  - lib/rage/templates/lib-.keep