rage-rb 1.7.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::Cable::Connection
4
+ # @private
5
+ attr_reader :__identified_by_map
6
+
7
+ # Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
8
+ # Common identifiers are `current_user` and `current_account`, but could be anything.
9
+ #
10
+ # @param identifiers [Symbol,Array<Symbol>]
11
+ def self.identified_by(*identifiers)
12
+ identifiers.each do |method_name|
13
+ define_method(method_name) do
14
+ @__identified_by_map[method_name]
15
+ end
16
+
17
+ define_method("#{method_name}=") do |data|
18
+ @__identified_by_map[method_name] = data
19
+ end
20
+
21
+ Rage::Cable::Channel.__prepare_id_method(method_name)
22
+ end
23
+ end
24
+
25
+ # @private
26
+ def initialize(env, identified_by = {})
27
+ @__env = env
28
+ @__identified_by_map = identified_by
29
+ end
30
+
31
+ # @private
32
+ def connect
33
+ end
34
+
35
+ # Reject the WebSocket connection.
36
+ def reject_unauthorized_connection
37
+ @rejected = true
38
+ end
39
+
40
+ def rejected?
41
+ !!@rejected
42
+ end
43
+
44
+ # Get the request object. See {Rage::Request}.
45
+ #
46
+ # @return [Rage::Request]
47
+ def request
48
+ @__request ||= Rage::Request.new(@__env)
49
+ end
50
+
51
+ # Get the cookie object. See {Rage::Cookies}.
52
+ #
53
+ # @return [Rage::Cookies]
54
+ def cookies
55
+ @__cookies ||= Rage::Cookies.new(@__env, ReadOnlyHash.new)
56
+ end
57
+
58
+ # Get the session object. See {Rage::Session}.
59
+ #
60
+ # @return [Rage::Session]
61
+ def session
62
+ @__session ||= Rage::Session.new(cookies)
63
+ end
64
+
65
+ # Get URL query parameters.
66
+ #
67
+ # @return [Hash{Symbol=>String,Array,Hash}]
68
+ def params
69
+ @__params ||= Iodine::Rack::Utils.parse_nested_query(@__env["QUERY_STRING"])
70
+ end
71
+
72
+ # @private
73
+ class ReadOnlyHash < Hash
74
+ def []=(_, _)
75
+ raise "Cookies cannot be set for WebSocket clients"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
5
+ # The class that defines a protocol should respond to the following methods:
6
+ #
7
+ # * `protocol_definition`
8
+ # * `init`
9
+ # * `on_open`
10
+ # * `on_message`
11
+ # * `serialize`
12
+ # * `subscribe`
13
+ # * `broadcast`
14
+ #
15
+ # The two optional methods are:
16
+ #
17
+ # * `on_shutdown`
18
+ # * `on_close`
19
+ #
20
+ class Rage::Cable::Protocol::ActioncableV1Json
21
+ module TYPE
22
+ WELCOME = "welcome"
23
+ DISCONNECT = "disconnect"
24
+ PING = "ping"
25
+ CONFIRM = "confirm_subscription"
26
+ REJECT = "reject_subscription"
27
+ end
28
+
29
+ module REASON
30
+ UNAUTHORIZED = "unauthorized"
31
+ INVALID = "invalid_request"
32
+ end
33
+
34
+ module COMMAND
35
+ SUBSCRIBE = "subscribe"
36
+ MESSAGE = "message"
37
+ end
38
+
39
+ module MESSAGES
40
+ WELCOME = { type: TYPE::WELCOME }.to_json
41
+ UNAUTHORIZED = { type: TYPE::DISCONNECT, reason: REASON::UNAUTHORIZED, reconnect: false }.to_json
42
+ INVALID = { type: TYPE::DISCONNECT, reason: REASON::INVALID, reconnect: true }.to_json
43
+ end
44
+
45
+ HANDSHAKE_HEADERS = { "Sec-WebSocket-Protocol" => "actioncable-v1-json" }
46
+
47
+ # The method defines the headers to send to the client after the handshake process.
48
+ def self.protocol_definition
49
+ HANDSHAKE_HEADERS
50
+ end
51
+
52
+ # This method serves as a constructor to prepare the object or set up recurring tasks (e.g. heartbeats).
53
+ #
54
+ # @param router [Rage::Cable::Router]
55
+ def self.init(router)
56
+ @router = router
57
+
58
+ ping_counter = Time.now.to_i
59
+ ::Iodine.run_every(3000) do
60
+ ping_counter += 1
61
+ ::Iodine.publish("cable:ping", { type: TYPE::PING, message: ping_counter }.to_json)
62
+ end
63
+
64
+ # Hash<String(stream name) => Array<Hash>(subscription params)>
65
+ @subscription_identifiers = Hash.new { |hash, key| hash[key] = [] }
66
+ end
67
+
68
+ # The method is called any time a new WebSocket connection is established.
69
+ # It is expected to call {Rage::Cable::Router#process_connection} and handle its return value.
70
+ #
71
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
72
+ # @see Rage::Cable::Router
73
+ def self.on_open(connection)
74
+ accepted = @router.process_connection(connection)
75
+
76
+ if accepted
77
+ connection.subscribe("cable:ping")
78
+ connection.write(MESSAGES::WELCOME)
79
+ else
80
+ connection.write(MESSAGES::UNAUTHORIZED)
81
+ connection.close
82
+ end
83
+ end
84
+
85
+ # The method processes messages from existing connections. It should parse the message, call either
86
+ # {Rage::Cable::Router#process_subscription} or {Rage::Cable::Router#process_message}, and handle its return value.
87
+ #
88
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
89
+ # @param raw_data [String] the message body
90
+ # @see Rage::Cable::Router
91
+ def self.on_message(connection, raw_data)
92
+ parsed_data = Rage::ParamsParser.json_parse(raw_data)
93
+
94
+ command, identifier = parsed_data[:command], parsed_data[:identifier]
95
+ params = Rage::ParamsParser.json_parse(identifier)
96
+
97
+ # process subscription messages
98
+ if command == COMMAND::SUBSCRIBE
99
+ status = @router.process_subscription(connection, identifier, params[:channel], params)
100
+ if status == :subscribed
101
+ connection.write({ identifier: identifier, type: TYPE::CONFIRM }.to_json)
102
+ elsif status == :rejected
103
+ connection.write({ identifier: identifier, type: TYPE::REJECT }.to_json)
104
+ elsif status == :invalid
105
+ connection.write(MESSAGES::INVALID)
106
+ end
107
+
108
+ return
109
+ end
110
+
111
+ # process data messages;
112
+ # plain `JSON` is used here to conform with the ActionCable API that passes `data` as a Hash with string keys;
113
+ data = JSON.parse(parsed_data[:data])
114
+
115
+ message_status = if command == COMMAND::MESSAGE && data.has_key?("action")
116
+ @router.process_message(connection, identifier, data["action"].to_sym, data)
117
+
118
+ elsif command == COMMAND::MESSAGE
119
+ @router.process_message(connection, identifier, :receive, data)
120
+ end
121
+
122
+ unless message_status == :processed
123
+ connection.write(MESSAGES::INVALID)
124
+ end
125
+ end
126
+
127
+ # The method should process client disconnections and call {Rage::Cable::Router#process_message}.
128
+ #
129
+ # @note This method is optional.
130
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
131
+ # @see Rage::Cable::Router
132
+ def self.on_close(connection)
133
+ @router.process_disconnection(connection)
134
+ end
135
+
136
+ # Serialize a Ruby object into the format the client would understand.
137
+ #
138
+ # @param params [Hash] parameters associated with the client
139
+ # @param data [Object] the object to serialize
140
+ def self.serialize(params, data)
141
+ { identifier: params.to_json, message: data }.to_json
142
+ end
143
+
144
+ # Subscribe to a stream.
145
+ #
146
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
147
+ # @param name [String] the stream name
148
+ # @param params [Hash] parameters associated with the client
149
+ def self.subscribe(connection, name, params)
150
+ connection.subscribe("cable:#{name}:#{params.hash}")
151
+ @subscription_identifiers[name] << params unless @subscription_identifiers[name].include?(params)
152
+ end
153
+
154
+ # Broadcast data to all clients connected to a stream.
155
+ #
156
+ # @param name [String] the stream name
157
+ # @param data [Object] the data to send
158
+ def self.broadcast(name, data)
159
+ i, identifiers = 0, @subscription_identifiers[name]
160
+
161
+ while i < identifiers.length
162
+ params = identifiers[i]
163
+ ::Iodine.publish("cable:#{name}:#{params.hash}", serialize(params, data))
164
+ i += 1
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::Cable::Router
4
+ # @private
5
+ def initialize
6
+ # Hash<String(channel name) => Proc(new channel instance)>
7
+ @channels_map = {}
8
+ init_connection_class
9
+ end
10
+
11
+ # Calls the `connect` method on the `Connection` class to handle authentication.
12
+ #
13
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
14
+ # @return [true] if the connection was accepted
15
+ # @return [false] if the connection was rejected
16
+ def process_connection(connection)
17
+ cable_connection = @connection_class.new(connection.env)
18
+ cable_connection.connect
19
+
20
+ if cable_connection.rejected?
21
+ Rage.logger.debug { "An unauthorized connection attempt was rejected" }
22
+ else
23
+ connection.env["rage.identified_by"] = cable_connection.__identified_by_map
24
+ connection.env["rage.cable"] = {}
25
+ end
26
+
27
+ !cable_connection.rejected?
28
+ end
29
+
30
+ # Calls the `subscribed` method on the specified channel.
31
+ #
32
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
33
+ # @param identifier [String] the identifier of the subscription
34
+ # @param channel_name [String] the name of the channel class
35
+ # @param params [Hash] the params hash associated with the subscription
36
+ #
37
+ # @return [:invalid] if the subscription class does not exist
38
+ # @return [:rejected] if the subscription was rejected
39
+ # @return [:subscribed] if the subscription was accepted
40
+ def process_subscription(connection, identifier, channel_name, params)
41
+ channel_class = @channels_map[channel_name] || begin
42
+ begin
43
+ klass = Object.const_get(channel_name)
44
+ rescue NameError
45
+ nil
46
+ end
47
+
48
+ if klass.nil? || !klass.ancestors.include?(Rage::Cable::Channel)
49
+ Rage.logger.debug { "Subscription class not found: #{channel_name}" }
50
+ return :invalid
51
+ end
52
+
53
+ klass.__register_actions.tap do |available_actions|
54
+ Rage.logger.debug { "Compiled #{channel_name}. Available remote actions: #{available_actions}." }
55
+ end
56
+
57
+ @channels_map[channel_name] = klass
58
+ end
59
+
60
+ channel = channel_class.new(connection, params, connection.env["rage.identified_by"])
61
+ channel.__run_action(:subscribed)
62
+
63
+ if channel.subscription_rejected?
64
+ Rage.logger.debug { "#{channel_name} is transmitting the subscription rejection" }
65
+ # if the subscription is rejected in the `subscribed` method, ActionCable will additionally run
66
+ # the `unsubscribed` method; this makes little sense to me as the client was never subscribed in
67
+ # the first place; additionally, I don't think this behaviour is documented anywhere;
68
+ # so, I'm going to leave this line commented out for now;
69
+ # channel.__run_action(:unsubscribed)
70
+ :rejected
71
+ else
72
+ Rage.logger.debug { "#{channel_name} is transmitting the subscription confirmation" }
73
+ connection.env["rage.cable"][identifier] = channel
74
+ :subscribed
75
+ end
76
+ end
77
+
78
+ # Calls the handler method on the specified channel.
79
+ #
80
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
81
+ # @param identifier [String] the identifier of the subscription
82
+ # @param action_name [Symbol] the name of the handler method
83
+ # @param data [Object] the data sent by the client
84
+ #
85
+ # @return [:no_subscription] if the client is not subscribed to the specified channel
86
+ # @return [:unknown_action] if the action does not exist on the specified channel
87
+ # @return [:processed] if the message has been successfully processed
88
+ def process_message(connection, identifier, action_name, data)
89
+ channel = connection.env["rage.cable"][identifier]
90
+ unless channel
91
+ Rage.logger.debug { "Unable to find the subscription" }
92
+ return :no_subscription
93
+ end
94
+
95
+ if channel.__has_action?(action_name)
96
+ channel.__run_action(action_name, data)
97
+ :processed
98
+ else
99
+ Rage.logger.debug { "Unable to process #{channel.class.name}##{action_name}" }
100
+ :unknown_action
101
+ end
102
+ end
103
+
104
+ # Runs the `unsubscribed` methods on all the channels the client is subscribed to.
105
+ #
106
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
107
+ def process_disconnection(connection)
108
+ connection.env["rage.cable"]&.each do |_, channel|
109
+ channel.__run_action(:unsubscribed)
110
+ end
111
+
112
+ if @connection_can_disconnect
113
+ cable_connection = @connection_class.new(connection.env, connection.env["rage.identified_by"])
114
+ cable_connection.disconnect
115
+ end
116
+ end
117
+
118
+ # @private
119
+ def reset
120
+ @channels_map.clear
121
+ init_connection_class
122
+ end
123
+
124
+ private
125
+
126
+ def init_connection_class
127
+ @connection_class = if Object.const_defined?("RageCable::Connection")
128
+ RageCable::Connection
129
+ elsif Object.const_defined?("ApplicationCable::Connection")
130
+ ApplicationCable::Connection
131
+ else
132
+ puts "WARNING: Could not find the RageCable connection class! All connections will be accepted by default."
133
+ Rage::Cable::Connection
134
+ end
135
+
136
+ @connection_can_disconnect = @connection_class.method_defined?(:disconnect)
137
+ end
138
+ end
@@ -30,8 +30,13 @@ class Rage::CodeLoader
30
30
 
