rage-rb 0.3.0 → 0.4.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: 7684c979952ee81bee165b9b667dea87199f3fa28dadadc4878b29a12211fdac
4
- data.tar.gz: 2428443ee2456ef375990decfbf36a2952670ee517ac07cf4c994f5b9b7076f9
3
+ metadata.gz: adfca83c806d24e4baf6e3c6401c7268e7e88495eaee9120da3a55fca08400b8
4
+ data.tar.gz: 7769fd22482975665a66741d0362ce5bf75318192bd6ee2850f98da952f5fe35
5
5
  SHA512:
6
- metadata.gz: f59da6b77e69fa2b89761b30a497b4c0fa8f02cbabeed65c953b0f5e807b349e5b305bf31564bcc3eaeed4957215766e8781af5a5b8bbd9754e0461fa18f7b87
7
- data.tar.gz: 733a15ef91abcbfc079a3bbbb83f82686038059b3c518c1c0a279e8000ad885c65cc5019cc691e2731fa1e59ca1f62e8fe95c8f454837ed0bdc65e181157ffc7
6
+ metadata.gz: 66667fbd047a4c27ebb840f4fc6fd2f60fb50d90b7f25b38048afeb5a92b1ad6f5310a4f0eacbab42b2d11a0c0de44ff4a131d197737abe13f1817a7b2267147
7
+ data.tar.gz: e75d8f5c615844fe837015ddaf7d2f9c4a96cdebc321ad63c79a3a1645924f2b3a7ef3fe75007243042a14bae18b001bd6cb0c919754719fb339ae42d0e90075
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2023-10-31
4
+
5
+ ### Added
6
+
7
+ - Expose the `params` object.
8
+ - Support header authentication with `authenticate_with_http_token`.
9
+ - Add the `resources` and `namespace` route helpers.
10
+ - Add the `mount` and `match` route helpers.
11
+ - Allow to access request headers.
12
+ - Support custom ports when starting the app with `rage s`.
13
+
3
14
  ## [0.3.0] - 2023-10-08
4
15
 
5
16
  ### Added
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+ gem "yard"
11
12
 
12
13
  gem "pg"
13
14
  gem "mysql2"
