rage-rb 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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