moesif_rack 1.5.1 → 2.0.2

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: ed6afe67a6d8b2aa3c05b23a704d2ccfaadf9afdb078e9d66998f69fd7ed3f45
4
- data.tar.gz: 3cb56517e37769f179aabf0f1621c0969259558a46fa8594c017f85098f77423
3
+ metadata.gz: ad15d06f64a1f2617468e24773e1be77bda0b774474e8a1f5aa91574b05f5576
4
+ data.tar.gz: 5456b4c5efc5b9239361603192cb0c3d0635cb63ed0be8ebd89fddaca96777b0
5
5
  SHA512:
6
- metadata.gz: 91987307ffe2aaf10763dc39e0dce6acc1d91ed8f614144811b2a0d7c579b06d4f4f112cf6de10a983153aa81f45d5dac3450be5c38658af71728656d4426d5c
7
- data.tar.gz: f6a677544fd723f0ab6842e5496586d93f22b8ddf77fc5ceb1c4fb98f770ce5443461a66f6c744f03b03a428acfa1c55c669d4f4f09841191c99842a40e614f6
6
+ metadata.gz: d162f1712129b7527cb40a7cb3d10387acd677f0e073c3ae8ae543eaff35b1ac72b797d0e5c09726fd725d07ee2b9db841ff173cd63be1f879d3e2bb8bd15f32
7
+ data.tar.gz: 11290586d6235cb8be998b95c43636d8c57b477e0007cb13dd4fb5272432f552c4ad7012f4895b9096b6f8e8761b4e889b3ddb65a43dc68efb0a1499b688e0e9
@@ -7,18 +7,37 @@ require_relative './moesif_helpers'
7
7
  require_relative './regex_config_helper'
8
8
 
9
9
  class AppConfig
10
+ attr_accessor :config, :recent_etag, :last_download_time
11
+
10
12
  def initialize(debug)
11
13
  @debug = debug
12
14
  @moesif_helpers = MoesifHelpers.new(debug)
13
15
  @regex_config_helper = RegexConfigHelper.new(debug)
14
16
  end
15
17
 
18
+ def should_reload(etag_from_create_event)
19
+ if @last_download_time.nil?
20
+ return true
21
+ elsif Time.now.utc > (@last_download_time + 300)
22
+ return true
23
+ elsif !etag_from_create_event.nil? && !@recent_etag.nil?
24
+ @moesif_helpers.log_debug('comparing if etag from event and recent etag match ' + etag_from_create_event + ' ' + @recent_etag)
25
+
26
+ return etag_from_create_event != @recent_etag
27
+ end
28
+ @moesif_helpers.log_debug('should skip reload config')
29
+ return false;
30
+ end
31
+
16
32
  def get_config(api_controller)
17
33
  # Get Application Config
18
- config_api_response = api_controller.get_app_config
34
+ @moesif_helpers.log_debug('try to loading etag')
35
+ config_json, _context = api_controller.get_app_config
36
+ @config = config_json
37
+ @recent_etag = _context.response.headers[:x_moesif_config_etag]
38
+ @last_download_time = Time.now.utc
19
39
  @moesif_helpers.log_debug('new config downloaded')
20
- @moesif_helpers.log_debug(config_api_response.to_s)
21
- config_api_response
40
+ @moesif_helpers.log_debug(config_json.to_s)
22
41
  rescue MoesifApi::APIException => e
23
42
  if e.response_code.between?(401, 403)
24
43
  @moesif_helpers.log_debug 'Unauthorized access getting application configuration. Please check your Appplication Id.'
@@ -29,38 +48,15 @@ class AppConfig
29
48
  @moesif_helpers.log_debug e.to_s
30
49
  end
31
50
 
