redcord 0.0.1.alpha → 0.1.1

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/redcord.rb +30 -2
  3. data/lib/redcord.rbi +0 -16
  4. data/lib/redcord/actions.rb +171 -40
  5. data/lib/redcord/attribute.rb +126 -21
  6. data/lib/redcord/base.rb +15 -0
  7. data/lib/redcord/configurations.rb +4 -0
  8. data/lib/redcord/logger.rb +1 -1
  9. data/lib/redcord/lua_script_reader.rb +16 -5
  10. data/lib/redcord/migration.rb +2 -0
  11. data/lib/redcord/migration/index.rb +57 -0
  12. data/lib/redcord/migration/migrator.rb +1 -1
  13. data/lib/redcord/migration/ttl.rb +9 -4
  14. data/lib/redcord/migration/version.rb +3 -0
  15. data/lib/redcord/railtie.rb +18 -0
  16. data/lib/redcord/redis.rb +200 -0
  17. data/lib/redcord/redis_connection.rb +38 -29
  18. data/lib/redcord/relation.rb +214 -38
  19. data/lib/redcord/serializer.rb +147 -49
  20. data/lib/redcord/server_scripts/create_hash.erb.lua +81 -0
  21. data/lib/redcord/server_scripts/delete_hash.erb.lua +17 -8
  22. data/lib/redcord/server_scripts/find_by_attr.erb.lua +50 -16
  23. data/lib/redcord/server_scripts/find_by_attr_count.erb.lua +45 -14
  24. data/lib/redcord/server_scripts/shared/index_helper_methods.erb.lua +45 -16
  25. data/lib/redcord/server_scripts/shared/lua_helper_methods.erb.lua +20 -4
  26. data/lib/redcord/server_scripts/shared/query_helper_methods.erb.lua +86 -14
  27. data/lib/redcord/server_scripts/update_hash.erb.lua +40 -26
  28. data/lib/redcord/tasks/redis.rake +15 -0
  29. data/lib/redcord/tracer.rb +48 -0
  30. data/lib/redcord/vacuum_helper.rb +90 -0
  31. metadata +13 -11
  32. data/lib/redcord/prepared_redis.rb +0 -18
  33. data/lib/redcord/server_scripts.rb +0 -78
  34. data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +0 -68
@@ -1,95 +1,271 @@
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
 
8
+ module Redcord
9
+ class InvalidQuery < StandardError; end
10
+ end
11
+
4
12
  class Redcord::Relation
5
13
  extend T::Sig
6
14
 
7
15
  sig { returns(T.class_of(Redcord::Base)) }
8
16
  attr_reader :model
9
-
10
- sig { returns(T::Hash[Symbol, T.untyped]) }
11
- attr_reader :query_conditions
12
17
 
13
18
  sig { returns(T::Set[Symbol]) }
14
19
  attr_reader :select_attrs
15
20
 
16
- # TODO: Add sig for []
17
- delegate :[], to: :to_a
21
+ sig { returns(T.nilable(Symbol)) }
22
+ attr_reader :custom_index_name
18
23
 
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
24
+ sig { returns(T::Hash[Symbol, T.untyped]) }
25
+ attr_reader :regular_index_query_conditions
26
+
27
+ sig { returns(T::Hash[Symbol, T.untyped]) }
28
+ attr_reader :custom_index_query_conditions
27
29
 
28
30
  sig do
29
31
  params(
30
32
  model: T.class_of(Redcord::Base),
31
- query_conditions: T::Hash[Symbol, T.untyped],
32
- select_attrs: T::Set[Symbol]
33
+ regular_index_query_conditions: T::Hash[Symbol, T.untyped],
34
+ custom_index_query_conditions: T::Hash[Symbol, T.untyped],
35
+ select_attrs: T::Set[Symbol],
36
+ custom_index_name: T.nilable(Symbol)
33
37
  ).void
34
38
  end
35
- def initialize(model, query_conditions={}, select_attrs=Set.new)
39
+ def initialize(
40
+ model,
41
+ regular_index_query_conditions = {},
42
+ custom_index_query_conditions = {},
43
+ select_attrs = Set.new,
44
+ custom_index_name: nil
45
+ )
36
46
  @model = model
