ru.Bee 1.11 → 2.0.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.
- checksums.yaml +4 -4
- data/lib/app/controllers/users_controller.rb +57 -0
- data/lib/app/controllers/welcome_controller.rb +2 -0
- data/lib/app/models/user.rb +2 -0
- data/lib/config/routes.rb +1 -0
- data/lib/inits/system.rb +7 -0
- data/lib/rubee/autoload.rb +10 -1
- data/lib/rubee/cli/console.rb +0 -8
- data/lib/rubee/cli/project.rb +6 -1
- data/lib/rubee/cli/server.rb +2 -2
- data/lib/rubee/configuration.rb +16 -0
- data/lib/rubee/controllers/base_controller.rb +55 -13
- data/lib/rubee/features.rb +22 -0
- data/lib/rubee/models/sequel_object.rb +7 -5
- data/lib/rubee/pubsub/container.rb +44 -0
- data/lib/rubee/pubsub/publisher.rb +18 -0
- data/lib/rubee/pubsub/redis.rb +99 -0
- data/lib/rubee/pubsub/subscriber.rb +25 -0
- data/lib/rubee/pubsub/test_one.rb +29 -0
- data/lib/rubee/websocket/websocket.rb +102 -0
- data/lib/rubee/websocket/websocket_connections.rb +35 -0
- data/lib/rubee.rb +15 -8
- data/lib/tests/controllers/base_controller_test.rb +1 -1
- data/lib/tests/controllers/users_controller_test.rb +41 -0
- data/lib/tests/models/user_model_test.rb +96 -2
- data/lib/tests/test.db +0 -0
- data/readme.md +137 -23
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd4247baa0c82fd81b4be82e8e63446b8b0d7934cee6243dda47ea3b350d4213
|
|
4
|
+
data.tar.gz: 1ba2c534bcebc7e898408fcf9d4e594c9535ca6fc218a5de8d51895edadc9d7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cff63aa019e6f5895908a7b1fd7122cefd3986c9a4c1aede556aee667cb750ca82ec95727babda0bd4a234a54b3b159fac5668e9a07de7f7974913015b7dcca5
|
|
7
|
+
data.tar.gz: b4ae2fb9d36dc6c06d87da67d479029fb7c4399f18b26dd57e10ad176a4135a81e2c7840f92393441287e29d470859ef9ec15043dad06b414cf13b680980e35c
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class UsersController < Rubee::BaseController
|
|
2
|
+
attach_websocket! # Method required to turn controller to been able to handle websocket requests
|
|
3
|
+
using ChargedHash
|
|
4
|
+
|
|
5
|
+
# Endpoint to find or create user
|
|
6
|
+
def create
|
|
7
|
+
user = User.where(**params).last
|
|
8
|
+
user ||= User.create(**params)
|
|
9
|
+
|
|
10
|
+
response_with(object: user, type: :json)
|
|
11
|
+
rescue StandardError => e
|
|
12
|
+
response_with(object: { error: e.message }, type: :json)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def subscribe
|
|
16
|
+
channel = params[:channel]
|
|
17
|
+
sender_id = params[:options][:id]
|
|
18
|
+
io = params[:options][:io]
|
|
19
|
+
|
|
20
|
+
User.sub(channel, sender_id, io) do |channel, args|
|
|
21
|
+
websocket_connections.register(channel, args[:io])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
response_with(object: { type: 'system', channel: params[:channel], status: :subscribed }, type: :websocket)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
response_with(object: { type: 'system', error: e.message }, type: :websocket)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def unsubscribe
|
|
30
|
+
channel = params[:channel]
|
|
31
|
+
sender_id = params[:options][:id]
|
|
32
|
+
io = params[:options][:io]
|
|
33
|
+
|
|
34
|
+
User.unsub(channel, sender_id, io) do |channel, args|
|
|
35
|
+
websocket_connections.remove(channel, args[:io])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
response_with(object: params.merge(type: 'system', status: :unsubscribed), type: :websocket)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
response_with(object: { type: 'system', error: e.message }, type: :websocket)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def publish
|
|
44
|
+
args = {}
|
|
45
|
+
User.pub(params[:channel], message: params[:message]) do |channel|
|
|
46
|
+
user = User.find(params[:options][:id])
|
|
47
|
+
args[:message] = params[:message]
|
|
48
|
+
args[:sender] = params[:options][:id]
|
|
49
|
+
args[:sender_name] = user.email
|
|
50
|
+
websocket_connections.stream(channel, args)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
response_with(object: { type: 'system', message: params[:message], status: :published }, type: :websocket)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
response_with(object: { type: 'system', error: e.message }, type: :websocket)
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/app/models/user.rb
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
# If you remove or modify it, make sure all changes are inlined
|
|
3
3
|
# with AuthTokenMiddleware and AuthTokenable modules
|
|
4
4
|
class User < Rubee::SequelObject
|
|
5
|
+
include Rubee::PubSub::Publisher
|
|
6
|
+
include Rubee::PubSub::Subscriber
|
|
5
7
|
attr_accessor :id, :email, :password, :created, :updated
|
|
6
8
|
end
|
data/lib/config/routes.rb
CHANGED
data/lib/inits/system.rb
ADDED
data/lib/rubee/autoload.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module Rubee
|
|
2
2
|
class Autoload
|
|
3
|
+
BLACKLIST = ['rubee.rb', 'test_helper.rb']
|
|
3
4
|
class << self
|
|
4
5
|
def call(black_list = [], **options)
|
|
5
6
|
load_whitelisted(options[:white_list_dirs]) && return if options[:white_list_dirs]
|
|
@@ -12,7 +13,7 @@ module Rubee
|
|
|
12
13
|
Dir.glob(File.join(Rubee::APP_ROOT, '**', '*.rb')).sort.each do |file|
|
|
13
14
|
base_name = File.basename(file)
|
|
14
15
|
|
|
15
|
-
unless base_name.end_with?('_test.rb') || (black_list +
|
|
16
|
+
unless base_name.end_with?('_test.rb') || (black_list + BLACKLIST).include?(base_name)
|
|
16
17
|
require_relative file
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -35,6 +36,14 @@ module Rubee
|
|
|
35
36
|
Dir[File.join(Rubee::APP_ROOT, 'inits/**', '*.rb')].each do |file|
|
|
36
37
|
require_relative file unless black_list.include?("#{file}.rb")
|
|
37
38
|
end
|
|
39
|
+
# rubee pub sub
|
|
40
|
+
Dir[File.join(root_directory, 'rubee/pubsub/**', '*.rb')].each do |file|
|
|
41
|
+
require_relative file unless black_list.include?("#{file}.rb")
|
|
42
|
+
end
|
|
43
|
+
# rubee websocket
|
|
44
|
+
Dir[File.join(root_directory, 'rubee/websocket/**', '*.rb')].each do |file|
|
|
45
|
+
require_relative file unless black_list.include?("#{file}.rb")
|
|
46
|
+
end
|
|
38
47
|
# rubee async
|
|
39
48
|
Dir[File.join(root_directory, 'rubee/async/**', '*.rb')].each do |file|
|
|
40
49
|
require_relative file unless black_list.include?("#{file}.rb")
|
data/lib/rubee/cli/console.rb
CHANGED
|
@@ -14,14 +14,6 @@ module Rubee
|
|
|
14
14
|
Rubee::Configuration.setup(env = :test) do |config|
|
|
15
15
|
config.database_url = { url: 'sqlite://lib/tests/test.db', env: }
|
|
16
16
|
end
|
|
17
|
-
# Rubee::Autoload.call
|
|
18
|
-
# Rubee::SequelObject.reconnect!
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def reload
|
|
22
|
-
app_files = Dir["./#{Rubee::APP_ROOT}/**/*.rb"]
|
|
23
|
-
app_files.each { |file| load(file) }
|
|
24
|
-
color_puts('Reloaded ..', color: :green)
|
|
25
17
|
end
|
|
26
18
|
|
|
27
19
|
begin
|
data/lib/rubee/cli/project.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Rubee
|
|
|
29
29
|
FileUtils.mkdir_p(target_dir)
|
|
30
30
|
# Define blacklist
|
|
31
31
|
blacklist_files = %w[rubee.rb print_colors.rb version.rb config.ru test_helper.rb Gemfile.lock test.yml test.db
|
|
32
|
-
development.db production.db]
|
|
32
|
+
development.db production.db users_controller.rb users_controller.rb]
|
|
33
33
|
blacklist_dirs = %w[rubee tests .git .github .idea node_modules db inits]
|
|
34
34
|
# Copy files, excluding blacklisted ones
|
|
35
35
|
copy_project_files(source_dir, target_dir, blacklist_files, blacklist_dirs)
|
|
@@ -108,6 +108,11 @@ module Rubee
|
|
|
108
108
|
gem 'json'
|
|
109
109
|
gem 'jwt'
|
|
110
110
|
|
|
111
|
+
# Websocket is required to use integrated websocket feature
|
|
112
|
+
gem 'websocket'
|
|
113
|
+
# Redis is required for pubsub and websocket
|
|
114
|
+
gem 'redis'
|
|
115
|
+
|
|
111
116
|
group :development do
|
|
112
117
|
gem 'rerun'
|
|
113
118
|
gem 'minitest'
|
data/lib/rubee/cli/server.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Rubee
|
|
|
7
7
|
| |_) | | | || _ \| _|
|
|
8
8
|
| _ <| |__| || |_) | |___
|
|
9
9
|
|_| \_\\____/ |____/|_____|
|
|
10
|
-
Ver: %s
|
|
10
|
+
Ver: %s ...bzzz
|
|
11
11
|
LOGO
|
|
12
12
|
|
|
13
13
|
class << self
|
|
@@ -23,7 +23,7 @@ LOGO
|
|
|
23
23
|
|
|
24
24
|
port ||= '7000'
|
|
25
25
|
print_logo
|
|
26
|
-
color_puts("Starting takeoff of ruBee
|
|
26
|
+
color_puts("Starting takeoff of ruBee on port: #{port}...", color: :yellow)
|
|
27
27
|
command = "#{jit_prefix(jit)}rackup #{ENV['RACKUP_FILE']} -p #{port}"
|
|
28
28
|
color_puts(command, color: :gray)
|
|
29
29
|
exec(command)
|
data/lib/rubee/configuration.rb
CHANGED
|
@@ -9,6 +9,7 @@ module Rubee
|
|
|
9
9
|
development: {
|
|
10
10
|
database_url: '',
|
|
11
11
|
port: 7000,
|
|
12
|
+
redis_url: '',
|
|
12
13
|
},
|
|
13
14
|
production: {},
|
|
14
15
|
test: {},
|
|
@@ -36,6 +37,11 @@ module Rubee
|
|
|
36
37
|
@configuraiton[args[:app].to_sym][args[:env].to_sym][:database_url] = args[:url]
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def redis_url=(args)
|
|
41
|
+
args[:app] ||= :app
|
|
42
|
+
@configuraiton[args[:app].to_sym][args[:env].to_sym][:redis_url] = args[:url]
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
def async_adapter=(args)
|
|
40
46
|
args[:app] ||= :app
|
|
41
47
|
@configuraiton[args[:app].to_sym][args[:env].to_sym][:async_adapter] = args[:async_adapter]
|
|
@@ -82,6 +88,16 @@ module Rubee
|
|
|
82
88
|
@configuraiton[args[:app].to_sym][ENV['RACK_ENV']&.to_sym || :development][:react] || {}
|
|
83
89
|
end
|
|
84
90
|
|
|
91
|
+
def pubsub_container=(args)
|
|
92
|
+
args[:app] ||= :app
|
|
93
|
+
@configuraiton[args[:app].to_sym][args[:env].to_sym][:pubsub_container] = args[:pubsub_container]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def pubsub_container(**args)
|
|
97
|
+
args[:app] ||= :app
|
|
98
|
+
@configuraiton[args[:app].to_sym][ENV['RACK_ENV']&.to_sym || :development][:pubsub_container] || ::Rubee::PubSub::Redis.instance
|
|
99
|
+
end
|
|
100
|
+
|
|
85
101
|
def method_missing(method_name, *args)
|
|
86
102
|
return unless method_name.to_s.start_with?('get_')
|
|
87
103
|
|
|
@@ -51,6 +51,8 @@ module Rubee
|
|
|
51
51
|
[status, headers.merge('content-type' => 'application/javascript'), [object]]
|
|
52
52
|
in :css
|
|
53
53
|
[status, headers.merge('content-type' => 'text/css'), [object]]
|
|
54
|
+
in :websocket
|
|
55
|
+
object # hash is expected
|
|
54
56
|
in :file
|
|
55
57
|
[
|
|
56
58
|
status,
|
|
@@ -101,23 +103,38 @@ module Rubee
|
|
|
101
103
|
erb_template.result(binding)
|
|
102
104
|
end
|
|
103
105
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
rescue StandardError
|
|
109
|
-
{}
|
|
110
|
-
end
|
|
111
|
-
begin
|
|
112
|
-
body.merge!(URI.decode_www_form(inputs).to_h.transform_keys(&:to_sym))
|
|
113
|
-
rescue StandardError
|
|
114
|
-
nil
|
|
106
|
+
def websocket
|
|
107
|
+
action = @params[:action]
|
|
108
|
+
unless ['subscribe', 'unsubscribe', 'publish'].include?(action)
|
|
109
|
+
response_with(object: "Unknown action: #{action}", type: :websocket)
|
|
115
110
|
end
|
|
111
|
+
|
|
112
|
+
public_send(action)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def params
|
|
116
|
+
# Read raw input safely (only once)
|
|
117
|
+
raw_input = @request.body.read.to_s.strip
|
|
118
|
+
@request.body.rewind if @request.body.respond_to?(:rewind)
|
|
119
|
+
|
|
120
|
+
# Try parsing JSON first, fall back to form-encoded data
|
|
121
|
+
parsed_input =
|
|
122
|
+
begin
|
|
123
|
+
JSON.parse(raw_input)
|
|
124
|
+
rescue StandardError
|
|
125
|
+
begin
|
|
126
|
+
URI.decode_www_form(raw_input).to_h.transform_keys(&:to_sym)
|
|
127
|
+
rescue
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Combine route params, request params, and body
|
|
116
133
|
@params ||= extract_params(@request.path, @route[:path])
|
|
117
|
-
.merge(
|
|
134
|
+
.merge(parsed_input)
|
|
118
135
|
.merge(@request.params)
|
|
119
136
|
.transform_keys(&:to_sym)
|
|
120
|
-
.reject { |k, _v|
|
|
137
|
+
.reject { |k, _v| k.to_sym == :_method }
|
|
121
138
|
end
|
|
122
139
|
|
|
123
140
|
def headers
|
|
@@ -125,6 +142,10 @@ module Rubee
|
|
|
125
142
|
.collect { |key, val| [key.sub(/^HTTP_/, ''), val] }
|
|
126
143
|
end
|
|
127
144
|
|
|
145
|
+
def websocket_connections
|
|
146
|
+
Rubee::WebSocketConnections.instance
|
|
147
|
+
end
|
|
148
|
+
|
|
128
149
|
def extract_params(path, pattern)
|
|
129
150
|
regex_pattern = pattern.gsub(/\{(\w+)\}/, '(?<\1>[^/]+)')
|
|
130
151
|
regex = Regexp.new("^#{regex_pattern}$")
|
|
@@ -135,5 +156,26 @@ module Rubee
|
|
|
135
156
|
|
|
136
157
|
{}
|
|
137
158
|
end
|
|
159
|
+
|
|
160
|
+
def handle_websocket
|
|
161
|
+
res = Rubee::WebSocket.call(@request.env) do |payload|
|
|
162
|
+
@params = payload
|
|
163
|
+
yield
|
|
164
|
+
end
|
|
165
|
+
res
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class << self
|
|
169
|
+
def attach_websocket!
|
|
170
|
+
around(
|
|
171
|
+
:websocket, :handle_websocket,
|
|
172
|
+
if: -> do
|
|
173
|
+
redis_available = Rubee::Features.redis_available?
|
|
174
|
+
Rubee::Logger.error(message: 'Please make sure redis server is running') unless redis_available
|
|
175
|
+
redis_available
|
|
176
|
+
end
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
138
180
|
end
|
|
139
181
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
class Features
|
|
3
|
+
class << self
|
|
4
|
+
def redis_available?
|
|
5
|
+
require "redis"
|
|
6
|
+
redis_url = Rubee::Configuration.get_redis_url
|
|
7
|
+
redis = redis_url&.empty? ? Redis.new : Redis.new(url: redis_url)
|
|
8
|
+
redis.ping
|
|
9
|
+
true
|
|
10
|
+
rescue LoadError, Redis::CannotConnectError
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def websocket_available?
|
|
15
|
+
require "websocket"
|
|
16
|
+
true
|
|
17
|
+
rescue LoadError
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -4,7 +4,7 @@ module Rubee
|
|
|
4
4
|
using ChargedString
|
|
5
5
|
using ChargedHash
|
|
6
6
|
|
|
7
|
-
before :
|
|
7
|
+
before :update, :save, :set_timestamps
|
|
8
8
|
|
|
9
9
|
def destroy(cascade: false, **_options)
|
|
10
10
|
if cascade
|
|
@@ -43,13 +43,15 @@ module Rubee
|
|
|
43
43
|
true
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def assign_attributes(args={})
|
|
47
|
-
self.class.dataset.columns do |attr|
|
|
48
|
-
|
|
46
|
+
def assign_attributes(args = {})
|
|
47
|
+
self.class.dataset.columns.each do |attr|
|
|
48
|
+
if args[attr.to_sym]
|
|
49
|
+
send("#{attr}=", args[attr.to_sym])
|
|
50
|
+
end
|
|
49
51
|
end
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
def update(args={})
|
|
54
|
+
def update(args = {})
|
|
53
55
|
assign_attributes(args)
|
|
54
56
|
args.merge!(updated:)
|
|
55
57
|
found_hash = self.class.dataset.where(id:)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
module PubSub
|
|
3
|
+
class Container
|
|
4
|
+
def pub(*)
|
|
5
|
+
raise NotImplementedError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Container Implementation of sub
|
|
9
|
+
def sub(*)
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Container Implementation of unsub
|
|
14
|
+
def unsub(*)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def retrieve_klasses(iterable)
|
|
21
|
+
iterable.map { |clazz| turn_to_class(clazz) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def turn_to_class(string)
|
|
25
|
+
string.split('::').inject(Object) { |o, c| o.const_get(c) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fan_out(clazzes, args, &block)
|
|
29
|
+
mutex = Mutex.new
|
|
30
|
+
|
|
31
|
+
mutex.synchronize do
|
|
32
|
+
clazzes.each do |clazz|
|
|
33
|
+
if block
|
|
34
|
+
block.call(clazz.name, args)
|
|
35
|
+
else
|
|
36
|
+
clazz.on_pub(clazz.name, args)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
module PubSub
|
|
3
|
+
module Publisher
|
|
4
|
+
Error = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def pub(channel, args = {}, &block)
|
|
12
|
+
Rubee::Configuration.pubsub_container.pub(channel, args, &block)
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "connection_pool"
|
|
2
|
+
require "redis"
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Rubee
|
|
7
|
+
module PubSub
|
|
8
|
+
class Redis < Container
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
DEFAULT_POOL_SIZE = 5
|
|
12
|
+
DEFAULT_TIMEOUT = 5
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
redis_url = Rubee::Configuration.get_redis_url
|
|
16
|
+
@pool = ConnectionPool.new(size: DEFAULT_POOL_SIZE, timeout: DEFAULT_TIMEOUT) do
|
|
17
|
+
if redis_url&.empty?
|
|
18
|
+
::Redis.new
|
|
19
|
+
else
|
|
20
|
+
::Redis.new(url: redis_url)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Example: pub("ok", message: "hello")
|
|
27
|
+
def pub(channel, args = {}, &block)
|
|
28
|
+
keys = with_redis { |r| r.scan_each(match: "#{channel}:*").to_a }
|
|
29
|
+
return false if keys.empty?
|
|
30
|
+
|
|
31
|
+
values = with_redis { |r| r.mget(*keys) }
|
|
32
|
+
|
|
33
|
+
iterable = values.each_with_index.each_with_object({}) do |(val, i), hash|
|
|
34
|
+
key = keys[i]
|
|
35
|
+
hash[key] = val
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
clazzes = retrieve_klasses(iterable)
|
|
39
|
+
fan_out(clazzes, args, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Example: sub("ok", "User", ["123"])
|
|
43
|
+
def sub(channel, klass_name, *args, &block)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
id = args.first
|
|
46
|
+
id_string = id ? ":#{id}" : ""
|
|
47
|
+
key = "#{channel}:#{klass_name}#{id_string}"
|
|
48
|
+
existing = with_redis { |r| r.get(key) }
|
|
49
|
+
io = args.last.respond_to?(:call) ? args.pop : nil
|
|
50
|
+
|
|
51
|
+
with_redis { |r| r.set(key, args.join(",")) } unless existing
|
|
52
|
+
block&.call(key, io: io)
|
|
53
|
+
end
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def unsub(channel, klass_name, *args, &block)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
id = args.first
|
|
60
|
+
id_string = id ? ":#{id}" : ""
|
|
61
|
+
key = "#{channel}:#{klass_name}#{id_string}"
|
|
62
|
+
value = with_redis { |r| r.get(key) }
|
|
63
|
+
return false unless value
|
|
64
|
+
|
|
65
|
+
io = args.pop if args.last.respond_to?(:call)
|
|
66
|
+
with_redis { |r| r.del(key) }
|
|
67
|
+
block&.call(key, io: io)
|
|
68
|
+
end
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def with_redis(&block)
|
|
75
|
+
@pool.with(&block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def retrieve_klasses(iterable)
|
|
79
|
+
iterable.each_with_object({}) do |(key, args), hash|
|
|
80
|
+
channel, clazz, id = key.split(":")
|
|
81
|
+
arg_list = args.to_s.split(",")
|
|
82
|
+
hash[key] = { channel:, clazz:, args: arg_list, id: }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def fan_out(clazzes, method_args = {}, &block)
|
|
87
|
+
clazzes.each do |_key, opts|
|
|
88
|
+
clazz = turn_to_class(opts[:clazz])
|
|
89
|
+
clazz_args = opts[:args]
|
|
90
|
+
|
|
91
|
+
clazz.on_pub(opts[:channel], *clazz_args, **method_args) if clazz.respond_to?(:on_pub)
|
|
92
|
+
id_string = opts[:id] ? ":#{opts[:id]}" : ""
|
|
93
|
+
block&.call("#{opts[:channel]}:#{opts[:clazz]}#{id_string}", **method_args)
|
|
94
|
+
end
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
module PubSub
|
|
3
|
+
module Subscriber
|
|
4
|
+
Error = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def sub(channel, *args, &block)
|
|
12
|
+
Rubee::Configuration.pubsub_container.sub(channel, name, *args, &block)
|
|
13
|
+
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def unsub(channel, *args, &block)
|
|
18
|
+
Rubee::Configuration.pubsub_container.unsub(channel, name, *args, &block)
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# class Subscriber
|
|
2
|
+
# include Rubee::PubSub::Subscriber
|
|
3
|
+
|
|
4
|
+
# def self.on_pub(channel, message, options = {})
|
|
5
|
+
# puts "channel=#{channel} message=#{message} options=#{options}"
|
|
6
|
+
# end
|
|
7
|
+
# end
|
|
8
|
+
|
|
9
|
+
# class SubscriberOne
|
|
10
|
+
# include Rubee::PubSub::Subscriber
|
|
11
|
+
|
|
12
|
+
# def self.on_pub(channel, message, options = {})
|
|
13
|
+
# puts "channel=#{channel} message=#{message} options=#{options}"
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
|
|
17
|
+
# class Publisher
|
|
18
|
+
# include Rubee::PubSub::Publisher
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
# Subscriber.sub("ok", ["123456"])
|
|
22
|
+
|
|
23
|
+
# SubscriberOne.sub("ok", ["123"])
|
|
24
|
+
|
|
25
|
+
# Publisher.pub("ok", { message: "hello" })
|
|
26
|
+
|
|
27
|
+
# SubscriberOne.unsub("ok", ["123"])
|
|
28
|
+
|
|
29
|
+
# Publisher.pub("ok", { message: "hello" })
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require 'websocket'
|
|
2
|
+
require 'websocket/frame'
|
|
3
|
+
require 'websocket/handshake'
|
|
4
|
+
require 'websocket/handshake/server'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'redis'
|
|
7
|
+
|
|
8
|
+
module Rubee
|
|
9
|
+
class WebSocket
|
|
10
|
+
using ChargedHash
|
|
11
|
+
class << self
|
|
12
|
+
def call(env, &controller_block)
|
|
13
|
+
unless env['rack.hijack']
|
|
14
|
+
return [500, { 'Content-Type' => 'text/plain' }, ['Hijack not supported']]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
env['rack.hijack'].call
|
|
18
|
+
io = env['rack.hijack_io']
|
|
19
|
+
|
|
20
|
+
handshake = ::WebSocket::Handshake::Server.new
|
|
21
|
+
handshake.from_rack(env)
|
|
22
|
+
unless handshake.valid?
|
|
23
|
+
io.write("HTTP/1.1 400 Bad Request\r\n\r\n")
|
|
24
|
+
io.close
|
|
25
|
+
return [-1, {}, []]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
io.write(handshake.to_s)
|
|
29
|
+
incoming = ::WebSocket::Frame::Incoming::Server.new(version: handshake.version)
|
|
30
|
+
|
|
31
|
+
outgoing = ->(data) do
|
|
32
|
+
frame = ::WebSocket::Frame::Outgoing::Server.new(
|
|
33
|
+
version: handshake.version,
|
|
34
|
+
type: :text,
|
|
35
|
+
data: data.to_json
|
|
36
|
+
)
|
|
37
|
+
io.write(frame.to_s)
|
|
38
|
+
rescue IOError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# --- Listen to incoming data ---
|
|
43
|
+
Thread.new do
|
|
44
|
+
loop do
|
|
45
|
+
data = io.readpartial(1024)
|
|
46
|
+
incoming << data
|
|
47
|
+
|
|
48
|
+
while (frame = incoming.next)
|
|
49
|
+
case frame.type
|
|
50
|
+
when :text
|
|
51
|
+
out = controller_out(frame, outgoing, &controller_block)
|
|
52
|
+
outgoing.call(**out)
|
|
53
|
+
|
|
54
|
+
when :close
|
|
55
|
+
# Client closed connection
|
|
56
|
+
handle_close(frame, io, handshake)
|
|
57
|
+
break
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
rescue EOFError, IOError
|
|
62
|
+
begin
|
|
63
|
+
handle_close(frame, io, handshake)
|
|
64
|
+
rescue
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
[101, handshake.headers, []]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def payload(frame)
|
|
73
|
+
JSON.parse(frame.data)
|
|
74
|
+
rescue
|
|
75
|
+
{}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_close(frame, io, handshake)
|
|
79
|
+
payload_hash = payload(frame)
|
|
80
|
+
channel = payload_hash["channel"]
|
|
81
|
+
subcriber = payload_hash["subcriber"]
|
|
82
|
+
::Rubee::WebSocketConnections.instance.remove("#{channel}:#{subcriber}", io)
|
|
83
|
+
io.write(::WebSocket::Frame::Outgoing::Server.new(
|
|
84
|
+
version: handshake.version,
|
|
85
|
+
type: :close
|
|
86
|
+
).to_s)
|
|
87
|
+
io.close
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def controller_out(frame, io, &block)
|
|
91
|
+
payload_hash = payload(frame)
|
|
92
|
+
action = payload_hash["action"]
|
|
93
|
+
channel = payload_hash["channel"]
|
|
94
|
+
message = payload_hash["message"]
|
|
95
|
+
options = payload_hash.select { |k, _| !["action", "channel", "message"].include?(k) }
|
|
96
|
+
options.merge!(io:)
|
|
97
|
+
|
|
98
|
+
block.call(channel:, message:, action:, options:)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
class WebSocketConnections
|
|
3
|
+
include Singleton
|
|
4
|
+
def initialize
|
|
5
|
+
@subscribers ||= Hash.new { |h, k| h[k] = [] }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def register(channel, io)
|
|
9
|
+
@subscribers[channel] << io unless @subscribers[channel].include?(io)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove(channel, io)
|
|
13
|
+
@subscribers[channel].delete(io)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def remove_all(io)
|
|
17
|
+
@subscribers.each_value { _1.delete(io) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def flush_all
|
|
21
|
+
@subscribers.each_value(&:clear)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stream(channel, args = {})
|
|
25
|
+
ios = @subscribers[channel]
|
|
26
|
+
if !ios&.empty? && ios.all? { _1.respond_to?(:call) }
|
|
27
|
+
ios.each { _1.call(args) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clear
|
|
32
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/rubee.rb
CHANGED
|
@@ -16,11 +16,13 @@ module Rubee
|
|
|
16
16
|
JS_DIR = File.join(APP_ROOT, LIB, 'js') unless defined?(JS_DIR)
|
|
17
17
|
CSS_DIR = File.join(APP_ROOT, LIB, 'css') unless defined?(CSS_DIR)
|
|
18
18
|
ROOT_PATH = File.expand_path(File.join(__dir__, '..')) unless defined?(ROOT_PATH)
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
VERSION = '2.0.0'
|
|
20
21
|
|
|
21
22
|
require_relative 'rubee/router'
|
|
22
23
|
require_relative 'rubee/logger'
|
|
23
24
|
require_relative 'rubee/generator'
|
|
25
|
+
require_relative 'rubee/features'
|
|
24
26
|
require_relative 'rubee/autoload'
|
|
25
27
|
require_relative 'rubee/configuration'
|
|
26
28
|
|
|
@@ -31,15 +33,10 @@ module Rubee
|
|
|
31
33
|
def call(env)
|
|
32
34
|
# autoload rb files
|
|
33
35
|
Autoload.call
|
|
34
|
-
|
|
35
|
-
# register images paths
|
|
36
|
+
# init rack request
|
|
36
37
|
request = Rack::Request.new(env)
|
|
37
38
|
# Add default path for assets
|
|
38
|
-
|
|
39
|
-
route.get('/images/{path}', to: 'base#image', namespace: 'Rubee')
|
|
40
|
-
route.get('/js/{path}', to: 'base#js', namespace: 'Rubee')
|
|
41
|
-
route.get('/css/{path}', to: 'base#css', namespace: 'Rubee')
|
|
42
|
-
end
|
|
39
|
+
register_assets_routes
|
|
43
40
|
# define route
|
|
44
41
|
route = Router.route_for(request)
|
|
45
42
|
# if react is the view so we would like to delegate not cauth by rubee routes to it.
|
|
@@ -62,5 +59,15 @@ module Rubee
|
|
|
62
59
|
# fire the action
|
|
63
60
|
controller.send(action)
|
|
64
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def register_assets_routes
|
|
66
|
+
Router.draw do |route|
|
|
67
|
+
route.get('/images/{path}', to: 'base#image', namespace: 'Rubee')
|
|
68
|
+
route.get('/js/{path}', to: 'base#js', namespace: 'Rubee')
|
|
69
|
+
route.get('/css/{path}', to: 'base#css', namespace: 'Rubee')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
65
72
|
end
|
|
66
73
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative '../test_helper'
|
|
2
|
+
|
|
3
|
+
class UsersControllerTest < Minitest::Test
|
|
4
|
+
include Rack::Test::Methods
|
|
5
|
+
|
|
6
|
+
def app
|
|
7
|
+
Rubee::Application.instance
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_websocket_handshake_written_to_io
|
|
11
|
+
env = Rack::MockRequest.env_for(
|
|
12
|
+
'/ws',
|
|
13
|
+
{
|
|
14
|
+
'REQUEST_METHOD' => 'GET',
|
|
15
|
+
'PATH_INFO' => '/ws',
|
|
16
|
+
'HTTP_CONNECTION' => 'keep-alive, Upgrade',
|
|
17
|
+
'HTTP_UPGRADE' => 'websocket',
|
|
18
|
+
'HTTP_HOST' => 'localhost:9292',
|
|
19
|
+
'HTTP_ORIGIN' => 'http://localhost:9292',
|
|
20
|
+
'HTTP_SEC_WEBSOCKET_KEY' => 'dGhlIHNhbXBsZSBub25jZQ==',
|
|
21
|
+
'HTTP_SEC_WEBSOCKET_VERSION' => '13',
|
|
22
|
+
'rack.url_scheme' => 'http'
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Mock hijack interface expected by Rubee::WebSocket
|
|
27
|
+
io = StringIO.new
|
|
28
|
+
env['rack.hijack'] = proc {}
|
|
29
|
+
env['rack.hijack_io'] = io
|
|
30
|
+
|
|
31
|
+
# Call the WebSocket handler
|
|
32
|
+
Rubee::WebSocket.call(env)
|
|
33
|
+
|
|
34
|
+
# Expect the handshake response written to IO
|
|
35
|
+
io.rewind
|
|
36
|
+
handshake_response = io.read
|
|
37
|
+
assert_includes(handshake_response, "HTTP/1.1 101 Switching Protocols")
|
|
38
|
+
assert_includes(handshake_response, "Upgrade: websocket")
|
|
39
|
+
assert_includes(handshake_response, "Connection: Upgrade")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -140,14 +140,14 @@ describe 'User model' do
|
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
-
describe 'when udpate existing user with no
|
|
143
|
+
describe 'when udpate existing user with no arguments' do
|
|
144
144
|
it 'update updated field' do
|
|
145
145
|
user = User.new(email: 'ok-test@test.com', password: '123')
|
|
146
146
|
user.save
|
|
147
147
|
updated_field_before_update = user.updated
|
|
148
148
|
|
|
149
149
|
user.update
|
|
150
|
-
_(user.updated > updated_field_before_update).must_equal(true)
|
|
150
|
+
_(user.reload.updated > updated_field_before_update).must_equal(true)
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
153
|
end
|
|
@@ -345,4 +345,98 @@ zip: '555555')
|
|
|
345
345
|
end
|
|
346
346
|
end
|
|
347
347
|
end
|
|
348
|
+
|
|
349
|
+
describe 'pubsub' do
|
|
350
|
+
describe 'when Rubee::PubSub::Subscriber is included' do
|
|
351
|
+
before { User.include(Rubee::PubSub::Subscriber) }
|
|
352
|
+
|
|
353
|
+
it 'reveals sub method' do
|
|
354
|
+
_(User.respond_to?(:sub)).must_equal(true)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it 'reveals unsub method' do
|
|
358
|
+
_(User.respond_to?(:unsub)).must_equal(true)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
describe 'when Rubee::PubSub::Publisher is included' do
|
|
363
|
+
before { User.include(Rubee::PubSub::Publisher) }
|
|
364
|
+
|
|
365
|
+
it 'reveals pub method' do
|
|
366
|
+
_(User.respond_to?(:pub)).must_equal(true)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
describe '.sub' do
|
|
371
|
+
before do
|
|
372
|
+
User.include(Rubee::PubSub::Subscriber)
|
|
373
|
+
User.include(Rubee::PubSub::Publisher)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
user = User.create(email: 'ok-test@test.com', password: '123')
|
|
377
|
+
|
|
378
|
+
describe 'when sub with channel and args' do
|
|
379
|
+
it 'returns true' do
|
|
380
|
+
_(User.sub("ok", [user.id.to_s])).must_equal(true)
|
|
381
|
+
|
|
382
|
+
User.unsub("ok", [user.id.to_s])
|
|
383
|
+
user.destroy
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
describe '.unsub' do
|
|
389
|
+
before do
|
|
390
|
+
User.include(Rubee::PubSub::Subscriber)
|
|
391
|
+
User.include(Rubee::PubSub::Publisher)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
describe 'when unsub with channel and args' do
|
|
395
|
+
it 'returns true' do
|
|
396
|
+
_(User.unsub("ok", ["123456"])).must_equal(true)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
describe 'pub flow' do
|
|
402
|
+
describe 'when pub with channel and args' do
|
|
403
|
+
after do
|
|
404
|
+
User.destroy_all(cascade: true)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
it 'fan out ouput' do
|
|
408
|
+
User.include(Rubee::PubSub::Subscriber)
|
|
409
|
+
User.include(Rubee::PubSub::Publisher)
|
|
410
|
+
|
|
411
|
+
user_one = User.create(email: 'ok-test1@test.com', password: '123')
|
|
412
|
+
user_two = User.create(email: 'ok-test@2test.com', password: '123')
|
|
413
|
+
user_three = User.create(email: 'ok-test3@test.com', password: '123')
|
|
414
|
+
|
|
415
|
+
User.singleton_class.define_method(:on_pub) do |channel, *args, **options|
|
|
416
|
+
id = args.first
|
|
417
|
+
user = User.find(id)
|
|
418
|
+
if user
|
|
419
|
+
user.update(password: '321')
|
|
420
|
+
else
|
|
421
|
+
raise "User with id=#{id} not found"
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
User.sub("ok", [user_one.id.to_s])
|
|
426
|
+
User.sub("ok", [user_two.id.to_s])
|
|
427
|
+
User.sub("ok", [user_three.id.to_s])
|
|
428
|
+
|
|
429
|
+
User.pub("ok", message: "hello")
|
|
430
|
+
|
|
431
|
+
User.unsub("ok", [user_one.id.to_s])
|
|
432
|
+
User.unsub("ok", [user_two.id.to_s])
|
|
433
|
+
User.unsub("ok", [user_three.id.to_s])
|
|
434
|
+
|
|
435
|
+
_(user_one.reload.password).must_equal('321')
|
|
436
|
+
_(user_two.reload.password).must_equal('321')
|
|
437
|
+
_(user_three.reload.password).must_equal('321')
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
348
442
|
end
|
data/lib/tests/test.db
CHANGED
|
Binary file
|
data/readme.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
# <img src="lib/images/rubee.svg" alt="RUBEE" height="40"> ... RUBEE
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
RUBEE is a Ruby-based web framework designed to streamline the development of modular monolith web applications. \
|
|
12
|
+
Under the hood, it leverages the power of Ruby and Rack backed by Puma, offering a clean, efficient, and flexible architecture. \
|
|
12
13
|
It offers a structured approach to building scalable, maintainable, and React-ready projects, \
|
|
13
14
|
making it an ideal choice for developers seeking a balance between monolithic simplicity and modular flexibility.
|
|
14
15
|
|
|
@@ -18,14 +19,14 @@ Want to get a quick API server up and runing? You can do it for real quick!
|
|
|
18
19
|
|
|
19
20
|
## Production ready
|
|
20
21
|
|
|
21
|
-
Take a look on the
|
|
22
|
+
Take a look on the RUBEE demo site with all documentation stored in there: https://rubee.dedyn.io/
|
|
22
23
|
Want to explore how it built? https://github.com/nucleom42/rubee-site
|
|
23
24
|
|
|
24
25
|
## Stress tested
|
|
25
26
|
|
|
26
27
|
```bash
|
|
27
|
-
wrk -t4 -c100 -d30s https://rubee.
|
|
28
|
-
Running 30s test @ https://rubee.
|
|
28
|
+
wrk -t4 -c100 -d30s https://rubee.dedyn.io/docs
|
|
29
|
+
Running 30s test @ https://rubee.dedyn.io/docs
|
|
29
30
|
4 threads and 100 connections
|
|
30
31
|
Thread Stats Avg Stdev Max +/- Stdev
|
|
31
32
|
Latency 304.95ms 33.22ms 551.86ms 90.38%
|
|
@@ -40,9 +41,27 @@ Transfer/sec: 140.07KB
|
|
|
40
41
|
- Average latency: ~305 ms
|
|
41
42
|
- Total requests handled: 9,721
|
|
42
43
|
- Hardware: Raspberry Pi 5(8 Gb) (single board computer)
|
|
43
|
-
- Server:
|
|
44
|
+
- Server: RUBEE app hosted via Nginx + HTTPS
|
|
44
45
|
|
|
45
|
-
This
|
|
46
|
+
This demonstrate RUBEE’s efficient architecture and suitability for lightweight deployments — even on low-power hardware.
|
|
47
|
+
|
|
48
|
+
## Comparison
|
|
49
|
+
Here below is a **short web frameworks comparison** built with Ruby, so you can evaluate your choice with RUBEE.
|
|
50
|
+
|
|
51
|
+
**Disclaimer:**
|
|
52
|
+
The comparison is based on a very generic and subjective information open in the Internet and is not a real benchmark. The comparison is aimed to give you a general idea of the differences between the frameworks and Rubee and not to compare the frameworks directly.
|
|
53
|
+
|
|
54
|
+
| Feature / Framework | **RUBEE** | Rails | Sinatra | Hanami | Padrino | Grape |
|
|
55
|
+
|---------------------|-----------|-------|---------|--------|---------|-------|
|
|
56
|
+
| **React readiness** | Built-in React integration (route generator can scaffold React components that fetch data via controllers) | React via webpacker/importmap, but indirect | No direct React support | Can integrate React | Can integrate via JS pipelines | API-focused, no React support |
|
|
57
|
+
| **Routing style** | Explicit, file-based routes with clear JSON/HTML handling | DSL, routes often implicit inside controllers | Explicit DSL, inline in code | Declarative DSL | Rails-like DSL | API-oriented DSL |
|
|
58
|
+
| **Modularity** | Lightweight core, pluggable projects | One project by default, but can be extended with repsecrive gem | Very modular (small DSL) | Designed for modularity | Semi-modular, still Rails-like | Modular (mount APIs) |
|
|
59
|
+
| **Startup / Load speed** | Very fast (minimal boot time, designed for modern Ruby) | Not very fast, especially on large apps | Very fast | Medium (slower than Sinatra, faster than Rails) | Similar to Rails (heavier) | Fast |
|
|
60
|
+
| **Ecosystem** | 🌱 Early-stage, focused on modern simplicity, but easily expandable over bundler | Huge ecosystem, gems, community | Large ecosystem, many gems work | Small, growing | Small, less active | Small, niche |
|
|
61
|
+
| **Learning curve** | Simple, explicit, minimal DSL | Steep (lots of conventions & magic) | Very low (DSL fits in one file) | Medium, more concepts (repositories, entities) | Similar to Rails, easier in parts | Low (API-only) |
|
|
62
|
+
| **Customizability** | High (explicit over implicit, hooks & generators) | Limited without monkey-patching | Very high (you control flow) | High, modular architecture | Medium | High (designed for APIs) |
|
|
63
|
+
| **Target use case** | Modern full-stack apps with React frontends or APIs, may be well suite if you prefer modular monolith over microservices | Large, full-stack, mature apps | Small apps, microservices | Modular apps, DDD | Rails-like but modular | APIs & microservices |
|
|
64
|
+
| **Early adopters support** | Personal early adopters support via fast extending and fixing | Not available | Not known | Not known | Not known | Not known |
|
|
46
65
|
|
|
47
66
|
## Content
|
|
48
67
|
|
|
@@ -55,6 +74,7 @@ This demonstrates RUBEE’s efficient architecture and suitability for lightweig
|
|
|
55
74
|
- [Views](#views)
|
|
56
75
|
- [Hooks](#hooks)
|
|
57
76
|
- [JWT based authentification](#jwt-based-authentification)
|
|
77
|
+
- [OAuth2 based authentification](#oauth-authentification)
|
|
58
78
|
- [Rubee commands](#rubee-commands)
|
|
59
79
|
- [Generate commands](#generate-commands)
|
|
60
80
|
- [Migration commands](#migration-commands)
|
|
@@ -64,16 +84,16 @@ This demonstrates RUBEE’s efficient architecture and suitability for lightweig
|
|
|
64
84
|
- [Modular](#modualar-application)
|
|
65
85
|
- [Logger](#logger)
|
|
66
86
|
|
|
67
|
-
You can read it on the demo [site](https://rubee.dedyn.io/)
|
|
87
|
+
You can read it on the demo: [site](https://rubee.dedyn.io/)
|
|
68
88
|
|
|
69
|
-
🚧 The doc site is on
|
|
70
|
-
Please refer to
|
|
89
|
+
🚧 The doc site is on update mode now. We are working on it.
|
|
90
|
+
Please refer to the documentation shown below.
|
|
71
91
|
|
|
72
92
|
## Features
|
|
73
93
|
|
|
74
94
|
Lightweight – A minimal footprint focused on serving Ruby applications efficiently.
|
|
75
95
|
<br>
|
|
76
|
-
Modular – A modular approach to application development. Build modular
|
|
96
|
+
Modular – A modular approach to application development. Build modular monolith app with ease by attaching
|
|
77
97
|
as many subprojects as you need.
|
|
78
98
|
<br>
|
|
79
99
|
Contract-driven – Define your API contracts in a simple, declarative way, then generate all the boilerplate you need.
|
|
@@ -86,9 +106,9 @@ Databases – Supports SQLite3, PostgreSQL, MySQL, and more via the Sequel gem.
|
|
|
86
106
|
<br>
|
|
87
107
|
Views – JSON, ERB, and plain HTML out of the box.
|
|
88
108
|
<br>
|
|
89
|
-
React Ready – React is supported as a first-class
|
|
109
|
+
React Ready – React is supported as a first-class RUBEE view engine.
|
|
90
110
|
<br>
|
|
91
|
-
Bundlable – Charge your
|
|
111
|
+
Bundlable – Charge your RUBEE app with any gem you need. Update effortlessly via Bundler.
|
|
92
112
|
<br>
|
|
93
113
|
ORM-agnostic – Models are native ORM objects, but you can use them as blueprints for any data source.
|
|
94
114
|
<br>
|
|
@@ -131,7 +151,7 @@ Make sure:
|
|
|
131
151
|
bundle install
|
|
132
152
|
```
|
|
133
153
|
|
|
134
|
-
4. Run
|
|
154
|
+
4. Run RUBER server. Default port is 7000
|
|
135
155
|
```bash
|
|
136
156
|
rubee start # or rubee start_dev for development
|
|
137
157
|
|
|
@@ -335,7 +355,7 @@ irb(main):023> User.all
|
|
|
335
355
|
=> []
|
|
336
356
|
```
|
|
337
357
|
|
|
338
|
-
Use complex queries chains and when ready serialize it back to
|
|
358
|
+
Use complex queries chains and when ready serialize it back to RUBEE object.
|
|
339
359
|
```Ruby
|
|
340
360
|
# user model
|
|
341
361
|
class User < Rubee::SequelObject
|
|
@@ -372,7 +392,7 @@ irb(main):009> .where(comment_id: Comment.where(text: "test").last.id)
|
|
|
372
392
|
irb(main):010> .then { |dataset| Comment.serialize(dataset) }
|
|
373
393
|
=> [#<Comment:0x0000000121889998 @id=30, @text="test", @user_id=702, @created=2025-09-28 22:03:07.011332 -0400, @updated=2025-09-28 22:03:07.011332 -0400>]
|
|
374
394
|
```
|
|
375
|
-
This is recommended when you want to run one query and serialize it back to
|
|
395
|
+
This is recommended when you want to run one query and serialize it back to RUBEE object only once.
|
|
376
396
|
So it may safe some resources.
|
|
377
397
|
|
|
378
398
|
[Back to content](#content)
|
|
@@ -388,7 +408,7 @@ If you feel comfortable you can play with retry configuration parameters:
|
|
|
388
408
|
config.db_busy_timeout = { env:, value: 1000 } # this is busy timeout in ms, before raising bussy error
|
|
389
409
|
```
|
|
390
410
|
|
|
391
|
-
For
|
|
411
|
+
For RUBEE model class persist methods create and update retry will be added automatically. However, \
|
|
392
412
|
if you want to do it with Sequel dataset you need to do it yourself:
|
|
393
413
|
|
|
394
414
|
```ruby
|
|
@@ -397,7 +417,7 @@ if you want to do it with Sequel dataset you need to do it yourself:
|
|
|
397
417
|
[Back to content](#content)
|
|
398
418
|
|
|
399
419
|
## Routing
|
|
400
|
-
|
|
420
|
+
RUBEE uses explicit routes. In the routes.rb yout can define routes for any of the main HTTP methods. \
|
|
401
421
|
You can also add any matched parameter denoted by a pair of `{ }` in the path of the route. \
|
|
402
422
|
Eg. `/path/to/{a_key}/somewhere`
|
|
403
423
|
|
|
@@ -423,7 +443,7 @@ route.{http_method} {path}, to: "{controller}#{action}",
|
|
|
423
443
|
```
|
|
424
444
|
|
|
425
445
|
### Defining Model attributes in routes
|
|
426
|
-
One of
|
|
446
|
+
One of RUBEE's unique traits is where we can define our models for generation. \
|
|
427
447
|
You've seen above one possible way you can set up.
|
|
428
448
|
|
|
429
449
|
```ruby
|
|
@@ -558,7 +578,7 @@ Main philosophy of attach functinality is to keep the main project clean and eas
|
|
|
558
578
|
share data with the main app. So where to define a border between the main app and subprojects is up to developer.
|
|
559
579
|
Howerver by attching new subproject you will get a new folder and files configured and namespaced respectively.
|
|
560
580
|
|
|
561
|
-
So if you need to extend your main app with a separate project, you can do it easily in
|
|
581
|
+
So if you need to extend your main app with a separate project, you can do it easily in ruBEE.
|
|
562
582
|
1. Attach new subrpoject
|
|
563
583
|
|
|
564
584
|
```bash
|
|
@@ -818,7 +838,8 @@ Starting from ver 1.11 hooks are able to be pinned to class methods.
|
|
|
818
838
|
|
|
819
839
|
```ruby
|
|
820
840
|
class AnyClass
|
|
821
|
-
|
|
841
|
+
include Rubee::Hookable
|
|
842
|
+
before :print_world, :print_hello, class_methods: true # you can use class method as a handler
|
|
822
843
|
|
|
823
844
|
class << self
|
|
824
845
|
def print_world
|
|
@@ -854,7 +875,7 @@ Feel free to customize it in the /db/create_users.rb file before running migrati
|
|
|
854
875
|
Then in the controller you can include the AuthTokenable module and use its methods:
|
|
855
876
|
```ruby
|
|
856
877
|
class UsersController < Rubee::BaseController
|
|
857
|
-
include AuthTokenable
|
|
878
|
+
include Rubee::AuthTokenable
|
|
858
879
|
# List methods you want to restrict
|
|
859
880
|
auth_methods :index # unless the user is authentificated it will return unauthentificated
|
|
860
881
|
|
|
@@ -887,10 +908,103 @@ class UsersController < Rubee::BaseController
|
|
|
887
908
|
end
|
|
888
909
|
end
|
|
889
910
|
```
|
|
911
|
+
## OAuth authentification
|
|
912
|
+
If you want to plug in the OAuth 2.0 authentication, you can use the following code using OAuth2 gem:
|
|
913
|
+
First thing you need to do is to add the gem to your Gemfile
|
|
914
|
+
```bash
|
|
915
|
+
gem 'oauth2'
|
|
916
|
+
```
|
|
917
|
+
Then use down below code as an example and add yours to your controller
|
|
918
|
+
```ruby
|
|
919
|
+
class UsersController < Rubee::BaseController
|
|
920
|
+
include Rubee::AuthTokenable
|
|
921
|
+
|
|
922
|
+
REDIRECT_URI = 'https://mysite.com/users/outh_callback'
|
|
923
|
+
CLIENT_ID = ENV['GOOGLE_CLIENT_ID']
|
|
924
|
+
CLIENT_SECRET = ENV['GOOGLE_CLIENT_SECRET']
|
|
925
|
+
|
|
926
|
+
# GET /login (login form page)
|
|
927
|
+
def edit
|
|
928
|
+
response_with
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
# POST /users/login (login logic)
|
|
932
|
+
def login
|
|
933
|
+
Rubee::Logger.info(message: "Login attempt for user #{params[:email]}")
|
|
934
|
+
if authentificate! # AuthTokenable method that init @token_header
|
|
935
|
+
Rubee::Logger.info(message: "Successful login for user #{@authentificated_user.email}")
|
|
936
|
+
response_with(type: :redirect, to: "/sections", headers: @token_header)
|
|
937
|
+
else
|
|
938
|
+
@error = "Wrong email or password"
|
|
939
|
+
response_with(render_view: "users_edit")
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# GET /users/outh_login
|
|
944
|
+
def outh_login
|
|
945
|
+
response_with(
|
|
946
|
+
type: :redirect,
|
|
947
|
+
to: auth_client.auth_code.authorize_url(
|
|
948
|
+
redirect_uri: REDIRECT_URI,
|
|
949
|
+
scope: 'email profile openid'
|
|
950
|
+
)
|
|
951
|
+
)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
# GET /users/outh_callback
|
|
955
|
+
def outh_callback
|
|
956
|
+
code = params[:code]
|
|
957
|
+
token = auth_client.auth_code.get_token(code, redirect_uri: REDIRECT_URI)
|
|
958
|
+
user_info = JSON.parse(token.get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json').body)
|
|
959
|
+
Rubee::Logger.debug(object: user_info, method: "outh_callback", class: "UsersController")
|
|
960
|
+
|
|
961
|
+
user = User.where(email: user_info['email'])&.last
|
|
962
|
+
unless user
|
|
963
|
+
raise "User with email #{user_info['email']} not found"
|
|
964
|
+
end
|
|
965
|
+
|
|
966
|
+
params[:email] = user_info['email']
|
|
967
|
+
params[:password] = user.password
|
|
968
|
+
|
|
969
|
+
if authentificate! # AuthTokenable method that init @token_header
|
|
970
|
+
Rubee::Logger.info(message: "Successful Outh login for user #{@authentificated_user.email}")
|
|
971
|
+
response_with(type: :redirect, to: "/sections", headers: @token_header)
|
|
972
|
+
else
|
|
973
|
+
@error = "Something went wrong"
|
|
974
|
+
response_with(render_view: "users_edit")
|
|
975
|
+
end
|
|
976
|
+
rescue OAuth2::Error => e
|
|
977
|
+
@error = "OAuth login failed"
|
|
978
|
+
response_with(render_view: "users_edit")
|
|
979
|
+
rescue StandardError => e
|
|
980
|
+
@error = "Something went wrong"
|
|
981
|
+
response_with(render_view: "users_edit")
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
# POST /users/logout (logout logic)
|
|
985
|
+
def logout
|
|
986
|
+
unauthentificate! # AuthTokenable method aimed to handle logout action.
|
|
987
|
+
# Make sure @zeroed_token_header is paRssed within headers options
|
|
988
|
+
response_with(type: :redirect, to: "/login", headers: @zeroed_token_header)
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
private
|
|
992
|
+
|
|
993
|
+
def auth_client
|
|
994
|
+
@client ||= OAuth2::Client.new(
|
|
995
|
+
CLIENT_ID,
|
|
996
|
+
CLIENT_SECRET,
|
|
997
|
+
site: 'https://accounts.google.com',
|
|
998
|
+
authorize_url: '/o/oauth2/auth',
|
|
999
|
+
token_url: 'https://oauth2.googleapis.com/token'
|
|
1000
|
+
)
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
1003
|
+
```
|
|
890
1004
|
|
|
891
1005
|
[Back to content](#content)
|
|
892
1006
|
|
|
893
|
-
##
|
|
1007
|
+
## RUBEE commands
|
|
894
1008
|
```bash
|
|
895
1009
|
rubee start # start the server
|
|
896
1010
|
rubee start_dev # start the server in dev mode, which restart server on changes
|
|
@@ -912,7 +1026,7 @@ rubee db run:create_apples # where create_apples is the name of the migration fi
|
|
|
912
1026
|
rubee db structure # generate migration file for the database structure
|
|
913
1027
|
```
|
|
914
1028
|
|
|
915
|
-
##
|
|
1029
|
+
## RUBEE console
|
|
916
1030
|
```bash
|
|
917
1031
|
rubee console # start the console
|
|
918
1032
|
# you can reload the console by typing reload, so it will pick up latest changes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ru.Bee
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Saltykov
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- bin/rubee
|
|
42
42
|
- bin/test.db
|
|
43
43
|
- lib/Dockerfile
|
|
44
|
+
- lib/app/controllers/users_controller.rb
|
|
44
45
|
- lib/app/controllers/welcome_controller.rb
|
|
45
46
|
- lib/app/models/user.rb
|
|
46
47
|
- lib/app/views/App.tsx
|
|
@@ -66,6 +67,7 @@ files:
|
|
|
66
67
|
- lib/inits/charged_hash.rb
|
|
67
68
|
- lib/inits/charged_string.rb
|
|
68
69
|
- lib/inits/print_colors.rb
|
|
70
|
+
- lib/inits/system.rb
|
|
69
71
|
- lib/js/app.js
|
|
70
72
|
- lib/js/app.js.map
|
|
71
73
|
- lib/js/bundle.js
|
|
@@ -254,18 +256,27 @@ files:
|
|
|
254
256
|
- lib/rubee/controllers/middlewares/auth_token_middleware.rb
|
|
255
257
|
- lib/rubee/extensions/hookable.rb
|
|
256
258
|
- lib/rubee/extensions/serializable.rb
|
|
259
|
+
- lib/rubee/features.rb
|
|
257
260
|
- lib/rubee/generator.rb
|
|
258
261
|
- lib/rubee/logger.rb
|
|
259
262
|
- lib/rubee/models/database_objectable.rb
|
|
260
263
|
- lib/rubee/models/db_tools.rb
|
|
261
264
|
- lib/rubee/models/sequel_object.rb
|
|
265
|
+
- lib/rubee/pubsub/container.rb
|
|
266
|
+
- lib/rubee/pubsub/publisher.rb
|
|
267
|
+
- lib/rubee/pubsub/redis.rb
|
|
268
|
+
- lib/rubee/pubsub/subscriber.rb
|
|
269
|
+
- lib/rubee/pubsub/test_one.rb
|
|
262
270
|
- lib/rubee/router.rb
|
|
271
|
+
- lib/rubee/websocket/websocket.rb
|
|
272
|
+
- lib/rubee/websocket/websocket_connections.rb
|
|
263
273
|
- lib/tests/async/thread_async_test.rb
|
|
264
274
|
- lib/tests/cli/attach_test.rb
|
|
265
275
|
- lib/tests/controllers/auth_tokenable_test.rb
|
|
266
276
|
- lib/tests/controllers/base_controller_test.rb
|
|
267
277
|
- lib/tests/controllers/hookable_test.rb
|
|
268
278
|
- lib/tests/controllers/rubeeapp_test.rb
|
|
279
|
+
- lib/tests/controllers/users_controller_test.rb
|
|
269
280
|
- lib/tests/example_models/account.rb
|
|
270
281
|
- lib/tests/example_models/address.rb
|
|
271
282
|
- lib/tests/example_models/comment.rb
|
|
@@ -300,7 +311,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
300
311
|
- !ruby/object:Gem::Version
|
|
301
312
|
version: '0'
|
|
302
313
|
requirements: []
|
|
303
|
-
rubygems_version: 3.7.
|
|
314
|
+
rubygems_version: 3.7.2
|
|
304
315
|
specification_version: 4
|
|
305
316
|
summary: Fast and lightweight Ruby application server designed for minimalism and
|
|
306
317
|
flexibility
|