ru.Bee 2.0.0 → 2.1.1
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/models/user.rb +0 -2
- data/lib/rubee/extensions/hookable.rb +53 -12
- data/lib/rubee/extensions/serializable.rb +2 -1
- data/lib/rubee/extensions/validatable.rb +169 -0
- data/lib/rubee/models/database_objectable.rb +1 -0
- data/lib/rubee/models/sequel_object.rb +14 -6
- data/lib/rubee.rb +1 -1
- data/lib/tests/models/account_model_test.rb +16 -0
- data/lib/tests/models/comment_model_test.rb +184 -5
- data/lib/tests/test.db +0 -0
- data/lib/tests/test_helper.rb +6 -0
- data/readme.md +250 -26
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc6a4fbeb28a460ca66f75ccf1f5c97fe1572f422392f9ee1b72d082e05cb287
|
|
4
|
+
data.tar.gz: f4dbeaf7338af243f6e225a2a798a87de75b001fa4196bed94345d78887ba8ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 996510c80521425797e17a1b641d767d6319877e127883692d519457c7573835ae2670a8ed17950f302b1934a0aae1270d8b065acfee079341935ddb8964a26f
|
|
7
|
+
data.tar.gz: 75a8ab899d3c98fd492a30ec5501982526318387c9214b78ebdf57ec2c39a91778268283db82cbeb5ae9072aa4c62507b1d175423e7f86beec892cddfea0bb90
|
data/lib/app/models/user.rb
CHANGED
|
@@ -2,7 +2,5 @@
|
|
|
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
|
|
7
5
|
attr_accessor :id, :email, :password, :created, :updated
|
|
8
6
|
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,169 @@
|
|
|
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_message = nil)
|
|
35
|
+
value = @instance.send(@attribute)
|
|
36
|
+
|
|
37
|
+
error_hash = assemble_error_hash(error_message, :required, attribute: @attribute)
|
|
38
|
+
if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
39
|
+
@state.add_error(@attribute, error_hash)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def optional(*)
|
|
46
|
+
@optional = true
|
|
47
|
+
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def attribute
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def type(expected_class, error_message = nil)
|
|
56
|
+
return self if @state.has_errors_for?(@attribute)
|
|
57
|
+
value = @instance.send(@attribute)
|
|
58
|
+
return self if @optional && value.nil?
|
|
59
|
+
|
|
60
|
+
error_hash = assemble_error_hash(error_message, :type, class: expected_class)
|
|
61
|
+
unless value.is_a?(expected_class)
|
|
62
|
+
@state.add_error(@attribute, error_hash)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def condition(handler, error_message = nil)
|
|
69
|
+
return self if @state.has_errors_for?(@attribute)
|
|
70
|
+
value = @instance.send(@attribute)
|
|
71
|
+
return self if @optional && value.nil?
|
|
72
|
+
|
|
73
|
+
error_hash = assemble_error_hash(error_message, :condition)
|
|
74
|
+
if handler.respond_to?(:call)
|
|
75
|
+
@state.add_error(@attribute, error_hash) unless handler.call
|
|
76
|
+
else
|
|
77
|
+
@instance.send(handler)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def assemble_error_hash(error_message, error_type, **options)
|
|
86
|
+
error_message ||= default_message(error_type, **options)
|
|
87
|
+
if error_message.is_a?(String)
|
|
88
|
+
error_message = { message: error_message }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
error_message
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def default_message(type, **options)
|
|
95
|
+
{
|
|
96
|
+
condition: "condition is not met",
|
|
97
|
+
required: "attribute '#{options[:attribute]}' is required",
|
|
98
|
+
type: "attribute must be #{options[:class]}",
|
|
99
|
+
}[type]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.included(base)
|
|
104
|
+
base.extend(ClassMethods)
|
|
105
|
+
base.prepend(Initializer)
|
|
106
|
+
base.include(InstanceMethods)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
module Initializer
|
|
110
|
+
def initialize(*)
|
|
111
|
+
@__validation_state = State.new
|
|
112
|
+
super
|
|
113
|
+
run_validations
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
module InstanceMethods
|
|
118
|
+
def valid?
|
|
119
|
+
run_validations
|
|
120
|
+
@__validation_state.valid
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def invalid?
|
|
124
|
+
!valid?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def errors
|
|
128
|
+
run_validations
|
|
129
|
+
@__validation_state.errors
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def run_validations
|
|
133
|
+
@__validation_state = State.new
|
|
134
|
+
if (block = self.class.validation_block)
|
|
135
|
+
instance_exec(&block)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def subject
|
|
140
|
+
@__validation_state.instance
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def attribute(name)
|
|
144
|
+
RuleChain.new(self, name, @__validation_state).attribute
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def required(attribute, options)
|
|
148
|
+
error_message = options
|
|
149
|
+
RuleChain.new(self, attribute, @__validation_state).required(error_message)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def optional(attribute)
|
|
153
|
+
RuleChain.new(self, attribute, @__validation_state).optional
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def add_error(attribute, hash)
|
|
157
|
+
@__validation_state.add_error(attribute, hash)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
module ClassMethods
|
|
162
|
+
attr_reader :validation_block
|
|
163
|
+
|
|
164
|
+
def validate(&block)
|
|
165
|
+
@validation_block = block
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
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
|
data/lib/rubee.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Rubee
|
|
|
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.
|
|
20
|
+
VERSION = '2.1.1'
|
|
21
21
|
|
|
22
22
|
require_relative 'rubee/router'
|
|
23
23
|
require_relative 'rubee/logger'
|
|
@@ -15,5 +15,21 @@ describe 'Account model' do
|
|
|
15
15
|
_(account.user.id).must_equal(user.id)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
describe '#validate_before_persist' do
|
|
20
|
+
it 'rasies error if account is not valid' do
|
|
21
|
+
Account.validate do
|
|
22
|
+
required(:addres, required: "address is required")
|
|
23
|
+
.type(String, type: "address must be string")
|
|
24
|
+
end
|
|
25
|
+
Account.validate_before_persist!
|
|
26
|
+
user = User.new(email: 'ok-test@test.com', password: '123')
|
|
27
|
+
_(raise_error { Account.create(addres: 1, user_id: user.id) }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
28
|
+
account = Account.create(addres: "13Th street", user_id: user.id)
|
|
29
|
+
_(account.persisted?).must_equal(true)
|
|
30
|
+
_(raise_error { account.update(addres: 1) }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
31
|
+
_(raise_error { Account.new(addres: 1, user_id: user.id).save }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
18
34
|
end
|
|
19
35
|
end
|
|
@@ -3,7 +3,7 @@ require_relative '../test_helper'
|
|
|
3
3
|
describe 'Comment model' do
|
|
4
4
|
describe 'owns_many :users, over: :posts' do
|
|
5
5
|
before do
|
|
6
|
-
comment = Comment.new(text: '
|
|
6
|
+
comment = Comment.new(text: 'test_enough')
|
|
7
7
|
comment.save
|
|
8
8
|
user = User.new(email: 'ok-test@test.com', password: '123')
|
|
9
9
|
user.save
|
|
@@ -17,18 +17,18 @@ describe 'Comment model' do
|
|
|
17
17
|
|
|
18
18
|
describe 'when there are associated comment records' do
|
|
19
19
|
it 'returns all records' do
|
|
20
|
-
_(Comment.where(text: '
|
|
21
|
-
_(Comment.where(text: '
|
|
20
|
+
_(Comment.where(text: 'test_enough').last.users.count).must_equal(1)
|
|
21
|
+
_(Comment.where(text: 'test_enough').last.users.first.email).must_equal('ok-test@test.com')
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
describe 'sequel dataset query' do
|
|
26
26
|
it 'returns all records' do
|
|
27
27
|
result = Comment.dataset.join(:posts, comment_id: :id)
|
|
28
|
-
.where(comment_id: Comment.where(text: '
|
|
28
|
+
.where(comment_id: Comment.where(text: 'test_enough').last.id)
|
|
29
29
|
.then { |dataset| Comment.serialize(dataset) }
|
|
30
30
|
|
|
31
|
-
_(result.first.text).must_equal('
|
|
31
|
+
_(result.first.text).must_equal('test_enough')
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -44,4 +44,183 @@ describe 'Comment model' do
|
|
|
44
44
|
_(Comment.find(comment.id).text).must_equal('test 2')
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
describe 'validatable' do
|
|
49
|
+
def include_and_validate(required: true)
|
|
50
|
+
required_or_optional = required ? :required : :optional
|
|
51
|
+
required_or_optional_args = required ? [required: "text filed is required"] : []
|
|
52
|
+
Comment.validate do
|
|
53
|
+
attribute(:text).send(
|
|
54
|
+
required_or_optional, *required_or_optional_args
|
|
55
|
+
)
|
|
56
|
+
.type(String, type: "text field must be string")
|
|
57
|
+
.condition(proc { text.length > 4 }, { length: "text length must be greater than 4" })
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'is valid' do
|
|
62
|
+
include_and_validate
|
|
63
|
+
comment = Comment.new(text: 'test it as valid')
|
|
64
|
+
|
|
65
|
+
_(comment.valid?).must_equal(true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'is not valid length' do
|
|
69
|
+
include_and_validate
|
|
70
|
+
comment = Comment.new(text: 'test')
|
|
71
|
+
|
|
72
|
+
_(comment.valid?).must_equal(false)
|
|
73
|
+
_(comment.errors[:text]).must_equal({ length: "text length must be greater than 4" })
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'is not valid type' do
|
|
77
|
+
include_and_validate
|
|
78
|
+
comment = Comment.new(text: 1)
|
|
79
|
+
|
|
80
|
+
_(comment.valid?).must_equal(false)
|
|
81
|
+
_(comment.errors[:text]).must_equal({ type: "text field must be string" })
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'is not valid required' do
|
|
85
|
+
include_and_validate
|
|
86
|
+
comment = Comment.new(user_id: 1)
|
|
87
|
+
|
|
88
|
+
_(comment.valid?).must_equal(false)
|
|
89
|
+
_(comment.errors[:text]).must_equal({ required: "text filed is required" })
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe 'when first validation is optional' do
|
|
93
|
+
it 'no text should be valid' do
|
|
94
|
+
include_and_validate required: false
|
|
95
|
+
|
|
96
|
+
comment = Comment.new(user_id: 1)
|
|
97
|
+
|
|
98
|
+
_(comment.valid?).must_equal(true)
|
|
99
|
+
_(comment.errors[:test]).must_equal(nil)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'text is a number should be invalid' do
|
|
103
|
+
include_and_validate required: false
|
|
104
|
+
comment = Comment.new(text: 1)
|
|
105
|
+
|
|
106
|
+
_(comment.valid?).must_equal(false)
|
|
107
|
+
_(comment.errors[:text]).must_equal({ type: "text field must be string" })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'text is short should be invalid' do
|
|
111
|
+
include_and_validate required: false
|
|
112
|
+
comment = Comment.new(text: 'test')
|
|
113
|
+
|
|
114
|
+
_(comment.valid?).must_equal(false)
|
|
115
|
+
_(comment.errors[:text]).must_equal({ length: "text length must be greater than 4" })
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe 'before save must be valid' do
|
|
120
|
+
it 'does not persit if record is invalid' do
|
|
121
|
+
include_and_validate
|
|
122
|
+
Comment.before(
|
|
123
|
+
:save, proc { |comment| raise Rubee::Validatable::Error, comment.errors.to_s },
|
|
124
|
+
if: ->(comment) { comment&.invalid? }
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
comment = Comment.new(text: 'test')
|
|
128
|
+
_(raise_error { comment.save }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
129
|
+
_(comment.persisted?).must_equal(false)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe 'when usig method' do
|
|
133
|
+
it 'does not persit if record is invalid' do
|
|
134
|
+
include_and_validate
|
|
135
|
+
Comment.before(:save, proc { |comment| raise Rubee::Validatable::Error, comment.errors.to_s }, if: :invalid?)
|
|
136
|
+
|
|
137
|
+
comment = Comment.new(text: 'test')
|
|
138
|
+
_(raise_error { comment.save }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
139
|
+
_(comment.persisted?).must_equal(false)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe 'before create must be invalid' do
|
|
145
|
+
it 'does not create if record is invalid' do
|
|
146
|
+
include_and_validate
|
|
147
|
+
Comment.before(:save, proc { |comment| raise Rubee::Validatable::Error, comment.errors.to_s }, if: :invalid?)
|
|
148
|
+
|
|
149
|
+
initial_comments_count = Comment.count
|
|
150
|
+
_(raise_error { Comment.create(text: 'te') }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
151
|
+
assert_equal(initial_comments_count, Comment.count)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe 'before update must be invalid' do
|
|
156
|
+
it 'does not update if record is invalid' do
|
|
157
|
+
include_and_validate
|
|
158
|
+
Comment.around(:update, proc do |_comment, args, &update_method|
|
|
159
|
+
com = Comment.new(*args)
|
|
160
|
+
raise Rubee::Validatable::Error, com.errors.to_s if com.invalid?
|
|
161
|
+
update_method.call
|
|
162
|
+
end)
|
|
163
|
+
comment = Comment.create(text: 'test123123')
|
|
164
|
+
|
|
165
|
+
initial_comments_count = Comment.count
|
|
166
|
+
_(raise_error { comment.update(text: 'te') }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
167
|
+
assert_equal(initial_comments_count, Comment.count)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'updates the record if record is valid' do
|
|
171
|
+
include_and_validate
|
|
172
|
+
Comment.before(:update, ->(model, args) do
|
|
173
|
+
if (instance = model.class.new(*args)) && instance.invalid?
|
|
174
|
+
raise Rubee::Validatable::Error, instance.errors.to_s
|
|
175
|
+
end
|
|
176
|
+
end)
|
|
177
|
+
comment = Comment.create(text: 'test123123')
|
|
178
|
+
|
|
179
|
+
comment.update(text: 'testerter')
|
|
180
|
+
assert_equal('testerter', comment.text)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe 'default errors as error' do
|
|
185
|
+
it 'assembles error hash' do
|
|
186
|
+
Comment.validate do
|
|
187
|
+
attribute(:text).required.type(String).condition(-> { text.length > 4 })
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
comment = Comment.new(text: 'test')
|
|
191
|
+
_(comment.valid?).must_equal(false)
|
|
192
|
+
_(comment.errors[:text]).must_equal({ message: "condition is not met" })
|
|
193
|
+
|
|
194
|
+
comment = Comment.new(text: 123)
|
|
195
|
+
_(comment.valid?).must_equal(false)
|
|
196
|
+
_(comment.errors[:text]).must_equal({ message: "attribute must be String" })
|
|
197
|
+
|
|
198
|
+
comment = Comment.new(user_id: User.last)
|
|
199
|
+
_(comment.valid?).must_equal(false)
|
|
200
|
+
_(comment.errors[:text]).must_equal({ message: "attribute 'text' is required" })
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
describe 'message instead hash as error' do
|
|
205
|
+
it 'assembles error hash' do
|
|
206
|
+
Comment.validate do
|
|
207
|
+
attribute(:text)
|
|
208
|
+
.required("Text is a mandatory field").type(String, "Text must be a string")
|
|
209
|
+
.condition(-> { text.length > 4 }, "Text length must be greater than 4")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
comment = Comment.new(text: 'test')
|
|
213
|
+
_(comment.valid?).must_equal(false)
|
|
214
|
+
_(comment.errors[:text]).must_equal({ message: "Text length must be greater than 4" })
|
|
215
|
+
|
|
216
|
+
comment = Comment.new(text: 123)
|
|
217
|
+
_(comment.valid?).must_equal(false)
|
|
218
|
+
_(comment.errors[:text]).must_equal({ message: "Text must be a string" })
|
|
219
|
+
|
|
220
|
+
comment = Comment.new(user_id: User.last)
|
|
221
|
+
_(comment.valid?).must_equal(false)
|
|
222
|
+
_(comment.errors[:text]).must_equal({ message: "Text is a mandatory field" })
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
47
226
|
end
|
data/lib/tests/test.db
CHANGED
|
Binary file
|
data/lib/tests/test_helper.rb
CHANGED
data/readme.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
# <img src="lib/images/rubee.svg" alt="
|
|
9
|
+
# <img src="lib/images/rubee.svg" alt="ru.Bee" height="40"> ... ru.Bee
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
ru.Bee is a Ruby-based web framework designed to streamline the development of modular monolith web applications. \
|
|
12
12
|
Under the hood, it leverages the power of Ruby and Rack backed by Puma, offering a clean, efficient, and flexible architecture. \
|
|
13
13
|
It offers a structured approach to building scalable, maintainable, and React-ready projects, \
|
|
14
14
|
making it an ideal choice for developers seeking a balance between monolithic simplicity and modular flexibility.
|
|
@@ -17,9 +17,13 @@ Want to get a quick API server up and runing? You can do it for real quick!
|
|
|
17
17
|
<br />
|
|
18
18
|
[](https://www.youtube.com/watch?v=ko7H70s7qq0)
|
|
19
19
|
|
|
20
|
+
Starting from ru.Bee 2.0.0, ru.Bee supports Websocket, which is a feature that allows you to build real-time applications with ease. \
|
|
21
|
+
<br />
|
|
22
|
+
[](https://www.youtube.com/watch?v=gp8IheKBNm4)
|
|
23
|
+
|
|
20
24
|
## Production ready
|
|
21
25
|
|
|
22
|
-
Take a look on the
|
|
26
|
+
Take a look on the ru.Bee demo site with all documentation stored in there: https://rubee.dedyn.io/
|
|
23
27
|
Want to explore how it built? https://github.com/nucleom42/rubee-site
|
|
24
28
|
|
|
25
29
|
## Stress tested
|
|
@@ -41,17 +45,17 @@ Transfer/sec: 140.07KB
|
|
|
41
45
|
- Average latency: ~305 ms
|
|
42
46
|
- Total requests handled: 9,721
|
|
43
47
|
- Hardware: Raspberry Pi 5(8 Gb) (single board computer)
|
|
44
|
-
- Server:
|
|
48
|
+
- Server: ru.Bee app hosted via Nginx + HTTPS
|
|
45
49
|
|
|
46
|
-
This demonstrate
|
|
50
|
+
This demonstrate ru.Bee’s efficient architecture and suitability for lightweight deployments — even on low-power hardware.
|
|
47
51
|
|
|
48
52
|
## Comparison
|
|
49
|
-
Here below is a **short web frameworks comparison** built with Ruby, so you can evaluate your choice with
|
|
53
|
+
Here below is a **short web frameworks comparison** built with Ruby, so you can evaluate your choice with ru.Bee.
|
|
50
54
|
|
|
51
55
|
**Disclaimer:**
|
|
52
56
|
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
57
|
|
|
54
|
-
| Feature / Framework | **
|
|
58
|
+
| Feature / Framework | **ru.Bee** | Rails | Sinatra | Hanami | Padrino | Grape |
|
|
55
59
|
|---------------------|-----------|-------|---------|--------|---------|-------|
|
|
56
60
|
| **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
61
|
| **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 |
|
|
@@ -73,6 +77,7 @@ The comparison is based on a very generic and subjective information open in the
|
|
|
73
77
|
- [Database](#database)
|
|
74
78
|
- [Views](#views)
|
|
75
79
|
- [Hooks](#hooks)
|
|
80
|
+
- [Validations](#validations)
|
|
76
81
|
- [JWT based authentification](#jwt-based-authentification)
|
|
77
82
|
- [OAuth2 based authentification](#oauth-authentification)
|
|
78
83
|
- [Rubee commands](#rubee-commands)
|
|
@@ -83,12 +88,10 @@ The comparison is based on a very generic and subjective information open in the
|
|
|
83
88
|
- [Background jobs](#background-jobs)
|
|
84
89
|
- [Modular](#modualar-application)
|
|
85
90
|
- [Logger](#logger)
|
|
91
|
+
- [Websocket](#websocket)
|
|
86
92
|
|
|
87
93
|
You can read it on the demo: [site](https://rubee.dedyn.io/)
|
|
88
94
|
|
|
89
|
-
🚧 The doc site is on update mode now. We are working on it.
|
|
90
|
-
Please refer to the documentation shown below.
|
|
91
|
-
|
|
92
95
|
## Features
|
|
93
96
|
|
|
94
97
|
Lightweight – A minimal footprint focused on serving Ruby applications efficiently.
|
|
@@ -106,9 +109,9 @@ Databases – Supports SQLite3, PostgreSQL, MySQL, and more via the Sequel gem.
|
|
|
106
109
|
<br>
|
|
107
110
|
Views – JSON, ERB, and plain HTML out of the box.
|
|
108
111
|
<br>
|
|
109
|
-
React Ready – React is supported as a first-class
|
|
112
|
+
React Ready – React is supported as a first-class ru.Bee view engine.
|
|
110
113
|
<br>
|
|
111
|
-
Bundlable – Charge your
|
|
114
|
+
Bundlable – Charge your ru.Bee app with any gem you need. Update effortlessly via Bundler.
|
|
112
115
|
<br>
|
|
113
116
|
ORM-agnostic – Models are native ORM objects, but you can use them as blueprints for any data source.
|
|
114
117
|
<br>
|
|
@@ -123,10 +126,14 @@ Asyncable – Plug in async adapters and use any popular background job engine.
|
|
|
123
126
|
Console – Start an interactive console and reload on the fly.
|
|
124
127
|
<br>
|
|
125
128
|
Background Jobs – Schedule and process background jobs using your preferred async stack.
|
|
129
|
+
<br>
|
|
130
|
+
Websocket – Serve and handle WebSocket connections.
|
|
131
|
+
<br>
|
|
132
|
+
Logger – Use any logger you want.
|
|
126
133
|
|
|
127
134
|
## Installation
|
|
128
135
|
|
|
129
|
-
1. Install
|
|
136
|
+
1. Install ru.Bee
|
|
130
137
|
```bash
|
|
131
138
|
gem install ru.Bee
|
|
132
139
|
```
|
|
@@ -216,7 +223,7 @@ This will generate the following files
|
|
|
216
223
|
[Back to content](#content)
|
|
217
224
|
|
|
218
225
|
## Model
|
|
219
|
-
Model in
|
|
226
|
+
Model in ru.Bee is just simple ruby object that can be serilalized in the view
|
|
220
227
|
in the way it required (ie json).
|
|
221
228
|
Here below is a simple example on how it can be used by rendering json from in memory object
|
|
222
229
|
|
|
@@ -355,7 +362,7 @@ irb(main):023> User.all
|
|
|
355
362
|
=> []
|
|
356
363
|
```
|
|
357
364
|
|
|
358
|
-
Use complex queries chains and when ready serialize it back to
|
|
365
|
+
Use complex queries chains and when ready serialize it back to ru.Bee object.
|
|
359
366
|
```Ruby
|
|
360
367
|
# user model
|
|
361
368
|
class User < Rubee::SequelObject
|
|
@@ -392,7 +399,7 @@ irb(main):009> .where(comment_id: Comment.where(text: "test").last.id)
|
|
|
392
399
|
irb(main):010> .then { |dataset| Comment.serialize(dataset) }
|
|
393
400
|
=> [#<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>]
|
|
394
401
|
```
|
|
395
|
-
This is recommended when you want to run one query and serialize it back to
|
|
402
|
+
This is recommended when you want to run one query and serialize it back to ru.Bee object only once.
|
|
396
403
|
So it may safe some resources.
|
|
397
404
|
|
|
398
405
|
[Back to content](#content)
|
|
@@ -408,7 +415,7 @@ If you feel comfortable you can play with retry configuration parameters:
|
|
|
408
415
|
config.db_busy_timeout = { env:, value: 1000 } # this is busy timeout in ms, before raising bussy error
|
|
409
416
|
```
|
|
410
417
|
|
|
411
|
-
For
|
|
418
|
+
For ru.Bee model class persist methods create and update retry will be added automatically. However, \
|
|
412
419
|
if you want to do it with Sequel dataset you need to do it yourself:
|
|
413
420
|
|
|
414
421
|
```ruby
|
|
@@ -417,7 +424,7 @@ if you want to do it with Sequel dataset you need to do it yourself:
|
|
|
417
424
|
[Back to content](#content)
|
|
418
425
|
|
|
419
426
|
## Routing
|
|
420
|
-
|
|
427
|
+
ru.Bee uses explicit routes. In the routes.rb yout can define routes for any of the main HTTP methods. \
|
|
421
428
|
You can also add any matched parameter denoted by a pair of `{ }` in the path of the route. \
|
|
422
429
|
Eg. `/path/to/{a_key}/somewhere`
|
|
423
430
|
|
|
@@ -443,7 +450,7 @@ route.{http_method} {path}, to: "{controller}#{action}",
|
|
|
443
450
|
```
|
|
444
451
|
|
|
445
452
|
### Defining Model attributes in routes
|
|
446
|
-
One of
|
|
453
|
+
One of ru.Bee's unique traits is where we can define our models for generation. \
|
|
447
454
|
You've seen above one possible way you can set up.
|
|
448
455
|
|
|
449
456
|
```ruby
|
|
@@ -572,7 +579,7 @@ Will generate:
|
|
|
572
579
|
|
|
573
580
|
### Modualar application
|
|
574
581
|
|
|
575
|
-
You can also use
|
|
582
|
+
You can also use ru.Bee to create modular applications.\
|
|
576
583
|
And attach as many subprojects you need.
|
|
577
584
|
Main philosophy of attach functinality is to keep the main project clean and easy to maintain. It will still\
|
|
578
585
|
share data with the main app. So where to define a border between the main app and subprojects is up to developer.
|
|
@@ -649,7 +656,7 @@ rubee start # or rubee start_dev for development
|
|
|
649
656
|
[Back to content](#content)
|
|
650
657
|
|
|
651
658
|
## Views
|
|
652
|
-
View in
|
|
659
|
+
View in ru.Bee is just a plain html/erb/react file that can be rendered from the controller.
|
|
653
660
|
|
|
654
661
|
## Templates over erb
|
|
655
662
|
|
|
@@ -794,7 +801,7 @@ function Users() {
|
|
|
794
801
|
|
|
795
802
|
## Object hooks
|
|
796
803
|
|
|
797
|
-
In
|
|
804
|
+
In ru.Bee by extending Hookable module any Ruby object can be charged with hooks (logic),
|
|
798
805
|
that can be executed before, after and around a specific method execution.
|
|
799
806
|
|
|
800
807
|
Here below a controller example. However it can be used in any Ruby object, like Model etc.
|
|
@@ -859,6 +866,106 @@ world!
|
|
|
859
866
|
|
|
860
867
|
[Back to content](#content)
|
|
861
868
|
|
|
869
|
+
## Validations
|
|
870
|
+
|
|
871
|
+
In ru.Bee any class can be charged with validations. This is done by including Validatable module.
|
|
872
|
+
Please note, ru.Bee model is validatable by default. No need to include it explicitly.
|
|
873
|
+
```ruby
|
|
874
|
+
class Foo
|
|
875
|
+
include Rubee::Validatable
|
|
876
|
+
|
|
877
|
+
attr_accessor :name, :age
|
|
878
|
+
|
|
879
|
+
def initialize(name, age)
|
|
880
|
+
@name = name
|
|
881
|
+
@age = age
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
validate do
|
|
885
|
+
attribute(:name).required.type(String).condition(->{ name.length > 2 })
|
|
886
|
+
|
|
887
|
+
attribute(:age)
|
|
888
|
+
.required('Age is a manadatory field')
|
|
889
|
+
.type(Integer, error_message: 'Must be an integerRRRRRRrrr!')
|
|
890
|
+
.condition(->{ age > 18 }, fancy_error: 'You must be at least 18 years old, dude!')
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
```
|
|
894
|
+
Then we can evaluate it in the ru.Bee console
|
|
895
|
+
```bash
|
|
896
|
+
=> #<Proc:0x000000010d389d80 (irb):32>
|
|
897
|
+
irb(main):041> Foo.new("Test", 20)
|
|
898
|
+
=> #<Foo:0x000000010d383fc0 @__validation_state=#<Rubee::Validatable::State:0x000000010d383de0 @errors={}, @valid=true>, @age=20, @name="Test">
|
|
899
|
+
irb(main):042> Foo.new("Test", 1)
|
|
900
|
+
=>
|
|
901
|
+
#<Foo:0x000000010ce61c40
|
|
902
|
+
@__validation_state=#<Rubee::Validatable::State:0x000000010ce61bc8 @errors={age: {fancy_error: "You must be at least 18 years old, dude!"}}, @valid=false>,
|
|
903
|
+
@age=1,
|
|
904
|
+
@name="Test">
|
|
905
|
+
irb(main):043> Foo.new("Test", nil)
|
|
906
|
+
=>
|
|
907
|
+
#<Foo:0x000000010c46f200
|
|
908
|
+
@__validation_state=#<Rubee::Validatable::State:0x000000010c46f070 @errors={age: {message: "Age is a manadatory field"}}, @valid=false>,
|
|
909
|
+
@age=nil,
|
|
910
|
+
@name="Test">
|
|
911
|
+
irb(main):044> Foo.new("Te", 20)
|
|
912
|
+
=>
|
|
913
|
+
#<Foo:0x000000010cfe9270
|
|
914
|
+
@__validation_state=#<Rubee::Validatable::State:0x000000010cfe91f8 @errors={name: {message: "condition is not met"}}, @valid=false>,
|
|
915
|
+
@age=20,
|
|
916
|
+
@name="Te">
|
|
917
|
+
irb(main):045> foo = Foo.new("Joe", "wrong")
|
|
918
|
+
=>
|
|
919
|
+
#<Foo:0x000000010d32eb38
|
|
920
|
+
...
|
|
921
|
+
irb(main):046> foo.valid?
|
|
922
|
+
=> false
|
|
923
|
+
irb(main):047> foo.errors
|
|
924
|
+
=> {age: {error_message: "Must be an integerRRRRRRrrr!"}}
|
|
925
|
+
```
|
|
926
|
+
Model example
|
|
927
|
+
```ruby
|
|
928
|
+
class User < Rubee::SequelObject
|
|
929
|
+
attr_accessor :id, :email, :password, :created, :updated
|
|
930
|
+
|
|
931
|
+
validate_before_persist! # This will validate and raise error in case invalid before saving to DB
|
|
932
|
+
validate do
|
|
933
|
+
attribute(:email).required
|
|
934
|
+
.condition(
|
|
935
|
+
->{ email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i) }, error: 'Wrong email format'
|
|
936
|
+
)
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
```
|
|
940
|
+
```bash
|
|
941
|
+
irb(main):074> user = User.new(email: "wrong", password: 123)
|
|
942
|
+
=>
|
|
943
|
+
#<User:0x000000010d2c3e78
|
|
944
|
+
...
|
|
945
|
+
irb(main):075> user.valid?
|
|
946
|
+
=> false
|
|
947
|
+
irb(main):076> user.errors
|
|
948
|
+
=> {email: {error: "Wrong email format"}}
|
|
949
|
+
irb(main):077> user.save
|
|
950
|
+
=>{email: {error: "Wrong email format"}} (Rubee::Validatable::Error) ..
|
|
951
|
+
irb(main):078> user.email = "ok@ok.com"
|
|
952
|
+
=> "ok@ok.com"
|
|
953
|
+
irb(main):079> user.valid?
|
|
954
|
+
=> true
|
|
955
|
+
irb(main):080> user.save
|
|
956
|
+
=> true
|
|
957
|
+
irb(main):081> user
|
|
958
|
+
=>
|
|
959
|
+
#<User:0x000000010d2c3e78
|
|
960
|
+
@__validation_state=#<Rubee::Validatable::State:0x000000010cb28628 @errors={}, @valid=true>,
|
|
961
|
+
@created=2025-11-30 17:18:52.254197 -0500,
|
|
962
|
+
@email="ok@ok.com",
|
|
963
|
+
@id=2260,
|
|
964
|
+
@password=123,
|
|
965
|
+
@updated=2025-11-30 17:18:52.254206 -0500>
|
|
966
|
+
```
|
|
967
|
+
[Back to content](#content)
|
|
968
|
+
|
|
862
969
|
## JWT based authentification
|
|
863
970
|
|
|
864
971
|
Charge you rpoject with token based authentification system and customize it for your needs.
|
|
@@ -1004,7 +1111,7 @@ end
|
|
|
1004
1111
|
|
|
1005
1112
|
[Back to content](#content)
|
|
1006
1113
|
|
|
1007
|
-
##
|
|
1114
|
+
## ru.Bee commands
|
|
1008
1115
|
```bash
|
|
1009
1116
|
rubee start # start the server
|
|
1010
1117
|
rubee start_dev # start the server in dev mode, which restart server on changes
|
|
@@ -1026,7 +1133,7 @@ rubee db run:create_apples # where create_apples is the name of the migration fi
|
|
|
1026
1133
|
rubee db structure # generate migration file for the database structure
|
|
1027
1134
|
```
|
|
1028
1135
|
|
|
1029
|
-
##
|
|
1136
|
+
## ru.Bee console
|
|
1030
1137
|
```bash
|
|
1031
1138
|
rubee console # start the console
|
|
1032
1139
|
# you can reload the console by typing reload, so it will pick up latest changes
|
|
@@ -1041,7 +1148,7 @@ rubee test auth_tokenable_test.rb # run specific tests
|
|
|
1041
1148
|
[Back to content](#content)
|
|
1042
1149
|
|
|
1043
1150
|
|
|
1044
|
-
If you want to run any
|
|
1151
|
+
If you want to run any ru.Bee command within a specific ENV make sure you added it before a command.
|
|
1045
1152
|
For instance if you want to run console in test environment you need to run the following command
|
|
1046
1153
|
|
|
1047
1154
|
```bash
|
|
@@ -1173,9 +1280,126 @@ When you trigger the controller action, the logs will look like this:
|
|
|
1173
1280
|
|
|
1174
1281
|
[Back to content](#content)
|
|
1175
1282
|
|
|
1283
|
+
## Websocket
|
|
1284
|
+
|
|
1285
|
+
With ru.Bee 2.0.0 you can use Websocket with ease!
|
|
1286
|
+
|
|
1287
|
+
Here are steps to get started:
|
|
1288
|
+
1. Make sure redis server is installed and running
|
|
1289
|
+
````bash
|
|
1290
|
+
sudo apt-get install -y redis # linux
|
|
1291
|
+
brew install redis # osx
|
|
1292
|
+
````
|
|
1293
|
+
2. Enable websocket and redis to your Gemfile
|
|
1294
|
+
```bash
|
|
1295
|
+
gem 'ru.Bee'
|
|
1296
|
+
gem 'redis'
|
|
1297
|
+
gem 'websocket'
|
|
1298
|
+
```
|
|
1299
|
+
3. Add the redis url to your configuration file, unless it connects to 127.0.0.1:6379
|
|
1300
|
+
```ruby
|
|
1301
|
+
# config/base_configuration.rb
|
|
1302
|
+
Rubee::Configuration.setup(env=:development) do |config|
|
|
1303
|
+
#...
|
|
1304
|
+
config.redis_url = { url: "redis://localhost:6378/0", env: }
|
|
1305
|
+
end
|
|
1306
|
+
```
|
|
1307
|
+
3. Add webscoket entry connection route
|
|
1308
|
+
```ruby
|
|
1309
|
+
# config/routes.rb
|
|
1310
|
+
Rubee::Router.draw do |router|
|
|
1311
|
+
#...
|
|
1312
|
+
router.get('/ws', to: 'users#websocket') # entry point to start websocket session
|
|
1313
|
+
# So, const ws = new WebSocket("ws://website/ws"); on the client side, should establish the connection
|
|
1314
|
+
end
|
|
1315
|
+
```
|
|
1316
|
+
4. Make model pubsubable
|
|
1317
|
+
```ruby
|
|
1318
|
+
# app/models/user.rb
|
|
1319
|
+
class User < Rubee::BaseModel
|
|
1320
|
+
include Rubee::PubSub::Publisher
|
|
1321
|
+
include Rubee::PubSub::Subscriber
|
|
1322
|
+
#...
|
|
1323
|
+
end
|
|
1324
|
+
```
|
|
1325
|
+
5. Enable websocket in your controller and implement required methods
|
|
1326
|
+
```ruby
|
|
1327
|
+
# app/controllers/users_controller.rb
|
|
1328
|
+
class UsersController < Rubee::BaseController
|
|
1329
|
+
attach_websocket! # this will handle websocket connections and direct them to the controller methods: publish, subscribe, unsubscribe
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
# Subscribe is expected to get next params from the client:
|
|
1333
|
+
# { action: 'subscribe', 'channel': 'default', 'id': '123', 'subscriber': 'User' }
|
|
1334
|
+
# where
|
|
1335
|
+
# - action corresponds to the method name
|
|
1336
|
+
# - channel is the name of the channel
|
|
1337
|
+
# - id is the id of the user
|
|
1338
|
+
# - subscriber is the name of the model
|
|
1339
|
+
def subscribe
|
|
1340
|
+
channel = params[:channel]
|
|
1341
|
+
sender_id = params[:options][:id] # id moved to options
|
|
1342
|
+
io = params[:options][:io] # io is a websocket connection
|
|
1343
|
+
|
|
1344
|
+
User.sub(channel, sender_id, io) do |channel, args| # subscribe the user for the channel updates
|
|
1345
|
+
websocket_connections.register(channel, args[:io]) # register the websocket connection
|
|
1346
|
+
end
|
|
1347
|
+
# return websocket response
|
|
1348
|
+
response_with(object: { type: 'system', channel: params[:channel], status: :subscribed }, type: :websocket)
|
|
1349
|
+
rescue StandardError => e
|
|
1350
|
+
response_with(object: { type: 'system', error: e.message }, type: :websocket)
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
# Unsubscribe is expected to get next params from the client:
|
|
1354
|
+
# { action: 'unsubscribe', 'channel': 'default', 'id': '123', 'subscriber': 'User' }
|
|
1355
|
+
# where
|
|
1356
|
+
# - action corresponds to the method name
|
|
1357
|
+
# - channel is the name of the channel
|
|
1358
|
+
# - id is the id of the user
|
|
1359
|
+
# - subscriber is the name of the model
|
|
1360
|
+
def unsubscribe
|
|
1361
|
+
channel = params[:channel]
|
|
1362
|
+
sender_id = params[:options][:id]
|
|
1363
|
+
io = params[:options][:io]
|
|
1364
|
+
|
|
1365
|
+
User.unsub(channel, sender_id, io) do |channel, args|
|
|
1366
|
+
websocket_connections.remove(channel, args[:io])
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
response_with(object: params.merge(type: 'system', status: :unsubscribed), type: :websocket)
|
|
1370
|
+
rescue StandardError => e
|
|
1371
|
+
response_with(object: { type: 'system', error: e.message }, type: :websocket)
|
|
1372
|
+
end
|
|
1373
|
+
# Publish is expected to get next params from the client:
|
|
1374
|
+
# { action: 'publish', 'channel': 'default', 'message': 'Hello world', 'id': '123', 'subscriber': 'User' }
|
|
1375
|
+
# where
|
|
1376
|
+
# - action corresponds to the method name
|
|
1377
|
+
# - channel is the name of the channel
|
|
1378
|
+
# - id is the id of the user
|
|
1379
|
+
# - subscriber is the name of the model
|
|
1380
|
+
def publish
|
|
1381
|
+
args = {}
|
|
1382
|
+
User.pub(params[:channel], message: params[:message]) do |channel|
|
|
1383
|
+
# Here we pack args with any additional data client might need
|
|
1384
|
+
user = User.find(params[:options][:id])
|
|
1385
|
+
args[:message] = params[:message]
|
|
1386
|
+
args[:sender] = params[:options][:id]
|
|
1387
|
+
args[:sender_name] = user.email
|
|
1388
|
+
websocket_connections.stream(channel, args)
|
|
1389
|
+
end
|
|
1390
|
+
|
|
1391
|
+
response_with(object: { type: 'system', message: params[:message], status: :published }, type: :websocket)
|
|
1392
|
+
rescue StandardError => e
|
|
1393
|
+
response_with(object: { type: 'system', error: e.message }, type: :websocket)
|
|
1394
|
+
end
|
|
1395
|
+
end
|
|
1396
|
+
```
|
|
1397
|
+
If you are interested to see chat app example, please check [chat](https://github.com/nucleom42/rubee-chat)
|
|
1398
|
+
|
|
1399
|
+
[Back to content](#content)
|
|
1176
1400
|
### Contributing
|
|
1177
1401
|
|
|
1178
|
-
If you are interested in contributing to
|
|
1402
|
+
If you are interested in contributing to ru.Bee,
|
|
1179
1403
|
please read the [Contributing]()https://github.com/nucleom42/rubee/blob/main/contribution.md) guide.
|
|
1180
1404
|
Also feel free to open an [issue](https://github.com/nucleom42/rubee/issues) if you apot one.
|
|
1181
1405
|
Have an idea or you wnat to discuss something?
|
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: 2.
|
|
4
|
+
version: 2.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Saltykov
|
|
@@ -256,6 +256,7 @@ files:
|
|
|
256
256
|
- lib/rubee/controllers/middlewares/auth_token_middleware.rb
|
|
257
257
|
- lib/rubee/extensions/hookable.rb
|
|
258
258
|
- lib/rubee/extensions/serializable.rb
|
|
259
|
+
- lib/rubee/extensions/validatable.rb
|
|
259
260
|
- lib/rubee/features.rb
|
|
260
261
|
- lib/rubee/generator.rb
|
|
261
262
|
- lib/rubee/logger.rb
|