Skip to content

Websockets #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
VERSION
coverage
dump.rdb

# Created by https://www.toptal.com/developers/gitignore/api/ruby,rails,macos,windows,linux,vim,emacs
# Edit at https://www.toptal.com/developers/gitignore?templates=ruby,rails,macos,windows,linux,vim,emacs
Expand Down
8 changes: 3 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ RAILS_VERSION = Gem::Version.new(
)
gem "rails", "~> #{RAILS_VERSION}"
gem "rake", ">= 12.0"

# Ruby 3 removed webrick, so we need to install it manually.
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3")
gem "webrick"
end
gem "puma"
gem "redis"

# Include gem active_model_serializers so we can test against their interface (Rails >=6 only).
if RAILS_VERSION > Gem::Version.new("6")
Expand All @@ -25,6 +22,7 @@ group :development do
gem "better_errors"
gem "binding_of_caller"
gem "byebug"
gem "foreman"
gem "pry-rails"
gem "rubocop-shopify", require: false
gem "web-console"
Expand Down
13 changes: 11 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ GEM
activesupport
coderay (1.1.3)
concurrent-ruby (1.1.10)
connection_pool (2.3.0)
coveralls (0.8.23)
json (>= 1.8, < 3)
simplecov (~> 0.16.1)
Expand All @@ -101,6 +102,7 @@ GEM
debug_inspector (1.1.0)
docile (1.4.0)
erubi (1.11.0)
foreman (0.87.2)
globalid (1.0.0)
activesupport (>= 5.0)
i18n (1.12.0)
Expand Down Expand Up @@ -138,6 +140,8 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
puma (6.0.0)
nio4r (~> 2.0)
racc (1.6.0)
rack (2.2.4)
rack-test (2.0.2)
Expand Down Expand Up @@ -170,6 +174,10 @@ GEM
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
redis (5.0.5)
redis-client (>= 0.9.0)
redis-client (0.11.2)
connection_pool
regexp_parser (2.6.0)
rexml (3.2.5)
rubocop (1.38.0)
Expand Down Expand Up @@ -208,7 +216,6 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webrick (1.7.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
Expand All @@ -223,16 +230,18 @@ DEPENDENCIES
binding_of_caller
byebug
coveralls
foreman
minitest (>= 5.0)
pg
pry-rails
puma
rails (~> 7.0.4)
rake (>= 12.0)
redis
rest_framework!
rubocop-shopify
sqlite3 (~> 1.4.0)
web-console
webrick

BUNDLED WITH
2.2.33
38 changes: 19 additions & 19 deletions lib/rest_framework/controller_mixins/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ def get_skip_actions(skip_undefined: true)

return skip
end

# Get the configured serializer class.
def get_serializer_class
return nil unless serializer_class = self.serializer_class

# Support dynamically resolving serializer given a symbol or string.
serializer_class = serializer_class.to_s if serializer_class.is_a?(Symbol)
if serializer_class.is_a?(String)
serializer_class = self.const_get(serializer_class)
end

# Wrap it with an adapter if it's an active_model_serializer.
if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
end

return serializer_class
end
end

def self.included(base)
Expand Down Expand Up @@ -92,27 +110,9 @@ def self.included(base)
end
end

# Helper to get the configured serializer class.
def get_serializer_class
return nil unless serializer_class = self.class.serializer_class

# Support dynamically resolving serializer given a symbol or string.
serializer_class = serializer_class.to_s if serializer_class.is_a?(Symbol)
if serializer_class.is_a?(String)
serializer_class = self.class.const_get(serializer_class)
end

# Wrap it with an adapter if it's an active_model_serializer.
if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
end

return serializer_class
end

# Helper to serialize data using the `serializer_class`.
def serialize(data, **kwargs)
return self.get_serializer_class.new(data, controller: self, **kwargs).serialize
return self.class.get_serializer_class.new(data, controller: self, **kwargs).serialize
end

# Helper to get filtering backends, defaulting to no backends.
Expand Down
18 changes: 18 additions & 0 deletions lib/rest_framework/controller_mixins/channels.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require_relative "models"

# This module extends the base REST controller to support Action Cable Channels. If the controller
# is associated to a specific model, then we can even support broadcasting table/record updates.
#
# The general idea is to dynamically construct a namespaced `::Channel` class to implement the
# channel functionality.
#
# For controllers associated to models, we will support 2 types of updates:
# - Table Updates: Table updates will notify the subscriber when ANY record of a table is modified,
# whether it's a create, update, or delete. The notification will not include details on the
# specific record or records affected.
# - Record Updates: Record updates will notify the subscriber when a record is created, updated, or
# deleted, and will include the entire record and the action that was performed.
# TODO: this must be constrained to the records in `get_recordset`.
#
module RESTFramework::ChannelControllerMixin
end
13 changes: 8 additions & 5 deletions lib/rest_framework/controller_mixins/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
module RESTFramework::BaseModelControllerMixin
include RESTFramework::BaseControllerMixin

module ClassMethods
# Get the configured serializer class, or `NativeSerializer` as a default.
def get_serializer_class
return super || RESTFramework::NativeSerializer
end
end

def self.included(base)
if base.is_a?(Class)
RESTFramework::BaseControllerMixin.included(base)
base.extend(ClassMethods)

# Add class attributes (with defaults) unless they already exist.
{
Expand Down Expand Up @@ -109,11 +117,6 @@ def get_allowed_parameters
) || self.fields
end

# Helper to get the configured serializer class, or `NativeSerializer` as a default.
def get_serializer_class
return super || RESTFramework::NativeSerializer
end

# Helper to get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
def get_filter_backends
return self.class.filter_backends || [
Expand Down
2 changes: 1 addition & 1 deletion lib/rest_framework/serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def _serialize(record, config, serializer_methods)
)
end

def serialize(*args)
def serialize(*_args)
config = self.get_serializer_config
serializer_methods = config.delete(:serializer_methods)

Expand Down
3 changes: 3 additions & 0 deletions test/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This Procfile is for running the test app on development systems.
rails: bin/rails server
redis: redis-server
4 changes: 4 additions & 0 deletions test/app/channels/application_cable/channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
9 changes: 9 additions & 0 deletions test/app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :random_token

def connect
self.random_token = SecureRandom.hex(4)
end
end
end
9 changes: 9 additions & 0 deletions test/app/channels/test_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class TestChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
41 changes: 41 additions & 0 deletions test/app/controllers/demo_api/things_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,45 @@ def toggle_is_discounted
thing.update!(is_discounted: !thing.is_discounted)
return api_response({message: "Is discounted toggled to #{thing.is_discounted}."})
end

class Channel < ApplicationCable::Channel
# Subscribe with:
# {"command": "subscribe","identifier": "{\"channel\": \"DemoApi::ThingsController::Channel\"}"}
def subscribed
logger.info("GNS: subscribed")
stream_from("demo_api/things")
end

def index(_data)
controller = self._controller.new
controller.request = ActionDispatch::Request.new({})
controller.request.format
controller.index
ActionCable.server.broadcast(
"demo_api/things",
self._controller.get_serializer_class.new(self._controller.get_recordset).serialize,
)
end

def _controller
return self.class.to_s.deconstantize.constantize
end
end

this_controller = self
Thing.define_method(:bcast) {
if self.in?(controller.get_recordset)
ActionCable.server.broadcast(
"demo_api/things",
this_controller.get_serializer_class.new(self).serialize,
)
end
}
Thing.after_commit(:bcast)
# Thing.after_commit do
# ActionCable.server.broadcast(
# "demo_api/things",
# this_controller.get_serializer_class.new(self).serialize,
# )
# end
end
3 changes: 3 additions & 0 deletions test/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"action_controller/railtie",
"action_view/railtie",
"active_job/railtie",
"action_cable/engine",
"rails/test_unit/railtie",
].each do |railtie|
require railtie
Expand All @@ -34,6 +35,8 @@ class Application < Rails::Application
config.cache_classes = false
config.action_controller.perform_caching = false

config.action_cable.disable_request_forgery_protection = true

if Rails::VERSION::MAJOR >= 7
config.active_support.remove_deprecated_time_with_zone_name = true
end
Expand Down
12 changes: 12 additions & 0 deletions test/config/cable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
development:
adapter: redis
url: redis://localhost:6379
channel_prefix: rails_rest_framework_development

test:
adapter: test

production:
adapter: redis
url: redis://localhost:6379
channel_prefix: rails_rest_framework_production