rage-rb 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 the 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,120 @@ 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
+ at = "/#{at}" unless at.start_with?("/")
331
+ at = at.delete_suffix("/") if at.end_with?("/")
332
+
333
+ http_methods = if via == :all || via.nil?
334
+ @default_match_methods.map { |method| method.to_s.upcase! }
335
+ else
336
+ Array(via).map! do |method|
337
+ raise ArgumentError, "Invalid HTTP method: #{method}" unless @default_match_methods.include?(method)
338
+ method.to_s.upcase!
339
+ end
340
+ end
341
+
342
+ @router.mount(at, app, http_methods)
343
+ end
344
+
147
345
  private
148
346
 
149
347
  def __on(method, path, to, constraints, defaults)
348
+ # handle calls without controller inside resources:
349
+ # resources :comments do
350
+ # post :like
351
+ # end
352
+ if !to
353
+ if @controllers.any?
354
+ to = "#{@controllers.last}##{path}"
355
+ else
356
+ raise "Missing :to key on routes definition, please check your routes."
357
+ end
358
+ end
359
+
360
+ # process path to ensure it starts with "/" and doesn't end with "/"
150
361
  if path != "/"
151
362
  path = "/#{path}" unless path.start_with?("/")
152
363
  path = path.delete_suffix("/") if path.end_with?("/")
153
364
  end
154
365
 
366
+ # correctly process root helpers inside `scope` calls
155
367
  if path == "/" && @path_prefixes.any?
156
368
  path = ""
157
369
  end
@@ -166,5 +378,23 @@ class Rage::Router::DSL
166
378
  @router.on(method, "#{path_prefix}#{path}", to, constraints: constraints || {}, defaults: defaults)
167
379
  end
168
380
  end
381
+
382
+ def to_singular(str)
383
+ @active_support_loaded ||= str.respond_to?(:singularize) || :false
384
+ return str.singularize if @active_support_loaded != :false
385
+
386
+ @endings ||= {
387
+ "ves" => "fe",
388
+ "ies" => "y",
389
+ "i" => "us",
390
+ "zes" => "ze",
391
+ "ses" => "s",
392
+ "es" => "",
393
+ "s" => ""
394
+ }
395
+ @regexp ||= Regexp.new("(#{@endings.keys.join("|")})$")
396
+
397
+ str.sub(@regexp, @endings)
398
+ end
169
399
  end
170
400
  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, route[:defaults])
25
+ create_params_object: compile_create_params_object(params, route[:defaults], route[:meta])
26
26
  }
27
27
 
28
28
  constraints_keys = constraints.keys
@@ -47,16 +47,19 @@ class Rage::Router::HandlerStorage
47
47
 
48
48
  private
49
49
 
50
- def compile_create_params_object(param_keys, defaults)
51
- lines = []
50
+ def compile_create_params_object(param_keys, defaults, meta)
51
+ lines = [
52
+ ":controller => '#{meta[:controller]}'.freeze",
53
+ ":action => '#{meta[:action]}'.freeze"
54
+ ]
52
55
 
53
56
  param_keys.each_with_index do |key, i|
54
- lines << "'#{key}' => param_values[#{i}]"
57
+ lines << ":#{key} => param_values[#{i}]"
55
58
  end
56
59
 
57
60
  if defaults
58
61
  defaults.except(*param_keys.map(&:to_sym)).each do |key, value|
59
- lines << "'#{key}' => '#{value}'"
62
+ lines << ":#{key} => '#{value}'.freeze"
60
63
  end
61
64
  end