37
- @query_conditions = query_conditions
47
+ @regular_index_query_conditions = regular_index_query_conditions
48
+ @custom_index_query_conditions = custom_index_query_conditions
38
49
  @select_attrs = select_attrs
50
+ @custom_index_name = custom_index_name
39
51
  end
40
52
 
41
53
  sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
42
54
  def where(args)
43
55
  encoded_args = args.map do |attr_key, attr_val|
44
- encoded_val = model.validate_and_encode_query(attr_key, attr_val)
56
+ encoded_val = model.validate_types_and_encode_query(attr_key, attr_val)
45
57
  [attr_key, encoded_val]
46
58
  end
47
- query_conditions.merge!(encoded_args.to_h)
59
+
60
+ regular_index_query_conditions.merge!(encoded_args.to_h)
61
+ if custom_index_name
62
+ with_index(custom_index_name)
63
+ end
48
64
  self
49
65
  end
50
66
 
51
67
  sig do
52
68
  params(
53
- args: Symbol,
69
+ args: T.untyped,
54
70
  blk: T.nilable(T.proc.params(arg0: T.untyped).void),
55
71
  ).returns(T.any(Redcord::Relation, T::Array[T.untyped]))
56
72
  end
57
73
  def select(*args, &blk)
58
- if block_given?
59
- return execute_query.select { |*item| blk.call(*item) }
74
+ Redcord::Base.trace(
75
+ 'redcord_relation_select',
76
+ model_name: model.name,
77
+ ) do
78
+ if block_given?
79
+ return execute_query.select do |*item|
80
+ blk.call(*item)
81
+ end
82
+ end
83
+
84
+ select_attrs.merge(args)
85
+ self
60
86
  end
61
- select_attrs.merge(args)
62
- self
63
87
  end
64
88
 
65
89
  sig { returns(Integer) }
66
90
  def count
67
- redis.find_by_attr_count(model.model_key, query_conditions)
91
+ Redcord::Base.trace(
92
+ 'redcord_relation_count',
93
+ model_name: model.name,
94
+ ) do
95
+ model.validate_index_attributes(query_conditions.keys, custom_index_name: custom_index_name)
96
+ redis.find_by_attr_count(
97
+ model.model_key,
98
+ extract_query_conditions!,
99
+ index_attrs: model._script_arg_index_attrs,
100
+ range_index_attrs: model._script_arg_range_index_attrs,
101
+ custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
102
+ hash_tag: extract_hash_tag!,
103
+ custom_index_name: custom_index_name
104
+ )
105
+ end
68
106
  end
69
107
 
70
- sig { returns(T::Array[T.untyped]) }
71
- def to_a
72
- execute_query
108
+ sig { params(index_name: T.nilable(Symbol)).returns(Redcord::Relation) }
109
+ def with_index(index_name)
110
+ @custom_index_name = index_name
111
+ adjusted_query_conditions = model.validate_and_adjust_custom_index_query_conditions(regular_index_query_conditions)
112
+ custom_index_query_conditions.merge!(adjusted_query_conditions)
113
+ self
73
114
  end
74
115
 
116
+ delegate(
117
+ :&,
118
+ :[],
119
+ :all?,
120
+ :any?,
121
+ :any?,
122
+ :at,
123
+ :collect!,
124
+ :collect,
125
+ :compact!,
126
+ :compact,
127
+ :each,
128
+ :each_index,
129
+ :empty?,
130
+ :eql?,
131
+ :exists?,
132
+ :fetch,
133
+ :fifth!,
134
+ :fifth,
135
+ :filter!,
136
+ :filter,
137
+ :first!,
138
+ :first,
139
+ :forty_two!,
140
+ :forty_two,
141
+ :fourth!,
142
+ :fourth,
143
+ :include?,
144
+ :inspect,
145
+ :last!,
146
+ :last,
147
+ :many?,
148
+ :map!,
149
+ :map,
150
+ :none?,
151
+ :one?,
152
+ :reject!,
153
+ :reject,
154
+ :reverse!,
155
+ :reverse,
156
+ :reverse_each,
157
+ :second!,
158
+ :second,
159
+ :second_to_last!,
160
+ :second_to_last,
161
+ :size,
162
+ :sort!,
163
+ :sort,
164
+ :sort_by!,
165
+ :take!,
166
+ :take,
167
+ :third!,
168
+ :third,
169
+ :third_to_last!,
170
+ :third_to_last,
171
+ :to_a,
172
+ :to_ary,
173
+ :to_h,
174
+ :to_s,
175
+ :zip,
176
+ :|,
177
+ to: :execute_query,
178
+ )
179
+
75
180
  private
