diffend 0.2.18 → 0.2.26

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.
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'json'
6
+
7
+ module Diffend
8
+ # Module responsible for doing request to Diffend
9
+ module Request
10
+ # Message displayed when connection issue occured and we will retry
11
+ CONNECTION_MESSAGE = 'We experienced a connection issue, retrying...'
12
+ # List of connection exceptions
13
+ CONNECTION_EXCEPTIONS = [
14
+ Errno::ECONNRESET,
15
+ Errno::ENETUNREACH,
16
+ Errno::EHOSTUNREACH,
17
+ Errno::ECONNREFUSED
18
+ ].freeze
19
+ # Message displayed when timeout occured and we will retry
20
+ TIMEOUT_MESSAGE = 'We experienced a connection issue, retrying...'
21
+ # List of timeout exceptions
22
+ TIMEOUT_EXCEPTIONS = [
23
+ Net::OpenTimeout,
24
+ Net::ReadTimeout
25
+ ].freeze
26
+ # Message displayed when server issue occured and we will retry
27
+ SERVER_ERROR_MESSAGE = 'We experienced a server-side issue, retrying...'
28
+ # List of server issues
29
+ #
30
+ # 500 - Internal Server Error
31
+ # 502 - Bad Gateway
32
+ # 503 - Service Unavailable
33
+ # 504 - Gateway Timeout
34
+ SERVER_ERRORS = [500, 502, 503, 504].freeze
35
+ # Number of retries
36
+ RETRIES = 3
37
+ # Request headers
38
+ HEADERS = { 'Content-Type': 'application/json' }.freeze
39
+
40
+ private_constant :HEADERS
41
+
42
+ class << self
43
+ # Execute request
44
+ #
45
+ # @param config [OpenStruct] diffend config
46
+ # @param endpoint_url [String]
47
+ # @param payload [Hash]
48
+ #
49
+ # @return [Net::HTTPResponse] response from Diffend
50
+ def call(config, endpoint_url, payload)
51
+ retry_count ||= -1
52
+
53
+ build_http(endpoint_url) do |http, uri|
54
+ response = http.request(build_request(uri, config, payload))
55
+
56
+ if SERVER_ERRORS.include?(response.code.to_i)
57
+ raise Diffend::Errors::RequestServerError, response.code.to_i
58
+ end
59
+
60
+ response
61
+ end
62
+ rescue Diffend::Errors::RequestServerError => e
63
+ retry_count += 1
64
+
65
+ retry if handle_retry(SERVER_ERROR_MESSAGE, retry_count)
66
+
67
+ Diffend::HandleErrors::Report.call(
68
+ exception: e,
69
+ payload: payload,
70
+ config: config,
71
+ message: :request_error
72
+ )
73
+ rescue *CONNECTION_EXCEPTIONS => e
74
+ retry_count += 1
75
+
76
+ retry if handle_retry(CONNECTION_MESSAGE, retry_count)
77
+
78
+ Diffend::HandleErrors::Report.call(
79
+ exception: e,
80
+ payload: payload,
81
+ config: config,
82
+ message: :request_error
83
+ )
84
+ rescue *TIMEOUT_EXCEPTIONS => e
85
+ retry_count += 1
86
+
87
+ retry if handle_retry(TIMEOUT_MESSAGE, retry_count)
88
+
89
+ Diffend::HandleErrors::Report.call(
90
+ exception: e,
91
+ payload: payload,
92
+ config: config,
93
+ message: :request_error
94
+ )
95
+ end
96
+
97
+ # Handle retry
98
+ #
99
+ # @param message [String] message we want to display
100
+ # @param retry_count [Integer]
101
+ def handle_retry(message, retry_count)
102
+ return false if retry_count == RETRIES
103
+
104
+ Bundler.ui.error(message)
105
+ sleep(exponential_backoff(retry_count))
106
+
107
+ retry_count < RETRIES
108
+ end
109
+
110
+ # Builds http connection object
111
+ #
112
+ # @param url [String] command endpoint url
113
+ def build_http(url)
114
+ uri = URI(url)
115
+
116
+ Net::HTTP.start(
117
+ uri.host,
118
+ uri.port,
119
+ use_ssl: uri.scheme == 'https',
120
+ verify_mode: OpenSSL::SSL::VERIFY_NONE,
121
+ open_timeout: 5,
122
+ read_timeout: 5
123
+ ) { |http| yield(http, uri) }
124
+ end
125
+
126
+ # Build http post request and assigns headers and payload
127
+ #
128
+ # @param uri [URI::HTTPS]
129
+ # @param config [OpenStruct] Diffend config
130
+ # @param payload [Hash] with versions to check
131
+ #
132
+ # @return [Net::HTTP::Post]
133
+ def build_request(uri, config, payload)
134
+ Net::HTTP::Post
135
+ .new(uri.request_uri, HEADERS)
136
+ .tap { |request| assign_auth(request, config) }
137
+ .tap { |request| assign_payload(request, payload) }
138
+ end
139
+
140
+ # Assigns basic authorization if provided in the config
141
+ #
142
+ # @param request [Net::HTTP::Post] prepared http post
143
+ # @param config [OpenStruct] Diffend config
144
+ def assign_auth(request, config)
145
+ return unless config
146
+ return unless config.shareable_id
147
+ return unless config.shareable_key
148
+
149
+ request.basic_auth(config.shareable_id, config.shareable_key)
150
+ end
151
+
152
+ # Assigns payload as json
153
+ #
154
+ # @param request [Net::HTTP::Post] prepared http post
155
+ # @param payload [Hash] with versions to check
156
+ def assign_payload(request, payload)
157
+ request.body = JSON.dump(payload: payload)
158
+ end
159
+
160
+ def exponential_backoff(retry_count)
161
+ 2**(retry_count + 1)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -7,23 +7,33 @@ module Diffend
7
7
  # Build verdict
