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.
@@ -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