redcord 0.0.1.alpha → 0.0.2.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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
|