rage-rb 0.2.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: 4570bfdf4f86125be1d6c35b9ccba08a956fe8a20dac392e5ae914db347ad396
4
- data.tar.gz: '019e919339f97e641e65f9aecc91c02740875ebcbe71edf14a07144950e47f07'
3
+ metadata.gz: adfca83c806d24e4baf6e3c6401c7268e7e88495eaee9120da3a55fca08400b8
4
+ data.tar.gz: 7769fd22482975665a66741d0362ce5bf75318192bd6ee2850f98da952f5fe35
5
5
  SHA512:
6
- metadata.gz: 9ad7b89eb46407831ae723c1c15b87ea12b898ae1784989374b07fcffcc3afcff2a237861c94565afe4c5f486b99edb15022b539ab9cad142778e4b2718b92ea
7
- data.tar.gz: 471cb6bcbf294d9a4eb3e5fd69827f26661effafc1df20447b8e7eddd0816aff411266d22104988978c46318dc08c4fa753f1e1a4fa1327268a5f24d8851ac5a
6
+ metadata.gz: 66667fbd047a4c27ebb840f4fc6fd2f60fb50d90b7f25b38048afeb5a92b1ad6f5310a4f0eacbab42b2d11a0c0de44ff4a131d197737abe13f1817a7b2267147
7
+ data.tar.gz: e75d8f5c615844fe837015ddaf7d2f9c4a96cdebc321ad63c79a3a1645924f2b3a7ef3fe75007243042a14bae18b001bd6cb0c919754719fb339ae42d0e90075
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
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
+
14
+ ## [0.3.0] - 2023-10-08
15
+
16
+ ### Added
17
+
18
+ - CLI `routes` task.
19
+ - CLI `console` task.
20
+ - `:if` and `:unless` options in `before_action`.
21
+ - Allow to set response headers.
22
+ - Block version of `before_action`.
23
+
3
24
  ## [0.2.0] - 2023-09-27
4
25
 
5
26
  ### Added
data/Gemfile CHANGED
@@ -8,9 +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"
14
-
15
- gem "benchmark-ips"
16
- gem "mustache"
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
 
@@ -142,8 +142,8 @@ end
142
142
  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
- 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.
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;
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 :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,13 +13,14 @@ 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
20
21
 
21
22
  rescue => e
22
- [500, {}, ["#{e.class}:#{e.message}\n\n#{e.backtrace.join("\n")}"]] # TODO: check Rage.env
23
+ [500, {}, ["#{e.class}:#{e.message}\n\n#{e.backtrace.join("\n")}"]]
23
24
 
24
25
  ensure
25
26
  # notify Iodine the request can now be served
data/lib/rage/cli.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "thor"
4
- require "rage"
3
+ require "rage/all"
4
+ require "irb"
5
5
 
6
6
  module Rage
7
7
  class CLI < Thor
@@ -15,21 +15,83 @@ 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
 
26
27
  ::Iodine.start
27
28
  end
29
+
30
+ desc 'routes', 'List all routes.'
31
+ option :grep, aliases: "-g", desc: "Filter routes by pattern"
32
+ def routes
33
+ # the result would be something like this:
34
+ # Verb Path Controller#Action
35
+ # GET / application#index
36
+
37
+ # load config/application.rb
38
+ environment
39
+
40
+ routes = Rage.__router.routes
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
48
+
49
+ key = [route[:path], route[:raw_handler]]
50
+ if memo[key]
51
+ memo[key][:method] += "|#{route[:method]}"
52
+ else
53
+ memo[key] = route
54
+ end
55
+ end
56
+
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
61
+ end
62
+
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
76
+ end
77
+ end
78
+
79
+ desc "c", "Start the app console."
80
+ def console
81
+ environment
82
+ ARGV.clear
83
+ IRB.start
84
+ end
85
+
86
+ private
87
+
88
+ def environment
89
+ require File.expand_path("config/application.rb", Dir.pwd)
90
+ end
28
91
  end
29
92
 
30
93
  class NewAppGenerator < Thor::Group
31
94
  include Thor::Actions
32
-
33
95
  argument :path, type: :string
34
96
 
35
97
  def self.source_root
@@ -12,13 +12,21 @@ class RageController::API
12
12
 
13
13
  before_actions_chunk = if @__before_actions
14
14
  filtered_before_actions = @__before_actions.select do |h|
15
- (h[:only].nil? || h[:only].include?(action)) &&
16
- (h[:except].nil? || !h[:except].include?(action))
15
+ (!h[:only] || h[:only].include?(action)) &&
16
+ (!h[:except] || !h[:except].include?(action))
17
17
  end
18
18
 
19
19
  lines = filtered_before_actions.map do |h|
20
+ condition = if h[:if] && h[:unless]
21
+ "if #{h[:if]} && !#{h[:unless]}"
22
+ elsif h[:if]
23
+ "if #{h[:if]}"
24
+ elsif h[:unless]
25
+ "unless #{h[:unless]}"
26
+ end
27
+
20
28
  <<-RUBY