31
31
  @reloading = true
32
32
  @loader.reload
33
+
33
34
  Rage.__router.reset_routes
34
35
  load("#{Rage.root}/config/routes.rb")
36
+
37
+ unless Rage.autoload?(:Cable) # the `Cable` component is loaded
38
+ Rage::Cable.__router.reset
39
+ end
35
40
  end
36
41
 
37
42
  # in Rails mode - reset the routes; everything else will be done by Rails
@@ -40,6 +45,10 @@ class Rage::CodeLoader
40
45
 
41
46
  @reloading = true
42
47
  Rage.__router.reset_routes
48
+
49
+ unless Rage.autoload?(:Cable) # the `Cable` component is loaded
50
+ Rage::Cable.__router.reset
51
+ end
43
52
  end
44
53
 
45
54
  def reloading?
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ##
4
+ # `Rage.configure` can be used to adjust the behavior of your Rage application:
5
+ #
6
+ # ```ruby
7
+ # Rage.configure do
8
+ # config.logger = Rage::Logger.new(STDOUT)
9
+ # config.server.workers_count = 2
10
+ # end
11
+ # ```
12
+ #
4
13
  # # General Configuration
5
14
  #
6
15
  # • _config.logger_
@@ -93,6 +102,20 @@
93
102
  #
94
103
  # > Specifies connection timeout.
