redcord 0.0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ module Redcord
3
+ end
4
+
5
+ require 'sorbet-runtime'
6
+
7
+ require 'redcord/base'
8
+ require 'redcord/migration'
9
+ require 'redcord/migration/migrator'
10
+ require 'redcord/migration/version'
11
+ require 'redcord/railtie'
@@ -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
@@ -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