21
- #{h[:name]}
29
+ #{h[:name]} #{condition}
22
30
  return [@__status, @__headers, @__body] if @__rendered
23
31
  RUBY
24
32
  end
@@ -65,6 +73,16 @@ class RageController::API
65
73
  klass.__rescue_handlers = @__rescue_handlers.freeze
66
74
  end
67
75
 
76
+ # @private
77
+ @@__tmp_name_seed = ("a".."i").to_a.permutation
78
+
79
+ # @private
80
+ # define temporary method based on a block
81
+ def define_tmp_method(block)
82
+ name = @@__tmp_name_seed.next.join
83
+ define_method("__rage_tmp_#{name}", block)
84
+ end
85
+
68
86
  ############
69
87
  #
70
88
  # PUBLIC API
@@ -85,12 +103,11 @@ class RageController::API
85
103
  # rescue_from User::NotAuthorized do |_|
86
104
  # head :forbidden
87
105
  # end
88
- # @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.
89
107
  def rescue_from(*klasses, with: nil, &block)
90
108
  unless with
91
109
  if block_given?
92
- name = ("a".."z").to_a.sample(15).join
93
- with = define_method("__#{name}", &block)
110
+ with = define_tmp_method(block)
94
111
  else
95
112
  raise "No handler provided. Pass the `with` keyword argument or provide a block."
96
113
  end
@@ -107,21 +124,53 @@ class RageController::API
107
124
 
108
125
  # Register a new `before_action` hook. Calls with the same `action_name` will overwrite the previous ones.
109
126
  #
110
- # @param action_name [String] the name of the callback to add
111
- # @param only [Symbol, Array<Symbol>] restrict the callback to run only for specific actions
112
- # @param except [Symbol, Array<Symbol>] restrict the callback to run for all actions except specified
127
+ # @param action_name [String, nil] the name of the callback to add
128
+ # @param [Hash] opts action options
129
+ # @option opts [Symbol, Array<Symbol>] :only restrict the callback to run only for specific actions
130
+ # @option opts [Symbol, Array<Symbol>] :except restrict the callback to run for all actions except specified
131
+ # @option opts [Symbol, Proc] :if only run the callback if the condition is true
132
+ # @option opts [Symbol, Proc] :unless only run the callback if the condition is false
113
133
  # @example
114
134
  # before_action :find_photo, only: :show
115
135
  #
116
136
  # def find_photo
117
137
  # Photo.first
118
138
  # end
119
- def before_action(action_name, only: nil, except: nil)
139
+ # @example
140
+ # before_action :require_user, unless: :logged_in?
141
+ # @example
142
+ # before_action :set_locale, if: -> { params[:locale] != "en-US" }
143
+ # @example
144
+ # before_action do
145
+ # unless logged_in? # would be `controller.send(:logged_in?)` in Rails
146
+ # head :unauthorized
147
+ # end
148
+ # end
149
+ # @note The block form doesn't receive an argument and is executed on the controller level as if it was a regular method.
150
+ def before_action(action_name = nil, **opts, &block)
151
+ if block_given?
152
+ action_name = define_tmp_method(block)
153
+ elsif action_name.nil?
154
+ raise "No handler provided. Pass the `action_name` parameter or provide a block."
155
+ end
156
+
157
+ _only, _except, _if, _unless = opts.values_at(:only, :except, :if, :unless)
158
+
120
159
  if @__before_actions && @__before_actions.frozen?
121
160
  @__before_actions = @__before_actions.dup
122
161
  end
123
162
 
124
- action = { name: action_name, only: only && Array(only), except: except && Array(except) }
163
+ action = {
164
+ name: action_name,
165
+ only: _only && Array(_only),
166
+ except: _except && Array(_except),
167
+ if: _if,
168
+ unless: _unless
169
+ }
170
+
171
+ action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
172
+ action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
173
+
125
174
  if @__before_actions.nil?
126
175
  @__before_actions = [action]
127
176
  elsif i = @__before_actions.find_index { |a| a[:name] == action_name }
@@ -140,7 +189,7 @@ class RageController::API
140
189
  # skip_before_action :find_photo, only: :create
141
190
  def skip_before_action(action_name, only: nil, except: nil)
142
191
  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
192
+ raise "The following action was specified to be skipped but couldn't be found: #{self}##{action_name}" unless i
144
193
 
145
194
  @__before_actions = @__before_actions.dup if @__before_actions.frozen?
146
195
 
@@ -172,6 +221,11 @@ class RageController::API
172
221
  @__rendered = false
173
222
  end
174
223
 
224
+ # Get the request object. See {Rage::Request}.
225
+ def request
226
+ @request ||= Rage::Request.new(@__env)
227
+ end
228
+
175
229
  # Send a response to the client.
176
230
  #
177
231
  # @param json [String, Object] send a json response to the client; objects like arrays will be serialized automatically
@@ -192,7 +246,7 @@ class RageController::API
192
246
  @__body << if json
193
247
  json.is_a?(String) ? json : json.to_json
194
248
  else
195
- __set_header("content-type", "text/plain; charset=utf-8")
249
+ headers["content-type"] = "text/plain; charset=utf-8"
196
250
  plain.to_s