32
- def parse_configuration(config_api_response)
33
- # Parse configuration object and return Etag, sample rate and last updated time
34
-
35
- # Rails return gzipped compressed response body, so decompressing it and getting JSON response body
36
- response_body = @moesif_helpers.decompress_gzip_body(config_api_response)
37
- @moesif_helpers.log_debug(response_body.to_json)
38
-
39
- # Check if response body is not nil
40
- return response_body, config_api_response.headers[:x_moesif_config_etag], Time.now.utc unless response_body.nil?
41
-
42
- # Return Etag, sample rate and last updated time
43
-
44
- @moesif_helpers.log_debug 'Response body is nil, assuming default behavior'
45
- # Response body is nil, so assuming default behavior
46
- [nil, nil, Time.now.utc]
47
- rescue StandardError => e
48
- @moesif_helpers.log_debug 'Error while parsing the configuration object, assuming default behavior'
49
- @moesif_helpers.log_debug e.to_s
50
- # Assuming default behavior
51
- [nil, nil, Time.now.utc]
52
- end
53
-
54
- def get_sampling_percentage(event_model, config_api_response, user_id, company_id)
51
+ def get_sampling_percentage(event_model, user_id, company_id)
52
+ # if we do not have config for some reason we return 100
53
+ if !@config.nil?
55
54
  # Get sampling percentage
56
-
57
- # Check if response body is not nil
58
- if !config_api_response.nil?
59
55
  @moesif_helpers.log_debug("Getting sample rate for user #{user_id} company #{company_id}")
60
- @moesif_helpers.log_debug(config_api_response.to_s)
56
+ @moesif_helpers.log_debug(@config.to_s)
61
57
 
62
58
  # Get Regex Sampling rate
63
- regex_config = config_api_response.fetch('regex_config', nil)
59
+ regex_config = @config.fetch('regex_config', nil)
64
60
 
65
61
  if !regex_config.nil? and !event_model.nil?
66
62
  config_mapping = @regex_config_helper.prepare_config_mapping(event_model)
@@ -70,10 +66,10 @@ class AppConfig
70
66
  end
71
67
 
72
68
  # Get user sample rate object
73
- user_sample_rate = config_api_response.fetch('user_sample_rate', nil)
69
+ user_sample_rate = @config.fetch('user_sample_rate', nil)
74
70
 
75
71
  # Get company sample rate object
76
- company_sample_rate = config_api_response.fetch('company_sample_rate', nil)
72
+ company_sample_rate = @config.fetch('company_sample_rate', nil)
77
73
 
78
74
  # Get sample rate for the user if exist
79
75
  if !user_id.nil? && !user_sample_rate.nil? && user_sample_rate.key?(user_id)
@@ -85,8 +81,8 @@ class AppConfig
85
81
  return company_sample_rate.fetch(company_id)
86
82
  end
87
83
 
88
- # Return sample rate
89
- config_api_response.fetch('sample_rate', 100)
84
+ # Return overall sample rate
85
+ @config.fetch('sample_rate', 100)
90
86
  else
91
87
  @moesif_helpers.log_debug 'Assuming default behavior as response body is nil - '
92
88
  100
@@ -109,10 +109,7 @@ class GovernanceRules
109
109
  # Get Application Config
110
110
  @last_fetch = Time.now.utc
111
111
  @moesif_helpers.log_debug('starting downlaoding rules')
112
- rules_response = api_controller.get_rules
113
- rules = @moesif_helpers.decompress_gzip_body(rules_response)
114
- @moesif_helpers.log_debug('new rules downloaded')
115
- @moesif_helpers.log_debug(rules.to_json)
112
+ rules, _context = api_controller.get_rules
116
113
 
117
114
  generate_rules_caching(rules)
118
115
  rescue MoesifApi::APIException => e
@@ -12,30 +12,6 @@ class MoesifHelpers
12
12
  puts("#{Time.now} [Moesif Middleware] PID #{Process.pid} TID #{Thread.current.object_id} #{message}")
13
13
  end
14
14
 