95
104
  #
105
+ # # Cable Configuration
106
+ #
107
+ # • _config.cable.protocol_
108
+ #
109
+ # > Specifies the protocol the server will use. The only value currently supported is `Rage::Cable::Protocol::ActioncableV1Json`. The client application will need to use [@rails/actioncable](https://www.npmjs.com/package/@rails/actioncable) to talk to the server.
110
+ #
111
+ # • _config.cable.allowed_request_origins_
112
+ #
113
+ # > Restricts the server to only accept requests from specified origins. The origins can be instances of strings or regular expressions, against which a check for the match will be performed.
114
+ #
115
+ # • _config.cable.disable_request_forgery_protection_
116
+ #
117
+ # > Allows requests from any origin.
118
+ #
96
119
  # # Transient Settings
97
120
  #
98
121
  # The settings described in this section should be configured using **environment variables** and are either temporary or will become the default in the future.
@@ -138,6 +161,10 @@ class Rage::Configuration
138
161
  @middleware ||= Middleware.new
139
162
  end
140
163
 
164
+ def cable
165
+ @cable ||= Cable.new
166
+ end
167
+
141
168
  def internal
142
169
  @internal ||= Internal.new
143
170
  end
@@ -193,6 +220,32 @@ class Rage::Configuration
193
220
  end