8
8
  #
9
9
  # @param command [String] either install or update
10
+ # @param config [OpenStruct] diffend config
10
11
  # @param definition [Bundler::Definition] definition for your source
11
- def call(command, definition)
12
+ def call(command, config, definition)
12
13
  Versions::Remote
13
- .call(command, definition)
14
- .tap { |response| build_message(command, response) }
14
+ .call(command, config, definition)
15
+ .tap { |response| build_message(command, config, response) }
15
16
  end
16
17
 
17
- def build_message(command, response)
18
+ # @param command [String] either install or update
19
+ # @param config [OpenStruct] diffend config
20
+ # @param response [Hash] response from diffend API
21
+ def build_message(command, config, response)
18
22
  if response.key?('error')
19
23
  build_error(response)
20
24
  elsif response.key?('action')
21
- build_verdict(command, response)
25
+ build_verdict(command, config, response)
22
26
  else
23
- raise UnsupportedResponse, response['action']
27
+ Diffend::HandleErrors::Report.call(
28
+ config: config,
29
+ message: :unsupported_response,
30
+ payload: response,
31
+ report: true
32
+ )
24
33
  end
25
34
  end
26
35
 
36
+ # @param response [Hash] response from diffend API
27
37
  def build_error(response)
28
38
  build_error_message(response)
29
39
  .tap(&Bundler.ui.method(:error))
@@ -31,21 +41,35 @@ module Diffend
31
41
  exit 1
32
42
  end
33
43
 
34
- def build_verdict(command, response)
44
+ # @param command [String] either install or update
45
+ # @param config [OpenStruct] diffend config
46
+ # @param response [Hash] response from diffend API
47
+ def build_verdict(command, config, response)
35
48
  case response['action']
36
49
  when 'allow'
37
50
  build_allow_message(command, response)
38
51
  .tap(&Bundler.ui.method(:confirm))
52
+ when 'warn'
53
+ build_warn_message(command, response)
54
+ .tap(&Bundler.ui.method(:warn))
39
55
  when 'deny'
40
56
  build_deny_message(command, response)
41
57
  .tap(&Bundler.ui.method(:error))
42
58
 
43
59
  exit 1
44
60
  else
45
- raise UnsupportedAction, response['action']
61
+ Diffend::HandleErrors::Report.call(
62
+ config: config,
63
+ message: :unsupported_verdict,
64
+ payload: response,
65
+ report: true
66
+ )
46
67
  end
47
68
  end
48
69
 
70
+ # @param response [Hash] response from diffend API
71
+ #
72
+ # @return [String]
49
73
  def build_error_message(response)
50
74
  <<~MSG
51
75
  \nDiffend returned an error for your request.\n
@@ -53,21 +77,56 @@ module Diffend
53
77
  MSG
54
78
  end
55
79
 
80
+ # @param command [String] either install or update
81
+ # @param response [Hash] response from diffend API
82
+ #
83
+ # @return [String]
56
84
  def build_allow_message(command, response)
57
85
  <<~MSG
