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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd4247baa0c82fd81b4be82e8e63446b8b0d7934cee6243dda47ea3b350d4213
4
- data.tar.gz: 1ba2c534bcebc7e898408fcf9d4e594c9535ca6fc218a5de8d51895edadc9d7f
3
+ metadata.gz: cc6a4fbeb28a460ca66f75ccf1f5c97fe1572f422392f9ee1b72d082e05cb287
4
+ data.tar.gz: f4dbeaf7338af243f6e225a2a798a87de75b001fa4196bed94345d78887ba8ea
5
5
  SHA512:
6
- metadata.gz: cff63aa019e6f5895908a7b1fd7122cefd3986c9a4c1aede556aee667cb750ca82ec95727babda0bd4a234a54b3b159fac5668e9a07de7f7974913015b7dcca5
7
- data.tar.gz: b4ae2fb9d36dc6c06d87da67d479029fb7c4399f18b26dd57e10ad176a4135a81e2c7840f92393441287e29d470859ef9ec15043dad06b414cf13b680980e35c
6
+ metadata.gz: 996510c80521425797e17a1b641d767d6319877e127883692d519457c7573835ae2670a8ed17950f302b1934a0aae1270d8b065acfee079341935ddb8964a26f
7
+ data.tar.gz: 75a8ab899d3c98fd492a30ec5501982526318387c9214b78ebdf57ec2c39a91778268283db82cbeb5ae9072aa4c62507b1d175423e7f86beec892cddfea0bb90
@@ -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.call : send(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.call : send(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.call do
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.call
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.call
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
- hash[var.to_s.delete('@')] = instance_variable_get(var)
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
@@ -8,6 +8,7 @@ module Rubee
8
8
 
9
9
  base.include(Rubee::Hookable)
10
10
  base.include(Rubee::Serializable)
11
+ base.include(Rubee::Validatable)
11
12
  end
12
13
 
13
14
  module ClassMethods
@@ -33,12 +33,11 @@ module Rubee
33
33
 
34
34
  else
35
35
  begin
36
- created_object = self.class.create(args)
36
+ created_id = self.class.dataset.insert(args)
37
37
  rescue StandardError => _e
38
38
  return false
39
39
  end
40
- self.id = created_object.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
- out_id = Rubee::DBTools.with_retry { dataset.insert(**attrs) }
213
- new(**attrs.merge(id: out_id))
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.0.0'
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: 'test')
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: 'test').last.users.count).must_equal(1)
21
- _(Comment.where(text: 'test').last.users.first.email).must_equal('ok-test@test.com')
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: 'test').last.id)
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('test')
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
@@ -39,3 +39,9 @@ ensure
39
39
  $stdout = old_stdout
40
40
  end
41
41
 
42
+ def raise_error
43
+ yield
44
+ rescue => e
45
+ e
46
+ end
47
+
data/readme.md CHANGED
@@ -6,9 +6,9 @@
6
6
  ![GitHub Repo stars](https://img.shields.io/github/stars/nucleom42/rubee?style=social)
7
7
 
8
8
 
9
- # <img src="lib/images/rubee.svg" alt="RUBEE" height="40"> ... RUBEE
9
+ # <img src="lib/images/rubee.svg" alt="ru.Bee" height="40"> ... ru.Bee
10
10
 
11
- RUBEE is a Ruby-based web framework designed to streamline the development of modular monolith web applications. \
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
  [![Watch the demo](https://img.youtube.com/vi/ko7H70s7qq0/hqdefault.jpg)](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
+ [![Watch the demo](https://img.youtube.com/vi/gp8IheKBNm4/hqdefault.jpg)](https://www.youtube.com/watch?v=gp8IheKBNm4)
23
+
20
24
  ## Production ready
21
25
 
22
- Take a look on the RUBEE demo site with all documentation stored in there: https://rubee.dedyn.io/
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: RUBEE app hosted via Nginx + HTTPS
48
+ - Server: ru.Bee app hosted via Nginx + HTTPS
45
49
 
46
- This demonstrate RUBEE’s efficient architecture and suitability for lightweight deployments — even on low-power hardware.
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 RUBEE.
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 | **RUBEE** | Rails | Sinatra | Hanami | Padrino | Grape |
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 RUBEE view engine.
112
+ React Ready – React is supported as a first-class ru.Bee view engine.
110
113
  <br>
111
- Bundlable – Charge your RUBEE app with any gem you need. Update effortlessly via Bundler.
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 RUBEE
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 RUBEE is just simple ruby object that can be serilalized in the view
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 RUBEE object.
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 RUBEE object only once.
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 RUBEE model class persist methods create and update retry will be added automatically. However, \
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
- RUBEE uses explicit routes. In the routes.rb yout can define routes for any of the main HTTP methods. \
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 RUBEE's unique traits is where we can define our models for generation. \
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 RUBEE to create modular applications.\
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 RUBEE is just a plain html/erb/react file that can be rendered from the controller.
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 RUBEE by extending Hookable module any Ruby object can be charged with hooks (logic),
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
- ## RUBEE commands
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
- ## RUBEE console
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 RUBEE command within a specific ENV make sure you added it before a command.
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 RUBEE,
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.0.0
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