194
221
  end
195
222
 
223
+ class Cable
224
+ attr_accessor :protocol, :allowed_request_origins, :disable_request_forgery_protection
225
+
226
+ def initialize
227
+ @protocol = Rage::Cable::Protocol::ActioncableV1Json
228
+ @allowed_request_origins = if Rage.env.development? || Rage.env.test?
229
+ /localhost/
230
+ end
231
+ end
232
+
233
+ # @private
234
+ def middlewares
235
+ @middlewares ||= begin
236
+ origin_middleware = if @disable_request_forgery_protection
237
+ []
238
+ else
239
+ [[Rage::OriginValidator, Array(@allowed_request_origins), nil]]
240
+ end
241
+
242
+ origin_middleware + Rage.config.middleware.middlewares.reject do |middleware, _, _|
243
+ middleware == Rage::FiberWrapper
244
+ end
245
+ end
246
+ end
247
+ end
248
+
196
249
  # @private
197
250
  class Internal
198
251
  attr_accessor :rails_mode
@@ -142,6 +142,7 @@ class RageController::API
142
142
 
143
143
  # @private
144
144
  attr_writer :__before_actions, :__after_actions, :__rescue_handlers
145
+ # @private
145
146
  attr_accessor :__wrap_parameters_key, :__wrap_parameters_options
