redcord 0.0.1.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/redcord.rb +11 -0
- data/lib/redcord.rbi +94 -0
- data/lib/redcord/actions.rb +135 -0
- data/lib/redcord/attribute.rb +75 -0
- data/lib/redcord/base.rb +59 -0
- data/lib/redcord/configurations.rb +72 -0
- data/lib/redcord/logger.rb +69 -0
- data/lib/redcord/lua_script_reader.rb +16 -0
- data/lib/redcord/migration.rb +27 -0
- data/lib/redcord/migration/migrator.rb +74 -0
- data/lib/redcord/migration/ttl.rb +11 -0
- data/lib/redcord/migration/version.rb +40 -0
- data/lib/redcord/prepared_redis.rb +18 -0
- data/lib/redcord/railtie.rb +16 -0
- data/lib/redcord/range_interval.rb +9 -0
- data/lib/redcord/redis_connection.rb +103 -0
- data/lib/redcord/relation.rb +95 -0
- data/lib/redcord/serializer.rb +129 -0
- data/lib/redcord/server_scripts.rb +78 -0
- data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +68 -0
- data/lib/redcord/server_scripts/delete_hash.erb.lua +48 -0
- data/lib/redcord/server_scripts/find_by_attr.erb.lua +67 -0
- data/lib/redcord/server_scripts/find_by_attr_count.erb.lua +52 -0
- data/lib/redcord/server_scripts/shared/index_helper_methods.erb.lua +61 -0
- data/lib/redcord/server_scripts/shared/lua_helper_methods.erb.lua +33 -0
- data/lib/redcord/server_scripts/shared/query_helper_methods.erb.lua +105 -0
- data/lib/redcord/server_scripts/update_hash.erb.lua +91 -0
- data/lib/redcord/tasks/redis.rake +34 -0
- metadata +210 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: abc776db5aa9171e1f8a478013a55179158d7c9dc97e65953c27c162ad4069c7
|
4
|
+
data.tar.gz: 55d37148a8069725c09637f29392a6461993d93f6999bc76247e646f96a94528
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1cfe27b4241554f39ce0f8f0dbce4492f308e3a1db5607f4dbb7153832f976b692987926a6cc9c7c051121f55819f9e9b9b10c02b82250986d3aa6fdf4964e8e
|
7
|
+
data.tar.gz: fa4865ee7e9ad1b70b4c7037d196bb60ed2aed8ab774356ec07b06a9cefd1f964d05f4da1cdd7cd2e09a5366f7bfb8da06ab84e50e282cfc74cccbb16a185621
|
data/lib/redcord.rb
ADDED
data/lib/redcord.rbi
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# typed: strong
|
2
|
+
module ModuleClassMethodsAsInstanceMethods
|
3
|
+
# Sorbet does not understand the ClassMethods modules are actually defining
|
4
|
+
# class methods. Hence this module redefines some top level class methods as
|
5
|
+
# instance methods. Sigs are copied from
|
6
|
+
# https://github.com/sorbet/sorbet/blob/f9380ec833047a834bbaca1eb3502ae96a0e4394/rbi/core/module.rbi
|
7
|
+
include Kernel
|
8
|
+
|
9
|
+
sig do
|
10
|
+
params(
|
11
|
+
arg0: T.any(Symbol, String),
|
12
|
+
)
|
13
|
+
.returns(T.untyped)
|
14
|
+
end
|
15
|
+
def class_variable_get(arg0); end
|
16
|
+
|
17
|
+
sig do
|
18
|
+
params(
|
19
|
+
arg0: T.any(Symbol, String),
|
20
|
+
arg1: BasicObject,
|
21
|
+
)
|
22
|
+
.returns(T.untyped)
|
23
|
+
end
|
24
|
+
def class_variable_set(arg0, arg1); end
|
25
|
+
|
26
|
+
sig {returns(String)}
|
27
|
+
def name(); end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Redcord::RedisConnection::ClassMethods
|
31
|
+
include ModuleClassMethodsAsInstanceMethods
|
32
|
+
end
|
33
|
+
|
34
|
+
module Redcord::RedisConnection::InstanceMethods
|
35
|
+
include Kernel
|
36
|
+
end
|
37
|
+
|
38
|
+
module Redcord::Attribute::ClassMethods
|
39
|
+
include Redcord::Serializer::ClassMethods
|
40
|
+
# from inherenting T::Struct
|
41
|
+
def prop(name, type, options={}); end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Redcord::TTL::ClassMethods
|
45
|
+
include Redcord::Serializer::ClassMethods
|
46
|
+
end
|
47
|
+
|
48
|
+
module Redcord::ServerScripts
|
49
|
+
include Kernel
|
50
|
+
|
51
|
+
sig do
|
52
|
+
params(
|
53
|
+
sha: String,
|
54
|
+
keys: T::Array[T.untyped],
|
55
|
+
argv: T::Array[T.untyped],
|
56
|
+
).returns(T.untyped)
|
57
|
+
end
|
58
|
+
def evalsha(sha, keys: [], argv:[]); end
|
59
|
+
|
60
|
+
sig { returns(T::Hash[Symbol, String]) }
|
61
|
+
def redcord_server_script_shas; end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Redcord::Actions::ClassMethods
|
65
|
+
include Kernel
|
66
|
+
include Redcord::RedisConnection::ClassMethods
|
67
|
+
include Redcord::Serializer::ClassMethods
|
68
|
+
end
|
69
|
+
|
70
|
+
module Redcord::Actions::InstanceMethods
|
71
|
+
include Kernel
|
72
|
+
include Redcord::RedisConnection::InstanceMethods
|
73
|
+
|
74
|
+
sig {returns(String)}
|
75
|
+
def to_json; end
|
76
|
+
|
77
|
+
sig {returns(T::Hash[String, T.untyped])}
|
78
|
+
def serialize; end
|
79
|
+
end
|
80
|
+
|
81
|
+
module Redcord::Base
|
82
|
+
include Redcord::Actions::InstanceMethods
|
83
|
+
extend Redcord::Serializer::ClassMethods
|
84
|
+
|
85
|
+
mixes_in_class_methods(Redcord::TTL::ClassMethods)
|
86
|
+
end
|
87
|
+
|
88
|
+
module Redcord::Serializer::ClassMethods
|
89
|
+
include ModuleClassMethodsAsInstanceMethods
|
90
|
+
|
91
|
+
# from inherenting T::Struct
|
92
|
+
def from_hash(args); end
|
93
|
+
def props; end
|
94
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'sorbet-coerce'
|
3
|
+
|
4
|
+
require 'redcord/relation'
|
5
|
+
|
6
|
+
module Redcord
|
7
|
+
# Raised by Model.find
|
8
|
+
class RecordNotFound < StandardError; end
|
9
|
+
# Raised by Model.where
|
10
|
+
class AttributeNotIndexed < StandardError; end
|
11
|
+
class WrongAttributeType < TypeError; end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Redcord::Actions
|
15
|
+
extend T::Sig
|
16
|
+
extend T::Helpers
|
17
|
+
|
18
|
+
sig { params(klass: T.class_of(T::Struct)).void }
|
19
|
+
def self.included(klass)
|
20
|
+
klass.extend(ClassMethods)
|
21
|
+
klass.include(InstanceMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
extend T::Sig
|
26
|
+
|
27
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
28
|
+
def create!(args)
|
29
|
+
args[:created_at] = args[:updated_at] = Time.zone.now
|
30
|
+
instance = TypeCoerce[self].new.from(args)
|
31
|
+
id = redis.create_hash_returning_id(model_key, to_redis_hash(args))
|
32
|
+
instance.send(:id=, id)
|
33
|
+
instance
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { params(id: T.untyped).returns(T.untyped) }
|
37
|
+
def find(id)
|
38
|
+
instance_key = "#{model_key}:id:#{id}"
|
39
|
+
args = redis.hgetall(instance_key)
|
40
|
+
if args.empty?
|
41
|
+
raise Redcord::RecordNotFound.new(
|
42
|
+
"Couldn't find #{name} with 'id'=#{id}"
|
43
|
+
)
|
44
|
+
end
|
45
|
+
coerce_and_set_id(args, id)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
|
49
|
+
def where(args)
|
50
|
+
Redcord::Relation.new(T.let(self, T.untyped)).where(args)
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { params(id: T.untyped).returns(T::Boolean) }
|
54
|
+
def destroy(id)
|
55
|
+
return redis.delete_hash(model_key, id) == 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module InstanceMethods
|
60
|
+
extend T::Sig
|
61
|
+
extend T::Helpers
|
62
|
+
|
63
|
+
abstract!
|
64
|
+
|
65
|
+
sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
|
66
|
+
def created_at; end
|
67
|
+
|
68
|
+
sig { abstract.params(time: ActiveSupport::TimeWithZone).returns(T.nilable(ActiveSupport::TimeWithZone)) }
|
69
|
+
def created_at=(time); end
|
70
|
+
|
71
|
+
sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
|
72
|
+
def updated_at; end
|
73
|
+
|
74
|
+
sig { abstract.params(time: ActiveSupport::TimeWithZone).returns(T.nilable(ActiveSupport::TimeWithZone)) }
|
75
|
+
def updated_at=(time); end
|
76
|
+
|
77
|
+
sig { void }
|
78
|
+
def save!
|
79
|
+
self.updated_at = Time.zone.now
|
80
|
+
_id = id
|
81
|
+
if _id.nil?
|
82
|
+
self.created_at = T.must(self.updated_at)
|
83
|
+
_id = redis.create_hash_returning_id(self.class.model_key, self.class.to_redis_hash(serialize))
|
84
|
+
send(:id=, _id)
|
85
|
+
else
|
86
|
+
redis.update_hash(self.class.model_key, _id, self.class.to_redis_hash(serialize))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).void }
|
91
|
+
def update!(args={})
|
92
|
+
_id = id
|
93
|
+
if _id.nil?
|
94
|
+
_set_args!(args)
|
95
|
+
save!
|
96
|
+
else
|
97
|
+
args[:updated_at] = Time.zone.now
|
98
|
+
_set_args!(args)
|
99
|
+
redis.update_hash(self.class.model_key, _id, self.class.to_redis_hash(args))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
sig { returns(T::Boolean) }
|
104
|
+
def destroy
|
105
|
+
return false if id.nil?
|
106
|
+
self.class.destroy(T.must(id))
|
107
|
+
end
|
108
|
+
|
109
|
+
sig { returns(String) }
|
110
|
+
def instance_key
|
111
|
+
"#{self.class.model_key}:id:#{T.must(id)}"
|
112
|
+
end
|
113
|
+
|
114
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).void }
|
115
|
+
def _set_args!(args)
|
116
|
+
args.each do |key, value|
|
117
|
+
send(:"#{key}=", value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { returns(T.nilable(Integer)) }
|
122
|
+
def id
|
123
|
+
instance_variable_get(:@_id)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
sig { params(id: Integer).returns(Integer) }
|
129
|
+
def id=(id)
|
130
|
+
instance_variable_set(:@_id, id)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
mixes_in_class_methods(ClassMethods)
|
135
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# typed: strict
|
2
|
+
module Redcord::Attribute
|
3
|
+
extend T::Sig
|
4
|
+
extend T::Helpers
|
5
|
+
|
6
|
+
# We implicitly determine what should be a range index on Redis based on Ruby type.
|
7
|
+
RangeIndexType = T.type_alias {
|
8
|
+
T.any(T.nilable(Time), T.nilable(Float), T.nilable(Integer))
|
9
|
+
}
|
10
|
+
|
11
|
+
sig { params(klass: T.class_of(T::Struct)).void }
|
12
|
+
def self.included(klass)
|
13
|
+
klass.extend(ClassMethods)
|
14
|
+
klass.class_variable_set(:@@index_attributes, Set.new)
|
15
|
+
klass.class_variable_set(:@@range_index_attributes, Set.new)
|
16
|
+
klass.class_variable_set(:@@ttl, nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
extend T::Sig
|
21
|
+
|
22
|
+
sig do
|
23
|
+
params(
|
24
|
+
name: Symbol,
|
25
|
+
type: T.untyped, # until smth better is proposed
|
26
|
+
options: T::Hash[Symbol, T.untyped],
|
27
|
+
).void
|
28
|
+
end
|
29
|
+
def attribute(name, type, options={})
|
30
|
+
# TODO: support uniq options
|
31
|
+
prop(name, type)
|
32
|
+
if options[:index]
|
33
|
+
index_attribute(name, type)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
sig { params(attr: Symbol, type: T.any(Class,T::Types::Base)).void }
|
39
|
+
def index_attribute(attr, type)
|
40
|
+
if should_range_index?(type)
|
41
|
+
class_variable_get(:@@range_index_attributes) << attr
|
42
|
+
sadd_proc_on_redis_connection("range_index_attrs", attr.to_s)
|
43
|
+
else
|
44
|
+
class_variable_get(:@@index_attributes) << attr
|
45
|
+
sadd_proc_on_redis_connection("index_attrs", attr.to_s)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(duration: T.nilable(ActiveSupport::Duration)).void }
|
50
|
+
def ttl(duration)
|
51
|
+
class_variable_set(:@@ttl, duration)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
sig { params(redis_key: String, item_to_add: String).void }
|
56
|
+
def sadd_proc_on_redis_connection(redis_key, item_to_add)
|
57
|
+
# TODO: Currently we're setting indexed attributes through procs that are run
|
58
|
+
# when a RedisConnection is established. This should be replaced with migrations
|
59
|
+
Redcord::RedisConnection.procs_to_prepare << Proc.new do |redis|
|
60
|
+
redis.sadd("#{model_key}:#{redis_key}", item_to_add)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { params(type: T.any(Class,T::Types::Base)).returns(T::Boolean) }
|
65
|
+
def should_range_index?(type)
|
66
|
+
# Change Ruby raw type to Sorbet type in order to call subtype_of?
|
67
|
+
if type.is_a?(Class)
|
68
|
+
type = T::Types::Simple.new(type)
|
69
|
+
end
|
70
|
+
return type.subtype_of?(RangeIndexType)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
mixes_in_class_methods(ClassMethods)
|
75
|
+
end
|
data/lib/redcord/base.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# typed: strict
|
2
|
+
#
|
3
|
+
# A Redis ORM API inspired by ActiveRecord:
|
4
|
+
# - It provides atomic CRUD operations
|
5
|
+
# - One round trip per operation
|
6
|
+
# - Model attributes are type-checked by sorbet
|
7
|
+
#
|
8
|
+
require 'redcord/actions'
|
9
|
+
require 'redcord/attribute'
|
10
|
+
require 'redcord/configurations'
|
11
|
+
require 'redcord/logger'
|
12
|
+
require 'redcord/redis_connection'
|
13
|
+
require 'redcord/serializer'
|
14
|
+
|
15
|
+
module Redcord::Base
|
16
|
+
extend T::Sig
|
17
|
+
extend T::Helpers
|
18
|
+
|
19
|
+
# Base level methods
|
20
|
+
# Redis logger can be configured at the baes level. Redis connections can
|
21
|
+
# be configured at the base-level, the model level, and Rails environment
|
22
|
+
# level.
|
23
|
+
include Redcord::Configurations
|
24
|
+
include Redcord::Logger
|
25
|
+
include Redcord::RedisConnection
|
26
|
+
|
27
|
+
abstract!
|
28
|
+
|
29
|
+
sig { params(klass: T.class_of(T::Struct)).void }
|
30
|
+
def self.included(klass)
|
31
|
+
# Redcord uses `T::Struct` to validate the attribute types. The
|
32
|
+
# Redcord models need to inherit `T::Struct` and include
|
33
|
+
# `Redcord::Base`, for example:
|
34
|
+
#
|
35
|
+
# class MyRedisModel < T::Struct
|
36
|
+
# include Redcord::Base
|
37
|
+
#
|
38
|
+
# attribute :my_redis_value, Integer
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# See more examples in spec/lib/redcord_spec.rb.
|
42
|
+
|
43
|
+
klass.class_eval do
|
44
|
+
# Redcord Model level methods
|
45
|
+
include Redcord::Serializer
|
46
|
+
include Redcord::Actions
|
47
|
+
include Redcord::Attribute
|
48
|
+
include Redcord::RedisConnection
|
49
|
+
|
50
|
+
# Redcord stores the serialized model as a hash on Redis. When
|
51
|
+
# reading a model from Redis, the hash fields are deserialized and
|
52
|
+
# coerced to the specified attribute types. Like ActiveRecord,
|
53
|
+
# Redcord manages the created_at and updated_at fields behind the
|
54
|
+
# scene.
|
55
|
+
prop :created_at, T.nilable(Time)
|
56
|
+
prop :updated_at, T.nilable(Time)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# typed: strict
|
2
|
+
#
|
3
|
+
# This allows us to configure Redis connections for Redcord. Redis
|
4
|
+
# connections can be set at the base level or model level.
|
5
|
+
#
|
6
|
+
# Connections are established by reading the connection configurations for the
|
7
|
+
# current Rails environment (development, test, or production). When a model
|
8
|
+
# level connection config is not found, the base level config will be used (which
|
9
|
+
# is a common case in the test environment).
|
10
|
+
#
|
11
|
+
# For example, in with yaml file:
|
12
|
+
# ```
|
13
|
+
# my_env:
|
14
|
+
# default:
|
15
|
+
# url: redis_url_1
|
16
|
+
# my_model:
|
17
|
+
# url: redis_url_2
|
18
|
+
# ```
|
19
|
+
#
|
20
|
+
# All models other than my model will connect to redis_url_1. My_model connects
|
21
|
+
# to redis_url_2.
|
22
|
+
#
|
23
|
+
# It is also possible to change the connection in runtime by setting the new
|
24
|
+
# configuration and call `establish_connection`. `establish_connection` clears
|
25
|
+
# out the current connection at the mode or base level, and make a new based on
|
26
|
+
# the latest connection config, which is similar to ActiveRecord.
|
27
|
+
#
|
28
|
+
# Unlike `ActiveRecord::Base.establish_connection`, it does not take any
|
29
|
+
# arguments and only uses the current configuration. We can change the
|
30
|
+
# connection config anytime we want, however, the connection won't actually
|
31
|
+
# change until we call establish_connection.
|
32
|
+
#
|
33
|
+
# For example,
|
34
|
+
# ```
|
35
|
+
# Redcord::Base.configurations = {env => {'spec_model' => {'url' => fake_url}}}
|
36
|
+
# Model_class.redis # the same connection
|
37
|
+
#
|
38
|
+
# model_class.establish_connection
|
39
|
+
# Model_class.redis # using the connection to fake_url
|
40
|
+
# ```
|
41
|
+
#
|
42
|
+
require 'redcord/redis_connection'
|
43
|
+
module Redcord::Configurations
|
44
|
+
extend T::Sig
|
45
|
+
extend T::Helpers
|
46
|
+
|
47
|
+
sig { params(klass: Module).void }
|
48
|
+
def self.included(klass)
|
49
|
+
klass.extend(ClassMethods)
|
50
|
+
end
|
51
|
+
|
52
|
+
module ClassMethods
|
53
|
+
extend T::Sig
|
54
|
+
|
55
|
+
@@configurations = T.let(
|
56
|
+
Redcord::RedisConnection.merge_and_resolve_default({}),
|
57
|
+
T::Hash[String, T.untyped]
|
58
|
+
)
|
59
|
+
|
60
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
61
|
+
def configurations
|
62
|
+
@@configurations
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { params(config: T::Hash[String, T.untyped]).void }
|
66
|
+
def configurations=(config)
|
67
|
+
@@configurations = Redcord::RedisConnection.merge_and_resolve_default(config)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
mixes_in_class_methods(ClassMethods)
|
72
|
+
end
|