ru.Bee 1.11.1 → 2.1.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/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/extensions/hookable.rb +53 -12
- data/lib/rubee/extensions/serializable.rb +2 -1
- data/lib/rubee/extensions/validatable.rb +130 -0
- data/lib/rubee/features.rb +22 -0
- data/lib/rubee/models/database_objectable.rb +1 -0
- data/lib/rubee/models/sequel_object.rb +14 -6
- 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/account_model_test.rb +17 -0
- data/lib/tests/models/comment_model_test.rb +142 -5
- data/lib/tests/models/user_model_test.rb +94 -0
- data/lib/tests/test.db +0 -0
- data/lib/tests/test_helper.rb +6 -0
- data/readme.md +329 -29
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c12dfae68f0595266581855939b1d4b80468ad9e4298438a0125b709e96c3fa3
|
|
4
|
+
data.tar.gz: 6fb74be8efa80754b66f603bbe41b2ba5cc9cbc26eddfcb55ee24f7073c31a74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 463291b6a407cc39fa1b26620416da495598aa56332091e37eb5c8545e0d1f1a35bbb23586b43958bd7c83d59c66663b382b7c54c7b45a5496803365ee1e1623
|
|
7
|
+
data.tar.gz: 9bc34459746bc4a060f1fdb697f332cc35246cc104cf1a211ec9533f92a301573f0270e98582d86c64bfe72585fb233170befbc7e5719cb7d88d76a465945d3f
|
|
@@ -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/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
|
|
@@ -11,7 +11,7 @@ module Rubee
|
|
|
11
11
|
hook = Module.new do
|
|
12
12
|
define_method(method) do |*args, &block|
|
|
13
13
|
if conditions_met?(options[:if], options[:unless])
|
|
14
|
-
handler.respond_to?(:call) ? handler
|
|
14
|
+
handler.respond_to?(:call) ? safe_call(handler, [self, args]) : send(handler)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
super(*args, &block)
|
|
@@ -29,7 +29,7 @@ module Rubee
|
|
|
29
29
|
result = super(*args, &block)
|
|
30
30
|
|
|
31
31
|
if conditions_met?(options[:if], options[:unless])
|
|
32
|
-
handler.respond_to?(:call) ? handler
|
|
32
|
+
handler.respond_to?(:call) ? safe_call(handler, [self, args]) : send(handler)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
result
|
|
@@ -47,7 +47,7 @@ module Rubee
|
|
|
47
47
|
if conditions_met?(options[:if], options[:unless])
|
|
48
48
|
if handler.respond_to?(:call)
|
|
49
49
|
result = nil
|
|
50
|
-
handler
|
|
50
|
+
safe_call(handler, [self, args]) do
|
|
51
51
|
result = super(*args, &block)
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -67,33 +67,74 @@ module Rubee
|
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def conditions_met?(if_condition = nil, unless_condition = nil)
|
|
70
|
+
def conditions_met?(if_condition = nil, unless_condition = nil, instance = nil)
|
|
71
71
|
return true if if_condition.nil? && unless_condition.nil?
|
|
72
|
-
|
|
73
72
|
if_condition_result =
|
|
74
73
|
if if_condition.nil?
|
|
75
74
|
true
|
|
76
75
|
elsif if_condition.respond_to?(:call)
|
|
77
|
-
if_condition
|
|
78
|
-
elsif respond_to?(if_condition)
|
|
79
|
-
send(if_condition)
|
|
76
|
+
safe_call(if_condition, [instance])
|
|
77
|
+
elsif instance.respond_to?(if_condition)
|
|
78
|
+
instance.send(if_condition)
|
|
80
79
|
end
|
|
81
80
|
unless_condition_result =
|
|
82
81
|
if unless_condition.nil?
|
|
83
82
|
false
|
|
84
83
|
elsif unless_condition.respond_to?(:call)
|
|
85
|
-
unless_condition
|
|
86
|
-
elsif respond_to?(unless_condition)
|
|
87
|
-
send(unless_condition)
|
|
84
|
+
safe_call(unless_condition, [instance])
|
|
85
|
+
elsif instance.respond_to?(unless_condition)
|
|
86
|
+
instance.send(unless_condition)
|
|
88
87
|
end
|
|
89
88
|
|
|
90
89
|
if_condition_result && !unless_condition_result
|
|
91
90
|
end
|
|
91
|
+
|
|
92
|
+
def safe_call(handler, call_args = [], &block)
|
|
93
|
+
if handler.is_a?(Proc)
|
|
94
|
+
wrapped = safe_lambda(handler, &block)
|
|
95
|
+
|
|
96
|
+
# Forward block to the handler lambda if present
|
|
97
|
+
if block
|
|
98
|
+
wrapped.call(*call_args, &block)
|
|
99
|
+
else
|
|
100
|
+
wrapped.call(*call_args)
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
handler.call
|
|
104
|
+
block&.call
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def safe_lambda(strict_lambda, &block)
|
|
109
|
+
return strict_lambda unless strict_lambda.is_a?(Proc)
|
|
110
|
+
return strict_lambda unless strict_lambda.lambda?
|
|
111
|
+
return strict_lambda unless strict_lambda.arity >= 0
|
|
112
|
+
|
|
113
|
+
proc do |*call_args|
|
|
114
|
+
lambda_arity = strict_lambda.arity
|
|
115
|
+
|
|
116
|
+
# Take only what lambda can handle, pad missing ones with nil
|
|
117
|
+
args_for_lambda = call_args.first(lambda_arity)
|
|
118
|
+
if args_for_lambda.length < lambda_arity
|
|
119
|
+
args_for_lambda += Array.new(lambda_arity - args_for_lambda.length, nil)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
strict_lambda.call(*args_for_lambda, &block)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
92
125
|
end
|
|
93
126
|
|
|
94
127
|
module InstanceMethods
|
|
95
128
|
def conditions_met?(if_condition = nil, unless_condition = nil)
|
|
96
|
-
self.class.conditions_met?(if_condition, unless_condition)
|
|
129
|
+
self.class.conditions_met?(if_condition, unless_condition, self)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def safe_lambda(strict_lambda)
|
|
133
|
+
self.class.safe_lambda(strict_lambda)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def safe_call(handler, call_args = [], &block)
|
|
137
|
+
self.class.safe_call(handler, call_args, &block)
|
|
97
138
|
end
|
|
98
139
|
end
|
|
99
140
|
end
|
|
@@ -20,7 +20,8 @@ module Rubee
|
|
|
20
20
|
|
|
21
21
|
def to_h
|
|
22
22
|
instance_variables.each_with_object({}) do |var, hash|
|
|
23
|
-
|
|
23
|
+
attr_name = var.to_s.delete('@')
|
|
24
|
+
hash[attr_name] = instance_variable_get(var) unless attr_name.start_with?('__')
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
module Validatable
|
|
3
|
+
class Error < StandardError; end
|
|
4
|
+
|
|
5
|
+
class State
|
|
6
|
+
attr_accessor :errors, :valid
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@valid = true
|
|
10
|
+
@errors = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add_error(attribute, hash)
|
|
14
|
+
@valid = false
|
|
15
|
+
@errors[attribute] ||= {}
|
|
16
|
+
@errors[attribute].merge!(hash)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def has_errors_for?(attribute)
|
|
20
|
+
@errors.key?(attribute)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class RuleChain
|
|
25
|
+
attr_reader :instance, :attribute
|
|
26
|
+
|
|
27
|
+
def initialize(instance, attribute, state)
|
|
28
|
+
@instance = instance
|
|
29
|
+
@attribute = attribute
|
|
30
|
+
@state = state
|
|
31
|
+
@optional = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def required(error_hash)
|
|
35
|
+
value = @instance.send(@attribute)
|
|
36
|
+
if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
37
|
+
@state.add_error(@attribute, error_hash)
|
|
38
|
+
end
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def optional(*)
|
|
43
|
+
@optional = true
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def type(expected_class, error_hash)
|
|
48
|
+
return self if @state.has_errors_for?(@attribute)
|
|
49
|
+
|
|
50
|
+
value = @instance.send(@attribute)
|
|
51
|
+
return self if @optional && value.nil?
|
|
52
|
+
|
|
53
|
+
unless value.is_a?(expected_class)
|
|
54
|
+
@state.add_error(@attribute, error_hash)
|
|
55
|
+
end
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def condition(handler, error_message)
|
|
60
|
+
return self if @state.has_errors_for?(@attribute)
|
|
61
|
+
value = @instance.send(@attribute)
|
|
62
|
+
return self if @optional && value.nil?
|
|
63
|
+
|
|
64
|
+
if handler.respond_to?(:call)
|
|
65
|
+
@state.add_error(@attribute, error_message) unless handler.call
|
|
66
|
+
else
|
|
67
|
+
@instance.send(handler)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.included(base)
|
|
75
|
+
base.extend(ClassMethods)
|
|
76
|
+
base.prepend(Initializer)
|
|
77
|
+
base.include(InstanceMethods)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
module Initializer
|
|
81
|
+
def initialize(*)
|
|
82
|
+
@__validation_state = State.new
|
|
83
|
+
super
|
|
84
|
+
run_validations
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
module InstanceMethods
|
|
89
|
+
def valid?
|
|
90
|
+
run_validations
|
|
91
|
+
@__validation_state.valid
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def invalid?
|
|
95
|
+
!valid?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def errors
|
|
99
|
+
run_validations
|
|
100
|
+
@__validation_state.errors
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def run_validations
|
|
104
|
+
@__validation_state = State.new
|
|
105
|
+
self.class&.validation_block&.call(self)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def required(attribute, options)
|
|
109
|
+
error_message = options
|
|
110
|
+
RuleChain.new(self, attribute, @__validation_state).required(error_message)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def optional(attribute)
|
|
114
|
+
RuleChain.new(self, attribute, @__validation_state).optional
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def add_error(attribute, hash)
|
|
118
|
+
@__validation_state.add_error(attribute, hash)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
module ClassMethods
|
|
123
|
+
attr_reader :validation_block
|
|
124
|
+
|
|
125
|
+
def validate(&block)
|
|
126
|
+
@validation_block = block
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
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
|
|
@@ -33,12 +33,11 @@ module Rubee
|
|
|
33
33
|
|
|
34
34
|
else
|
|
35
35
|
begin
|
|
36
|
-
|
|
36
|
+
created_id = self.class.dataset.insert(args)
|
|
37
37
|
rescue StandardError => _e
|
|
38
38
|
return false
|
|
39
39
|
end
|
|
40
|
-
self.id =
|
|
41
|
-
|
|
40
|
+
self.id = created_id
|
|
42
41
|
end
|
|
43
42
|
true
|
|
44
43
|
end
|
|
@@ -208,9 +207,9 @@ module Rubee
|
|
|
208
207
|
if dataset.columns.include?(:created) && dataset.columns.include?(:updated)
|
|
209
208
|
attrs.merge!(created: Time.now, updated: Time.now)
|
|
210
209
|
end
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
instance = new(**attrs)
|
|
211
|
+
Rubee::DBTools.with_retry { instance.save }
|
|
212
|
+
instance
|
|
214
213
|
end
|
|
215
214
|
|
|
216
215
|
def destroy_all(cascade: false)
|
|
@@ -225,6 +224,15 @@ module Rubee
|
|
|
225
224
|
klass.new(**klass_attributes)
|
|
226
225
|
end
|
|
227
226
|
end
|
|
227
|
+
|
|
228
|
+
def validate_before_persist!
|
|
229
|
+
before(:save, proc { |model| raise Rubee::Validatable::Error, model.errors.to_s }, if: :invalid?)
|
|
230
|
+
before(:update, proc do |model, args|
|
|
231
|
+
if (instance = model.class.new(*args)) && instance.invalid?
|
|
232
|
+
raise Rubee::Validatable::Error, instance.errors.to_s
|
|
233
|
+
end
|
|
234
|
+
end)
|
|
235
|
+
end
|
|
228
236
|
end
|
|
229
237
|
end
|
|
230
238
|
end
|
|
@@ -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
|