146
147
 
147
148
  # @private
@@ -299,14 +300,12 @@ class RageController::API
299
300
  @__before_actions[i] = action
300
301
  end
301
302
 
302
- # Initialize controller params wrapping into a nested hash.
303
- # If initialized, params wrapping logic will be added to the controller.
304
- # Params get wrapped only if the CONTENT_TYPE header is present and params hash doesn't contain a param that
305
- # has the same name as the wrapper key.
303
+ # Wraps the parameters hash into a nested hash. This will allow clients to submit requests without having to specify any root elements.
304
+ # Params get wrapped only if the `Content-Type` header is present and the `params` hash doesn't contain a param with the same name as the wrapper key.
306
305
  #
307
- # @param key [Symbol] key that the wrapped params hash will nested under
308
- # @param include [Array] array of params that should be included to the wrapped params hash
309
- # @param exclude [Array] array of params that should be excluded from the wrapped params hash
306
+ # @param key [Symbol] the wrapper key
307
+ # @param include [Symbol, Array<Symbol>] the list of attribute names which parameters wrapper will wrap into a nested hash
308
+ # @param exclude [Symbol, Array<Symbol>] the list of attribute names which parameters wrapper will exclude from a nested hash
310
309
  # @example
311
310
  # wrap_parameters :user, include: %i[name age]
312
311
  # @example
@@ -366,13 +365,13 @@ class RageController::API
366
365
  # Get the cookie object. See {Rage::Cookies}.
367
366
  # @return [Rage::Cookies]
368
367
  def cookies
369
- @cookies ||= Rage::Cookies.new(@__env, self)
368
+ @cookies ||= Rage::Cookies.new(@__env, @__headers)
370
369
  end
371
370
 
372
371
  # Get the session object. See {Rage::Session}.
373
372
  # @return [Rage::Session]
374
373
  def session
375
- @session ||= Rage::Session.new(self)
374
+ @session ||= Rage::Session.new(cookies)
376
375
  end
377
376
 
378
377
  # Send a response to the client.
data/lib/rage/cookies.rb CHANGED
@@ -14,9 +14,9 @@ end
14
14
 
15
15
  class Rage::Cookies
16
16
  # @private
17
- def initialize(env, controller)
17
+ def initialize(env, headers)
18
18
  @env = env
19
- @headers = controller.headers
19
+ @headers = headers
20
20
  @request_cookies = {}
21
21
  @parsed = false
22
22
 
@@ -7,7 +7,9 @@
7
7
  class Rage::FiberWrapper