181
+
182
+ sig { returns(T.nilable(String)) }
183
+ def extract_hash_tag!
184
+ attr = model.shard_by_attribute
185
+ return nil if attr.nil?
186
+
187
+ if !query_conditions.keys.include?(attr)
188
+ raise(
189
+ Redcord::InvalidQuery,
190
+ "Queries must contain attribute '#{attr}' since model #{model.name} is sharded by this attribute"
191
+ )
192
+ end
193
+
194
+ # Query conditions on custom index are always in form of range, even when query is by value condition is [value_x, value_x]
195
+ # When in fact query is by value, range is trasformed to a single value to pass the validation.
196
+ condition = query_conditions[attr]
197
+ if custom_index_name and condition.first == condition.last
198
+ condition = condition.first
199
+ end
200
+ case condition
201
+ when Integer, String
202
+ "{#{condition}}"
203
+ else
204
+ raise(
205
+ Redcord::InvalidQuery,
206
+ "Does not support query condition #{condition} on a Redis Cluster",
207
+ )
208
+ end
209
+ end
210
+
76
211
  sig { returns(T::Array[T.untyped]) }
77
212
  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)
213
+ Redcord::Base.trace(
214
+ 'redcord_relation_execute_query',
215
+ model_name: model.name,
216
+ ) do
217
+ model.validate_index_attributes(query_conditions.keys, custom_index_name: custom_index_name)
218
+ if !select_attrs.empty?
219
+ res_hash = redis.find_by_attr(
220
+ model.model_key,
221
+ extract_query_conditions!,
222
+ select_attrs: select_attrs,
223
+ index_attrs: model._script_arg_index_attrs,
224
+ range_index_attrs: model._script_arg_range_index_attrs,
225
+ custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
226
+ hash_tag: extract_hash_tag!,
227
+ custom_index_name: custom_index_name
228
+ )
229
+
230
+ res_hash.map do |id, args|
231
+ model.from_redis_hash(args).map do |k, v|
232
+ [k.to_sym, TypeCoerce[model.get_attr_type(k.to_sym)].new.from(v)]
233
+ end.to_h.merge(id: id)
234
+ end
235
+ else
236
+ res_hash = redis.find_by_attr(
237
+ model.model_key,
238
+ extract_query_conditions!,
239
+ index_attrs: model._script_arg_index_attrs,
240
+ range_index_attrs: model._script_arg_range_index_attrs,
241
+ custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
242
+ hash_tag: extract_hash_tag!,
243
+ custom_index_name: custom_index_name
244
+ )
245
+
246
+ res_hash.map { |id, args| model.coerce_and_set_id(args, id) }
84
247
  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
248
  end
89
249
  end
90
250
 
91
- sig { returns(Redcord::PreparedRedis) }
251
+ sig { returns(Redcord::Redis) }
92
252
  def redis
93
253
  model.redis
94
254
  end
255
+
256
+ sig { returns(T::Hash[Symbol, T.untyped]) }
257
+ def query_conditions
258
+ custom_index_name ? custom_index_query_conditions : regular_index_query_conditions
259
+ end
260
+
261
+ sig { returns(T::Hash[Symbol, T.untyped]) }
262
+ def extract_query_conditions!
263
+ attr = model.shard_by_attribute
264
+ return query_conditions if attr.nil?
265
+
266
+ cond = query_conditions.reject { |key| key == attr }
267
+ raise Redcord::InvalidQuery, "Cannot query only by shard_by_attribute: #{attr}" if cond.empty?
268
+
269
+ cond
270
+ end
95
271
  end
