redcord 0.0.1.alpha
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 +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
|