valkey-objects 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66451b3bbdc217925b068c50c8cdcd5cd392a6ddef99adbc7300c683c112d152
4
- data.tar.gz: eb06049f2987c87b8cb7fb188e15960fab83d2f7a820205b3ddd06df1ed11828
3
+ metadata.gz: 8a6bb452e823041de80c00be7bbea235bb937feef6937cac1ec122dd216a47f9
4
+ data.tar.gz: 15edeaffe86bf2eb97360e78930befcba9a7ab41c4a9f9a56e99cd7fedba1e3e
5
5
  SHA512:
6
- metadata.gz: 78d4470e2aa1cc2b9763265c7a940f4f2da58798460fbce998e2aedd32cdcd6136accca257d83719328e59011b6da185c59f590863642d430488966b9a77cb9e
7
- data.tar.gz: ad6b0974b14ebd9bc022aa259a4a953556d5ec567d370a0335b279fc695f655713cba4d0499e8cb7d6bc15e610a5ba4918d8a81bdeca4ce8820bd0d65f8f6439
6
+ metadata.gz: 1380445b88e8949132e7967b16ece714f74a3118bab76373e7d412a95709a062b63b1bfe56844d0f315d0b672e66b0f4ba67761472ec4c816aadbfc1de7ddb57
7
+ data.tar.gz: 6871da9972c3489afbedf59fb356edf2ba340deb9e4fe4ff47e172e144bb99624e0707497ed89fe4042206ad650b3e83938eb6eca2ce82245625509619779a38
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Valkey
4
4
  module Objects
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
@@ -5,8 +5,14 @@ require_relative "objects/version"
5
5
  require 'redis-client'
6
6
  require 'json'
7
7
  require 'ruby-duration'
8
+ require 'amatch'
8
9
  require 'ap'
9
10
  module VK
11
+
12
+ @@XX = {}
13
+
14
+
15
+
10
16
  def self.included(x)
11
17
  x.extend VK
12
18
  end
@@ -16,6 +22,8 @@ module VK
16
22
  # Example:
17
23
  # class ExmpleObject
18
24
  # include VK
25
+ #
26
+ ## Add Object Containers by Value Type:
19
27
  # value :myValue
20
28
  # counter :myCounter
21
29
  # hashkey :myHashKey
@@ -25,49 +33,83 @@ module VK
25
33
  # place :myPlace
26
34
  # pipe :myPipe
27
35
  # toggle :myToggle
36
+ # ticker :myTicker
37
+ # entry :myEntry
38
+ #
39
+ ## Add an Object Container With an Implicit Expiration:
40
+ # value :myExpiringValue, ttl: (seconds to live without interaction)
41
+ #
28
42
  # def initialize k
29
43
  # @id = k
30
44
  # end
31
45
  # end
32
- #
33
- # Instantiation: @obj = ExampleObject.new('object id...')
46
+ ##
47
+ # Create the Object:
48
+ # @obj = ExampleObject.new('object id...')
49
+ ##
50
+ # For all object methods:
51
+ # @obj.myObjectContainer.expire(seconds)
52
+ # @obj.myObjectContainer.delete!
53
+ #
34
54
  xx = x.name.gsub("::", "-")
35
55
  ##
56
+ # Object Method Types:
57
+ ##
58
+ # A String Value
59
+ ##
36
60
  # value :myValue
37
- # @obj.myValue => A string
61
+ # @obj.myValue
62
+ # @obj.myValue.exist?
38
63
  # @obj.myValue.value = "my value"
39
64
  # @obj.myValue.value => "my value"