data/README.md CHANGED
@@ -11,7 +11,7 @@ Inspired by [Deno](https://deno.com) and built on top of [Iodine](https://github
11
11
 
12
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.
13
13
 
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.
14
+ * **API-only** - the only technology we should be using to create web UI is JavaScript. Using native technologies is always the most flexible, scalable, and simple solution in the long run. Check out [Vite](https://vitejs.dev) if you don't know where to start.
15
15
 
16
16
  * **Acceptance of modern Ruby** - the framework includes a fiber scheduler, which means your code never blocks while waiting on IO.
17
17
 
@@ -46,9 +46,9 @@ This gem is designed to be a drop-in replacement for Rails in API mode. Public A
46
46
 
47
47
  Check out in-depth API docs for more information:
48
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)
49
+ - [Controller API](https://rage-rb.pages.dev/RageController/API)
50
+ - [Routing API](https://rage-rb.pages.dev/Rage/Router/DSL/Handler)
51
+ - [Fiber API](https://rage-rb.pages.dev/Fiber)
52
52
 
53
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
54
 
@@ -143,7 +143,7 @@ Version | Changes
143
143
  ------- |------------
144
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;~~
145
145
  0.3 :white_check_mark: | ~~CLI updates:<br>&emsp;• `routes` task;<br>&emsp;• `console` task;<br>Support the `:if` and `:unless` options in `before_action`.<br>Allow to set response headers.~~
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;
146
+ 0.4 :white_check_mark: | ~~Expose the `params` object.<br>Support header authentication with `authenticate_with_http_token`.<br>Router updates:<br>&emsp;• add the `resources` route helper;<br>&emsp;• add the `namespace` route helper;<br>~~&emsp;• support regexp constraints (postponed)
147
147
  0.5 | Implement Iodine-based equivalent of `ActionController::Live`.<br>Use `ActionDispatch::RemoteIp`.
148
148
  0.6 | Expose the `cookies` object.<br>Expose the `send_data` and `send_file` methods.<br>Support conditional get with `etag` and `last_modified`.
149
149
  0.7 | Add request logging.
data/lib/rage/all.rb ADDED
@@ -0,0 +1,20 @@
1
+ require_relative "../rage-rb"
2
+
3
+ require_relative "version"
4
+ require_relative "application"
5
+ require_relative "fiber"
6
+ require_relative "fiber_scheduler"
7
+ require_relative "configuration"
8
+ require_relative "request"
9
+ require_relative "uploaded_file"
10
+ require_relative "errors"
11
+ require_relative "params_parser"
12
+
13
+ require_relative "router/strategies/host"
14
+ require_relative "router/backend"
15
+ require_relative "router/constrainer"
16
+ require_relative "router/dsl"
17
+ require_relative "router/handler_storage"
18
+ require_relative "router/node"
19
+
20
+ require_relative "controller/api"
@@ -13,7 +13,8 @@ class Rage::Application
13
13
  handler = @router.lookup(env)
14
14
 
15
15
  if handler
16
- handler[:handler].call(env, handler[:params])
16
+ params = Rage::ParamsParser.prepare(env, handler[:params])
17
+ handler[:handler].call(env, params)
17
18
  else
18
19
  [404, {}, ["Not Found"]]
19
20
  end
data/lib/rage/cli.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "thor"
3
- require "rage"
3
+ require "rage/all"
4
4
  require "irb"
5
5
 
6
6
  module Rage
@@ -15,11 +15,12 @@ module Rage
15
15
  end
16
16
 
17
17
  desc "s", "Start the app server."
18
+ option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000."
18
19
  def server
19
20
  app = ::Rack::Builder.parse_file("config.ru")
20
21
  app = app[0] if app.is_a?(Array)
21
22
 
22
- ::Iodine.listen service: :http, handler: app, port: Rage.config.port
23
+ ::Iodine.listen service: :http, handler: app, port: options[:port] || Rage.config.port
23
24
  ::Iodine.threads = Rage.config.threads_count
24
25
  ::Iodine.workers = Rage.config.workers_count
25
26
 
@@ -30,57 +31,48 @@ module Rage
30
31
  option :grep, aliases: "-g", desc: "Filter routes by pattern"
31
32
  def routes
32
33
  # the result would be something like this:
33
- # Action Verb Path Controller#Action
34
- # index GET / application#index
34
+ # Verb Path Controller#Action
35
+ # GET / application#index
35
36
 
36
37
  # load config/application.rb
37
38
  environment
38
39
 
39
40
  routes = Rage.__router.routes
40
-
41
41
  pattern = options[:grep]
42
+ routes.unshift({ method: "Verb", path: "Path", raw_handler: "Controller#Action" })
43
+
44
+ grouped_routes = routes.each_with_object({}) do |route, memo|
45
+ if pattern && !memo.empty?
46
+ next unless route[:path].match?(pattern) || route[:raw_handler].to_s.match?(pattern) || route[:method].match?(pattern)
47
+ end
42
48
 
43
- if pattern
44
- routes = routes.select do |route|
45
- route[:path].match?(pattern) || route[:raw_handler].to_s.match?(pattern) || route[:method].match?(pattern)
49
+ key = [route[:path], route[:raw_handler]]
50
+ if memo[key]
51
+ memo[key][:method] += "|#{route[:method]}"
52
+ else
53
+ memo[key] = route
46
54
  end
47
55
  end
48
56
 
49
- return puts 'Action Verb Path Controller#Action' if routes.empty?
50
-
51
- # construct a table
52
- table = []
53
-
54
- # longest_path is either the length of the longest path or 5
55
- longest_path = routes.map { |route| route[:path].length }.max + 3
56
- longest_path = longest_path > 5 ? longest_path : 5
57
-
58
- longest_verb = routes.map { |route| route[:method].length }.max + 3
59
- longest_verb = longest_verb > 4 ? longest_verb : 7
60
-
61
- # longest_handler is either the length of the longest handler or 7, since DELETE is the longest HTTP method
62
- longest_handler = routes.map { |route| route[:raw_handler].is_a?(Proc) ? 7 : route[:raw_handler].split('#').last.length }.max + 3
63
- longest_handler = longest_handler > 7 ? longest_handler : 7
64
-
65
- # longest_controller is either the length of the longest controller or 12, since Controller#{length} is the longest controller
66
- longest_controller = routes.map { |route| route[:raw_handler].is_a?(Proc) ? 7 : route[:raw_handler].to_s.length }.max + 3
67
- longest_controller = longest_controller > 12 ? longest_controller : 12
68
-
69
- routes.each do |route|
70
- table << [
71
- format("%- #{longest_handler}s", route[:raw_handler].is_a?(Proc) ? 'Lambda' : route[:raw_handler].split('#').last),
72
- format("%- #{longest_verb}s", route[:method]),
73
- format("%- #{longest_path}s", route[:path]),
74
- format("%- #{longest_controller}s", route[:raw_handler].is_a?(Proc) ? 'Lambda' : route[:raw_handler])
75
- ]
57
+ longest_path = longest_method = 0
58
+ grouped_routes.each do |_, route|
59
+ longest_path = route[:path].length if route[:path].length > longest_path
60
+ longest_method = route[:method].length if route[:method].length > longest_method
76
61
  end
77
62
 
78
- table.unshift([format("%- #{longest_handler}s", 'Action'), format("%- #{longest_verb}s", 'Verb'), format("%- #{longest_path}s", 'Path'),
79
- format("%- #{longest_path}s", "Controller#Action\n")])
80
- # print the table
81
- table.each do |row|
82
- # this should be changed to use the main logger when added
83
- puts row.join
63
+ margin = 3
64
+ longest_path += margin
65
+ longest_method += margin
66
+
67
+ grouped_routes.each_with_index do |(_, route), i|
68
+ meta = route[:constraints]
69
+ meta.merge!(route[:defaults]) if route[:defaults]
70
+
71
+ handler = route[:raw_handler]
72
+ handler = "#{handler} #{meta}" unless meta&.empty?
73
+
74
+ puts format("%-#{longest_method}s%-#{longest_path}s%s", route[:method], route[:path], handler)
75
+ puts "\n" if i == 0
84
76
  end
85
77
  end
86
78
 
@@ -103,7 +103,7 @@ class RageController::API
103
103
  # rescue_from User::NotAuthorized do |_|
104
104
  # head :forbidden
105
105
  # end
106
- # @note Unlike Rails, the handler must always take an argument. Use `_` if you don't care about the actual exception.
106
+ # @note Unlike in Rails, the handler must always take an argument. Use `_` if you don't care about the actual exception.
107
107
  def rescue_from(*klasses, with: nil, &block)
108
108
  unless with
109
109
  if block_given?
@@ -167,7 +167,7 @@ class RageController::API
167
167
  if: _if,
168
168
  unless: _unless
169
169
  }
170
-
170
+
171
171
  action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
172
172
  action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
173
173
 
@@ -189,7 +189,7 @@ class RageController::API
189
189
  # skip_before_action :find_photo, only: :create
190
190
  def skip_before_action(action_name, only: nil, except: nil)
191
191
  i = @__before_actions&.find_index { |a| a[:name] == action_name }
192
- raise "The following action was specified to be skipped but cannot be found: #{self}##{action_name}" unless i
192
+ raise "The following action was specified to be skipped but couldn't be found: #{self}##{action_name}" unless i
193
193
 
194
194
  @__before_actions = @__before_actions.dup if @__before_actions.frozen?
195
195
 
@@ -221,6 +221,11 @@ class RageController::API
221
221
  @__rendered = false
222
222
  end
223
223
 
224
+ # Get the request object. See {Rage::Request}.
225
+ def request
226
+ @request ||= Rage::Request.new(@__env)
227
+ end
228
+
224
229
  # Send a response to the client.
225
230
  #
226
231
  # @param json [String, Object] send a json response to the client; objects like arrays will be serialized automatically
@@ -283,4 +288,47 @@ class RageController::API
283
288
  @__headers = {}.merge!(@__headers) if DEFAULT_HEADERS.equal?(@__headers)
284
289
  @__headers
285
290
  end
291
+
292
+ # Authenticate using an HTTP Bearer token. Returns the value of the block if a token is found. Returns `nil` if no token is found.
293
+ #
294
+ # @yield [token] token value extracted from the `Authorization` header
295
+ # @example
296
+ # user = authenticate_with_http_token do |token|
297
+ # User.find_by(key: token)
298
+ # end
299
+ def authenticate_with_http_token
300
+ auth_header = @__env["HTTP_AUTHORIZATION"]
301
+
302
+ if auth_header&.start_with?("Bearer")
303
+ yield auth_header[7..]
304
+ elsif auth_header&.start_with?("Token")
305
+ yield auth_header[6..]
306
+ end
307
+ end
308
+
309
+ if !defined?(::ActionController::Parameters)
310
+ # Get the request data. The keys inside the hash are symbols, so `params.keys` returns an array of `Symbol`.<br>
311
+ # You can also load Strong Params to have Rage automatically wrap `params` in an instance of `ActionController::Parameters`.<br>
312
+ # At the same time, if you are not implementing complex filtering rules or working with nested structures, consider using native `Hash#fetch` and `Hash#slice` instead.
313
+ #
314
+ # For multipart file uploads, the uploaded files are represented by an instance of {Rage::UploadedFile}.
315
+ #
316
+ # @return [Hash{Symbol=>String,Array,Hash,Numeric,NilClass,TrueClass,FalseClass}]
317
+ # @example
318
+ # # make sure to load strong params before the `require "rage/all"` call
319
+ # require "active_support/all"
320
+ # require "action_controller/metal/strong_parameters"
321
+ #
322
+ # params.permit(:user).require(:full_name, :dob)
323
+ # @example
324
+ # # without strong params
325
+ # params.fetch(:user).slice(:full_name, :dob)
326
+ def params
327
+ @__params
328
+ end
329
+ else
330
+ def params
331
+ @params ||= ActionController::Parameters.new(@__params)
332
+ end
333
+ end
286
334
  end
@@ -0,0 +1,4 @@
1
+ module Rage::Errors
2
+ class BadRequest < StandardError
3
+ end
4
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::ParamsParser
4
+ def self.prepare(env, url_params)
5
+ has_body, query_string, content_type = env["IODINE_HAS_BODY"], env["QUERY_STRING"], env["CONTENT_TYPE"]
6
+
7
+ query_params = Iodine::Rack::Utils.parse_nested_query(query_string) if query_string != ""
8
+ unless has_body
9
+ if query_params
10
+ return query_params.merge!(url_params)
11
+ else
12
+ return url_params
13
+ end
14
+ end
15
+
16
+ request_params = if content_type.start_with?("application/json")
17
+ json_parse(env["rack.input"].read)
18
+ elsif content_type.start_with?("application/x-www-form-urlencoded")
19
+ Iodine::Rack::Utils.parse_urlencoded_nested_query(env["rack.input"].read)
20
+ else
21
+ Iodine::Rack::Utils.parse_multipart(env["rack.input"], content_type)
22
+ end
23
+
24
+ if request_params && !query_params
25
+ request_params.merge!(url_params)
26
+ elsif request_params && query_params
27
+ request_params.merge!(query_params, url_params)
28
+ else
29
+ url_params
30
+ end
31
+
32
+ rescue => e
33
+ raise Rage::Errors::BadRequest
34
+ end
35
+
36
+ if defined?(::FastJsonparser)
37
+ def self.json_parse(json)
38
+ FastJsonparser.parse(json, symbolize_keys: true)
39
+ end
40
+ else
41
+ def self.json_parse(json)
42
+ JSON.parse(json, symbolize_names: true)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::Request
4
+ # @private
5
+ def initialize(env)
6
+ @env = env
7
+ end
8
+
9
+ # Get the request headers.
10
+ # @example
11
+ # request.headers["Content-Type"] # => "application/json"
12
+ # request.headers["Connection"] # => "keep-alive"
13
+ def headers
14
+ @headers ||= Headers.new(@env)
15
+ end
16
+
17
+ # @private
18
+ class Headers
19
+ HTTP = "HTTP_"
20
+
21
+ def initialize(env)
22
+ @env = env
23
+ end
24
+
25
+ def [](requested_header)
26
+ if requested_header.start_with?(HTTP)
27
+ @env[requested_header]
28
+ else
29
+ (requested_header = requested_header.tr("-", "_")).upcase!
30
+
31
+ if "CONTENT_TYPE" == requested_header || "CONTENT_LENGTH" == requested_header
32
+ @env[requested_header]
33
+ else
34
+ @env["#{HTTP}#{requested_header}"]
35
+ end
36
+ end
37
+ end
38
+
39
+ def inspect
40
+ headers = @env.select { |k| k == "CONTENT_TYPE" || k == "CONTENT_LENGTH" || k.start_with?(HTTP) }
41
+ "#<#{self.class.name} @headers=#{headers.inspect}"
42
+ end
43
+ end # class Headers
44
+ end
@@ -9,14 +9,54 @@ class Rage::Router::DSL
9
9
  Handler.new(@router).instance_eval(&block)
10
10
  end
11
11
 
12
+ ##
13
+ # This class implements routing logic for your application, providing API similar to Rails.
14
+ #
15
+ # Compared to Rails router, the most notable difference is that a wildcard segment can only be in the last section of the path and cannot be named.
16
+ # Example:
17
+ # ```ruby
18
+ # get "/photos/*"
19
+ # ```
20
+ #
21
+ # Also, as this is an API-only framework, route helpers, like `photos_path` or `photos_url` are not being generated.
22
+ #
23
+ # #### Constraints
24
+ #
25
+ # Currently, the only constraint supported is the `host` constraint. The constraint value can be either string or a regular expression.
26
+ # Example:
27
+ # ```ruby
28
+ # get "/photos", to: "photos#index", constraints: { host: "myhost.com" }
29
+ # ```
30
+ #
31
+ # Parameter constraints are likely to be added in the future versions. Custom/lambda constraints are unlikely to be ever added.
32
+ #
33
+ # @example Set up a root handler
34
+ # root to: "pages#main"
35
+ # @example Set up multiple resources
36
+ # resources :magazines do
37
+ # resources :ads
38
+ # end
39
+ # @example Scope a set of routes to the given default options.
40
+ # scope path: ":account_id" do
41
+ # resources :projects
42
+ # end
43
+ # @example Scope routes to a specific namespace.
44
+ # namespace :admin do
45
+ # resources :posts
46
+ # end
12
47
  class Handler
13
48
  # @private
14
49
  def initialize(router)
15
50
  @router = router
16
51
 
52
+ @default_actions = %i(index create show update destroy)
53
+ @default_match_methods = %i(get post put patch delete head)
54
+ @scope_opts = %i(module path controller)
55
+
17
56
  @path_prefixes = []
18
57
  @module_prefixes = []
19
58
  @defaults = []
59
+ @controllers = []
20
60
  end
21
61
 
22
62
  # Register a new GET route.
@@ -29,7 +69,7 @@ class Rage::Router::DSL
29
69
  # get "/photos/:id", to: "photos#show", constraints: { host: /myhost/ }
30
70
  # @example
31
71
  # get "/photos(/:id)", to: "photos#show", defaults: { id: "-1" }
32
- def get(path, to:, constraints: nil, defaults: nil)
72
+ def get(path, to: nil, constraints: nil, defaults: nil)
33
73
  __on("GET", path, to, constraints, defaults)
34
74
  end
35
75
 
@@ -43,7 +83,7 @@ class Rage::Router::DSL
43
83
  # post "/photos", to: "photos#create", constraints: { host: /myhost/ }
44
84
  # @example
45
85
  # post "/photos", to: "photos#create", defaults: { format: "jpg" }
46
- def post(path, to:, constraints: nil, defaults: nil)
86
+ def post(path, to: nil, constraints: nil, defaults: nil)
47
87
  __on("POST", path, to, constraints, defaults)
48
88
  end
49
89
 
@@ -57,7 +97,7 @@ class Rage::Router::DSL
57
97
  # put "/photos/:id", to: "photos#update", constraints: { host: /myhost/ }
58
98
  # @example
59
99
  # put "/photos(/:id)", to: "photos#update", defaults: { id: "-1" }
60
- def put(path, to:, constraints: nil, defaults: nil)
100
+ def put(path, to: nil, constraints: nil, defaults: nil)
61
101
  __on("PUT", path, to, constraints, defaults)
62
102
  end
63
103
 
@@ -71,7 +111,7 @@ class Rage::Router::DSL
71
111
  # patch "/photos/:id", to: "photos#update", constraints: { host: /myhost/ }
72
112
  # @example
73
113
  # patch "/photos(/:id)", to: "photos#update", defaults: { id: "-1" }
74
- def patch(path, to:, constraints: nil, defaults: nil)
114
+ def patch(path, to: nil, constraints: nil, defaults: nil)
75
115
  __on("PATCH", path, to, constraints, defaults)
76
116
  end
77
117
 
@@ -85,7 +125,7 @@ class Rage::Router::DSL
85
125
  # delete "/photos/:id", to: "photos#destroy", constraints: { host: /myhost/ }
86
126
  # @example
87
127
  # delete "/photos(/:id)", to: "photos#destroy", defaults: { id: "-1" }
88
- def delete(path, to:, constraints: nil, defaults: nil)
128
+ def delete(path, to: nil, constraints: nil, defaults: nil)
89
129
  __on("DELETE", path, to, constraints, defaults)
90
130
  end
91
131
 
@@ -98,6 +138,70 @@ class Rage::Router::DSL
98
138
  __on("GET", "/", to, nil, nil)
99
139
  end
100
140
 
141
+ # Match a URL pattern to one or more routes.
142
+ #
143
+ # @param path [String] the path for the route handler
144
+ # @param to [String, #call] the route handler in the format of "controller#action" or a callable
145
+ # @param constraints [Hash] a hash of constraints for the route
146
+ # @param defaults [Hash] a hash of default parameters for the route
147
+ # @param via [Symbol, Array<Symbol>] an array of HTTP methods to accept
148
+ # @example
149
+ # match "/photos/:id", to: "photos#show", via: [:get, :post]
150
+ # @example
151
+ # match "/photos/:id", to: "photos#show", via: :all
152
+ # @example
153
+ # match "/health", to: -> (env) { [200, {}, ["healthy"]] }
154
+ def match(path, to:, constraints: {}, defaults: nil, via: :all)
155
+ # via is either nil, or an array of symbols or its :all
156
+ http_methods = via
157
+ # if its :all or nil, then we use the default HTTP methods
158
+ if via == :all || via.nil?
159
+ http_methods = @default_match_methods
160
+ else
161
+ # if its an array of symbols, then we use the symbols as HTTP methods
162
+ http_methods = Array(via)
163
+ # then we check if the HTTP methods are valid
164
+ http_methods.each do |method|
165
+ raise ArgumentError, "Invalid HTTP method: #{method}" unless @default_match_methods.include?(method)
166
+ end
167
+ end
168
+
169
+ http_methods.each do |method|
170
+ __on(method.to_s.upcase, path, to, constraints, defaults)
171
+ end
172
+ end
173
+
174
+ # Register a new namespace.
175
+ #
176
+ # @param path [String] the path for the namespace
177
+ # @param options [Hash] a hash of options for the namespace
178
+ # @option options [String] :module the module name for the namespace
179
+ # @option options [String] :path the path for the namespace
180
+ # @example
181
+ # namespace :admin do
182
+ # get "/photos", to: "photos#index"
183
+ # end
184
+ # @example
185
+ # namespace :admin, path: "panel" do
186
+ # get "/photos", to: "photos#index"
187
+ # end
188
+ # @example
189
+ # namespace :admin, module: "admin" do
190
+ # get "/photos", to: "photos#index"
191
+ # end
192
+ def namespace(path, **options, &block)
193
+ path_prefix = options[:path] || path
194
+ module_prefix = options[:module] || path
195
+
196
+ @path_prefixes << path_prefix
197
+ @module_prefixes << module_prefix
198
+
199
+ instance_eval &block
200
+
201
+ @path_prefixes.pop
202
+ @module_prefixes.pop
203
+ end
204
+
101
205
  # Scopes a set of routes to the given default options.
102
206
  #
103
207
  # @param [Hash] opts scope options.
@@ -120,15 +224,17 @@ class Rage::Router::DSL
120
224
  # end
121
225
  # end
122
226
  def scope(opts, &block)
123
- raise ArgumentError, "only 'module' and 'path' options are accepted" if (opts.keys - %i(module path)).any?
227
+ raise ArgumentError, "only :module, :path, and :controller options are accepted" if (opts.keys - @scope_opts).any?
124
228
 
125
229
  @path_prefixes << opts[:path].delete_prefix("/").delete_suffix("/") if opts[:path]
126
230
  @module_prefixes << opts[:module] if opts[:module]
231
+ @controllers << opts[:controller] if opts[:controller]
127
232
 
128
233
  instance_eval &block
129
234
 
130
235
  @path_prefixes.pop if opts[:path]
131
236
  @module_prefixes.pop if opts[:module]
237
+ @controllers.pop if opts[:controller]
132
238
  end
133
239
 
134
240
  # Specify default parameters for a set of routes.
@@ -144,14 +250,109 @@ class Rage::Router::DSL
144
250
  @defaults.pop
145
251
  end
146
252
 
253
+ # Add a route to the collection.
254
+ #
255
+ # @example Add a `photos/search` path instead of `photos/:photo_id/search`
256
+ # resources :photos do
257
+ # collection do
258
+ # get "search"
259
+ # end
260
+ # end
261
+ def collection(&block)
262
+ orig_path_prefixes = @path_prefixes
263
+ @path_prefixes = @path_prefixes[0...-1] if @path_prefixes.last&.start_with?(":")
264
+ instance_eval &block
265
+ @path_prefixes = orig_path_prefixes
266
+ end
267
+
268
+ # Automatically create REST routes for a resource.
269
+ #
270
+ # @example Create five REST routes, all mapping to the `Photos` controller:
271
+ # resources :photos
272
+ # # GET /photos => photos#index
273
+ # # POST /photos => photos#create
274
+ # # GET /photos/:id => photos#show
275
+ # # PATCH/PUT /photos/:id => photos#update
276
+ # # DELETE /photos/:id => photos#destroy
277
+ # @note This helper doesn't generate the `new` and `edit` routes.
278
+ def resources(*_resources, **opts, &block)
279
+ # support calls with multiple resources, e.g. `resources :albums, :photos`
280
+ if _resources.length > 1
281
+ _resources.each { |_resource| resources(_resource, **opts, &block) }
282
+ return
283
+ end
284
+
285
+ _module, _path, _only, _except, _param = opts.values_at(:module, :path, :only, :except, :param)
286
+ raise ":param option can't contain colons" if _param&.include?(":")
287
+
288
+ _only = Array(_only) if _only
289
+ _except = Array(_except) if _except
290
+ actions = @default_actions.select do |action|
291
+ (_only.nil? || _only.include?(action)) && (_except.nil? || !_except.include?(action))
292
+ end
293
+
294
+ resource = _resources[0].to_s
295
+ _path ||= resource
296
+ _param ||= "id"
297
+
298
+ scope_opts = { path: _path }
299
+ scope_opts[:module] = _module if _module
300
+
301
+ scope(scope_opts) do
302
+ get("/", to: "#{resource}#index") if actions.include?(:index)
303
+ post("/", to: "#{resource}#create") if actions.include?(:create)
304
+ get("/:#{_param}", to: "#{resource}#show") if actions.include?(:show)
305
+ patch("/:#{_param}", to: "#{resource}#update") if actions.include?(:update)
306
+ put("/:#{_param}", to: "#{resource}#update") if actions.include?(:update)
307
+ delete("/:#{_param}", to: "#{resource}#destroy") if actions.include?(:destroy)
308
+
309
+ scope(path: ":#{to_singular(resource)}_#{_param}", controller: resource, &block) if block
310
+ end
311
+ end
312
+
313
+ # Mount a Rack-based application to be used within the application.
314
+ #
315
+ # @example
316
+ # mount Sidekiq::Web => "/sidekiq"
317
+ # @example
318
+ # mount Sidekiq::Web, at: "/sidekiq", via: :get
319
+ def mount(*args)
320
+ if args.first.is_a?(Hash)
321
+ app = args.first.keys.first
322
+ at = args.first.values.first
323
+ via = args[0][:via]
324
+ else
325
+ app = args.first
326
+ at = args[1][:at]
327
+ via = args[1][:via]
328
+ end
329
+
330
+ # Use match with via: :all to mount the Rack-based application
331
+ match(at, to: app, via:)
332
+ end
333
+
147
334
  private
148
335
 
149
336
  def __on(method, path, to, constraints, defaults)
337
+ # handle calls without controller inside resources:
338
+ # resources :comments do
339
+ # post :like
340
+ # end
341
+ if !to
342
+ if @controllers.any?
343
+ to = "#{@controllers.last}##{path}"
344
+ else
345
+ raise "Missing :to key on routes definition, please check your routes."
346
+ end
347
+ end
348
+
349
+ # process path to ensure it starts with "/" and doesn't end with "/"
150
350
  if path != "/"
151
351
  path = "/#{path}" unless path.start_with?("/")
152
352
  path = path.delete_suffix("/") if path.end_with?("/")
153
353
  end
154
354
 
355
+ # correctly process root helpers inside `scope` calls
155
356
  if path == "/" && @path_prefixes.any?
156
357
  path = ""
157
358
  end
@@ -166,5 +367,23 @@ class Rage::Router::DSL
166
367
  @router.on(method, "#{path_prefix}#{path}", to, constraints: constraints || {}, defaults: defaults)
167
368
  end
168
369
  end
370
+
371
+ def to_singular(str)
372
+ @active_support_loaded ||= str.respond_to?(:singularize) || :false
373
+ return str.singularize if @active_support_loaded != :false
374
+
375
+ @endings ||= {
376
+ "ves" => "fe",
377
+ "ies" => "y",
378
+ "i" => "us",
379
+ "zes" => "ze",
380
+ "ses" => "s",
381
+ "es" => "",
382
+ "s" => ""
383
+ }
384
+ @regexp ||= Regexp.new("(#{@endings.keys.join("|")})$")
385
+
386
+ str.sub(@regexp, @endings)
387
+ end
169
388
  end
170
389
  end
@@ -51,12 +51,12 @@ class Rage::Router::HandlerStorage
51
51
  lines = []
52
52
 
53
53
  param_keys.each_with_index do |key, i|
54
- lines << "'#{key}' => param_values[#{i}]"
54
+ lines << ":#{key} => param_values[#{i}]"
55
55
  end
56
56
 
57
57
  if defaults
58
58
  defaults.except(*param_keys.map(&:to_sym)).each do |key, value|
59
- lines << "'#{key}' => '#{value}'"
59
+ lines << ":#{key} => '#{value}'"
60
60
  end
61
61
  end
62
62
 
@@ -1,6 +1,10 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "rage-rb"
3
+ gem "rage-rb", "<%= Rage::VERSION %>"
4
4
 
5
5
  # Build JSON APIs with ease
6
6
  # gem "alba"
7
+
8
+ # Get 50% to 150% boost when parsing JSON.
9
+ # Rage will automatically use FastJsonparser if it is available.
10
+ # gem "fast_jsonparser"
@@ -1,6 +1,6 @@
1
1
  require "bundler/setup"
2
-
3
- require "rage"
4
2
  Bundler.require(*Rage.groups)
5
3
 
4
+ require "rage/all"
5
+
6
6
  require "rage/setup"
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Models uploaded files.
5
+ #
6
+ # The actual file is accessible via the `file` accessor, though some
7
+ # of its interface is available directly for convenience.
8
+ #
9
+ # Rage will automatically unlink the files, so there is no need to clean them with a separate maintenance task.
10
+ class Rage::UploadedFile
11
+ # The basename of the file in the client.
12
+ attr_reader :original_filename
13
+
14
+ # A string with the MIME type of the file.
15
+ attr_reader :content_type
16
+
17
+ # A `File` object with the actual uploaded file. Note that some of its interface is available directly.
18
+ attr_reader :file
19
+ alias_method :tempfile, :file
20
+
21
+ def initialize(file, original_filename, content_type)
22
+ @file = file
23
+ @original_filename = original_filename
24
+ @content_type = content_type
25
+ end
26
+
27
+ # Shortcut for `file.read`.
28
+ def read(length = nil, buffer = nil)
29
+ @file.read(length, buffer)
30
+ end
31
+
32
+ # Shortcut for `file.open`.
33
+ def open
34
+ @file.open
35
+ end
36
+
37
+ # Shortcut for `file.close`.
38
+ def close(unlink_now = false)
39
+ @file.close(unlink_now)
40
+ end
41
+
42
+ # Shortcut for `file.path`.
43
+ def path
44
+ @file.path
45
+ end
46
+
47
+ # Shortcut for `file.to_path`.
48
+ def to_path
49
+ @file.to_path
50
+ end
51
+
52
+ # Shortcut for `file.rewind`.
53
+ def rewind
54
+ @file.rewind
55
+ end
56
+
57
+ # Shortcut for `file.size`.
58
+ def size
59
+ @file.size
60
+ end
61
+
62
+ # Shortcut for `file.eof?`.
63
+ def eof?
64
+ @file.eof?
65
+ end
66
+
67
+ def to_io
68
+ @file.to_io
69
+ end
70
+ 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.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -46,17 +46,3 @@ end
46
46
 
47
47
  module RageController
48
48
  end
49
-
50
- require_relative "rage/application"
51
- require_relative "rage/fiber"
52
- require_relative "rage/fiber_scheduler"
53
- require_relative "rage/configuration"
54
-
55
- require_relative "rage/router/strategies/host"
56
- require_relative "rage/router/backend"
57
- require_relative "rage/router/constrainer"
58
- require_relative "rage/router/dsl"
59
- require_relative "rage/router/handler_storage"
60
- require_relative "rage/router/node"
61
-
62
- require_relative "rage/controller/api"
data/rage.gemspec CHANGED
@@ -29,5 +29,5 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  spec.add_dependency "thor", "~> 1.0"
31
31
  spec.add_dependency "rack", "~> 2.0"
32
- spec.add_dependency "rage-iodine", "~> 1.0"
32
+ spec.add_dependency "rage-iodine", "~> 2.0"
33
33
  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.3.0
4
+ version: 0.4.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-10-08 00:00:00.000000000 Z
11
+ date: 2023-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.0'
47
+ version: '2.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.0'
54
+ version: '2.0'
55
55
  description:
56
56
  email:
57
57
  - rsamoi@icloud.com
@@ -71,12 +71,16 @@ files:
71
71
  - exe/rage
72
72
  - lib/rage-rb.rb
73
73
  - lib/rage.rb
74
+ - lib/rage/all.rb
74
75
  - lib/rage/application.rb
75
76
  - lib/rage/cli.rb
76
77
  - lib/rage/configuration.rb
77
78
  - lib/rage/controller/api.rb
79
+ - lib/rage/errors.rb
78
80
  - lib/rage/fiber.rb
79
81
  - lib/rage/fiber_scheduler.rb
82
+ - lib/rage/params_parser.rb
83
+ - lib/rage/request.rb
80
84
  - lib/rage/router/README.md
81
85
  - lib/rage/router/backend.rb
82
86
  - lib/rage/router/constrainer.rb
@@ -96,6 +100,7 @@ files:
96
100
  - lib/rage/templates/lib-.keep
97
101
  - lib/rage/templates/log-.keep
98
102
  - lib/rage/templates/public-.keep
103
+ - lib/rage/uploaded_file.rb
99
104
  - lib/rage/version.rb
100
105
  - rage.gemspec
101
106
  homepage: https://github.com/rage-rb/rage