58
- \nDiffend reported an allow verdict for #{command} command for this project.\n
59
- All of our #{response['allows_count'] + response['denies_count']} checks succeeded.\n
86
+ #{build_message_header('an allow', command)}
87
+ #{build_message_info(response)}\n
60
88
  #{response['review_url']}\n
61
89
  MSG
62
90
  end
63
91
 
92
+ # @param command [String] either install or update
93
+ # @param response [Hash] response from diffend API
94
+ #
95
+ # @return [String]
96
+ def build_warn_message(command, response)
97
+ <<~MSG
98
+ #{build_message_header('a warn', command)}
99
+ #{build_message_info(response)} Please go to the url below and review the issues.\n
100
+ #{response['review_url']}\n
101
+ MSG
102
+ end
103
+
104
+ # @param command [String] either install or update
105
+ # @param response [Hash] response from diffend API
106
+ #
107
+ # @return [String]
64
108
  def build_deny_message(command, response)
65
109
  <<~MSG
66
- \nDiffend reported a deny verdict for #{command} command for this project.\n
67
- #{response['denies_count']} out of our #{response['allows_count'] + response['denies_count']} checks failed. Please go to the url below and review the issues.\n
110
+ #{build_message_header('a deny', command)}
111
+ #{build_message_info(response)} Please go to the url below and review the issues.\n
68
112
  #{response['review_url']}\n
69
113
  MSG
70
114
  end
115
+
116
+ # @param type [String] verdict type
117
+ # @param command [String] either install or update
118
+ #
119
+ # @return [String]
120
+ def build_message_header(type, command)
121
+ "\nDiffend reported #{type} verdict for #{command} command for this project."
122
+ end
123
+
124
+ # @param response [Hash] response from diffend API
125
+ #
126
+ # @return [String]
127
+ def build_message_info(response)
128
+ "\nQuality score: #{response['quality_score']}, allows: #{response['allows_count']}, warnings: #{response['warns_count']}, denies: #{response['denies_count']}."
129
+ end
71
130
  end
72
131
  end
73
132
  end
@@ -13,17 +13,22 @@ module Diffend
13
13
  Bundler::Source::Gemspec,
14
14
  Bundler::Source::Path
15
15
  ].freeze