62
65
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "base64"
5
+
6
+ ##
7
+ # Used **specifically** for compatibility with Sidekiq's Web interface.
8
+ # Remove once we have real sessions or once Sidekiq's author decides they
9
+ # don't need cookie sessions to protect against CSRF.
10
+ #
11
+ class Rage::SidekiqSession
12
+ KEY = Digest::SHA2.hexdigest(ENV["SECRET_KEY_BASE"] || File.read("Gemfile.lock") + File.read("config/routes.rb"))
13
+ SESSION_KEY = "rage.sidekiq.session"
14
+
15
+ def self.with_session(env)
16
+ env["rack.session"] = session = self.new(env)
17
+ response = yield
18
+
19
+ if session.changed
20
+ Rack::Utils.set_cookie_header!(
21
+ response[1],
22
+ SESSION_KEY,
23
+ { path: env["SCRIPT_NAME"], httponly: true, same_site: true, value: session.dump }
24
+ )
25
+ end
26
+
27
+ response
28
+ end
29
+
30
+ attr_reader :changed
31
+
32
+ def initialize(env)
33
+ @env = env
34
+ session = Rack::Utils.parse_cookies(@env)[SESSION_KEY]
35
+ @data = decode_session(session)
36
+ end
37
+
38
+ def [](key)
39
+ @data[key]
40
+ end
41
+
42
+ def[]=(key, value)
43
+ @changed = true
44
+ @data[key] = value
45
+ end
46
+
47
+ def to_hash
48
+ @data
49
+ end
50
+
51
+ def dump
52
+ encoded_data = Marshal.dump(@data)
53
+ signature = OpenSSL::HMAC.hexdigest("SHA256", KEY, encoded_data)
54
+
55
+ Base64.urlsafe_encode64("#{encoded_data}--#{signature}")
56
+ end
57
+
58
+ private
59
+
60
+ def decode_session(session)
61
+ return {} unless session
62
+
63
+ encoded_data, signature = Base64.urlsafe_decode64(session).split("--")
64
+ ref_signature = OpenSSL::HMAC.hexdigest("SHA256", KEY, encoded_data)
65
+
66
+ if Rack::Utils.secure_compare(signature, ref_signature)
67
+ Marshal.load(encoded_data)
68
+ else
69
+ {}
70
+ end
71
+ end
72
+ end
@@ -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"
@@ -0,0 +1 @@
1
+ require_relative "config/application"
@@ -1,6 +1,11 @@
1
1
  require "bundler/setup"
2
-
3
2
  require "rage"
4
3
  Bundler.require(*Rage.groups)
5
4
 
5
+ require "rage/all"
6
+
7
+ Rage.configure do
8
+ # use this to add settings that are constant across all environments
9
+ end
10
+
6
11
  require "rage/setup"
@@ -1,7 +1,10 @@
1
- Rage.configure do |config|
1
+ Rage.configure do
2
2
  # Specify the number of server processes to run. Defaults to number of CPU cores.
3
- config.workers_count = 1
3
+ config.server.workers_count = 1
4
4
 
5
5
  # Specify the port the server will listen on.
6
- config.port = 3000
6
+ config.server.port = 3000
7
+
8
+ # Specify the logger
9
+ config.logger = Rage::Logger.new(STDOUT)
7
10
  end
@@ -1,7 +1,11 @@
1
- Rage.configure do |config|
1
+ Rage.configure do
2
2
  # Specify the number of server processes to run. Defaults to number of CPU cores.
3
- # config.workers_count = ENV.fetch("WEB_CONCURRENCY", 1)
3
+ # config.server.workers_count = ENV.fetch("WEB_CONCURRENCY", 1)
4
4
 
5
5
  # Specify the port the server will listen on.
6
- config.port = 3000
6
+ config.server.port = 3000
7
+
8
+ # Specify the logger
9
+ config.logger = Rage::Logger.new("log/production.log")
10
+ config.log_level = Logger::INFO
7
11
  end
@@ -1,7 +1,10 @@
1
- Rage.configure do |config|
1
+ Rage.configure do
2
2
  # Specify the number of server processes to run. Defaults to number of CPU cores.
3
- config.workers_count = 1
3
+ config.server.workers_count = 1
4
4
 
5
5
  # Specify the port the server will listen on.
6
- config.port = 3000
6
+ config.server.port = 3000
7
+
8
+ # Specify the logger
9
+ config.logger = Rage::Logger.new("log/test.log")
7
10
  end
@@ -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.5.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -22,8 +22,9 @@ module Rage
22
22
  @config ||= Rage::Configuration.new
23
23
  end
24
24
 
25
- def self.configure
26
- yield(config)
25
+ def self.configure(&)
26
+ config.instance_eval(&)
27
+ config.__finalize
27
28
  end
28
29
 
29
30
  def self.env
@@ -38,6 +39,10 @@ module Rage
38
39
  @root ||= Pathname.new(".").expand_path
39
40
  end
40
41
 
42
+ def self.logger
43
+ @logger ||= config.logger
44
+ end
45
+
41
46
  module Router
42
47
  module Strategies
43
48
  end
@@ -46,17 +51,3 @@ end
46
51
 
47
52
  module RageController
48
53
  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.2"
33
33
  end