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,69 @@
1
+ # typed: strict
2
+ require 'rails'
3
+ module Redcord::Logger
4
+ extend T::Sig
5
+ extend T::Helpers
6
+
7
+ sig { params(klass: Module).void }
8
+ def self.included(klass)
9
+ klass.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ extend T::Sig
14
+
15
+ @@logger = T.let(Rails.logger, T.untyped)
16
+
17
+ sig { returns(T.untyped) }
18
+ def logger
19
+ @@logger
20
+ end
21
+
22
+ sig { params(logger: T.untyped).void }
23
+ def logger=(logger)
24
+ @@logger = logger
25
+ end
26
+ end
27
+
28
+ module LoggerMethods
29
+ extend T::Sig
30
+
31
+ #
32
+ # Forward all the logger methods call to a module -- almost all logger
33
+ # methods are missing and handled by method_missing. We use this trick to
34
+ # dynamically swap loggers without reconfiguring the Redis clients.
35
+ #
36
+ sig do
37
+ params(
38
+ method: Symbol,
39
+ args: T.untyped,
40
+ blk: T.nilable(T.proc.returns(T.untyped))
41
+ ).returns(T.untyped)
42
+ end
43
+ def self.method_missing(method, *args, &blk)
44
+ logger = Redcord::Base.logger
45
+ return if logger.nil?
46
+ logger.send(method, *args)
47
+ end
48
+ end
49
+
50
+ # If we set the logger to nil, but we're not rebuilding the connection(s) at
51
+ # all.
52
+ # Example:
53
+ #
54
+ # 2.5.5 :001 > Redcord::Base.redis.ping
55
+ # [Redis] command=PING args=
56
+ # [Redis] call_time=0.80 ms
57
+ # => "PONG"
58
+ # 2.5.5 :002 > Redcord::Base.logger = nil
59
+ # => nil
60
+ # 2.5.5 :003 > Redcord::Base.redis.ping # show no logs
61
+ # => "PONG"
62
+ #
63
+ sig { returns(T.untyped) }
64
+ def self.proxy
65
+ Redcord::Logger::LoggerMethods
66
+ end
67
+
68
+ mixes_in_class_methods(ClassMethods)
69
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+ module Redcord::LuaScriptReader
3
+ extend T::Sig
4
+
5
+ sig {params(script_name: String).returns(String) }
6
+ def self.read_lua_script(script_name)
7
+ path = File.join(File.dirname(__FILE__), "server_scripts/#{script_name}.erb.lua")
8
+ ERB.new(File.read(path)).result(binding)
9
+ end
10
+
11
+ sig {params(relative_path: String).returns(String) }
12
+ def self.include_lua(relative_path)
13
+ path = File.join(File.dirname(__FILE__), "server_scripts/#{relative_path}.erb.lua")
14
+ File.read(path)
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+ class Redcord::Migration
3
+ end
4
+
5
+ require 'redcord/migration/ttl'
6
+
7
+ class Redcord::Migration
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ include Redcord::Migration::TTL
11
+
12
+ abstract!
13
+
14
+ sig { returns(Redis) }
15
+ attr_reader :redis
16
+
17
+ sig { params(redis: Redis).void }
18
+ def initialize(redis)
19
+ @redis = redis
20
+ end
21
+
22
+ sig { abstract.void }
23
+ def up; end
24
+
25
+ sig { abstract.void }
26
+ def down; end
27
+ end
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ require 'redcord/migration'
3
+ class Redcord::Migration::Migrator
4
+ extend T::Sig
5
+
6
+ sig { params(redis: Redis).returns(T::Boolean) }
7
+ def self.need_to_migrate?(redis)
8
+ local_version = Redcord::Migration::Version.new
9
+ remote_version = Redcord::Migration::Version.new(redis: redis)
10
+ !(local_version.all - remote_version.all).empty?
11
+ end
12
+
13
+ sig { params(redis: Redis, version: String, direction: Symbol).void }
14
+ def self.migrate(redis:, version:, direction:)
15
+ migration = load_version(version)
16
+ print [
17
+ T.must("#{redis.inspect.match('(redis://.*)>')[1]}"[0...30]),
18
+ direction.to_s.upcase,
19
+ version,
20
+ T.must(migration.name).underscore.humanize,
21
+ ].map { |str| str.ljust(30) }.join("\t")
22
+
23
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
+ migration.new(redis).send(direction)
25
+ if direction == :up
26
+ redis.sadd(
27
+ Redcord::Migration::Version::MIGRATION_VERSIONS_REDIS_KEY,
28
+ version,
29
+ )
30
+ else
31
+ redis.srem(
32
+ Redcord::Migration::Version::MIGRATION_VERSIONS_REDIS_KEY,
33
+ version,
34
+ )
35
+ end
36
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
+ puts "\t#{(end_time - start_time) * 1000.0.round(3)} ms"
38
+ end
39
+
40
+ private
41
+
42
+ sig { params(version: String).returns(T.class_of(Redcord::Migration)) }
43
+ def self.load_version(version)
44
+ file = T.must(migration_files.select { |f| f.match(version) }.first)
45
+ require(File.expand_path(file))
46
+ underscore_const_name = parse_migration_filename(file)[1]
47
+ Object.const_get(underscore_const_name.camelize)
48
+ end
49
+
50
+ MIGRATION_FILENAME_REGEX = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/
51
+
52
+ @@migrations_paths = T.let(
53
+ ['db/redisrecord/migrate'],
54
+ T::Array[String],
55
+ )
56
+
57
+ sig { returns(T::Array[String]) }
58
+ def self.migrations_paths
59
+ @@migrations_paths
60
+ end
61
+
62
+ sig { returns(T::Array[String]) }
63
+ def self.migration_files
64
+ paths = migrations_paths
65
+ # Use T.unsafe to workaround sorbet: splat the paths
66
+ T.unsafe(Dir)[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
67
+ end
68
+
69
+
70
+ sig { params(filename: String).returns([String, String, String]) }
71
+ def self.parse_migration_filename(filename)
72
+ T.unsafe(File.basename(filename).scan(MIGRATION_FILENAME_REGEX).first)
73
+ end
74
+ end
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ module Redcord::Migration::TTL
3
+ extend T::Sig
4
+
5
+ # This won't change ttl until we call update on a record
6
+ sig { params(model: T.class_of(Redcord::Base)).void }
7
+ def change_ttl_passive(model)
8
+ ttl = model.class_variable_get(:@@ttl)
9
+ model.redis.set("#{model.model_key}:ttl", ttl ? ttl : -1)
10
+ end
11
+ end
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ class Redcord::Migration::Version
3
+ extend T::Sig
4
+
5
+ MIGRATION_VERSIONS_REDIS_KEY = 'Redcord:__migration_versions__'
6
+
7
+ sig { params(redis: T.nilable(Redis)).void }
8
+ def initialize(redis: nil)
9
+ @redis = T.let(redis, T.nilable(Redis))
10
+ end
11
+
12
+ sig { returns(T.nilable(String)) }
13
+ def current
14
+ all.sort.last
15
+ end
16
+
17
+ sig { returns(T::Array[String]) }
18
+ def all
19
+ if @redis
20
+ remote_versions
21
+ else
22
+ local_versions
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ sig { returns(T::Array[String]) }
29
+ def local_versions
30
+ Redcord::Migration::Migrator.migration_files.map do |filename|
31
+ fields = Redcord::Migration::Migrator.parse_migration_filename(filename)
32
+ fields[0]
33
+ end
34
+ end
35
+
36
+ sig { returns(T::Array[String]) }
37
+ def remote_versions
38
+ T.must(@redis).smembers(MIGRATION_VERSIONS_REDIS_KEY)
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ require 'redis'
3
+ require 'redcord/server_scripts'
4
+
5
+ class Redcord::PreparedRedis < Redis
6
+ extend T::Sig
7
+ include Redcord::ServerScripts
8
+
9
+ sig { returns(T::Hash[Symbol, String]) }
10
+ def redcord_server_script_shas
11
+ instance_variable_get(:@_redcord_server_script_shas)
12
+ end
13
+
14
+ sig { params(shas: T::Hash[Symbol, String]).void }
15
+ def redcord_server_script_shas=(shas)
16
+ instance_variable_set(:@_redcord_server_script_shas, shas)
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+ require 'rails'
3
+
4
+ class Redcord::Railtie < Rails::Railtie
5
+ railtie_name 'redcord'
6
+
7
+ rake_tasks do
8
+ path = File.expand_path(T.must(__dir__))
9
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
10
+ end
11
+
12
+ # Load necessary dependency to configure redcord
13
+ config.before_configuration do
14
+ require 'redcord/base'
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ # typed: strict
2
+ require 'redcord/attribute'
3
+
4
+ class Redcord::RangeInterval < T::Struct
5
+ prop :min, T.nilable(Redcord::Attribute::RangeIndexType), default: nil
6
+ prop :min_exclusive, T::Boolean, default: false
7
+ prop :max, T.nilable(Redcord::Attribute::RangeIndexType), default: nil
8
+ prop :max_exclusive, T::Boolean, default: false
9
+ end
@@ -0,0 +1,103 @@
1
+ # typed: strict
2
+ require 'rails'
3
+ require 'redcord/prepared_redis'
4
+ require 'redcord/lua_script_reader'
5
+
6
+ module Redcord::RedisConnection
7
+ extend T::Sig
8
+ extend T::Helpers
9
+
10
+ @connections = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
11
+ @procs_to_prepare = T.let([], T::Array[Proc])
12
+
13
+ sig { params(klass: T.any(Module, T.class_of(T::Struct))).void }
14
+ def self.included(klass)
15
+ klass.extend(ClassMethods)
16
+ klass.include(InstanceMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+ extend T::Sig
21
+
22
+ sig { returns(T::Hash[Symbol, T.untyped]) }
23
+ def connection_config
24
+ env_config = Redcord::Base.configurations[Rails.env]
25
+ (env_config[name.underscore] || env_config['default']).symbolize_keys
26
+ end
27
+
28
+ sig { returns(Redcord::PreparedRedis) }
29
+ def redis
30
+ Redcord::RedisConnection.connections[name.underscore] ||= prepare_redis!
31
+ end
32
+
33
+ sig { returns(Redcord::PreparedRedis) }
34
+ def establish_connection
35
+ Redcord::RedisConnection.connections[name.underscore] = prepare_redis!
36
+ end
37
+
38
+ sig { params(redis: Redis).returns(Redcord::PreparedRedis) }
39
+ def redis=(redis)
40
+ Redcord::RedisConnection.connections[name.underscore] = prepare_redis!(redis)
41
+ end
42
+
43
+ # We prepare the model definition such as TTL, index, and uniq when we
44
+ # establish a Redis connection (once per connection) instead of sending the
45
+ # definitions in each Redis query.
46
+ #
47
+ # TODO: Replace this with Redcord migrations
48
+ sig { params(client: T.nilable(Redis)).returns(Redcord::PreparedRedis) }
49
+ def prepare_redis!(client=nil)
50
+ return client if client.is_a?(Redcord::PreparedRedis)
51
+
52
+ client = Redcord::PreparedRedis.new(
53
+ **(client.nil? ? connection_config : client.instance_variable_get(:@options)),
54
+ logger: Redcord::Logger.proxy,
55
+ )
56
+
57
+ client.pipelined do
58
+ Redcord::RedisConnection.procs_to_prepare.each do |proc_to_prepare|
59
+ proc_to_prepare.call(client)
60
+ end
61
+ end
62
+
63
+ script_names = Redcord::ServerScripts.instance_methods
64
+ res = client.pipelined do
65
+ script_names.each do |script_name|
66
+ client.script(:load, Redcord::LuaScriptReader.read_lua_script(script_name.to_s))
67
+ end
68
+ end
69
+
70
+ client.redcord_server_script_shas = script_names.zip(res).to_h
71
+ client
72
+ end
73
+ end
74
+
75
+ module InstanceMethods
76
+ extend T::Sig
77
+
78
+ sig { returns(Redcord::PreparedRedis) }
79
+ def redis
80
+ self.class.redis
81
+ end
82
+ end
83
+
84
+ sig { params(config: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
85
+ def self.merge_and_resolve_default(config)
86
+ env = Rails.env
87
+ config[env] = {} if !config.include?(env)
88
+ config[env]['default'] = {} if !config[env].include?('default')
89
+ config
90
+ end
91
+
92
+ sig { returns(T::Hash[String, T.untyped]) }
93
+ def self.connections
94
+ @connections ||= {}
95
+ end
96
+
97
+ sig { returns(T::Array[Proc]) }
98
+ def self.procs_to_prepare
99
+ @procs_to_prepare
100
+ end
101
+
102
+ mixes_in_class_methods(ClassMethods)
103
+ end
@@ -0,0 +1,95 @@
1
+ # typed: strict
2
+ require 'active_support/core_ext/module'
3
+
4
+ class Redcord::Relation
5
+ extend T::Sig
6
+
7
+ sig { returns(T.class_of(Redcord::Base)) }
8
+ attr_reader :model
9
+
10
+ sig { returns(T::Hash[Symbol, T.untyped]) }
11
+ attr_reader :query_conditions
12
+
13
+ sig { returns(T::Set[Symbol]) }
14
+ attr_reader :select_attrs
15
+
16
+ # TODO: Add sig for []
17
+ delegate :[], to: :to_a
18
+
19
+ sig do
20
+ type_parameters(:U).params(
21
+ blk: T.proc.params(arg0: Redcord::Base).returns(T.type_parameter(:U)),
22
+ ).returns(T::Array[T.type_parameter(:U)])
23
+ end
24
+ def map(&blk)
25
+ to_a.map(&blk)
26
+ end
27
+
28
+ sig do
29
+ params(
30
+ model: T.class_of(Redcord::Base),
31
+ query_conditions: T::Hash[Symbol, T.untyped],
32
+ select_attrs: T::Set[Symbol]
33
+ ).void
34
+ end
35
+ def initialize(model, query_conditions={}, select_attrs=Set.new)
36
+ @model = model
37
+ @query_conditions = query_conditions
38
+ @select_attrs = select_attrs
39
+ end
40
+
41
+ sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
42
+ def where(args)
43
+ encoded_args = args.map do |attr_key, attr_val|
44
+ encoded_val = model.validate_and_encode_query(attr_key, attr_val)
45
+ [attr_key, encoded_val]
46
+ end
47
+ query_conditions.merge!(encoded_args.to_h)
48
+ self
49
+ end
50
+
51
+ sig do
52
+ params(
53
+ args: Symbol,
54
+ blk: T.nilable(T.proc.params(arg0: T.untyped).void),
55
+ ).returns(T.any(Redcord::Relation, T::Array[T.untyped]))
56
+ end
57
+ def select(*args, &blk)
58
+ if block_given?
59
+ return execute_query.select { |*item| blk.call(*item) }
60
+ end
61
+ select_attrs.merge(args)
62
+ self
63
+ end
64
+
65
+ sig { returns(Integer) }
66
+ def count
67
+ redis.find_by_attr_count(model.model_key, query_conditions)
68
+ end
69
+
70
+ sig { returns(T::Array[T.untyped]) }
71
+ def to_a
72
+ execute_query
73
+ end
74
+
75
+ private
76
+ sig { returns(T::Array[T.untyped]) }
77
+ def execute_query
78
+ if !select_attrs.empty?
79
+ res_hash = redis.find_by_attr(model.model_key, query_conditions, select_attrs)
80
+ return res_hash.map do |id, args|
81
+ args = model.from_redis_hash(args)
82
+ args = args.map { |k, v| [k.to_sym, TypeCoerce[model.get_attr_type(k.to_sym)].new.from(v)] }.to_h
83
+ args.merge!(:id => id)
84
+ end
85
+ else
86
+ res_hash = redis.find_by_attr(model.model_key, query_conditions)
87
+ return res_hash.map { |id, args| model.coerce_and_set_id(args, id) }
88
+ end
89
+ end
90
+
91
+ sig { returns(Redcord::PreparedRedis) }
92
+ def redis
93
+ model.redis
94
+ end
95
+ end