15
- def decompress_gzip_body(moesif_api_response)
16
- # Decompress gzip response body
17
-
18
- # Check if the content-encoding header exist and is of type zip
19
- if moesif_api_response.headers.key?(:content_encoding) && moesif_api_response.headers[:content_encoding].eql?('gzip')
20
-
21
- # Create a GZipReader object to read data
22
- gzip_reader = Zlib::GzipReader.new(StringIO.new(moesif_api_response.raw_body.to_s))
23
-
24
- # Read the body
25
- uncompressed_string = gzip_reader.read
26
-
27
- # Return the parsed body
28
- JSON.parse(uncompressed_string)
29
- else
30
- @moesif_helpers.log_debug 'Content Encoding is of type other than gzip, returning nil'
31
- nil
32
- end
33
- rescue StandardError => e
34
- @moesif_helpers.log_debug 'Error while decompressing the response body'
35
- @moesif_helpers.log_debug e.to_s
36
- nil
37
- end
38
-
39
15
  def format_replacement_body(replacement_body, original_body)
40
16
  # replacement_body is an hash or array json in this case.
41
17
  # but original body could be in chunks already. we want to follow suit.
@@ -18,7 +18,7 @@ module MoesifRack
18
18
  @app = app
19
19
  raise 'application_id required for Moesif Middleware' unless options['application_id']
20
20
 
21
- @api_client = MoesifApi::MoesifAPIClient.new(options['application_id'])
21
+ @api_client = MoesifApi::MoesifAPIClient.new(options['application_id'], 'moesif-rack/2.0.2')
22
22
  @api_controller = @api_client.api
23
23
 
24
24
  @api_version = options['api_version']
@@ -32,9 +32,6 @@ module MoesifRack
32
32
  @app_config = AppConfig.new(@debug)
33
33
  @moesif_helpers = MoesifHelpers.new(@debug)
34
34
 
35
- @config_etag = nil
36
- @last_config_download_time = Time.now.utc
37
- @config_dict = {}
38
35
  @disable_transaction_id = options['disable_transaction_id'] || false
39
36
  @log_body = options.fetch('log_body', true)
40
37
  @batch_size = options['batch_size'] || 200
@@ -49,22 +46,20 @@ module MoesifRack
49
46
  start_worker
50
47
 
51
48
  begin
52
- new_config = @app_config.get_config(@api_controller)
53
- unless new_config.nil?
54
- @config, @config_etag, @last_config_download_time = @app_config.parse_configuration(new_config)
55
- end
49
+ @app_config.get_config(@api_controller)
56
50
  @governance_manager.load_rules(@api_controller)
57
51
  rescue StandardError => e
58
52
  @moesif_helpers.log_debug 'Error while parsing application configuration on initialization'
59
53
  @moesif_helpers.log_debug e.to_s
60
54
  end
55
+ # backwards compatibility for a typo in docs
61
56
  @capture_outoing_requests = options['capture_outoing_requests']
62
57
  @capture_outgoing_requests = options['capture_outgoing_requests']
63
58
  return unless @capture_outoing_requests || @capture_outgoing_requests
64
59
 
65
60
  @moesif_helpers.log_debug 'Start Capturing outgoing requests'
66
61
  require_relative '../../moesif_capture_outgoing/httplog'
67
- MoesifCaptureOutgoing.start_capture_outgoing(options)
62
+ MoesifCaptureOutgoing.start_capture_outgoing(options, @app_config, @events_queue)
68
63
  end
69
64
 
70
65
  def update_user(user_profile)
@@ -167,20 +162,10 @@ module MoesifRack
167
162
  end
168
163
 
169
164
  @moesif_helpers.log_debug('No events to read from the queue') if @events_queue.empty?
170
- if (!@event_response_config_etag.nil? && !@config_etag.nil? && @config_etag != @event_response_config_etag) || (Time.now.utc > (@last_config_download_time + 300))
171
- begin
172
- @moesif_helpers.log_debug('try to reload config and rules again')
173
- new_config = @app_config.get_config(@api_controller)
174
- unless new_config.nil?
175
- @config, @config_etag, @last_config_download_time = @app_config.parse_configuration(new_config)
176
- end
177
- # since logic to reload config is already here for every 5 minutes,
178
- # reload rules here also.
179
- @governance_manager.load_rules(@api_controller)
180
- rescue StandardError => e
181
- @moesif_helpers.log_debug 'Error while updating the application configuration'
182
- @moesif_helpers.log_debug e.to_s
183
- end
165
+
166
+ if @app_config.should_reload(@event_response_config_etag)
167
+ @app_config.get_config(@api_controller)
168
+ @governance_manager.load_rules(@api_controller)
184
169
  end