@@ -1,14 +1,19 @@
1
- require 'redcord/range_interval'
1
+ # frozen_string_literal: true
2
+
2
3
  # typed: strict
3
- #
4
- # This module defines various helper methods on Redcord for serialization between the
5
- # Ruby client and Redis server.
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
11
+ class CustomIndexInvalidQuery < StandardError; end
12
+ class CustomIndexInvalidDesign < StandardError; end
10
13
  end
11
14
 
15
+ # This module defines various helper methods on Redcord for serialization
16
+ # between the Ruby client and Redis server.
12
17
  module Redcord::Serializer
13
18
  extend T::Sig
14
19
 
@@ -16,84 +21,160 @@ module Redcord::Serializer
16
21
  def self.included(klass)
17
22
  klass.extend(ClassMethods)
18
23
  end
19
-
24
+
20
25
  module ClassMethods
21
26
  extend T::Sig
22
27
 
23
- # Redis only allows range queries on floats. To allow range queries on the Ruby Time
24
- # type, encode_attr_value and decode_attr_value will implicitly encode and decode
25
- # Time attributes to a float.
28
+ # Redis only allows range queries on floats. To allow range queries on the
29
+ # Ruby Time type, encode_attr_value and decode_attr_value will implicitly
30
+ # encode and decode Time attributes to a float.
26
31
  TIME_TYPES = T.let(Set[Time, T.nilable(Time)], T::Set[T.untyped])
32
+
27
33
  sig { params(attribute: Symbol, val: T.untyped).returns(T.untyped) }
28
34
  def encode_attr_value(attribute, val)
29
- if val && TIME_TYPES.include?(props[attribute][:type])
30
- val = val.to_f
35
+ if !val.blank? && TIME_TYPES.include?(props[attribute][:type])
36
+ time_in_nano_sec = val.to_i * 1_000_000_000
37
+ time_in_nano_sec >= 0 ? time_in_nano_sec + val.nsec : time_in_nano_sec - val.nsec
38
+ elsif val.is_a?(Float)
39
+ # Encode as round-trippable float64
40
+ '%1.16e' % [val]
41
+ else
42
+ val
31
43
  end
32
- val
33
44
  end
34
45
 
35
46
  sig { params(attribute: Symbol, val: T.untyped).returns(T.untyped) }
36
47
  def decode_attr_value(attribute, val)
37
- if val && TIME_TYPES.include?(props[attribute][:type])
38
- val = Time.zone.at(val.to_f)
48
+ if !val.blank? && TIME_TYPES.include?(props[attribute][:type])
49
+ val = val.to_i
50
+ nsec = val >= 0 ? val % 1_000_000_000 : -val % 1_000_000_000
51
+
52
+ Time.zone.at(val / 1_000_000_000).change(nsec: nsec)
53
+ else
54
+ val
39
55
  end
40
- val
41
56
  end
42
57
 
43
58
  sig { params(attr_key: Symbol, attr_val: T.untyped).returns(T.untyped)}
44
- def validate_and_encode_query(attr_key, attr_val)
45
- # Validate that attributes queried for are index attributes
46
- if !class_variable_get(:@@index_attributes).include?(attr_key) &&
47
- !class_variable_get(:@@range_index_attributes).include?(attr_key)
48
- raise Redcord::AttributeNotIndexed.new(
49
- "#{attr_key} is not an indexed attribute."
50
- )
51
- end
52
- # Validate attribute types for normal index attributes
59
+ def validate_types_and_encode_query(attr_key, attr_val)
60
+ # Validate attribute types for index attributes
53
61
  attr_type = get_attr_type(attr_key)
54
- if class_variable_get(:@@index_attributes).include?(attr_key)
62
+ if class_variable_get(:@@index_attributes).include?(attr_key) || attr_key == shard_by_attribute
55
63
  validate_attr_type(attr_val, attr_type)
56
64
  else