16
- DEPENDENCIES = {
16
+ # List of dependency types
17
+ DEPENDENCIES_TYPES = {
17
18
  direct: 0,
18
- dep: 1
19
+ dependency: 1
19
20
  }.freeze
20
- SOURCES = {
21
+ # List of sources types
22
+ SOURCES_TYPES = {
21
23
  valid: 0,
22
24
  multiple_primary: 1
23
25
  }.freeze
24
- GEM_SOURCES = {
26
+ # List of gem sources types
27
+ GEM_SOURCES_TYPES = {
25
28
  local: 0,
26
- gemfile: 1
29
+ gemfile_source: 1,
30
+ gemfile_git: 2,
31
+ gemfile_path: 3
27
32
  }.freeze
28
33
 
29
34
  class << self
@@ -49,43 +54,45 @@ module Diffend
49
54
  def initialize(definition)
50
55
  @definition = definition
51
56
  @direct_dependencies = Hash[definition.dependencies.map { |val| [val.name, val] }]
52
- @main_source = definition.send(:sources).rubygems_sources.last
53
57
  # Support case without Gemfile.lock
54
58
  @locked_specs = @definition.locked_gems ? @definition.locked_gems.specs : []
55
59
  end
56
60
 
61
+ # Build install specification
62
+ #
63
+ # @return [Hash]
57
64
  def build_install
58
65
  hash = build_main
59
66
 
60
- @definition.requested_specs.each do |spec|
67
+ @definition.specs.each do |spec|
61
68
  next if skip?(spec.source)
62
69
 
63
70
  locked_spec = @locked_specs.find { |s| s.name == spec.name }
64
71
 
65
- spec2 = locked_spec || spec
66
-
67
- hash['dependencies'][spec2.name] = {
68
- 'platform' => build_spec_platform(spec2),
69
- 'source' => build_spec_source(spec2),
70
- 'type' => build_dependency_type(spec2.name),
71
- 'versions' => build_versions(spec2)
72
+ hash['dependencies'][spec.name] = {
73
+ 'platform' => build_spec_platform(spec, locked_spec),
74
+ 'source' => build_spec_source(spec),
75
+ 'type' => build_dependency_type(spec.name),
76
+ 'versions' => build_versions(spec, locked_spec)
72
77
  }
73
78
  end
74
79
 
75
80
  hash
76
81
  end
77
82
 
78
- # @param definition [Bundler::Definition] definition for your source
83
+ # Build update specification
84
+ #
85
+ # @return [Hash]
79
86
  def build_update
80
87
  hash = build_main
81
88
 
82
- @definition.requested_specs.each do |spec|
89
+ @definition.specs.each do |spec|
83
90
  next if skip?(spec.source)
84
91
 
85
92
  locked_spec = @locked_specs.find { |s| s.name == spec.name }
86
93
 
87
94
  hash['dependencies'][spec.name] = {
88
- 'platform' => build_spec_platform(spec),
95
+ 'platform' => build_spec_platform(spec, locked_spec),
89
96
  'source' => build_spec_source(spec),
90
97
  'type' => build_dependency_type(spec.name),
91
98
  'versions' => build_versions(spec, locked_spec)
@@ -97,6 +104,9 @@ module Diffend
97
104
 
98
105
  private
99
106
 
107
+ # Build default specification
108
+ #
109
+ # @return [Hash]
100
110
  def build_main
101
111
  {
102
112
  'dependencies' => {},
@@ -106,8 +116,18 @@ module Diffend
106
116
  }
107
117
  end
108
118
 
119
+ # Build gem versions
120
+ #
121
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
122
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
123
+ #
124
+ # @return [Array<String>]
109
125
  def build_versions(spec, locked_spec = nil)
110
- locked_spec ? [locked_spec.version.to_s, spec.version.to_s] : [spec.version.to_s]
126
+ if locked_spec && locked_spec.version.to_s != spec.version.to_s
127
+ [locked_spec.version.to_s, spec.version.to_s]
128
+ else
129
+ [spec.version.to_s]
130
+ end
111
131
  end
112
132
 
113
133
  # @param specs [Array] specs that are direct dependencies
@@ -115,78 +135,162 @@ module Diffend
115
135
  #
116
136
  # @return [Boolean] dependency type
117
137
  def build_dependency_type(name)
118
- @direct_dependencies.key?(name) ? DEPENDENCIES[:direct] : DEPENDENCIES[:dep]
138
+ if @direct_dependencies.key?(name)
139
+ DEPENDENCIES_TYPES[:direct]
140
+ else
141
+ DEPENDENCIES_TYPES[:dependency]
142
+ end
143
+ end
144
+
145
+ # Build gem platform
146
+ #
147
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
148
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
149
+ #
150
+ # @return [String]
151
+ def build_spec_platform(spec, locked_spec)
152
+ parse_platform(
153
+ spec.platform || locked_spec&.platform || spec.send(:generic_local_platform)
154
+ )
155
+ end
156
+
157
+ # Parse gem platform
158
+ #
159
+ # @param platform [String, Gem::Platform]
160
+ #
161
+ # @return [String]
162
+ def parse_platform(platform)
163
+ case platform
164
+ when String then platform
165
+ when Gem::Platform then platform.os
166
+ end
119
167
  end
120
168
 
121
- def build_spec_platform(spec)
122
- spec.platform || spec.send(:generic_local_platform)
169
+ # Build gem source type
170
+ #
171
+ # @param source [Bundler::Source] gem source type
172
+ #
173
+ # @return [Integer] internal gem source type
174
+ def build_spec_gem_source_type(source)
175
+ case source
176
+ when Bundler::Source::Metadata
177
+ GEM_SOURCES_TYPES[:local]
178
+ when Bundler::Source::Rubygems, Bundler::Source::Rubygems::Remote
179
+ GEM_SOURCES_TYPES[:gemfile_source]
180
+ when Bundler::Source::Git
181
+ GEM_SOURCES_TYPES[:gemfile_git]
182
+ when Bundler::Source::Path
183
+ GEM_SOURCES_TYPES[:gemfile_path]
184
+ else
185
+ raise ArgumentError, "unknown source #{source.class}"
186
+ end
123
187
  end
124
188
 
189
+ # Build gem source
190
+ #
191
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
192
+ #
193
+ # @return [Hash]
125
194
  def build_spec_source(spec)
126
- if @direct_dependencies.key?(spec.name)
127
- dep_spec = @direct_dependencies[spec.name]
195
+ source = source_for_spec(spec)
128
196
 
129
- if dep_spec.source
130
- { 'type' => GEM_SOURCES[:gemfile], 'url' => source_name_from_source(dep_spec.source) }
131
- else
132
- { 'type' => GEM_SOURCES[:gemfile], 'url' => source_name_from_source(@main_source) }
133
- end
197
+ {
198
+ 'type' => build_spec_gem_source_type(source),
199
+ 'value' => source_name_from_source(source)
200
+ }
201
+ end
202
+
203
+ # Figure out source for gem
204
+ #
205
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
206
+ #
207
+ # @return [Bundler::Source] gem source type
208
+ def source_for_spec(spec)
209
+ return spec.remote if spec.remote
210
+
211
+ case spec.source
212
+ when Bundler::Source::Rubygems
213
+ spec
214
+ .source
215
+ .send(:remote_specs)
216
+ .search(Bundler::Dependency.new(spec.name, spec.version))
217
+ .last
218
+ .remote
219
+ when Bundler::Source::Metadata, Bundler::Source::Git, Bundler::Source::Path
220
+ spec.source
134
221
  else
135
- if spec.source.respond_to?(:remotes)
136
- { 'type' => GEM_SOURCES[:gemfile], 'url' => source_name_from_source(spec.source) }
137
- else
138
- { 'type' => GEM_SOURCES[:local], 'url' => '' }
139
- end
222
+ raise ArgumentError, "unknown source #{spec.source.class}"
140
223
  end
141
224
  end
142
225
 
226
+ # Build gem source name
227
+ #
228
+ # @param source [Bundler::Source] gem source type
229
+ #
230
+ # @return [String]
143
231
  def source_name_from_source(source)
144
- source_name(source.remotes.first)
232
+ case source
233
+ when Bundler::Source::Metadata
234
+ ''
235
+ when Bundler::Source::Rubygems::Remote
236
+ source_name(source.anonymized_uri)
237
+ when Bundler::Source::Git
238
+ source.instance_variable_get(:@safe_uri)
239
+ when Bundler::Source::Path
240
+ source.path
241
+ else
242
+ raise ArgumentError, "unknown source #{source.class}"
243
+ end
145
244
  end
146
245
 
147
- def source_name(name)
148
- name.to_s[0...-1]
246
+ # @param uri [Bundler::URI]
247
+ #
248
+ # @return [String]
249
+ def source_name(uri)
250
+ uri.to_s[0...-1]
149
251
  end
150
252
 
253
+ # Build sources used in the Gemfile
254
+ #
255
+ # @return [Array<Hash>]
151
256
  def build_sources
152
257
  sources = @definition.send(:sources).rubygems_sources
153
- hash = []
258
+ hash = {}
154
259
 
155
260
  sources.each do |source|
156
- type = source.remotes.count > 1 ? SOURCES[:multiple_primary] : SOURCES[:valid]
261
+ type = build_source_type(source.remotes)
157
262
 
158
263
  source.remotes.each do |src|
159
- hash << { 'name' => source_name(src), 'type' => type }
264
+ hash[source_name(src)] = type
160
265
  end
161
266
  end
162
267
 
163
- hash
268
+ hash.map { |name, type| { 'name' => name, 'type' => type } }
269
+ end
270
+
271
+ # Build gem source type
272
+ #
273
+ # @param remotes [Array<Bundler::URI>]
274
+ #
275
+ # @return [Integer] internal source type
276
+ def build_source_type(remotes)
277
+ remotes.count > 1 ? SOURCES_TYPES[:multiple_primary] : SOURCES_TYPES[:valid]
164
278
  end
165
279
 
166
280
  # Checks if we should skip a source
167
281
  #
168
- # @param source [Bundler::Source::Git, Bundler::Source::Rubygems]
282
+ # @param source [Bundler::Source] gem source type
169
283
  #
170
284
  # @return [Boolean] true if we should skip this source, false otherwise
171
285
  def skip?(source)
172
- return true if git?(source)
173
286
  return true if me?(source)
174
287
 
175
288
  false
176
289
  end
177
290
 
178
- # Checks if it's a git source
179
- #
180
- # @param source [Bundler::Source::Git, Bundler::Source::Rubygems]
181
- #
182
- # @return [Boolean] true if it's a git source, false otherwise
183
- def git?(source)
184
- source.instance_of?(Bundler::Source::Git)
185
- end
186
-
187
291
  # Checks if it's a self source, this happens for repositories that are a gem
188
292
  #
189
- # @param source [Bundler::Source::Path,Bundler::Source::Git,Bundler::Source::Rubygems]
293
+ # @param source [Bundler::Source] gem source type
190
294
  #
191
295
  # @return [Boolean] true if it's a self source, false otherwise
192
296
  def me?(source)