185
170
 
186
171
  sleep @batch_max_time
@@ -326,7 +311,7 @@ module MoesifRack
326
311
  random_percentage = Random.rand(0.00..100.00)
327
312
 
328
313
  begin
329
- sampling_percentage = @app_config.get_sampling_percentage(_event_model, @config, _event_model.user_id,
314
+ sampling_percentage = @app_config.get_sampling_percentage(_event_model, _event_model.user_id,
330
315
  _event_model.company_id)
331
316
  @moesif_helpers.log_debug "Using sample rate #{sampling_percentage}"
332
317
  rescue StandardError => e
@@ -367,7 +352,7 @@ module MoesifRack
367
352
  # now we can do govern based on
368
353
  # override_response = govern(env, event_model)
369
354
  # return override_response if override_response
370
- new_response = @governance_manager.govern_request(@config, env, event_model, status, headers, body)
355
+ new_response = @governance_manager.govern_request(@app_config.config, env, event_model, status, headers, body)
371
356
 
372
357
  # update the event model
373
358
  if new_response
@@ -7,7 +7,7 @@ require_relative '../../lib/moesif_rack/app_config'
7
7
 
8
8
  module MoesifCaptureOutgoing
9
9
  class << self
10
- def start_capture_outgoing(options)
10
+ def start_capture_outgoing(options, app_config_manager, events_queue)
11
11
  @moesif_options = options
12
12
  raise 'application_id required for Moesif Middleware' unless @moesif_options['application_id']
13
13
 
@@ -21,22 +21,13 @@ module MoesifCaptureOutgoing
21
21
  @skip_outgoing = options['skip_outgoing']
22
22
  @mask_data_outgoing = options['mask_data_outgoing']
23
23
  @log_body_outgoing = options.fetch('log_body_outgoing', true)
24
- @app_config = AppConfig.new(@debug)
25
- @config_etag = nil
24
+
25
+ @app_config = app_config_manager
26
+ # @app_config and @events_queue should be shared instance from the middleware
27
+ # so that we can use the same queue and same loaded @app_config
28
+ @events_queue = events_queue
26
29
  @sampling_percentage = 100
27
30
  @last_updated_time = Time.now.utc
28
- @config_dict = {}
29
- begin
30
- new_config = @app_config.get_config(@api_controller)
31
- unless new_config.nil?
32
- @config, @config_etag, @last_config_download_time = @app_config.parse_configuration(new_config)
33
- end
34
- rescue StandardError => e
35
- if @debug
36
- puts 'Error while parsing application configuration on initialization'
37
- puts e
38
- end
39
- end
40
31
  end
41
32
 
42
33
  def call(url, request, request_time, response, response_time)
@@ -152,7 +143,7 @@ module MoesifCaptureOutgoing
152
143
  begin
153
144
  @random_percentage = Random.rand(0.00..100.00)
154
145
  begin
155
- @sampling_percentage = @app_config.get_sampling_percentage(event_model, @config, event_model.user_id,
146
+ @sampling_percentage = @app_config.get_sampling_percentage(event_model, event_model.user_id,
156
147
  event_model.company_id)
157
148
  rescue StandardError => e
158
149
  if @debug
@@ -168,23 +159,13 @@ module MoesifCaptureOutgoing
168
159
  puts 'Sending Outgoing Request Data to Moesif'
169
160
  puts event_model.to_json
170
161
  end
171
- event_api_response = @api_controller.create_event(event_model)
172
- event_response_config_etag = event_api_response[:x_moesif_config_etag]
173
-
174
- if !event_response_config_etag.nil? && !@config_etag.nil? && @config_etag != event_response_config_etag && Time.now.utc > @last_updated_time + 300
175
- begin
176
- new_config = @app_config.get_config(@api_controller)
177
- unless new_config.nil?
178
- @config, @config_etag, @last_config_download_time = @app_config.parse_configuration(new_config)
179
- end
180
- rescue StandardError => e
181
- if @debug
182
- puts 'Error while updating the application configuration'
183
- puts e
184
- end
185
- end
162
+
163
+ # we put in the queue and format abot it.
164
+ unless @events_queue.nil?
165
+ @events_queue << event_model
166
+ puts('Outgoing Event successfully added to event queue') if @debug
167
+ return
186
168
  end
187
- puts('Event successfully sent to Moesif') if @debug
188
169
  elsif @debug
189
170
  puts('Skipped outgoing Event due to sampling percentage: ' + @sampling_percentage.to_s + ' and random percentage: ' + @random_percentage.to_s)
190
171
  end
@@ -1469,9 +1469,5 @@
1469
1469
  ]
1470
1470
  },
