redcord 0.0.1.alpha → 0.0.2.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 +4 -4
- data/lib/redcord/actions.rb +38 -14
- data/lib/redcord/attribute.rb +26 -18
- data/lib/redcord/base.rb +3 -2
- data/lib/redcord/lua_script_reader.rb +16 -5
- data/lib/redcord/migration/migrator.rb +1 -1
- data/lib/redcord/migration/version.rb +3 -0
- data/lib/redcord/redis_connection.rb +26 -8
- data/lib/redcord/relation.rb +96 -28
- data/lib/redcord/serializer.rb +80 -33
- data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +1 -0
- data/lib/redcord/server_scripts/shared/query_helper_methods.erb.lua +5 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 679b0154429c95be05676a47608f0778c3d34e714d60eb5ec065863691fa9461
|
4
|
+
data.tar.gz: 43847db0f9fd339c8a91db402b8b2da70ea4b3f874b6faf8a05d20f234cf0aea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b01c381b8e3436aaf0f4d2ebc8604bfd582c64663601a17044b0a29ca45c46e4e8ced22969ed5c53123af65808e1c4749cf262bec17e26468c1624c0023d2b9d
|
7
|
+
data.tar.gz: cb4403b960c7f2e017143738edcbbfbe356e7e8c8e5b761d1f98482296541dcc2255b480d1285ce233d5add5c7e7e78fda29ab8f5fafd561f106a64e9e144460
|
data/lib/redcord/actions.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# typed: strict
|
4
|
+
|
2
5
|
require 'sorbet-coerce'
|
3
6
|
|
4
7
|
require 'redcord/relation'
|
@@ -6,9 +9,6 @@ require 'redcord/relation'
|
|
6
9
|
module Redcord
|
7
10
|
# Raised by Model.find
|
8
11
|
class RecordNotFound < StandardError; end
|
9
|
-
# Raised by Model.where
|
10
|
-
class AttributeNotIndexed < StandardError; end
|
11
|
-
class WrongAttributeType < TypeError; end
|
12
12
|
end
|
13
13
|
|
14
14
|
module Redcord::Actions
|
@@ -38,13 +38,17 @@ module Redcord::Actions
|
|
38
38
|
instance_key = "#{model_key}:id:#{id}"
|
39
39
|
args = redis.hgetall(instance_key)
|
40
40
|
if args.empty?
|
41
|
-
raise Redcord::RecordNotFound
|
42
|
-
"Couldn't find #{name} with 'id'=#{id}"
|
43
|
-
)
|
41
|
+
raise Redcord::RecordNotFound, "Couldn't find #{name} with 'id'=#{id}"
|
44
42
|
end
|
43
|
+
|
45
44
|
coerce_and_set_id(args, id)
|
46
45
|
end
|
47
46
|
|
47
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
48
|
+
def find_by(args)
|
49
|
+
where(args).to_a.first
|
50
|
+
end
|
51
|
+
|
48
52
|
sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
|
49
53
|
def where(args)
|
50
54
|
Redcord::Relation.new(T.let(self, T.untyped)).where(args)
|
@@ -52,7 +56,7 @@ module Redcord::Actions
|
|
52
56
|
|
53
57
|
sig { params(id: T.untyped).returns(T::Boolean) }
|
54
58
|
def destroy(id)
|
55
|
-
|
59
|
+
redis.delete_hash(model_key, id) == 1
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
@@ -65,13 +69,21 @@ module Redcord::Actions
|
|
65
69
|
sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
|
66
70
|
def created_at; end
|
67
71
|
|
68
|
-
sig {
|
72
|
+
sig {
|
73
|
+
abstract.params(
|
74
|
+
time: ActiveSupport::TimeWithZone,
|
75
|
+
).returns(T.nilable(ActiveSupport::TimeWithZone))
|
76
|
+
}
|
69
77
|
def created_at=(time); end
|
70
78
|
|
71
79
|
sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
|
72
80
|
def updated_at; end
|
73
81
|
|
74
|
-
sig {
|
82
|
+
sig {
|
83
|
+
abstract.params(
|
84
|
+
time: ActiveSupport::TimeWithZone,
|
85
|
+
).returns(T.nilable(ActiveSupport::TimeWithZone))
|
86
|
+
}
|
75
87
|
def updated_at=(time); end
|
76
88
|
|
77
89
|
sig { void }
|
@@ -80,15 +92,22 @@ module Redcord::Actions
|
|
80
92
|
_id = id
|
81
93
|
if _id.nil?
|
82
94
|
self.created_at = T.must(self.updated_at)
|
83
|
-
_id = redis.create_hash_returning_id(
|
95
|
+
_id = redis.create_hash_returning_id(
|
96
|
+
self.class.model_key,
|
97
|
+
self.class.to_redis_hash(serialize),
|
98
|
+
)
|
84
99
|
send(:id=, _id)
|
85
100
|
else
|
86
|
-
redis.update_hash(
|
101
|
+
redis.update_hash(
|
102
|
+
self.class.model_key,
|
103
|
+
_id,
|
104
|
+
self.class.to_redis_hash(serialize),
|
105
|
+
)
|
87
106
|
end
|
88
107
|
end
|
89
108
|
|
90
109
|
sig { params(args: T::Hash[Symbol, T.untyped]).void }
|
91
|
-
def update!(args={})
|
110
|
+
def update!(args = {})
|
92
111
|
_id = id
|
93
112
|
if _id.nil?
|
94
113
|
_set_args!(args)
|
@@ -96,13 +115,18 @@ module Redcord::Actions
|
|
96
115
|
else
|
97
116
|
args[:updated_at] = Time.zone.now
|
98
117
|
_set_args!(args)
|
99
|
-
redis.update_hash(
|
118
|
+
redis.update_hash(
|
119
|
+
self.class.model_key,
|
120
|
+
_id,
|
121
|
+
self.class.to_redis_hash(args),
|
122
|
+
)
|
100
123
|
end
|
101
124
|
end
|
102
125
|
|
103
126
|
sig { returns(T::Boolean) }
|
104
127
|
def destroy
|
105
128
|
return false if id.nil?
|
129
|
+
|
106
130
|
self.class.destroy(T.must(id))
|
107
131
|
end
|
108
132
|
|
@@ -123,7 +147,7 @@ module Redcord::Actions
|
|
123
147
|
instance_variable_get(:@_id)
|
124
148
|
end
|
125
149
|
|
126
|
-
|
150
|
+
private
|
127
151
|
|
128
152
|
sig { params(id: Integer).returns(Integer) }
|
129
153
|
def id=(id)
|
data/lib/redcord/attribute.rb
CHANGED
@@ -1,11 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# typed: strict
|
4
|
+
|
2
5
|
module Redcord::Attribute
|
3
6
|
extend T::Sig
|
4
7
|
extend T::Helpers
|
5
8
|
|
6
|
-
# We implicitly determine what should be a range index on Redis based on Ruby
|
9
|
+
# We implicitly determine what should be a range index on Redis based on Ruby
|
10
|
+
# type.
|
7
11
|
RangeIndexType = T.type_alias {
|
8
|
-
T.any(
|
12
|
+
T.any(
|
13
|
+
T.nilable(Float),
|
14
|
+
T.nilable(Integer),
|
15
|
+
T.nilable(Time),
|
16
|
+
)
|
9
17
|
}
|
10
18
|
|
11
19
|
sig { params(klass: T.class_of(T::Struct)).void }
|
@@ -26,23 +34,22 @@ module Redcord::Attribute
|
|
26
34
|
options: T::Hash[Symbol, T.untyped],
|
27
35
|
).void
|
28
36
|
end
|
29
|
-
def attribute(name, type, options={})
|
37
|
+
def attribute(name, type, options = {})
|
30
38
|
# TODO: support uniq options
|
39
|
+
# TODO: validate types
|
31
40
|
prop(name, type)
|
32
|
-
|
33
|
-
|
34
|
-
end
|
41
|
+
|
42
|
+
index_attribute(name, type) if options[:index]
|
35
43
|
end
|
36
|
-
|
37
44
|
|
38
|
-
sig { params(attr: Symbol, type: T.any(Class,T::Types::Base)).void }
|
45
|
+
sig { params(attr: Symbol, type: T.any(Class, T::Types::Base)).void }
|
39
46
|
def index_attribute(attr, type)
|
40
47
|
if should_range_index?(type)
|
41
48
|
class_variable_get(:@@range_index_attributes) << attr
|
42
|
-
sadd_proc_on_redis_connection(
|
49
|
+
sadd_proc_on_redis_connection('range_index_attrs', attr.to_s)
|
43
50
|
else
|
44
51
|
class_variable_get(:@@index_attributes) << attr
|
45
|
-
sadd_proc_on_redis_connection(
|
52
|
+
sadd_proc_on_redis_connection('index_attrs', attr.to_s)
|
46
53
|
end
|
47
54
|
end
|
48
55
|
|
@@ -52,22 +59,23 @@ module Redcord::Attribute
|
|
52
59
|
end
|
53
60
|
|
54
61
|
private
|
62
|
+
|
55
63
|
sig { params(redis_key: String, item_to_add: String).void }
|
56
64
|
def sadd_proc_on_redis_connection(redis_key, item_to_add)
|
57
|
-
# TODO: Currently we're setting indexed attributes through procs that are
|
58
|
-
# when a RedisConnection is established. This should be replaced with
|
59
|
-
|
65
|
+
# TODO: Currently we're setting indexed attributes through procs that are
|
66
|
+
# run when a RedisConnection is established. This should be replaced with
|
67
|
+
# migrations
|
68
|
+
Redcord::RedisConnection.procs_to_prepare << proc do |redis|
|
60
69
|
redis.sadd("#{model_key}:#{redis_key}", item_to_add)
|
61
70
|
end
|
62
71
|
end
|
63
72
|
|
64
|
-
sig { params(type: T.any(Class,T::Types::Base)).returns(T::Boolean) }
|
73
|
+
sig { params(type: T.any(Class, T::Types::Base)).returns(T::Boolean) }
|
65
74
|
def should_range_index?(type)
|
66
75
|
# Change Ruby raw type to Sorbet type in order to call subtype_of?
|
67
|
-
if type.is_a?(Class)
|
68
|
-
|
69
|
-
|
70
|
-
return type.subtype_of?(RangeIndexType)
|
76
|
+
type = T::Types::Simple.new(type) if type.is_a?(Class)
|
77
|
+
|
78
|
+
type.subtype_of?(RangeIndexType)
|
71
79
|
end
|
72
80
|
end
|
73
81
|
|
data/lib/redcord/base.rb
CHANGED
@@ -52,8 +52,9 @@ module Redcord::Base
|
|
52
52
|
# coerced to the specified attribute types. Like ActiveRecord,
|
53
53
|
# Redcord manages the created_at and updated_at fields behind the
|
54
54
|
# scene.
|
55
|
-
|
56
|
-
|
55
|
+
attribute :id, T.nilable(Integer), index: true
|
56
|
+
attribute :created_at, T.nilable(Time), index: true
|
57
|
+
attribute :updated_at, T.nilable(Time), index: true
|
57
58
|
end
|
58
59
|
end
|
59
60
|
end
|
@@ -1,16 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# typed: strict
|
4
|
+
|
5
|
+
require 'erb'
|
6
|
+
|
2
7
|
module Redcord::LuaScriptReader
|
3
8
|
extend T::Sig
|
4
9
|
|
5
|
-
sig {params(script_name: String).returns(String) }
|
10
|
+
sig { params(script_name: String).returns(String) }
|
6
11
|
def self.read_lua_script(script_name)
|
7
|
-
path = File.join(
|
12
|
+
path = File.join(
|
13
|
+
File.dirname(__FILE__),
|
14
|
+
"server_scripts/#{script_name}.erb.lua",
|
15
|
+
)
|
8
16
|
ERB.new(File.read(path)).result(binding)
|
9
17
|
end
|
10
18
|
|
11
|
-
sig {params(relative_path: String).returns(String) }
|
19
|
+
sig { params(relative_path: String).returns(String) }
|
12
20
|
def self.include_lua(relative_path)
|
13
|
-
path = File.join(
|
21
|
+
path = File.join(
|
22
|
+
File.dirname(__FILE__),
|
23
|
+
"server_scripts/#{relative_path}.erb.lua",
|
24
|
+
)
|
14
25
|
File.read(path)
|
15
26
|
end
|
16
|
-
end
|
27
|
+
end
|
@@ -1,7 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# typed: strict
|
4
|
+
|
2
5
|
require 'rails'
|
3
|
-
|
6
|
+
|
4
7
|
require 'redcord/lua_script_reader'
|
8
|
+
require 'redcord/prepared_redis'
|
5
9
|
|
6
10
|
module Redcord::RedisConnection
|
7
11
|
extend T::Sig
|
@@ -37,7 +41,8 @@ module Redcord::RedisConnection
|
|
37
41
|
|
38
42
|
sig { params(redis: Redis).returns(Redcord::PreparedRedis) }
|
39
43
|
def redis=(redis)
|
40
|
-
Redcord::RedisConnection.connections[name.underscore] =
|
44
|
+
Redcord::RedisConnection.connections[name.underscore] =
|
45
|
+
prepare_redis!(redis)
|
41
46
|
end
|
42
47
|
|
43
48
|
# We prepare the model definition such as TTL, index, and uniq when we
|
@@ -46,11 +51,17 @@ module Redcord::RedisConnection
|
|
46
51
|
#
|
47
52
|
# TODO: Replace this with Redcord migrations
|
48
53
|
sig { params(client: T.nilable(Redis)).returns(Redcord::PreparedRedis) }
|
49
|
-
def prepare_redis!(client=nil)
|
54
|
+
def prepare_redis!(client = nil)
|
50
55
|
return client if client.is_a?(Redcord::PreparedRedis)
|
51
56
|
|
52
57
|
client = Redcord::PreparedRedis.new(
|
53
|
-
**(
|
58
|
+
**(
|
59
|
+
if client.nil?
|
60
|
+
connection_config
|
61
|
+
else
|
62
|
+
client.instance_variable_get(:@options)
|
63
|
+
end
|
64
|
+
),
|
54
65
|
logger: Redcord::Logger.proxy,
|
55
66
|
)
|
56
67
|
|
@@ -63,7 +74,10 @@ module Redcord::RedisConnection
|
|
63
74
|
script_names = Redcord::ServerScripts.instance_methods
|
64
75
|
res = client.pipelined do
|
65
76
|
script_names.each do |script_name|
|
66
|
-
client.script(
|
77
|
+
client.script(
|
78
|
+
:load,
|
79
|
+
Redcord::LuaScriptReader.read_lua_script(script_name.to_s),
|
80
|
+
)
|
67
81
|
end
|
68
82
|
end
|
69
83
|
|
@@ -81,11 +95,15 @@ module Redcord::RedisConnection
|
|
81
95
|
end
|
82
96
|
end
|
83
97
|
|
84
|
-
sig {
|
98
|
+
sig {
|
99
|
+
params(
|
100
|
+
config: T::Hash[String, T.untyped],
|
101
|
+
).returns(T::Hash[String, T.untyped])
|
102
|
+
}
|
85
103
|
def self.merge_and_resolve_default(config)
|
86
104
|
env = Rails.env
|
87
|
-
config[env] = {}
|
88
|
-
config[env]['default'] = {}
|
105
|
+
config[env] = {} unless config.include?(env)
|
106
|
+
config[env]['default'] = {} unless config[env].include?('default')
|
89
107
|
config
|
90
108
|
end
|
91
109
|
|
data/lib/redcord/relation.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# typed: strict
|
4
|
+
|
5
|
+
require 'active_support/core_ext/array'
|
2
6
|
require 'active_support/core_ext/module'
|
3
7
|
|
4
8
|
class Redcord::Relation
|
@@ -6,33 +10,25 @@ class Redcord::Relation
|
|
6
10
|
|
7
11
|
sig { returns(T.class_of(Redcord::Base)) }
|
8
12
|
attr_reader :model
|
9
|
-
|
13
|
+
|
10
14
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
11
15
|
attr_reader :query_conditions
|
12
16
|
|
13
17
|
sig { returns(T::Set[Symbol]) }
|
14
18
|
attr_reader :select_attrs
|
15
19
|
|
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
20
|
sig do
|
29
21
|
params(
|
30
22
|
model: T.class_of(Redcord::Base),
|
31
23
|
query_conditions: T::Hash[Symbol, T.untyped],
|
32
|
-
select_attrs: T::Set[Symbol]
|
24
|
+
select_attrs: T::Set[Symbol],
|
33
25
|
).void
|
34
26
|
end
|
35
|
-
def initialize(
|
27
|
+
def initialize(
|
28
|
+
model,
|
29
|
+
query_conditions = {},
|
30
|
+
select_attrs = Set.new
|
31
|
+
)
|
36
32
|
@model = model
|
37
33
|
@query_conditions = query_conditions
|
38
34
|
@select_attrs = select_attrs
|
@@ -50,14 +46,17 @@ class Redcord::Relation
|
|
50
46
|
|
51
47
|
sig do
|
52
48
|
params(
|
53
|
-
args:
|
49
|
+
args: T.untyped,
|
54
50
|
blk: T.nilable(T.proc.params(arg0: T.untyped).void),
|
55
51
|
).returns(T.any(Redcord::Relation, T::Array[T.untyped]))
|
56
52
|
end
|
57
53
|
def select(*args, &blk)
|
58
54
|
if block_given?
|
59
|
-
return execute_query.select
|
55
|
+
return execute_query.select do |*item|
|
56
|
+
blk.call(*item)
|
57
|
+
end
|
60
58
|
end
|
59
|
+
|
61
60
|
select_attrs.merge(args)
|
62
61
|
self
|
63
62
|
end
|
@@ -67,24 +66,93 @@ class Redcord::Relation
|
|
67
66
|
redis.find_by_attr_count(model.model_key, query_conditions)
|
68
67
|
end
|
69
68
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
69
|
+
delegate(
|
70
|
+
:&,
|
71
|
+
:[],
|
72
|
+
:all?,
|
73
|
+
:any?,
|
74
|
+
:any?,
|
75
|
+
:at,
|
76
|
+
:collect!,
|
77
|
+
:collect,
|
78
|
+
:compact!,
|
79
|
+
:compact,
|
80
|
+
:each,
|
81
|
+
:each_index,
|
82
|
+
:empty?,
|
83
|
+
:eql?,
|
84
|
+
:exists?,
|
85
|
+
:fetch,
|
86
|
+
:fifth!,
|
87
|
+
:fifth,
|
88
|
+
:filter!,
|
89
|
+
:filter,
|
90
|
+
:first!,
|
91
|
+
:first,
|
92
|
+
:forty_two!,
|
93
|
+
:forty_two,
|
94
|
+
:fourth!,
|
95
|
+
:fourth,
|
96
|
+
:include?,
|
97
|
+
:inspect,
|
98
|
+
:last!,
|
99
|
+
:last,
|
100
|
+
:many?,
|
101
|
+
:map!,
|
102
|
+
:map,
|
103
|
+
:none?,
|
104
|
+
:one?,
|
105
|
+
:reject!,
|
106
|
+
:reject,
|
107
|
+
:reverse!,
|
108
|
+
:reverse,
|
109
|
+
:reverse_each,
|
110
|
+
:second!,
|
111
|
+
:second,
|
112
|
+
:second_to_last!,
|
113
|
+
:second_to_last,
|
114
|
+
:size,
|
115
|
+
:sort!,
|
116
|
+
:sort,
|
117
|
+
:sort_by!,
|
118
|
+
:take!,
|
119
|
+
:take,
|
120
|
+
:third!,
|
121
|
+
:third,
|
122
|
+
:third_to_last!,
|
123
|
+
:third_to_last,
|
124
|
+
:to_a,
|
125
|
+
:to_ary,
|
126
|
+
:to_h,
|
127
|
+
:to_s,
|
128
|
+
:zip,
|
129
|
+
:|,
|
130
|
+
to: :execute_query,
|
131
|
+
)
|
74
132
|
|
75
133
|
private
|
134
|
+
|
76
135
|
sig { returns(T::Array[T.untyped]) }
|
77
136
|
def execute_query
|
78
137
|
if !select_attrs.empty?
|
79
|
-
res_hash = redis.find_by_attr(
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
138
|
+
res_hash = redis.find_by_attr(
|
139
|
+
model.model_key,
|
140
|
+
query_conditions,
|
141
|
+
select_attrs,
|
142
|
+
)
|
143
|
+
|
144
|
+
res_hash.map do |id, args|
|
145
|
+
model.from_redis_hash(args).map do |k, v|
|
146
|
+
[k.to_sym, TypeCoerce[model.get_attr_type(k.to_sym)].new.from(v)]
|
147
|
+
end.to_h.merge(id: id)
|
84
148
|
end
|
85
149
|
else
|
86
|
-
res_hash = redis.find_by_attr(
|
87
|
-
|
150
|
+
res_hash = redis.find_by_attr(
|
151
|
+
model.model_key,
|
152
|
+
query_conditions,
|
153
|
+
)
|
154
|
+
|
155
|
+
res_hash.map { |id, args| model.coerce_and_set_id(args, id) }
|
88
156
|
end
|
89
157
|
end
|
90
158
|
|
data/lib/redcord/serializer.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
# typed: strict
|
3
|
-
|
4
|
-
|
5
|
-
|
4
|
+
|
5
|
+
require 'redcord/range_interval'
|
6
|
+
|
6
7
|
module Redcord
|
7
8
|
# Raised by Model.where
|
8
9
|
class AttributeNotIndexed < StandardError; end
|
9
10
|
class WrongAttributeType < TypeError; end
|
10
11
|
end
|
11
12
|
|
13
|
+
# This module defines various helper methods on Redcord for serialization
|
14
|
+
# between the Ruby client and Redis server.
|
12
15
|
module Redcord::Serializer
|
13
16
|
extend T::Sig
|
14
17
|
|
@@ -16,27 +19,30 @@ module Redcord::Serializer
|
|
16
19
|
def self.included(klass)
|
17
20
|
klass.extend(ClassMethods)
|
18
21
|
end
|
19
|
-
|
22
|
+
|
20
23
|
module ClassMethods
|
21
24
|
extend T::Sig
|
22
25
|
|
23
|
-
# Redis only allows range queries on floats. To allow range queries on the
|
24
|
-
# type, encode_attr_value and decode_attr_value will implicitly
|
25
|
-
# Time attributes to a float.
|
26
|
+
# Redis only allows range queries on floats. To allow range queries on the
|
27
|
+
# Ruby Time type, encode_attr_value and decode_attr_value will implicitly
|
28
|
+
# encode and decode Time attributes to a float.
|
26
29
|
TIME_TYPES = T.let(Set[Time, T.nilable(Time)], T::Set[T.untyped])
|
30
|
+
|
27
31
|
sig { params(attribute: Symbol, val: T.untyped).returns(T.untyped) }
|
28
32
|
def encode_attr_value(attribute, val)
|
29
|
-
if val && TIME_TYPES.include?(props[attribute][:type])
|
33
|
+
if !val.blank? && TIME_TYPES.include?(props[attribute][:type])
|
30
34
|
val = val.to_f
|
31
35
|
end
|
36
|
+
|
32
37
|
val
|
33
38
|
end
|
34
39
|
|
35
40
|
sig { params(attribute: Symbol, val: T.untyped).returns(T.untyped) }
|
36
41
|
def decode_attr_value(attribute, val)
|
37
|
-
if val && TIME_TYPES.include?(props[attribute][:type])
|
42
|
+
if !val.blank? && TIME_TYPES.include?(props[attribute][:type])
|
38
43
|
val = Time.zone.at(val.to_f)
|
39
44
|
end
|
45
|
+
|
40
46
|
val
|
41
47
|
end
|
42
48
|
|
@@ -44,11 +50,13 @@ module Redcord::Serializer
|
|
44
50
|
def validate_and_encode_query(attr_key, attr_val)
|
45
51
|
# Validate that attributes queried for are index attributes
|
46
52
|
if !class_variable_get(:@@index_attributes).include?(attr_key) &&
|
47
|
-
|
48
|
-
raise
|
49
|
-
|
53
|
+
!class_variable_get(:@@range_index_attributes).include?(attr_key)
|
54
|
+
raise(
|
55
|
+
Redcord::AttributeNotIndexed,
|
56
|
+
"#{attr_key} is not an indexed attribute.",
|
50
57
|
)
|
51
58
|
end
|
59
|
+
|
52
60
|
# Validate attribute types for normal index attributes
|
53
61
|
attr_type = get_attr_type(attr_key)
|
54
62
|
if class_variable_get(:@@index_attributes).include?(attr_key)
|
@@ -56,44 +64,66 @@ module Redcord::Serializer
|
|
56
64
|
else
|
57
65
|
# Validate attribute types for range index attributes
|
58
66
|
if attr_val.is_a?(Redcord::RangeInterval)
|
59
|
-
validate_attr_type(
|
60
|
-
|
67
|
+
validate_attr_type(
|
68
|
+
attr_val.min,
|
69
|
+
T.cast(T.nilable(attr_type), T::Types::Base),
|
70
|
+
)
|
71
|
+
validate_attr_type(
|
72
|
+
attr_val.max,
|
73
|
+
T.cast(T.nilable(attr_type), T::Types::Base),
|
74
|
+
)
|
61
75
|
else
|
62
76
|
validate_attr_type(attr_val, attr_type)
|
63
77
|
end
|
64
|
-
|
65
|
-
|
78
|
+
|
79
|
+
# Range index attributes need to be further encoded into a format
|
80
|
+
# understood by the Lua script.
|
81
|
+
unless attr_val.nil?
|
66
82
|
attr_val = encode_range_index_attr_val(attr_key, attr_val)
|
67
83
|
end
|
68
84
|
end
|
85
|
+
|
69
86
|
attr_val
|
70
87
|
end
|
71
88
|
|
72
|
-
sig {
|
89
|
+
sig {
|
90
|
+
params(
|
91
|
+
attr_val: T.untyped,
|
92
|
+
attr_type: T.any(Class, T::Types::Base),
|
93
|
+
).void
|
94
|
+
}
|
73
95
|
def validate_attr_type(attr_val, attr_type)
|
74
96
|
if (attr_type.is_a?(Class) && !attr_val.is_a?(attr_type)) ||
|
75
|
-
|
76
|
-
raise
|
77
|
-
|
97
|
+
(attr_type.is_a?(T::Types::Base) && !attr_type.valid?(attr_val))
|
98
|
+
raise(
|
99
|
+
Redcord::WrongAttributeType,
|
100
|
+
"Expected type #{attr_type}, got #{attr_val.class.name}",
|
78
101
|
)
|
79
102
|
end
|
80
103
|
end
|
81
104
|
|
82
|
-
sig {
|
105
|
+
sig {
|
106
|
+
params(
|
107
|
+
attribute: Symbol,
|
108
|
+
val: T.untyped,
|
109
|
+
).returns([T.untyped, T.untyped])
|
110
|
+
}
|
83
111
|
def encode_range_index_attr_val(attribute, val)
|
84
112
|
if val.is_a?(Redcord::RangeInterval)
|
85
|
-
# nil is treated as -inf and +inf. This is supported in Redis sorted
|
86
|
-
# so clients aren't required to know the highest and lowest scores
|
113
|
+
# nil is treated as -inf and +inf. This is supported in Redis sorted
|
114
|
+
# sets so clients aren't required to know the highest and lowest scores
|
115
|
+
# in a range
|
87
116
|
min_val = !val.min ? '-inf' : encode_attr_value(attribute, val.min)
|
88
117
|
max_val = !val.max ? '+inf' : encode_attr_value(attribute, val.max)
|
89
118
|
|
90
|
-
# In Redis, by default min and max is closed. You can prefix the score
|
91
|
-
# specify an open interval.
|
119
|
+
# In Redis, by default min and max is closed. You can prefix the score
|
120
|
+
# with '(' to specify an open interval.
|
92
121
|
min_val = val.min_exclusive ? '(' + min_val.to_s : min_val.to_s
|
93
122
|
max_val = val.max_exclusive ? '(' + max_val.to_s : max_val.to_s
|
94
|
-
|
123
|
+
[min_val, max_val]
|
95
124
|
else
|
96
|
-
# Equality queries for range indices are be passed to redis as a range
|
125
|
+
# Equality queries for range indices are be passed to redis as a range
|
126
|
+
# [val, val].
|
97
127
|
encoded_val = encode_attr_value(attribute, val)
|
98
128
|
[encoded_val, encoded_val]
|
99
129
|
end
|
@@ -104,24 +134,41 @@ module Redcord::Serializer
|
|
104
134
|
props[attr_key][:type_object]
|
105
135
|
end
|
106
136
|
|
107
|
-
sig {
|
137
|
+
sig {
|
138
|
+
params(
|
139
|
+
redis_hash: T::Hash[T.untyped, T.untyped],
|
140
|
+
id: Integer,
|
141
|
+
).returns(T.untyped)
|
142
|
+
}
|
108
143
|
def coerce_and_set_id(redis_hash, id)
|
109
|
-
# Coerce each serialized result returned from Redis back into Model
|
144
|
+
# Coerce each serialized result returned from Redis back into Model
|
145
|
+
# instance
|
110
146
|
instance = TypeCoerce.send(:[], self).new.from(from_redis_hash(redis_hash))
|
111
147
|
instance.send(:id=, id)
|
112
148
|
instance
|
113
149
|
end
|
150
|
+
|
114
151
|
sig { returns(String) }
|
115
152
|
def model_key
|
116
153
|
"Redcord:#{name}"
|
117
154
|
end
|
118
155
|
|
119
|
-
sig {
|
156
|
+
sig {
|
157
|
+
params(
|
158
|
+
args: T::Hash[T.any(String, Symbol), T.untyped],
|
159
|
+
).returns(T::Hash[Symbol, T.untyped])
|
160
|
+
}
|
120
161
|
def to_redis_hash(args)
|
121
|
-
args.map
|
162
|
+
args.map do |key, val|
|
163
|
+
[key.to_sym, encode_attr_value(key.to_sym, val)]
|
164
|
+
end.to_h
|
122
165
|
end
|
123
166
|
|
124
|
-
sig {
|
167
|
+
sig {
|
168
|
+
params(
|
169
|
+
args: T::Hash[T.untyped, T.untyped],
|
170
|
+
).returns(T::Hash[T.untyped, T.untyped])
|
171
|
+
}
|
125
172
|
def from_redis_hash(args)
|
126
173
|
args.map { |key, val| [key, decode_attr_value(key.to_sym, val)] }.to_h
|
127
174
|
end
|
@@ -60,6 +60,7 @@ if #index_attr_keys > 0 then
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
local range_index_attr_keys = redis.call('smembers', model .. ':range_index_attrs')
|
63
|
+
attrs_hash['id'] = id
|
63
64
|
if #range_index_attr_keys > 0 then
|
64
65
|
for _, attr_key in ipairs(range_index_attr_keys) do
|
65
66
|
add_id_to_range_index_attr(model, attr_key, attrs_hash[attr_key], id)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redcord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2.alpha
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chan Zuckerberg Initiative
|
@@ -14,28 +14,28 @@ dependencies:
|
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '5'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: railties
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '5'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
@@ -203,7 +203,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
203
|
- !ruby/object:Gem::Version
|
204
204
|
version: 1.3.1
|
205
205
|
requirements: []
|
206
|
-
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 2.7.6.2
|
207
208
|
signing_key:
|
208
209
|
specification_version: 4
|
209
210
|
summary: A Ruby ORM like Active Record, but for Redis
|