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 +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +6 -0
- data/README.md +77 -7
- data/lib/rage/application.rb +4 -2
- data/lib/rage/cli.rb +5 -4
- data/lib/rage/configuration.rb +10 -0
- data/lib/rage/controller/api.rb +123 -11
- data/lib/rage/fiber.rb +29 -0
- data/lib/rage/fiber_scheduler.rb +12 -0
- data/lib/rage/router/backend.rb +7 -6
- data/lib/rage/router/dsl.rb +49 -14
- data/lib/rage/router/handler_storage.rb +8 -2
- data/lib/rage/setup.rb +1 -0
- data/lib/rage/templates/config-application.rb +3 -1
- data/lib/rage/templates/config-environments-development.rb +7 -0
- data/lib/rage/templates/config-environments-production.rb +7 -0
- data/lib/rage/templates/config-environments-test.rb +7 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +17 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4570bfdf4f86125be1d6c35b9ccba08a956fe8a20dac392e5ae914db347ad396
|
4
|
+
data.tar.gz: '019e919339f97e641e65f9aecc91c02740875ebcbe71edf14a07144950e47f07'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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.
|
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
|
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.
|
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/
|
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> • make the `root` helper work correctly with `scope`;<br> • 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> • make the `root` helper work correctly with `scope`;<br> • support the `defaults` option;~~
|
75
145
|
0.3 | CLI updates:<br> • `routes` task;<br> • `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> • add the `resources` route helper;<br> • add the `namespace` route helper;<br> • support regexp constraints;
|
77
147
|
0.5 | Implement Iodine-based equivalent of `ActionController::Live`.<br>Use `ActionDispatch::RemoteIp`.
|
data/lib/rage/application.rb
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
class Rage::Application
|
4
4
|
def initialize(router)
|
5
|
-
|
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 "
|
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 =
|
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
|
data/lib/rage/controller/api.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
data/lib/rage/fiber_scheduler.rb
CHANGED
@@ -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
|
|
data/lib/rage/router/backend.rb
CHANGED
@@ -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
|
data/lib/rage/router/dsl.rb
CHANGED
@@ -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
|
-
|
28
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
61
|
-
|
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
|
-
|
72
|
-
|
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
data/lib/rage/version.rb
CHANGED
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.
|
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-
|
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
|