57
- # Validate attribute types for range index attributes
58
- if attr_val.is_a?(Redcord::RangeInterval)
59
- validate_attr_type(attr_val.min, T.cast(T.nilable(attr_type), T::Types::Base))
60
- validate_attr_type(attr_val.max, T.cast(T.nilable(attr_type), T::Types::Base))
61
- else
62
- validate_attr_type(attr_val, attr_type)
63
- end
64
- # Range index attributes need to be further encoded into a format understood by the Lua script.
65
- if attr_val != nil
65
+ validate_range_attr_types(attr_val, attr_type)
66
+
67
+ # Range index attributes need to be further encoded into a format
68
+ # understood by the Lua script.
69
+ unless attr_val.nil?
66
70
  attr_val = encode_range_index_attr_val(attr_key, attr_val)
67
71
  end
68
72
  end
69
73
  attr_val
70
74
  end
71
75
 
72
- sig { params(attr_val: T.untyped, attr_type: T.any(Class, T::Types::Base)).void }
76
+ # Validate that attributes queried for are index attributes
77
+ # For custom index: validate that attributes are present in specified index
78
+ sig { params(attr_keys: T::Array[Symbol], custom_index_name: T.nilable(Symbol)).void}
79
+ def validate_index_attributes(attr_keys, custom_index_name: nil)
80
+ custom_index_attributes = class_variable_get(:@@custom_index_attributes)[custom_index_name]
81
+ attr_keys.each do |attr_key|
82
+ next if attr_key == shard_by_attribute
83
+
84
+ if !custom_index_attributes.empty?
85
+ if !custom_index_attributes.include?(attr_key)
86
+ raise(
87
+ Redcord::AttributeNotIndexed,
88
+ "#{attr_key} is not a part of #{custom_index_name} index.",
89
+ )
90
+ end
91
+ else
92
+ if !class_variable_get(:@@index_attributes).include?(attr_key) &&
93
+ !class_variable_get(:@@range_index_attributes).include?(attr_key)
94
+ raise(
95
+ Redcord::AttributeNotIndexed,
96
+ "#{attr_key} is not an indexed attribute.",
97
+ )
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ # Validate exclusive ranges not used; Change all query conditions to range form;
104
+ # The position of the attribute and type of query is validated on Lua side
105
+ sig { params(query_conditions: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped])}
106
+ def validate_and_adjust_custom_index_query_conditions(query_conditions)
107
+ adjusted_query_conditions = query_conditions.clone
108
+ query_conditions.each do |attr_key, condition|
109
+ if !condition.is_a?(Array)
110
+ adjusted_query_conditions[attr_key] = [condition, condition]
111
+ elsif condition[0].to_s[0] == '(' or condition[1].to_s[0] == '('
112
+ raise(Redcord::CustomIndexInvalidQuery, "Custom index doesn't support exclusive ranges")
113
+ end
114
+ end
115
+ adjusted_query_conditions
116
+ end
117
+
118
+ sig {
119
+ params(
120
+ attr_val: T.untyped,
121
+ attr_type: T.any(Class, T::Types::Base),
122
+ ).void
123
+ }
124
+ def validate_range_attr_types(attr_val, attr_type)
125
+ # Validate attribute types for range index attributes
126
+ if attr_val.is_a?(Redcord::RangeInterval)
127
+ validate_attr_type(
128
+ attr_val.min,
129
+ T.cast(T.nilable(attr_type), T::Types::Base),
130
+ )
131
+ validate_attr_type(
132
+ attr_val.max,
133
+ T.cast(T.nilable(attr_type), T::Types::Base),
134
+ )
135
+ else
136
+ validate_attr_type(attr_val, attr_type)
137
+ end
138
+ end
139
+
140
+ sig {
141
+ params(
142
+ attr_val: T.untyped,
143
+ attr_type: T.any(Class, T::Types::Base),
144
+ ).void
145
+ }
73
146
  def validate_attr_type(attr_val, attr_type)
74
147
  if (attr_type.is_a?(Class) && !attr_val.is_a?(attr_type)) ||
