redcord 0.0.1.alpha → 0.1.1

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