8
8
  def initialize(app)
9
9
  Iodine.on_state(:on_start) do
10
- Fiber.set_scheduler(Rage::FiberScheduler.new)
10
+ unless Fiber.scheduler
11
+ Fiber.set_scheduler(Rage::FiberScheduler.new)
12
+ end
11
13
  end
12
14
  @app = app
13
15
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OriginValidator
4
+ def initialize(app, *allowed_origins)
5
+ @app = app
6
+ @validator = build_validator(allowed_origins)
7
+ end
8
+
9
+ def call(env)
10
+ if @validator.call(env)
11
+ @app.call(env)
12
+ else
13
+ Rage.logger.error("Request origin not allowed: #{env["HTTP_ORIGIN"]}")
14
+ [404, {}, ["Not Found"]]
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def build_validator(allowed_origins)
21
+ if allowed_origins.empty?
22
+ ->(env) { false }
23
+ else
24
+ origins_eval = allowed_origins.map { |origin|
25
+ origin.is_a?(Regexp) ?
26
+ "origin =~ /#{origin.source}/.freeze" :
27
+ "origin == '#{origin}'.freeze"
28
+ }.join(" || ")
29
+
30
+ eval <<-RUBY
31
+ ->(env) do
32
+ origin = env["HTTP_ORIGIN".freeze]
33
+ #{origins_eval}
34
+ end
35
+ RUBY
36
+ end
37
+ end
38
+ end
@@ -17,7 +17,7 @@ class Rage::Router::DSL
17
17
  end
18
18
 
19
19
  ##
20
- # This class implements routing logic for your application, providing API similar to Rails.
20
+ # This class implements routing logic for your application, providing an API similar to Rails.
21
21
  #
22
22
  # 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.
23
23
  # Example:
data/lib/rage/session.rb CHANGED
@@ -7,8 +7,8 @@ class Rage::Session
7
7
  KEY = Rack::RACK_SESSION.to_sym
8
8
 
9
9
  # @private
10
- def initialize(controller)
11
- @cookies = controller.cookies.encrypted
10
+ def initialize(cookies)
11
+ @cookies = cookies.encrypted
12
12
  end
13
13
 
14
14
  # Writes the value to the session.
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "1.7.0"
4
+ VERSION = "1.8.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -7,27 +7,17 @@ require "pathname"
7
7
 
8
8
  module Rage
9
9
  def self.application
10
- app = Application.new(__router)
11
-
12
- config.middleware.middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
13
- # in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
14
- # if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`
15
- if Rage.config.internal.rails_mode
16
- rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
17
- end
18
-
19
- if rails_middleware
20
- rails_middleware.build(next_in_chain)
21
- else
22
- middleware.new(next_in_chain, *args, &block)
23
- end
24
- end
10
+ with_middlewares(Application.new(__router), config.middleware.middlewares)
25
11
  end
26
12
 
27
13
  def self.multi_application
28
14
  Rage::Router::Util::Cascade.new(application, Rails.application)
29
15
  end
30
16
 
17
+ def self.cable
18
+ Rage::Cable
19
+ end
20
+
31
21
  def self.routes
32
22
  Rage::Router::DSL.new(__router)
33
23
  end
@@ -90,6 +80,23 @@ module Rage
90
80
  end
91
81
  end
92
82
 
83
+ # @private
84
+ def self.with_middlewares(app, middlewares)
85
+ middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
86
+ # in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
87
+ # if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`
88
+ if Rage.config.internal.rails_mode
89
+ rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
90
+ end
91
+
92
+ if rails_middleware
93
+ rails_middleware.build(next_in_chain)
94
+ else
95
+ middleware.new(next_in_chain, *args, &block)
96
+ end
97
+ end
98
+ end
99
+
93
100
  module Router
94
101
  module Strategies
95
102
  end
@@ -106,6 +113,7 @@ module Rage
106
113
 
107
114
  autoload :Cookies, "rage/cookies"
108
115
  autoload :Session, "rage/session"
116
+ autoload :Cable, "rage/cable/cable"
109
117
  end
110
118
 
111
119
  module RageController