75
- (attr_type.is_a?(T::Types::Base) && !attr_type.valid?(attr_val))
76
- raise Redcord::WrongAttributeType.new(
77
- "Expected type #{attr_type}, got #{attr_val.class}"
148
+ (attr_type.is_a?(T::Types::Base) && !attr_type.valid?(attr_val))
149
+ raise(
150
+ Redcord::WrongAttributeType,
151
+ "Expected type #{attr_type}, got #{attr_val.class.name}",
78
152
  )
79
153
  end
80
154
  end
81
155
 
82
- sig { params(attribute: Symbol, val: T.untyped).returns([T.untyped, T.untyped]) }
156
+ sig {
157
+ params(
158
+ attribute: Symbol,
159
+ val: T.untyped,
160
+ ).returns([T.untyped, T.untyped])
161
+ }
83
162
  def encode_range_index_attr_val(attribute, val)
84
163
  if val.is_a?(Redcord::RangeInterval)
85
- # nil is treated as -inf and +inf. This is supported in Redis sorted sets
86
- # so clients aren't required to know the highest and lowest scores in a range
164
+ # nil is treated as -inf and +inf. This is supported in Redis sorted
165
+ # sets so clients aren't required to know the highest and lowest scores
166
+ # in a range
87
167
  min_val = !val.min ? '-inf' : encode_attr_value(attribute, val.min)
88
168
  max_val = !val.max ? '+inf' : encode_attr_value(attribute, val.max)
89
169
 
90
- # In Redis, by default min and max is closed. You can prefix the score with '(' to
91
- # specify an open interval.
170
+ # In Redis, by default min and max is closed. You can prefix the score
171
+ # with '(' to specify an open interval.
92
172
  min_val = val.min_exclusive ? '(' + min_val.to_s : min_val.to_s
93
173
  max_val = val.max_exclusive ? '(' + max_val.to_s : max_val.to_s
94
- return [min_val, max_val]
174
+ [min_val, max_val]
95
175
  else
96
- # Equality queries for range indices are be passed to redis as a range [val, val].
176
+ # Equality queries for range indices are be passed to redis as a range
177
+ # [val, val].
97
178
  encoded_val = encode_attr_value(attribute, val)
98
179
  [encoded_val, encoded_val]
99
180
  end
@@ -104,24 +185,41 @@ module Redcord::Serializer
104
185
  props[attr_key][:type_object]
105
186
  end
106
187
 
107
- sig { params(redis_hash: T::Hash[T.untyped, T.untyped], id: Integer).returns(T.untyped) }
188
+ sig {
189
+ params(
190
+ redis_hash: T::Hash[T.untyped, T.untyped],
191
+ id: String,
192
+ ).returns(T.untyped)
193
+ }
108
194
  def coerce_and_set_id(redis_hash, id)
109
- # Coerce each serialized result returned from Redis back into Model instance
195
+ # Coerce each serialized result returned from Redis back into Model
196
+ # instance
110
197
  instance = TypeCoerce.send(:[], self).new.from(from_redis_hash(redis_hash))
111
198
  instance.send(:id=, id)
112
199
  instance
113
200
  end
201
+
114
202
  sig { returns(String) }
115
203
  def model_key
116
204
  "Redcord:#{name}"
117
205
  end
118
206
 
119
- sig { params(args: T::Hash[T.any(String, Symbol), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
207
+ sig {
208
+ params(
209
+ args: T::Hash[T.any(String, Symbol), T.untyped],
210
+ ).returns(T::Hash[Symbol, T.untyped])
211
+ }
120
212
  def to_redis_hash(args)
121
- args.map { |key, val| [key.to_sym, encode_attr_value(key.to_sym, val)] }.to_h
213
+ args.map do |key, val|
214
+ [key.to_sym, encode_attr_value(key.to_sym, val)]
215
+ end.to_h
122
216
  end
123
217
 
124
- sig { params(args: T::Hash[T.untyped, T.untyped]).returns(T::Hash[T.untyped, T.untyped]) }
218
+ sig {
219
+ params(
220
+ args: T::Hash[T.untyped, T.untyped],
221
+ ).returns(T::Hash[T.untyped, T.untyped])
222
+ }
125
223
  def from_redis_hash(args)
126
224
  args.map { |key, val| [key, decode_attr_value(key.to_sym, val)] }.to_h
127
225
  end