197
251
  end
198
252
 
@@ -225,11 +279,56 @@ class RageController::API
225
279
  end
226
280
  end
227
281
 
228
- private
282
+ # Set response headers.
283
+ #
284
+ # @example
285
+ # headers["Content-Type"] = "application/pdf"
286
+ def headers
287
+ # copy-on-write implementation for the headers object
288
+ @__headers = {}.merge!(@__headers) if DEFAULT_HEADERS.equal?(@__headers)
289
+ @__headers
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
229
308
 
230
- # copy-on-write implementation for the headers object
231
- def __set_header(key, value)
232
- @__headers = @__headers.dup if DEFAULT_HEADERS.equal?(@__headers)
233
- @__headers[key] = value
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
234
333
  end
235
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
@@ -3,6 +3,8 @@
3
3
  require "uri"
4
4
 
5
5
  class Rage::Router::Backend
6
+ attr_reader :routes
7
+
6
8
  OPTIONAL_PARAM_REGEXP = /\/?\(\/?(:\w+)\/?\)/
7
9
  STRING_HANDLER_REGEXP = /^([a-z0-9_\/]+)#([a-z_]+)$/
8
10
 
@@ -13,6 +15,7 @@ class Rage::Router::Backend
13
15
  end
14
16
 
15
17
  def on(method, path, handler, constraints: {}, defaults: nil)
18
+ raw_handler = handler
16
19
  raise "Path could not be empty" if path&.empty?
17
20
 
18
21
  if match_index = (path =~ OPTIONAL_PARAM_REGEXP)
@@ -42,7 +45,7 @@ class Rage::Router::Backend
42
45
  handler = ->(env, _params) { orig_handler.call(env) }
43
46
  end
44
47
 
45
- __on(method, path, handler, constraints, defaults)
48
+ __on(method, path, handler, raw_handler, constraints, defaults)
46
49
  end
47
50
 
48
51
  def lookup(env)
@@ -52,7 +55,7 @@ class Rage::Router::Backend
52
55
 
53
56
  private
54
57
 
55
- def __on(method, path, handler, constraints, defaults)
58
+ def __on(method, path, handler, raw_handler, constraints, defaults)
56
59
  @constrainer.validate_constraints(constraints)
57
60
  # Let the constrainer know if any constraints are being used now
58
61
  @constrainer.note_usage(constraints)
@@ -159,7 +162,7 @@ class Rage::Router::Backend
159
162
  end
160
163
  end
161
164
 
162
- route = { method: method, path: path, pattern: pattern, params: params, constraints: constraints, handler: handler, defaults: defaults }
165
+ route = { method: method, path: path, pattern: pattern, params: params, constraints: constraints, handler: handler, raw_handler: raw_handler, defaults: defaults }
163
166
  @routes << route
164
167
  current_node.add_route(route, @constrainer)
165
168
  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
 
data/lib/rage/setup.rb CHANGED
@@ -1,7 +1,23 @@
1
1
  Iodine.patch_rack
2
2
 
3
- project_root = Pathname.new(".").expand_path
3
+ require_relative "#{Rage.root}/config/environments/#{Rage.env}"
4
4
 
5
- require_relative "#{project_root}/config/environments/#{Rage.env}"
6
- Dir["#{project_root}/app/**/*.rb"].each { |path| require_relative path }
7
- require_relative "#{project_root}/config/routes"
5
+
6
+ # load application files
7
+ app, bad = Dir["#{Rage.root}/app/**/*.rb"], []
8
+
9
+ loop do
10
+ path = app.shift
11
+ break if path.nil?
12
+
13
+ require_relative path
14
+
15
+ # push the file to the end of the list in case it depends on another file that has not yet been required;
16
+ # re-raise if only errored out files are left
17
+ rescue NameError
18
+ raise if (app - bad).empty?
19
+ app << path
20
+ bad << path
21
+ end
22
+
23
+ require_relative "#{Rage.root}/config/routes"
@@ -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.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "rack"
4
4
  require "json"
5
5
  require "iodine"
6
+ require "pathname"
6
7
 
7
8
  module Rage
8
9
  def self.application
@@ -33,6 +34,10 @@ module Rage
33
34
  [:default, Rage.env.to_sym]
34
35
  end
35
36
 
37
+ def self.root
38
+ @root ||= Pathname.new(".").expand_path
39
+ end
40
+
36
41
  module Router
37
42
  module Strategies
38
43
  end
@@ -41,17 +46,3 @@ end
41
46
 
42
47
  module RageController
43
48
  end
44
-
45
- require_relative "rage/application"
46
- require_relative "rage/fiber"
47
- require_relative "rage/fiber_scheduler"
48
- require_relative "rage/configuration"
49
-
50
- require_relative "rage/router/strategies/host"
51
- require_relative "rage/router/backend"
52
- require_relative "rage/router/constrainer"
53
- require_relative "rage/router/dsl"
54
- require_relative "rage/router/handler_storage"
55
- require_relative "rage/router/node"
56
-
57
- 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.2.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-09-27 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