tng-gtk-utils 0.2.1 → 0.2.3
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/.gitignore +5 -0
- data/README.md +33 -6
- data/lib/tng/gtk/utils/cache.rb +8 -10
- data/lib/tng/gtk/utils/fetch.rb +19 -16
- data/lib/tng/gtk/utils/logger.rb +17 -16
- data/lib/tng/gtk/utils/version.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/tng/gtk/utils/fetch_spec.rb +24 -6
- data/spec/tng/gtk/utils/logger_spec.rb +1 -1
- data/tng-gtk-utils.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0e290048720e0757b10dfc4db1d2634eee413962757f410db71000cac302ee9
|
4
|
+
data.tar.gz: 86567f1d1136aeeaefd81411e7d0b4bd6233f67d6775c0768dbded76086dd0b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6bb18b7732f2915afac16dbedd850139795447e63933da2929121fd7661b53fe679a4a5cb41e3ce819b52fea4713972daf9209a92b358450f8f45b80c0761af2
|
7
|
+
data.tar.gz: 881e2543e51d3481a35d0f4750c776e23d1d9718cbf682565733509439cf80d16d35a756837926cfae71174333c1289c82b0d6a4754e7e0675ef8a26e8af3704
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -13,21 +13,31 @@ For details on the overall 5GTANGO architecture please check [here](https://5gta
|
|
13
13
|
This library implements some of the features that were being duplicated in the 5GTANGO Gatekeeper repositories mentioned above, [`tng-gtk-common`](https://github.com/sonata-nfv/tng-gtk-common), [`tng-gtk-sp`](https://github.com/sonata-nfv/tng-gtk-sp) and [`tng-gtk-vnv`](https://github.com/sonata-nfv/tng-gtk-vnv).
|
14
14
|
|
15
15
|
### Logger
|
16
|
-
Ruby's standard library already provides a [Logger](https://ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger.html), which we want to extend and make it providing outputs such as the following (in JSON format):
|
16
|
+
Ruby's standard library already provides a [Logger](https://ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger.html), which we want to extend and make it providing outputs such as the following (in JSON format -- see [this user story](https://git.cs.upb.de/5gtango/UserStories/issues/376), authentication is needed):
|
17
17
|
|
18
18
|
```json
|
19
19
|
{
|
20
20
|
"type": "I",
|
21
21
|
"timestamp": "2018-10-18 15:49:08 UTC",
|
22
|
-
"
|
22
|
+
"start_stop": "STOP",
|
23
23
|
"component": "tng-api-gtw",
|
24
24
|
"operation": "package upload",
|
25
|
-
"
|
26
|
-
"
|
25
|
+
"message": "package uploaded 201",
|
26
|
+
"status": "201",
|
27
27
|
"time_elapsed": "00:01:20"
|
28
28
|
}
|
29
29
|
```
|
30
|
-
|
30
|
+
These fields have the following meanings/values:
|
31
|
+
* `type`: **mandatory**, can be `I`(nfo), `W`(arning), `D`(ebug), `E`(rror), `F`(atal) or `U`(nknown);
|
32
|
+
* `timestamp`: **mandatory**, it's the timestamp of the logged message, in UTC format;
|
33
|
+
* `start_stop`: optional (can be empty), `START` or `STOP`
|
34
|
+
* `component`: **mandatory**, holds the name of the component logging the message;
|
35
|
+
* `operation`: **mandatory**, holds the operation logging the message;
|
36
|
+
* `message`: **mandatory**, the message to be logged;
|
37
|
+
* `status`: optional, makes sense for `start_stop='STOP'`, e.g., to hold the HTTP status;
|
38
|
+
* `time_elapsed`: optional, makes sense for `start_stop='STOP'`, to hold the time the operation took to complete.
|
39
|
+
|
40
|
+
The usual approach of redefining the formatter in the `Logger` standard library (see below) doesn't have the full flexibility we need:
|
31
41
|
|
32
42
|
```ruby
|
33
43
|
Logger.new(logdev, formatter: proc {|severity, datetime, progname, msg|
|
@@ -35,13 +45,30 @@ Logger.new(logdev, formatter: proc {|severity, datetime, progname, msg|
|
|
35
45
|
})
|
36
46
|
```
|
37
47
|
|
38
|
-
It should also support a `LOGLEVEL` variable that may assume one of the usual values `debug`, `info`, `warning`, `error`, `fatal` or `unknown` (defaults to `
|
48
|
+
It should also support a `LOGLEVEL` variable that may assume one of the usual values `debug`, `info`, `warning`, `error`, `fatal` or `unknown` (defaults to `info`, so only logging messages marked as `unknown`, `fatal`, `error`, `warning` or `info` are shown -- see [this user story](https://git.cs.upb.de/5gtango/UserStories/issues/410)).
|
49
|
+
|
50
|
+
This output is achieved by calling methods such as `Logger.debug(...)`, with the minimum number of mandatory fields.
|
39
51
|
|
40
52
|
#### Example
|
53
|
+
An example of using the above described logger is the following:
|
41
54
|
```ruby
|
55
|
+
LOG_COMPONENT=self.name
|
56
|
+
LOGGER=Tng::Gtk::Utils::Logger
|
57
|
+
...
|
58
|
+
LOGGER.error(component: LOG_COMPONENT, operation:__method__.to_s, message:"key :uuid is missing in record #{record}")
|
59
|
+
```
|
60
|
+
|
61
|
+
For example, if the `time_elapsed` field is to be filled, we can do the following:
|
62
|
+
```ruby
|
63
|
+
began_at = Time.now.utc
|
64
|
+
do_some_lengthy_processing()
|
65
|
+
LOGGER.info(component: LOG_COMPONENT, operation:__method__.to_s, message:"Done!", time_elapsed: Time.now.utc-began_at)
|
42
66
|
```
|
43
67
|
|
44
68
|
#### Dependencies
|
69
|
+
The `Tng::Gtk::Utils::Logger` class depends on the following ruby gems:
|
70
|
+
|
71
|
+
* `json`;
|
45
72
|
|
46
73
|
### Cache
|
47
74
|
The first ...
|
data/lib/tng/gtk/utils/cache.rb
CHANGED
@@ -66,39 +66,37 @@ module Tng
|
|
66
66
|
|
67
67
|
class Cache
|
68
68
|
CACHE_PREFIX='cache'
|
69
|
-
LOG_COMPONENT=
|
69
|
+
LOG_COMPONENT=self.name
|
70
|
+
LOGGER=Tng::Gtk::Utils::Logger
|
70
71
|
STRATEGIES = {
|
71
72
|
redis: RedisCache,
|
72
73
|
memory: MemoryCache
|
73
74
|
}
|
74
75
|
class << self; attr_accessor :strategy; end
|
75
76
|
|
76
|
-
#def strategy=(value) self.class.strategy = value end
|
77
|
-
#def strategy() self.class.strategy end
|
78
|
-
|
79
77
|
self.strategy = ENV['REDIS_URL'] ? STRATEGIES[:redis] : STRATEGIES[:memory]
|
80
|
-
|
78
|
+
LOGGER.info(start_stop: 'START', component:LOG_COMPONENT, operation:'class definition', message:"Strategy used: #{self.strategy}")
|
81
79
|
|
82
80
|
def self.cache(record)
|
83
81
|
unless record.key?(:uuid)
|
84
|
-
|
82
|
+
LOGGER.error(component: LOG_COMPONENT, operation:__method__.to_s, message:"key :uuid is missing in record #{record}")
|
85
83
|
return nil
|
86
84
|
end
|
87
|
-
|
85
|
+
LOGGER.info(component: LOG_COMPONENT, operation:__method__.to_s, message:"key '#{CACHE_PREFIX}:#{record[:uuid]}' with value '#{record}' (strategy #{self.strategy})")
|
88
86
|
self.strategy.set("#{CACHE_PREFIX}:#{record[:uuid]}", record.to_json)
|
89
87
|
record
|
90
88
|
end
|
91
89
|
def self.cached?(key)
|
92
|
-
|
90
|
+
LOGGER.info(component: LOG_COMPONENT, operation:__method__.to_s, message:"key '#{CACHE_PREFIX}:#{key}' (strategy #{self.strategy})")
|
93
91
|
data = self.strategy.get("#{CACHE_PREFIX}:#{key}")
|
94
92
|
return '' if data.nil?
|
95
93
|
JSON.parse(data, symbolize_names: :true)
|
96
94
|
end
|
97
95
|
def self.clear(key)
|
98
|
-
|
96
|
+
LOGGER.info(component: LOG_COMPONENT, operation:__method__.to_s, message:"key '#{key}' (strategy #{self.strategy})")
|
99
97
|
self.strategy.del(key)
|
100
98
|
end
|
101
|
-
|
99
|
+
LOGGER.info(start_stop: 'STOP', component:LOG_COMPONENT, operation:'class definition', message:"Strategy used: #{self.strategy}")
|
102
100
|
end
|
103
101
|
end
|
104
102
|
end
|
data/lib/tng/gtk/utils/fetch.rb
CHANGED
@@ -34,6 +34,7 @@
|
|
34
34
|
require 'net/http'
|
35
35
|
require 'json'
|
36
36
|
require 'redis'
|
37
|
+
require 'tng/gtk/utils/logger'
|
37
38
|
require 'tng/gtk/utils/cache'
|
38
39
|
|
39
40
|
module Tng
|
@@ -41,22 +42,21 @@ module Tng
|
|
41
42
|
module Utils
|
42
43
|
|
43
44
|
class Fetch
|
44
|
-
NO_CACHE=ENV.fetch('NO_CACHE', nil)
|
45
45
|
|
46
46
|
class << self; attr_accessor :site; end
|
47
|
-
|
48
|
-
#def site=(value) self.class.site = value end
|
49
|
-
#def site() self.class.site end
|
50
47
|
|
51
48
|
def self.call(params)
|
52
49
|
msg=self.name+'#'+__method__.to_s
|
53
|
-
|
50
|
+
began_at=Time.now.utc
|
51
|
+
Tng::Gtk::Utils::Logger.info(start_stop: 'START', component:self.name, operation:__method__.to_s, message:"params=#{params} site=#{self.site}")
|
54
52
|
original_params = params.dup
|
55
53
|
begin
|
56
54
|
if params.key?(:uuid)
|
57
|
-
|
55
|
+
no_cache=ENV.fetch('NO_CACHE', nil)
|
56
|
+
|
57
|
+
unless no_cache
|
58
58
|
cached = Tng::Gtk::Utils::Cache.cached?(params[:uuid])
|
59
|
-
return cached
|
59
|
+
return cached unless (cached.nil? || cached.empty?)
|
60
60
|
end
|
61
61
|
uuid = params.delete :uuid
|
62
62
|
uri = URI.parse("#{self.site}/#{uuid}")
|
@@ -65,28 +65,29 @@ module Tng
|
|
65
65
|
uri = URI.parse(self.site)
|
66
66
|
uri.query = URI.encode_www_form(sanitize(params))
|
67
67
|
end
|
68
|
-
|
68
|
+
Tng::Gtk::Utils::Logger.debug(component:self.name, operation:__method__.to_s, message:"uri=#{uri}")
|
69
69
|
request = Net::HTTP::Get.new(uri)
|
70
70
|
request['content-type'] = 'application/json'
|
71
71
|
response = Net::HTTP.start(uri.hostname, uri.port) {|http| http.request(request)}
|
72
|
-
|
72
|
+
Tng::Gtk::Utils::Logger.debug(component:self.name, operation:__method__.to_s, message:"response=#{response.inspect}")
|
73
73
|
case response
|
74
74
|
when Net::HTTPSuccess
|
75
75
|
body = response.read_body
|
76
|
-
|
76
|
+
Tng::Gtk::Utils::Logger.debug(component:self.name, operation:__method__.to_s, message:"body=#{body}", status: '200')
|
77
77
|
result = JSON.parse(body, quirks_mode: true, symbolize_names: true)
|
78
78
|
cache_result(result)
|
79
|
+
Tng::Gtk::Utils::Logger.info(start_stop: 'STOP', component:self.name, operation:__method__.to_s, message:"result=#{result} site=#{self.site}", time_elapsed: Time.now.utc - began_at)
|
79
80
|
return result
|
80
81
|
when Net::HTTPNotFound
|
81
|
-
|
82
|
+
Tng::Gtk::Utils::Logger.info(start_stop: 'STOP', component:self.name, operation:__method__.to_s, message:"body=#{body}", status:'404', time_elapsed: Time.now.utc - began_at)
|
82
83
|
return {} unless uuid.nil?
|
83
84
|
return []
|
84
85
|
else
|
85
|
-
|
86
|
+
Tng::Gtk::Utils::Logger.error(start_stop: 'STOP', component:self.name, operation:__method__.to_s, message:"#{response.message}", status:'404', time_elapsed: Time.now.utc - began_at)
|
86
87
|
return nil
|
87
88
|
end
|
88
89
|
rescue Exception => e
|
89
|
-
|
90
|
+
Tng::Gtk::Utils::Logger.error(start_stop: 'STOP', component:self.name, operation:__method__.to_s, message:"#{e.message}", time_elapsed: Time.now.utc - began_at)
|
90
91
|
end
|
91
92
|
nil
|
92
93
|
end
|
@@ -99,14 +100,16 @@ module Tng
|
|
99
100
|
end
|
100
101
|
|
101
102
|
def self.cache_result(result)
|
102
|
-
|
103
|
-
STDERR.puts "#{msg} result=#{result})"
|
103
|
+
Tng::Gtk::Utils::Logger.debug(component:self.name, operation:__method__.to_s, message:"result=#{result}")
|
104
104
|
if result.is_a?(Hash)
|
105
|
+
STDERR.puts "Caching #{result}"
|
105
106
|
Tng::Gtk::Utils::Cache.cache(result)
|
106
107
|
return
|
107
108
|
end
|
109
|
+
STDERR.puts "#{result} is not an Hash"
|
108
110
|
result.each do |record|
|
109
|
-
|
111
|
+
STDERR.puts "Caching #{record}"
|
112
|
+
Tng::Gtk::Utils::Cache.cache(record)
|
110
113
|
end
|
111
114
|
end
|
112
115
|
end
|
data/lib/tng/gtk/utils/logger.rb
CHANGED
@@ -32,7 +32,6 @@
|
|
32
32
|
# encoding: utf-8
|
33
33
|
# frozen_string_literal: true
|
34
34
|
require 'json'
|
35
|
-
require 'logger'
|
36
35
|
|
37
36
|
module Tng
|
38
37
|
module Gtk
|
@@ -44,28 +43,28 @@ module Tng
|
|
44
43
|
LOGGER_LEVELS = ['D', 'I', 'W', 'E', 'F', 'U'].freeze
|
45
44
|
|
46
45
|
class << self
|
47
|
-
def error(start_stop: '', component:, operation:, message:, status:
|
48
|
-
generic(type: 'E', start_stop: start_stop, component:component, operation: operation, message:message, status: status, time_elapsed:time_elapsed)
|
46
|
+
def error(start_stop: '', component:, operation:, message:, status:'', time_elapsed:'')
|
47
|
+
generic(type: 'E', start_stop: start_stop, component: component, operation: operation, message: message, status: status, time_elapsed: time_elapsed)
|
49
48
|
end
|
50
|
-
def warning(start_stop: '', component:, operation: , message:, status:
|
51
|
-
generic(type: 'W', start_stop: start_stop, component:component, operation:operation, message:message, status:status, time_elapsed:time_elapsed)
|
49
|
+
def warning(start_stop: '', component:, operation: , message:, status:'', time_elapsed:'')
|
50
|
+
generic(type: 'W', start_stop: start_stop, component: component, operation: operation, message: message, status: status, time_elapsed: time_elapsed)
|
52
51
|
end
|
53
|
-
def info(start_stop: '', component:, operation
|
54
|
-
generic(type: 'I', start_stop: start_stop, component:component, operation:operation, message:message, status:status, time_elapsed:time_elapsed)
|
52
|
+
def info(start_stop: '', component:, operation:, message:, status:'', time_elapsed:'')
|
53
|
+
generic(type: 'I', start_stop: start_stop, component: component, operation: operation, message: message, status: status, time_elapsed: time_elapsed)
|
55
54
|
end
|
56
|
-
def fatal(start_stop: '', component:, operation: , message:, status:
|
57
|
-
generic(type: 'F', start_stop: start_stop, component:component, operation:operation, message:message, status:status, time_elapsed:time_elapsed)
|
55
|
+
def fatal(start_stop: '', component:, operation: , message:, status:'', time_elapsed:'')
|
56
|
+
generic(type: 'F', start_stop: start_stop, component: component, operation: operation, message: message, status: status, time_elapsed: time_elapsed)
|
58
57
|
end
|
59
58
|
def debug(start_stop: '', component:, operation:, message:, status:'', time_elapsed:'')
|
60
|
-
generic(type: 'D', start_stop: start_stop, component:component, operation:operation, message:message, status:status, time_elapsed:time_elapsed)
|
59
|
+
generic(type: 'D', start_stop: start_stop, component: component, operation: operation, message: message, status: status, time_elapsed: time_elapsed)
|
61
60
|
end
|
62
|
-
def unknown(start_stop: '', component:, operation: , message:, status:
|
63
|
-
generic(type: 'U', start_stop: start_stop, component:
|
61
|
+
def unknown(start_stop: '', component:, operation: , message:, status:'', time_elapsed:'')
|
62
|
+
generic(type: 'U', start_stop: start_stop, component: component, operation: operation, message: message, status: status, time_elapsed: time_elapsed)
|
64
63
|
end
|
65
64
|
|
66
65
|
private
|
67
66
|
def generic(type:, start_stop:, component:, operation:, message:, status:, time_elapsed:)
|
68
|
-
|
67
|
+
message = {
|
69
68
|
type: type, # mandatory, can be I(nfo), W(arning), D(ebug), E(rror), F(atal) or U(nknown)
|
70
69
|
timestamp: Time.now.utc, # mandatory
|
71
70
|
start_stop: start_stop, # optional, can be empty, 'START' or 'STOP'
|
@@ -74,11 +73,13 @@ module Tng
|
|
74
73
|
message: message, # mandatory
|
75
74
|
status: status, # optional, makes sense for start_stop='END'
|
76
75
|
time_elapsed: time_elapsed # optional, makes sense for start_stop='END'
|
77
|
-
}
|
76
|
+
}
|
77
|
+
LOGFILE.puts "#{message.to_json}" if log?(type)
|
78
|
+
message
|
78
79
|
end
|
79
80
|
|
80
|
-
def
|
81
|
-
LOGGER_LEVELS.
|
81
|
+
def log?(level)
|
82
|
+
LOGGER_LEVELS.index(level) >= LOGGER_LEVELS.index(LOGLEVEL[0].upcase)
|
82
83
|
end
|
83
84
|
end
|
84
85
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "bundler/setup"
|
2
2
|
require "tng/gtk/utils"
|
3
|
+
require 'webmock/rspec'
|
3
4
|
|
4
5
|
RSpec.configure do |config|
|
5
6
|
# Enable flags like --only-failures and --next-failure
|
@@ -12,3 +13,4 @@ RSpec.configure do |config|
|
|
12
13
|
c.syntax = :expect
|
13
14
|
end
|
14
15
|
end
|
16
|
+
WebMock.disable_net_connect!() #allow_localhost: true)
|
@@ -33,15 +33,33 @@
|
|
33
33
|
# frozen_string_literal: true
|
34
34
|
require_relative '../../../spec_helper'
|
35
35
|
require 'tng/gtk/utils/fetch'
|
36
|
+
require 'securerandom'
|
36
37
|
|
37
38
|
RSpec.describe Tng::Gtk::Utils::Fetch do
|
38
|
-
|
39
|
-
|
40
|
-
it 'should
|
41
|
-
|
39
|
+
let(:data) {{uuid: SecureRandom.uuid, whatever: 'else'}}
|
40
|
+
context 'with NO_CACHE' do
|
41
|
+
it 'should fetch requested data' do
|
42
|
+
allow(ENV).to receive(:[]).with("NO_CACHE").and_return("true")
|
43
|
+
Tng::Gtk::Utils::Fetch.site='http://example.com'
|
44
|
+
WebMock.stub_request(:get, Tng::Gtk::Utils::Fetch.site+'/'+data[:uuid]).to_return(body: data.to_json, status: 200)
|
45
|
+
expect(Tng::Gtk::Utils::Fetch.call({uuid: data[:uuid]})).to eq(data)
|
42
46
|
end
|
43
47
|
end
|
44
|
-
context 'without
|
45
|
-
|
48
|
+
context 'without NO_CACHE' do
|
49
|
+
before(:each){allow(ENV).to receive(:[]).with("NO_CACHE").and_return("")}
|
50
|
+
it 'should cache passed data the first time' do
|
51
|
+
allow(Tng::Gtk::Utils::Cache).to receive(:cached?).with(data[:uuid]).and_return('')
|
52
|
+
allow(Tng::Gtk::Utils::Cache).to receive(:cache).with(data).and_return(data)
|
53
|
+
Tng::Gtk::Utils::Fetch.site='http://example.com'
|
54
|
+
WebMock.stub_request(:get, Tng::Gtk::Utils::Fetch.site+'/'+data[:uuid]).to_return(body: data.to_json, status: 200)
|
55
|
+
Tng::Gtk::Utils::Fetch.call({uuid: data[:uuid]})
|
56
|
+
expect(Tng::Gtk::Utils::Cache).to have_received(:cache).with(data)
|
57
|
+
end
|
58
|
+
it 'should fetch from cache passed data the next time' do
|
59
|
+
allow(Tng::Gtk::Utils::Cache).to receive(:cached?).with(data[:uuid]).and_return(data)
|
60
|
+
allow(Tng::Gtk::Utils::Cache).to receive(:cache).with(data).and_return(data)
|
61
|
+
Tng::Gtk::Utils::Fetch.call({uuid: data[:uuid]})
|
62
|
+
expect(Tng::Gtk::Utils::Cache).not_to have_received(:cache).with(data)
|
63
|
+
end
|
46
64
|
end
|
47
65
|
end
|
data/tng-gtk-utils.gemspec
CHANGED
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_development_dependency "rake", "~> 10.0"
|
35
35
|
spec.add_development_dependency "rspec", "~> 3.0"
|
36
36
|
spec.add_development_dependency "pry", '~> 0.12.0'
|
37
|
+
spec.add_development_dependency "webmock", '~> 3.1.1'
|
37
38
|
|
38
39
|
spec.add_dependency 'redis', '~> 4.0'
|
39
40
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tng-gtk-utils
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- José Bonnet
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-11-
|
11
|
+
date: 2018-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.12.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: webmock
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.1.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.1.1
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: redis
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|