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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
module PubSub
|
|
3
|
+
module Publisher
|
|
4
|
+
Error = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def pub(channel, args = {}, &block)
|
|
12
|
+
Rubee::Configuration.pubsub_container.pub(channel, args, &block)
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "connection_pool"
|
|
2
|
+
require "redis"
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Rubee
|
|
7
|
+
module PubSub
|
|
8
|
+
class Redis < Container
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
DEFAULT_POOL_SIZE = 5
|
|
12
|
+
DEFAULT_TIMEOUT = 5
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
redis_url = Rubee::Configuration.get_redis_url
|
|
16
|
+
@pool = ConnectionPool.new(size: DEFAULT_POOL_SIZE, timeout: DEFAULT_TIMEOUT) do
|
|
17
|
+
if redis_url&.empty?
|
|
18
|
+
::Redis.new
|
|
19
|
+
else
|
|
20
|
+
::Redis.new(url: redis_url)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Example: pub("ok", message: "hello")
|
|
27
|
+
def pub(channel, args = {}, &block)
|
|
28
|
+
keys = with_redis { |r| r.scan_each(match: "#{channel}:*").to_a }
|
|
29
|
+
return false if keys.empty?
|
|
30
|
+
|
|
31
|
+
values = with_redis { |r| r.mget(*keys) }
|
|
32
|
+
|
|
33
|
+
iterable = values.each_with_index.each_with_object({}) do |(val, i), hash|
|
|
34
|
+
key = keys[i]
|
|
35
|
+
hash[key] = val
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
clazzes = retrieve_klasses(iterable)
|
|
39
|
+
fan_out(clazzes, args, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Example: sub("ok", "User", ["123"])
|
|
43
|
+
def sub(channel, klass_name, *args, &block)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
id = args.first
|
|
46
|
+
id_string = id ? ":#{id}" : ""
|
|
47
|
+
key = "#{channel}:#{klass_name}#{id_string}"
|
|
48
|
+
existing = with_redis { |r| r.get(key) }
|
|
49
|
+
io = args.last.respond_to?(:call) ? args.pop : nil
|
|
50
|
+
|
|
51
|
+
with_redis { |r| r.set(key, args.join(",")) } unless existing
|
|
52
|
+
block&.call(key, io: io)
|
|
53
|
+
end
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def unsub(channel, klass_name, *args, &block)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
id = args.first
|
|
60
|
+
id_string = id ? ":#{id}" : ""
|
|
61
|
+
key = "#{channel}:#{klass_name}#{id_string}"
|
|
62
|
+
value = with_redis { |r| r.get(key) }
|
|
63
|
+
return false unless value
|
|
64
|
+
|
|
65
|
+
io = args.pop if args.last.respond_to?(:call)
|
|
66
|
+
with_redis { |r| r.del(key) }
|
|
67
|
+
block&.call(key, io: io)
|
|
68
|
+
end
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def with_redis(&block)
|
|
75
|
+
@pool.with(&block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def retrieve_klasses(iterable)
|
|
79
|
+
iterable.each_with_object({}) do |(key, args), hash|
|
|
80
|
+
channel, clazz, id = key.split(":")
|
|
81
|
+
arg_list = args.to_s.split(",")
|
|
82
|
+
hash[key] = { channel:, clazz:, args: arg_list, id: }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def fan_out(clazzes, method_args = {}, &block)
|
|
87
|
+
clazzes.each do |_key, opts|
|
|
88
|
+
clazz = turn_to_class(opts[:clazz])
|
|
89
|
+
clazz_args = opts[:args]
|
|
90
|
+
|
|
91
|
+
clazz.on_pub(opts[:channel], *clazz_args, **method_args) if clazz.respond_to?(:on_pub)
|
|
92
|
+
id_string = opts[:id] ? ":#{opts[:id]}" : ""
|
|
93
|
+
block&.call("#{opts[:channel]}:#{opts[:clazz]}#{id_string}", **method_args)
|
|
94
|
+
end
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
module PubSub
|
|
3
|
+
module Subscriber
|
|
4
|
+
Error = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def sub(channel, *args, &block)
|
|
12
|
+
Rubee::Configuration.pubsub_container.sub(channel, name, *args, &block)
|
|
13
|
+
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def unsub(channel, *args, &block)
|
|
18
|
+
Rubee::Configuration.pubsub_container.unsub(channel, name, *args, &block)
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# class Subscriber
|
|
2
|
+
# include Rubee::PubSub::Subscriber
|
|
3
|
+
|
|
4
|
+
# def self.on_pub(channel, message, options = {})
|
|
5
|
+
# puts "channel=#{channel} message=#{message} options=#{options}"
|
|
6
|
+
# end
|
|
7
|
+
# end
|
|
8
|
+
|
|
9
|
+
# class SubscriberOne
|
|
10
|
+
# include Rubee::PubSub::Subscriber
|
|
11
|
+
|
|
12
|
+
# def self.on_pub(channel, message, options = {})
|
|
13
|
+
# puts "channel=#{channel} message=#{message} options=#{options}"
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
|
|
17
|
+
# class Publisher
|
|
18
|
+
# include Rubee::PubSub::Publisher
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
# Subscriber.sub("ok", ["123456"])
|
|
22
|
+
|
|
23
|
+
# SubscriberOne.sub("ok", ["123"])
|
|
24
|
+
|
|
25
|
+
# Publisher.pub("ok", { message: "hello" })
|
|
26
|
+
|
|
27
|
+
# SubscriberOne.unsub("ok", ["123"])
|
|
28
|
+
|
|
29
|
+
# Publisher.pub("ok", { message: "hello" })
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require 'websocket'
|
|
2
|
+
require 'websocket/frame'
|
|
3
|
+
require 'websocket/handshake'
|
|
4
|
+
require 'websocket/handshake/server'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'redis'
|
|
7
|
+
|
|
8
|
+
module Rubee
|
|
9
|
+
class WebSocket
|
|
10
|
+
using ChargedHash
|
|
11
|
+
class << self
|
|
12
|
+
def call(env, &controller_block)
|
|
13
|
+
unless env['rack.hijack']
|
|
14
|
+
return [500, { 'Content-Type' => 'text/plain' }, ['Hijack not supported']]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
env['rack.hijack'].call
|
|
18
|
+
io = env['rack.hijack_io']
|
|
19
|
+
|
|
20
|
+
handshake = ::WebSocket::Handshake::Server.new
|
|
21
|
+
handshake.from_rack(env)
|
|
22
|
+
unless handshake.valid?
|
|
23
|
+
io.write("HTTP/1.1 400 Bad Request\r\n\r\n")
|
|
24
|
+
io.close
|
|
25
|
+
return [-1, {}, []]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
io.write(handshake.to_s)
|
|
29
|
+
incoming = ::WebSocket::Frame::Incoming::Server.new(version: handshake.version)
|
|
30
|
+
|
|
31
|
+
outgoing = ->(data) do
|
|
32
|
+
frame = ::WebSocket::Frame::Outgoing::Server.new(
|
|
33
|
+
version: handshake.version,
|
|
34
|
+
type: :text,
|
|
35
|
+
data: data.to_json
|
|
36
|
+
)
|
|
37
|
+
io.write(frame.to_s)
|
|
38
|
+
rescue IOError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# --- Listen to incoming data ---
|
|
43
|
+
Thread.new do
|
|
44
|
+
loop do
|
|
45
|
+
data = io.readpartial(1024)
|
|
46
|
+
incoming << data
|
|
47
|
+
|
|
48
|
+
while (frame = incoming.next)
|
|
49
|
+
case frame.type
|
|
50
|
+
when :text
|
|
51
|
+
out = controller_out(frame, outgoing, &controller_block)
|
|
52
|
+
outgoing.call(**out)
|
|
53
|
+
|
|
54
|
+
when :close
|
|
55
|
+
# Client closed connection
|
|
56
|
+
handle_close(frame, io, handshake)
|
|
57
|
+
break
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
rescue EOFError, IOError
|
|
62
|
+
begin
|
|
63
|
+
handle_close(frame, io, handshake)
|
|
64
|
+
rescue
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
[101, handshake.headers, []]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def payload(frame)
|
|
73
|
+
JSON.parse(frame.data)
|
|
74
|
+
rescue
|
|
75
|
+
{}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_close(frame, io, handshake)
|
|
79
|
+
payload_hash = payload(frame)
|
|
80
|
+
channel = payload_hash["channel"]
|
|
81
|
+
subcriber = payload_hash["subcriber"]
|
|
82
|
+
::Rubee::WebSocketConnections.instance.remove("#{channel}:#{subcriber}", io)
|
|
83
|
+
io.write(::WebSocket::Frame::Outgoing::Server.new(
|
|
84
|
+
version: handshake.version,
|
|
85
|
+
type: :close
|
|
86
|
+
).to_s)
|
|
87
|
+
io.close
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def controller_out(frame, io, &block)
|
|
91
|
+
payload_hash = payload(frame)
|
|
92
|
+
action = payload_hash["action"]
|
|
93
|
+
channel = payload_hash["channel"]
|
|
94
|
+
message = payload_hash["message"]
|
|
95
|
+
options = payload_hash.select { |k, _| !["action", "channel", "message"].include?(k) }
|
|
96
|
+
options.merge!(io:)
|
|
97
|
+
|
|
98
|
+
block.call(channel:, message:, action:, options:)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Rubee
|
|
2
|
+
class WebSocketConnections
|
|
3
|
+
include Singleton
|
|
4
|
+
def initialize
|
|
5
|
+
@subscribers ||= Hash.new { |h, k| h[k] = [] }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def register(channel, io)
|
|
9
|
+
@subscribers[channel] << io unless @subscribers[channel].include?(io)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove(channel, io)
|
|
13
|
+
@subscribers[channel].delete(io)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def remove_all(io)
|
|
17
|
+
@subscribers.each_value { _1.delete(io) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def flush_all
|
|
21
|
+
@subscribers.each_value(&:clear)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stream(channel, args = {})
|
|
25
|
+
ios = @subscribers[channel]
|
|
26
|
+
if !ios&.empty? && ios.all? { _1.respond_to?(:call) }
|
|
27
|
+
ios.each { _1.call(args) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clear
|
|
32
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/rubee.rb
CHANGED
|
@@ -16,11 +16,13 @@ module Rubee
|
|
|
16
16
|
JS_DIR = File.join(APP_ROOT, LIB, 'js') unless defined?(JS_DIR)
|
|
17
17
|
CSS_DIR = File.join(APP_ROOT, LIB, 'css') unless defined?(CSS_DIR)
|
|
18
18
|
ROOT_PATH = File.expand_path(File.join(__dir__, '..')) unless defined?(ROOT_PATH)
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
VERSION = '2.1.0'
|
|
20
21
|
|
|
21
22
|
require_relative 'rubee/router'
|
|
22
23
|
require_relative 'rubee/logger'
|
|
23
24
|
require_relative 'rubee/generator'
|
|
25
|
+
require_relative 'rubee/features'
|
|
24
26
|
require_relative 'rubee/autoload'
|
|
25
27
|
require_relative 'rubee/configuration'
|
|
26
28
|
|
|
@@ -31,15 +33,10 @@ module Rubee
|
|
|
31
33
|
def call(env)
|
|
32
34
|
# autoload rb files
|
|
33
35
|
Autoload.call
|
|
34
|
-
|
|
35
|
-
# register images paths
|
|
36
|
+
# init rack request
|
|
36
37
|
request = Rack::Request.new(env)
|
|
37
38
|
# Add default path for assets
|
|
38
|
-
|
|
39
|
-
route.get('/images/{path}', to: 'base#image', namespace: 'Rubee')
|
|
40
|
-
route.get('/js/{path}', to: 'base#js', namespace: 'Rubee')
|
|
41
|
-
route.get('/css/{path}', to: 'base#css', namespace: 'Rubee')
|
|
42
|
-
end
|
|
39
|
+
register_assets_routes
|
|
43
40
|
# define route
|
|
44
41
|
route = Router.route_for(request)
|
|
45
42
|
# if react is the view so we would like to delegate not cauth by rubee routes to it.
|
|
@@ -62,5 +59,15 @@ module Rubee
|
|
|
62
59
|
# fire the action
|
|
63
60
|
controller.send(action)
|
|
64
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def register_assets_routes
|
|
66
|
+
Router.draw do |route|
|
|
67
|
+
route.get('/images/{path}', to: 'base#image', namespace: 'Rubee')
|
|
68
|
+
route.get('/js/{path}', to: 'base#js', namespace: 'Rubee')
|
|
69
|
+
route.get('/css/{path}', to: 'base#css', namespace: 'Rubee')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
65
72
|
end
|
|
66
73
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative '../test_helper'
|
|
2
|
+
|
|
3
|
+
class UsersControllerTest < Minitest::Test
|
|
4
|
+
include Rack::Test::Methods
|
|
5
|
+
|
|
6
|
+
def app
|
|
7
|
+
Rubee::Application.instance
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_websocket_handshake_written_to_io
|
|
11
|
+
env = Rack::MockRequest.env_for(
|
|
12
|
+
'/ws',
|
|
13
|
+
{
|
|
14
|
+
'REQUEST_METHOD' => 'GET',
|
|
15
|
+
'PATH_INFO' => '/ws',
|
|
16
|
+
'HTTP_CONNECTION' => 'keep-alive, Upgrade',
|
|
17
|
+
'HTTP_UPGRADE' => 'websocket',
|
|
18
|
+
'HTTP_HOST' => 'localhost:9292',
|
|
19
|
+
'HTTP_ORIGIN' => 'http://localhost:9292',
|
|
20
|
+
'HTTP_SEC_WEBSOCKET_KEY' => 'dGhlIHNhbXBsZSBub25jZQ==',
|
|
21
|
+
'HTTP_SEC_WEBSOCKET_VERSION' => '13',
|
|
22
|
+
'rack.url_scheme' => 'http'
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Mock hijack interface expected by Rubee::WebSocket
|
|
27
|
+
io = StringIO.new
|
|
28
|
+
env['rack.hijack'] = proc {}
|
|
29
|
+
env['rack.hijack_io'] = io
|
|
30
|
+
|
|
31
|
+
# Call the WebSocket handler
|
|
32
|
+
Rubee::WebSocket.call(env)
|
|
33
|
+
|
|
34
|
+
# Expect the handshake response written to IO
|
|
35
|
+
io.rewind
|
|
36
|
+
handshake_response = io.read
|
|
37
|
+
assert_includes(handshake_response, "HTTP/1.1 101 Switching Protocols")
|
|
38
|
+
assert_includes(handshake_response, "Upgrade: websocket")
|
|
39
|
+
assert_includes(handshake_response, "Connection: Upgrade")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -15,5 +15,22 @@ 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 |account|
|
|
22
|
+
account
|
|
23
|
+
.required(:addres, required: "address is required")
|
|
24
|
+
.type(String, type: "address must be string")
|
|
25
|
+
end
|
|
26
|
+
Account.validate_before_persist!
|
|
27
|
+
user = User.new(email: 'ok-test@test.com', password: '123')
|
|
28
|
+
_(raise_error { Account.create(addres: 1, user_id: user.id) }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
29
|
+
account = Account.create(addres: "13Th street", user_id: user.id)
|
|
30
|
+
_(account.persisted?).must_equal(true)
|
|
31
|
+
_(raise_error { account.update(addres: 1) }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
32
|
+
_(raise_error { Account.new(addres: 1, user_id: user.id).save }.is_a?(Rubee::Validatable::Error)).must_equal(true)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
18
35
|
end
|
|
19
36
|
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,141 @@ 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
|
+
# Comment.include(Rubee::Validatable)
|
|
51
|
+
required_or_optional = required ? :required : :optional
|
|
52
|
+
required_or_optional_args = required ? [:text, required: "text filed is required"] : [:text]
|
|
53
|
+
Comment.validate do |comment|
|
|
54
|
+
comment.send(
|
|
55
|
+
required_or_optional, *required_or_optional_args
|
|
56
|
+
)
|
|
57
|
+
.type(String, type: "text field must be string")
|
|
58
|
+
.condition(proc { comment.text.length > 4 }, { length: "text length must be greater than 4" })
|
|
59
|
+
end
|
|
60
|
+
end
|
|
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
|
+
end
|
|
47
184
|
end
|