1471
1471
  "ip_addresses_blocked_by_name": {},
1472
- "regex_config": [],
1473
- "billing_config_jsons": {
1474
- "stripe": "{\"user_id_field\":\"id\",\"company_id_field\":\"metadata.uuid\"}",
1475
- "recurly": "{\"user_id_field\":\"account_code\",\"company_id_field\":\"uuid\"}"
1476
- }
1472
+ "regex_config": []
1477
1473
  }
@@ -1,6 +1,6 @@
1
1
  require 'moesif_api'
2
2
  require 'test/unit'
3
- require 'rack'
3
+
4
4
  require 'net/http'
5
5
  require_relative '../lib/moesif_rack/app_config'
6
6
  require_relative '../lib/moesif_rack'
@@ -61,10 +61,10 @@ class GovernanceRulesTest < Test::Unit::TestCase
61
61
  #for user id matched rules it depends on getting from config_rules_values
62
62
  #for that particular user id.
63
63
  # for this test case I will use this rule as fake input
64
- #https://www.moesif.com/wrap/app/88:210-1051:5/governance-rule/64a5b8f9aca3042266d36ebc
64
+ #https://www.moesif.com/wrap/app/88:210-660:387/governance-rule/64a783a3e7d62b036d16006e
65
65
  config_user_rules_values = [
66
66
  {
67
- "rules" => "64a5b8f9aca3042266d36ebc",
67
+ "rules" => "64a783a3e7d62b036d16006e",
68
68
  "values" => {
69
69
  "0" => "rome",
70
70
  "1" => "some value for 1",
@@ -105,9 +105,10 @@ class GovernanceRulesTest < Test::Unit::TestCase
105
105
  }
106
106
  user_id = 'vancouver1'
107
107
 
108
+ # https://www.moesif.com/wrap/app/88:210-660:387/governance-rule/64a783a43660b60f7c766a06
108
109
  config_user_rules_values = [
109
110
  {
110
- "rules" => "64a5b8fa3660b60f7c7662fc",
111
+ "rules" => "64a783a43660b60f7c766a06",
111
112
  "values" => {
112
113
  "0" => "city",
113
114
  "1" => "some value for 1",
@@ -45,6 +45,7 @@ class MoesifRackTest < Test::Unit::TestCase
45
45
  event_model
46
46
  }
47
47
  }
48
+
48
49
  @moesif_rack_app = MoesifRack::MoesifMiddleware.new(@app, @options)
49
50
  @app_config = AppConfig.new(true)
50
51
  end
@@ -72,9 +73,9 @@ class MoesifRackTest < Test::Unit::TestCase
72
73
  campaign_model = {"utm_source" => "Newsletter",
73
74
  "utm_medium" => "Email"}
74
75
 
75
- user_model = { "user_id" => "12345",
76
+ user_model = { "user_id" => "12345",
76
77
  "company_id" => "67890",
77
- "modified_time" => Time.now.utc.iso8601,
78
+ "modified_time" => Time.now.utc.iso8601,
78
79
  "metadata" => metadata,
79
80
  "campaign" => campaign_model}
80
81
 
@@ -91,14 +92,14 @@ class MoesifRackTest < Test::Unit::TestCase
91
92
 
92
93
  user_models = []
93
94
 
94
- user_model_A = { "user_id" => "12345",
95
+ user_model_A = { "user_id" => "12345",
95
96
  "company_id" => "67890",
96
- "modified_time" => Time.now.utc.iso8601,
97
+ "modified_time" => Time.now.utc.iso8601,
97
98
  "metadata" => metadata }
98
-
99
+
99
100
  user_model_B = { "user_id" => "1234",
100
- "company_id" => "6789",
101
- "modified_time" => Time.now.utc.iso8601,
101
+ "company_id" => "6789",
102
+ "modified_time" => Time.now.utc.iso8601,
102
103
  "metadata" => metadata }
103
104
 
104
105
  user_models << user_model_A << user_model_B
@@ -116,9 +117,10 @@ class MoesifRackTest < Test::Unit::TestCase
116
117
  def test_get_config
117
118
  @api_client = MoesifApi::MoesifAPIClient.new(@options['application_id'])
118
119
  @api_controller = @api_client.api
119
- @config = @app_config.get_config(@api_controller)
120
- @config_body, @config_etag, @last_updated_time = @app_config.parse_configuration(@config)
121
- @sampling_percentage = @app_config.get_sampling_percentage(nil, @config_body, nil, nil)
120
+ @app_config.get_config(@api_controller)
121
+ @sampling_percentage = @app_config.get_sampling_percentage(nil, nil, nil)
122
+ assert_instance_of Hash, @app_config.config
123
+ print "app config from test " + @app_config.config.to_s
122
124
  assert_operator 100, :>=, @sampling_percentage
123
125
  end
124
126
 
@@ -132,8 +134,8 @@ class MoesifRackTest < Test::Unit::TestCase
132
134
  campaign_model = {"utm_source" => "Adwords",
133
135
  "utm_medium" => "Twitter"}
134
136
 
135
- company_model = { "company_id" => "12345",
136
- "company_domain" => "acmeinc.com",
137
+ company_model = { "company_id" => "12345",
138
+ "company_domain" => "acmeinc.com",
137
139
  "metadata" => metadata,
138
140
  "campaign" => campaign_model }
139
141
 
@@ -151,11 +153,11 @@ class MoesifRackTest < Test::Unit::TestCase
151
153
  company_models = []
152
154
 
153
155
  company_model_A = { "company_id" => "12345",
154
- "company_domain" => "nowhere.com",
156
+ "company_domain" => "nowhere.com",
155
157
  "metadata" => metadata }
156
-
158
+
157
159
  company_model_B = { "company_id" => "1234",
158
- "company_domain" => "acmeinc.com",
160
+ "company_domain" => "acmeinc.com",
159
161
  "metadata" => metadata }
160
162
 
161
163
  company_models << company_model_A << company_model_B
@@ -163,4 +165,4 @@ class MoesifRackTest < Test::Unit::TestCase
163
165
  assert_equal response, nil
164
166
  end
165
167
 
166
- end
168
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moesif_rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moesif, Inc
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-07-10 00:00:00.000000000 Z
12
+ date: 2023-08-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-unit
@@ -37,14 +37,20 @@ dependencies:
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 1.2.17
40
+ version: '2'
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.0.2
41
44
  type: :runtime
42
45
  prerelease: false
43
46
  version_requirements: !ruby/object:Gem::Requirement
44
47
  requirements:
45
48
  - - "~>"
46
49
  - !ruby/object:Gem::Version
47
- version: 1.2.17
50
+ version: '2'
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.0.2
48
54
  description: Rack/Rails middleware to log API calls to Moesif API analytics and monitoring
49
55
  email: xing@moesif.com
50
56
  executables: []
@@ -67,8 +73,8 @@ files:
67
73
  - moesif_capture_outgoing/httplog/http_log.rb
68
74
  - test/config_example.json
69
75
  - test/govrule_example.json
70
- - test/moesif_rack_test.rb
71
76
  - test/test_governance_rules.rb
77
+ - test/test_moesif_rack.rb
72
78
  homepage: https://moesif.com
73
79
  licenses:
74
80
  - Apache-2.0
@@ -88,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
94
  requirements:
89
95
  - - ">="
90
96
  - !ruby/object:Gem::Version
91
- version: '2.0'
97
+ version: '2.6'
92
98
  required_rubygems_version: !ruby/object:Gem::Requirement
93
99
  requirements:
94
100
  - - ">="