40
- define_method(:value) { |k| define_method(k.to_sym) { V.new(%[#{xx}:value:#{k}:#{@id}]) } };
65
+ define_method(:value) { |k, h={}| define_method(k.to_sym) { VALUE.new(%[#{xx}:value:#{k}:#{@id}], h) } };
66
+ ##
67
+ # A Number Value
41
68
  ##
42
- # counter :myCounter => A number
69
+ # counter :myCounter
43
70
  # @obj.myCounter
71
+ # @obj.myCounter.exist?
44
72
  # @obj.myCounter.value = number
45
- # @obj.myCounter.value => ...
73
+ # @obj.myCounter.value => number
46
74
  # @obj.myCounter.incr number
47
75
  # @obj.myCounter.decr number
48
76
  #
49
- define_method(:counter) { |k| define_method(k.to_sym) { C.new(%[#{xx}:counter:#{k}:#{@id}]); } };
77
+ define_method(:counter) { |k, h={}| define_method(k.to_sym) { COUNTER.new(%[#{xx}:counter:#{k}:#{@id}], h); } };
50
78
  ##
51
- #
52
- define_method(:timestamp) { |k| define_method(k.to_sym) { N.new(%[#{xx}:counter:#{k}:#{@id}]) } }
53
- ##
54
- # hashkey :myHashKey => The ubiquitous Ruby Hash in valkey/redis
79
+ # An Epoch Value
80
+ ##
81
+ # timestamp :myTimestamp
82
+ # @obj.myTimestamp
83
+ # @obj.myTimestamp.exist?
84
+ # @obj.myTimestamp.value!
85
+ # @obj.myTimestamp.value => epoch
86
+ # @obj.myTimestamp.ago => Seconds since epoch
87
+ # @obj.myTimestamp.to_time => Time object
88
+ define_method(:timestamp) { |k, h={}| define_method(k.to_sym) { TIMESTAMP.new(%[#{xx}:timestamp:#{k}:#{@id}], h) } }
89
+ ##
90
+ # A Hash Value
91
+ ##
92
+ # hashkey :myHashKey
55
93
  # @obj.myHashKey
56
94
  # @obj.myHashKey[:key] = value
57
- # @obj.myHashKey[:key] => ...
95
+ # @obj.myHashKey[:key] => "value"
58
96
  #
59
- define_method(:hashkey) { |k| define_method(k.to_sym) { H.new(%[#{xx}:hash:#{k}:#{@id}]); } };
60
- ##
61
- # sortedset :mySortedSet => A ranked collection of members
97
+ define_method(:hashkey) { |k, h={}| define_method(k.to_sym) { HASH.new(%[#{xx}:hashkey:#{k}:#{@id}], h); } };
98
+ ##
99
+ # A Sorted Set Value
100
+ ##
101
+ # sortedset :mySortedSet
62
102
  # @obj.mySortedSet
63
103
  # @obj.mySortedSet[:key] = value
64
104
  # @obj.mySortedSet[:key] => ...
65
105
  # @obj.mySortedSet.value { |key, i| ... }
66
106
  # @obj.mySortedSet.poke key, number
67
107
  #
68
- define_method(:sortedset) { |k| define_method(k.to_sym) { S.new(%[#{xx}:sortedset:#{k}:#{@id}]); } };
69
- ##
70
- # set :mySet => A collect of unique members
108
+ define_method(:sortedset) { |k, h={}| define_method(k.to_sym) { SORTEDSET.new(%[#{xx}:sortedset:#{k}:#{@id}], h); } };
109
+ ##
110
+ # A Collection of Values
111
+ ##
112
+ # set :mySet
71
113
  # @obj.mySet
72
114
  # @obj.mySet << "x"
73
115
  # @obj.myset.rm "x"
@@ -76,17 +118,21 @@ module VK
76
118
  # @obj.myset["pattern"]
77
119
  # @obj.mySet.value { |key, i| ... }
78
120
  #
79
- define_method(:set) { |k| define_method(k.to_sym) { G.new(%[#{xx}:set:#{k}:#{@id}]); } };
80
- ##
81
- # queue :myQueue => An array with push and pop utility
121
+ define_method(:set) { |k, h={}| define_method(k.to_sym) { SET.new(%[#{xx}:set:#{k}:#{@id}], h); } };
122
+ ##
123
+ # A List of Values
124
+ ##
125
+ # queue :myQueue
82
126
  # @obj.myQueue
83
127
  # @obj.myQueue << "x"
84
128
  # @obj.myQueue.front => "x" and pop
85
129
  # @obj.myQueue.value { |key, i| ... }
86
130
  #
87
- define_method(:queue) { |k| define_method(k.to_sym) { Q.new(%[#{xx}:queue:#{k}:#{@id}]); } };
88
- ##
89
- # place :myPlace => GPS Coordinates
131
+ define_method(:queue) { |k, h={}| define_method(k.to_sym) { QUEUE.new(%[#{xx}:queue:#{k}:#{@id}], h); } };
132
+ ##
133
+ # A Collection of Places
134
+ ##
135
+ # place :myPlace
90
136
  # @obj.myPlace
91
137
  # @obj.myPlace.add "key", longitude, latitude
92
138
  # @obj.myPlace["key"] => { longitude: xx, latitude: yy }
@@ -94,24 +140,45 @@ module VK
94
140
  # @obj.myPlace.radius longitude, latitude, distance
95
141
  # @obj.myPlace.value { |key, i| ... }
96
142
  #
97
- define_method(:place) { |k| define_method(k.to_sym) { P.new(%[#{xx}:place:#{k}:#{@id}]); } };
98
- ##
99
- # pipe :myPipe => Subscibe and handle, and publish.
100
- # @obj.myPipe
101
- # @obj.myPipe.on { |msg| ... }
102
- # @obj.myPipe << "input" => publish { input: "input" }
103
- # @obj.myPipe << ["input","input"] => publish { inputs: ["input", "input"] }
104
- # @obj.myPipe << {} => publish {}
105
- #
106
- define_method(:pipe) { |k| define_method(k.to_sym) { B.new(%[#{xx}:pipe:#{k}:#{@id}]); } };
107
- ##
108
- # toggle :myToggle => Boolean toggle.
143
+ define_method(:place) { |k, h={}| define_method(k.to_sym) { PLACE.new(%[#{xx}:place:#{k}:#{@id}], h); } };
144
+ ##
145
+ # A Boolean Value
146
+ ##
147
+ # toggle :myToggle
109
148
  # @obj.myToggle
110
- # @obj.value = bool
111
- # @obj.value => ...
112
- # @obj.value! => value = !value
149
+ # @obj.myToggle.exist?
150
+ # @obj.myToggle.value = bool
151
+ # @obj.myToggle.value => ...
152
+ # @obj.myToggle.value! => value = !value
113
153
  #
114
- define_method(:toggle) { |k| define_method(k.to_sym) { T.new(%[#{xx}:toggle:#{k}:#{@id}]); } };
154
+ define_method(:toggle) { |k, h={}| define_method(k.to_sym) { TOGGLE.new(%[#{xx}:toggle:#{k}:#{@id}], h); } };
155
+ ##
156
+ # A Sorted Hash of Values
157
+ ##
158
+ # ticker :myTicker
159
+ # @obj.myTicker
160
+ # @obj.myTicker[:key] = value
161
+ # @obj.myTicker[:key] => "value"
162
+ # @obj.myticker.value { |i,e| ... }
163
+ define_method(:ticker) { |k, h={}| define_method(k.to_sym) { SORTEDHASH.new(%[#{xx}:ticker:#{k}:#{@id}], h); } };
164
+ ##
165
+ # A List of Hashes
166
+ ##
167
+ # entry :myEntry
168
+ # @obj.myEntry
169
+ # @obj.myEntry << { key: 'value', ... }
170
+ # @obj.myEntry.value { |i,e| ... }
171
+ define_method(:entry) { |k, h={}| define_method(k.to_sym) { HASHLIST.new(%[#{xx}:entry:#{k}:#{@id}], h); } };
172
+ ##
173
+ # A list of Strings
174
+ ##
175
+ # vector :myVector
176
+ # @obj.myVector
177
+ # @obj.myVector << "An Entry of Text."
178
+ # @obj.myVector.value { |i,e| ... }
179
+ # @obj.myvector[0] = "An Entry of Text."
180
+ define_method(:vector) { |k, h={}| define_method(k.to_sym) { VECTOR.new(%[#{xx}:vector:#{k}:#{@id}], h); } };
181
+
115
182
  end
116
183
 
117
184
  def id
@@ -161,11 +228,15 @@ module VK
161
228
  def self.redis
162
229
  RedisClient.config(host: "127.0.0.1", port: 6379, db: 0).new_client
163
230
  end
164
-
231
+
165
232
  class O
166
233
  attr_reader :key
167
- def initialize k
234
+ def initialize k, h={}
168
235
  @key = k
236
+ @opts = h
237
+ if @opts.has_key?(:ttl)
238
+ expire @opts[:ttl]
239
+ end
169
240
  end
170
241
  def delete!
171
242
  VK.redis.call("DEL", key);
@@ -175,13 +246,19 @@ module VK
175
246
  end
176
247
  end
177
248
 
178
- class N < O
249
+ class TIMESTAMP < O
179
250
  def value
180
251
  VK.redis.call("GET", key).to_i;
252
+ if @opts.has_key?(:flush) == true
253
+ delete!
254
+ end
181
255
  end
182
256
  def value!
183
257
  VK.redis.call("SET", key, "#{VK.clock.to_i}");
184
258
  end
259
+ def exist?
260
+ VK.redis.call("GET", key) ? true : false
261
+ end
185
262
  def ago
186
263
  VK.clock.to_i - value;
187
264
  end
@@ -190,10 +267,16 @@ module VK
190
267
  end
191
268
  end
192
269
 
193
- class T < O
270
+ class TOGGLE < O
194
271
  def value
195
- VK.redis.call("GET", key) == 'true' ? true : false
272
+ VK.redis.call("GET", key) == 'true' ? true : false
273
+ if @opts.has_key?(:flush) == true
274
+ delete!
275
+ end
196
276
  end
277
+ def exist?
278
+ VK.redis.call("GET", key) ? true : false
279
+ end
197
280
  def value= x
198
281
  VK.redis.call("SET", key, "#{x.to_s}")
199
282
  end
@@ -206,42 +289,19 @@ module VK
206
289
  end
207
290
  end
208
291
 
209
- class B < O
210
- def on &b
211
- pubsub = VK.redis.pubsub
212
- pubsub.call("SUBSCRIBE", key)
213
- Process.detach( fork do
214
- loop do
215
- if m = pubsub.next_event(0)
216
- cn, ty, na, id = key.split(":")
217
- if m[0] == "message"
218
- b.call({ stub: na, object: cn.gsub("-", "::"), type: ty, id: id, event: m[0], data: JSON.parse(m[2]) })
219
- else
220
- ap({ stub: na, object: cn.gsub("-", "::"), type: ty, id: id, event: m[0], data: m[2] })
221
- end
222
- end
223
- end
224
- end
225
- );
226
- end
227
- def << x
228
- if x.class == String
229
- VK.redis.call("PUBLISH", key, JSON.generate({ input: x }))
230
- elsif x.class == Array
231
- VK.redis.call("PUBLISH", key, JSON.generate({ inputs: x }))
232
- elsif x.class == Hash
233
- VK.redis.call("PUBLISH", key, JSON.generate(x))
234
- end
235
- end
236
- end
237
-
238
- class V < O
292
+ class VALUE < O
239
293
  def value
240
294
  VK.redis.call("GET", key)
295
+ if @opts.has_key?(:flush) == true
296
+ delete!
297
+ end
241
298
  end
242
299
  def value= x
243
300
  VK.redis.call("SET", key, x)
244
301
  end
302
+ def exist?
303
+ VK.redis.call("GET", key) ? true : false
304
+ end
245
305
  def match r, &b
246
306
  m = Regexp.new(r).match(value)
247
307
  if block_given?
@@ -251,8 +311,48 @@ module VK
251
311
  end
252
312
  end
253
313
  end
314
+
315
+ class VECTOR < O
316
+ include Amatch
317
+ def value &b
318
+ VK.redis.call("LRANGE", key, 0, -1).each_with_index { |e, i|
319
+ b.call(i, VK.redis.call("GET", e))
320
+ if @opts.has_key?(:flush) == true
321
+ VK.redis.call("DEL", e);
322
+ end
323
+ }
324
+ if @opts.has_key?(:flush) == true
325
+ delete!
326
+ end
327
+ end
328
+ def [] k
329
+ VK.redis.call("GET", "#{@key}-#{k}");
330
+ end
331
+ def << i
332
+ kk = %[#{@key}-#{VK.redis.call("LLEN",@key)}]
333
+ VK.redis.call("SET", kk, i);
334
+ VK.redis.call("RPUSH", key, kk)
335
+ end
336
+ def nearest p
337
+ h = {}
338
+ value { |i,v|
339
+ h[i] = {
340
+ value: v,
341
+ levenshtein: p.levenshtein_similar(v),
342
+ damerau: p.damerau_levenshtein_similar(v),
343
+ hamming: p.hamming_similar(v),
344
+ distance: p.pair_distance_similar(v),
345
+ subsequence: p.longest_subsequence_similar(v),
346
+ substring: p.longest_substring_similar(v),
347
+ jaro: p.jaro_similar(v),
348
+ winkler: p.jarowinkler_similar(v)
349
+ }
350
+ }
351
+ return h
352
+ end
353
+ end
254
354
 
255
- class C < O
355
+ class COUNTER < O
256
356
  def incr n
257
357
  VK.redis.call("SET", key, value + n.to_f)
258
358
  end
@@ -261,27 +361,36 @@ module VK
261
361
  end
262
362
  def value
263
363
  VK.redis.call("GET", key).to_f
364
+ if @opts.has_key?(:flush) == true
365
+ delete!
366
+ end
264
367
  end
265
368
  def value= n
266
369
  VK.redis.call("SET", key, n.to_f)
267
370
  end
371
+ def exist?
372
+ VK.redis.call("GET", key) ? true : false
373
+ end
268
374
  end
269
375
 
270
- class H < O
376
+ class HASH < O
271
377
  def [] k
272
378
  VK.redis.call("HGET", key, k);
273
379
  end
274
380
  def []= k,v
275
- VK.redis.call("HMSET", key, k, v);
381
+ VK.redis.call("HSET", key, k, v);
276
382
  end
277
383
  def to_h
278
384
  VK.redis.call("HGETALL", key);
279
385
  end
280
386
  end
281
387
 
282
- class Q < O
388
+ class QUEUE < O
283
389
  def value &b
284
390
  VK.redis.call("LRANGE", key, 0, -1).each_with_index { |e, i| b.call(i, e) }
391
+ if @opts.has_key?(:flush) == true
392
+ delete!
393
+ end
285
394
  end
286
395
  def length
287
396
  VK.redis.call("LLEN", key)
@@ -294,9 +403,12 @@ module VK
294
403
  end
295
404
  end
296
405
 
297
- class S < O
406
+ class SORTEDSET < O
298
407
  def value &b
299
408
  VK.redis.call("ZREVRANGE", key, 0, -1, 'WITHSCORES').each_with_index { |e, i| b.call(i, e) }
409
+ if @opts.has_key?(:flush) == true
410
+ delete!
411
+ end
300
412
  end
301
413
  def [] k
302
414
  VK.redis.call("ZSCORE", key, k).to_f;
@@ -309,9 +421,20 @@ module VK
309
421
  end
310
422
  end
311
423
 
312
- class G < O
424
+ class SET < O
313
425
  def value &b
314
- VK.redis.call("SMEMBERS", key).each_with_index { |e, i| b.call(i, e) }
426
+ a = Set.new
427
+ VK.redis.call("SMEMBERS", key).each_with_index { |e, i|
428
+ if block_given?
429
+ a << b.call(i, e)
430
+ else
431
+ a << e
432
+ end
433
+ }
434
+ if @opts.has_key?(:flush) == true
435
+ delete!
436
+ end
437
+ return aa
315
438
  end
316
439
  def include? k
317
440
  if VK.redis.call("SMISMEMBER", key, k)[0] == 0
@@ -342,9 +465,20 @@ module VK
342
465
  end
343
466
  end
344
467
 
345
- class P < O
468
+ class PLACE < O
346
469
  def value &b
347
- VK.redis.call("ZRANGE", key, 0, -1).each_with_index { |e, i| b.call(i, e) };
470
+ a = []
471
+ VK.redis.call("ZRANGE", key, 0, -1).each_with_index { |e, i|
472
+ if block_given?
473
+ a << b.call(i, e)
474
+ else
475
+ a << e
476
+ end
477
+ };
478
+ if @opts.has_key?(:flush) == true
479
+ delete!
480
+ end
481
+ return a
348
482
  end
349
483
  def add i, lon, lat
350
484
  VK.redis.call("GEOADD", key, lon, lat, i)
@@ -363,6 +497,68 @@ module VK
363
497
  end
364
498
  end
365
499
 
500
+ class SORTEDHASH < O
501
+ def value &b
502
+ VK.redis.call("ZREVRANGE", key, 0, -1, 'WITHSCORES').each_with_index { |e, i|
503
+ kx = %[#{@key}-#{e[0]}]
504
+ a = []
505
+ if block_given?
506
+ b.call(i, { key: e[0], value: VK.redis.call("GET", kx), score: e[1] } )
507
+ else
508
+ a << { key: e[0], value: VK.redis.call("GET", kx), score: e[1] }
509
+ end
510
+ if @opts.has_key?(:flush) == true
511
+ VK.redis.call("DEL", kx)
512
+ end
513
+ }
514
+ if @opts.has_key?(:flush) == true
515
+ delete!
516
+ end
517
+ return a
518
+ end
519
+ def [] k
520
+ kx = %[#{@key}-#{k}]
521
+ VK.redis.call("GET", kx)
522
+ end
523
+ def []= k, v
524
+ kx = %[#{@key}-#{k}]
525
+ VK.redis.call("SET", kx, v)
526
+ VK.redis.call("ZINCRBY", key, 1, k)
527
+ end
528
+ end
529
+
530
+ class HASHLIST < O
531
+ def value &b
532
+ a = []
533
+ VK.redis.call("LRANGE", key, 0, -1).each_with_index { |e, i|
534
+ if block_given?
535
+ a << b.call(i, JSON.parse(VK.redis.call("GET", e)))
536
+ else
537
+ a << JSON.parse(VK.redis.call("GET", e))
538
+ end
539
+ if @opts.has_key?(:flush) == true
540
+ VK.redis.call("DEL", e)
541
+ end
542
+ }
543
+ if @opts.has_key?(:flush) == true
544
+ delete!
545
+ end
546
+ return a
547
+ end
548
+ def length
549
+ VK.redis.call("LLEN", key)
550
+ end
551
+ def [] k
552
+ hx = %[#{key}-#{k}]
553
+ JSON.parse(VK.redis.call("GET", hx));
554
+ end
555
+ def push h={}
556
+ hx = %[#{key}-#{length}]
557
+ VK.redis.call("SET", hx, JSON.generate(h));
558
+ VK.redis.call("RPUSH", key, hx)
559
+ end
560
+ end
561
+
366
562
  def self.flushdb!
367
563
  VK.redis.call("FLUSHDB")
368
564
  end
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_dependency "json"
36
36
  spec.add_dependency "ruby-duration"
37
37
  spec.add_dependency "pry"
38
+ spec.add_dependency "amatch"
38
39
  spec.add_dependency "awesome_print"
39
40
  # For more information and examples about making a new gem, check out our
40
41
  # guide at: https://bundler.io/guides/creating_gem.html
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: valkey-objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Olson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-12 00:00:00.000000000 Z
11
+ date: 2025-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: amatch
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: awesome_print
71
85
  requirement: !ruby/object:Gem::Requirement