diffend 0.2.16 → 0.2.23

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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ # Module responsible for displaying exception payload to stdout
6
+ module DisplayToStdout
7
+ class << self
8
+ # Display exception payload to stdout
9
+ #
10
+ # @param exception_payload [Hash]
11
+ def call(exception_payload)
12
+ puts exception_payload.to_json
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ module Messages
6
+ PAYLOAD_DUMP = '^^^ Above is the dump of your request ^^^'
7
+ UNHANDLED_EXCEPTION = <<~MSG
8
+ \nSomething went really wrong. We recorded this incident in our system and will review it.\n
9
+ This is a bug, don't hesitate.\n
10
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
11
+ MSG
12
+ REQUEST_ERROR = <<~MSG
13
+ \nWe were unable to process your request at this time. We recorded this incident in our system and will review it.\n
14
+ If you think that this is a bug, don't hesitate.\n
15
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
16
+ MSG
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ # Module responsible for reporting errors to diffend
6
+ module Report
7
+ class << self
8
+ # Execute request to Diffend
9
+ #
10
+ # @param exception [Exception] expection that was raised
11
+ # @param payload [Hash] with versions to check
12
+ # @param config [OpenStruct] Diffend config
13
+ # @param message [Symbol] message that we want to display
14
+ # @param report [Boolean] if true we will report the issue to diffend
15
+ #
16
+ # @return [Net::HTTPResponse] response from Diffend
17
+ def call(exception:, payload: {}, config:, message:, report: false)
18
+ exception_payload = prepare_exception_payload(exception, payload)
19
+
20
+ Bundler.ui.error(Diffend::HandleErrors::Messages::PAYLOAD_DUMP)
21
+ Bundler.ui.error(Diffend::HandleErrors::Messages.const_get(message.to_s.upcase))
22
+
23
+ if report
24
+ Diffend::Request.call(
25
+ config,
26
+ errors_url(config.project_id),
27
+ exception_payload
28
+ )
29
+ end
30
+
31
+ exit 1
32
+ end
33
+
34
+ # Prepare exception payload and display it to stdout
35
+ #
36
+ # @param exception [Exception] expection that was raised
37
+ # @param payload [Hash] with versions to check
38
+ #
39
+ # @return [Hash]
40
+ def prepare_exception_payload(exception, payload)
41
+ Diffend::HandleErrors::BuildExceptionPayload
42
+ .call(exception, payload)
43
+ .tap(&Diffend::HandleErrors::DisplayToStdout.method(:call))
44
+ end
45
+
46
+ # Provides diffend errors endpoint url
47
+ #
48
+ # @param project_id [String] diffend project_id
49
+ #
50
+ # @return [String] diffend endpoint
51
+ def errors_url(project_id)
52
+ return ENV['DIFFEND_ERROR_URL'] if ENV.key?('DIFFEND_ERROR_URL')
53
+
54
+ "https://my.diffend.io/api/projects/#{project_id}/errors"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,139 @@
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
+ # Number of retries
27
+ RETRIES = 3
28
+ # Request headers
29
+ HEADERS = { 'Content-Type': 'application/json' }.freeze
30
+
31
+ private_constant :HEADERS
32
+
33
+ class << self
34
+ # Execute request
35
+ #
36
+ # @param config [OpenStruct] diffend config
37
+ # @param endpoint_url [String]
38
+ # @param payload [Hash]
39
+ #
40
+ # @return [Net::HTTPResponse] response from Diffend
41
+ def call(config, endpoint_url, payload)
42
+ retry_count ||= -1
43
+
44
+ build_http(endpoint_url) do |http, uri|
45
+ http.request(build_request(uri, config, payload))
46
+ end
47
+ rescue *CONNECTION_EXCEPTIONS => e
48
+ retry_count += 1
49
+
50
+ retry if handle_retry(CONNECTION_MESSAGE, retry_count)
51
+
52
+ Diffend::HandleErrors::Report.call(
53
+ exception: e,
54
+ payload: payload,
55
+ config: config,
56
+ message: :request_error
57
+ )
58
+ rescue *TIMEOUT_EXCEPTIONS => e
59
+ retry_count += 1
60
+
61
+ retry if handle_retry(TIMEOUT_MESSAGE, retry_count)
62
+
63
+ Diffend::HandleErrors::Report.call(
64
+ exception: e,
65
+ payload: payload,
66
+ config: config,
67
+ message: :request_error
68
+ )
69
+ end
70
+
71
+ # Handle retry
72
+ #
73
+ # @param message [String] message we want to display
74
+ # @param retry_count [Integer]
75
+ def handle_retry(message, retry_count)
76
+ return false if retry_count == RETRIES
77
+
78
+ Bundler.ui.error(message)
79
+ sleep(exponential_backoff(retry_count))
80
+
81
+ retry_count < RETRIES
82
+ end
83
+
84
+ # Builds http connection object
85
+ #
86
+ # @param url [String] command endpoint url
87
+ def build_http(url)
88
+ uri = URI(url)
89
+
90
+ Net::HTTP.start(
91
+ uri.host,
92
+ uri.port,
93
+ use_ssl: uri.scheme == 'https',
94
+ verify_mode: OpenSSL::SSL::VERIFY_NONE,
95
+ open_timeout: 5,
96
+ read_timeout: 5
97
+ ) { |http| yield(http, uri) }
98
+ end
99
+
100
+ # Build http post request and assigns headers and payload
101
+ #
102
+ # @param uri [URI::HTTPS]
103
+ # @param config [OpenStruct] Diffend config
104
+ # @param payload [Hash] with versions to check
105
+ #
106
+ # @return [Net::HTTP::Post]
107
+ def build_request(uri, config, payload)
108
+ Net::HTTP::Post
109
+ .new(uri.request_uri, HEADERS)
110
+ .tap { |request| assign_auth(request, config) }
111
+ .tap { |request| assign_payload(request, payload) }
112
+ end
113
+
114
+ # Assigns basic authorization if provided in the config
115
+ #
116
+ # @param request [Net::HTTP::Post] prepared http post
117
+ # @param config [OpenStruct] Diffend config
118
+ def assign_auth(request, config)
119
+ return unless config
120
+ return unless config.shareable_id
121
+ return unless config.shareable_key
122
+
123
+ request.basic_auth(config.shareable_id, config.shareable_key)
124
+ end
125
+
126
+ # Assigns payload as json
127
+ #
128
+ # @param request [Net::HTTP::Post] prepared http post
129
+ # @param payload [Hash] with versions to check
130
+ def assign_payload(request, payload)
131
+ request.body = JSON.dump(payload: payload)
132
+ end
133
+
134
+ def exponential_backoff(retry_count)
135
+ 2**(retry_count + 1)
136
+ end
137
+ end
138
+ end
139
+ 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
@@ -54,38 +59,41 @@ module Diffend
54
59
  @locked_specs = @definition.locked_gems ? @definition.locked_gems.specs : []
55
60
  end
56
61
 
62
+ # Build install specification
63
+ #
64
+ # @return [Hash]
57
65
  def build_install
58
66
  hash = build_main
59
67
 
60
- @definition.requested_specs.each do |spec|
68
+ @definition.specs.each do |spec|
61
69
  next if skip?(spec.source)
62
70
 
63
71
  locked_spec = @locked_specs.find { |s| s.name == spec.name }
64
72
 
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)
73
+ hash['dependencies'][spec.name] = {
74
+ 'platform' => build_spec_platform(spec, locked_spec),
75
+ 'source' => build_spec_source(spec),
76
+ 'type' => build_dependency_type(spec.name),
77
+ 'versions' => build_versions(spec, locked_spec)
72
78
  }
73
79
  end
74
80
 
75
81
  hash
76
82
  end
77
83
 
78
- # @param definition [Bundler::Definition] definition for your source
84
+ # Build update specification
85
+ #
86
+ # @return [Hash]
79
87
  def build_update
80
88
  hash = build_main
81
89
 
82
- @definition.requested_specs.each do |spec|
90
+ @definition.specs.each do |spec|
83
91
  next if skip?(spec.source)
84
92
 
85
93
  locked_spec = @locked_specs.find { |s| s.name == spec.name }
86
94
 
87
95
  hash['dependencies'][spec.name] = {
88
- 'platform' => build_spec_platform(spec),
96
+ 'platform' => build_spec_platform(spec, locked_spec),
89
97
  'source' => build_spec_source(spec),
90
98
  'type' => build_dependency_type(spec.name),
91
99
  'versions' => build_versions(spec, locked_spec)
@@ -97,6 +105,9 @@ module Diffend
97
105
 
98
106
  private
99
107
 
108
+ # Build default specification
109
+ #
110
+ # @return [Hash]
100
111
  def build_main
101
112
  {
102
113
  'dependencies' => {},
@@ -106,8 +117,18 @@ module Diffend
106
117
  }
107
118
  end
108
119
 
120
+ # Build gem versions
121
+ #
122
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
123
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
124
+ #
125
+ # @return [Array<String>]
109
126
  def build_versions(spec, locked_spec = nil)
110
- locked_spec ? [locked_spec.version.to_s, spec.version.to_s] : [spec.version.to_s]
127
+ if locked_spec && locked_spec.version.to_s != spec.version.to_s
128
+ [locked_spec.version.to_s, spec.version.to_s]
129
+ else
130
+ [spec.version.to_s]
131
+ end
111
132
  end
112
133
 
113
134
  # @param specs [Array] specs that are direct dependencies
@@ -115,78 +136,152 @@ module Diffend
115
136
  #
116
137
  # @return [Boolean] dependency type
117
138
  def build_dependency_type(name)
118
- @direct_dependencies.key?(name) ? DEPENDENCIES[:direct] : DEPENDENCIES[:dep]
139
+ if @direct_dependencies.key?(name)
140
+ DEPENDENCIES_TYPES[:direct]
141
+ else
142
+ DEPENDENCIES_TYPES[:dependency]
143
+ end
119
144
  end
120
145
 
121
- def build_spec_platform(spec)
122
- spec.platform || spec.send(:generic_local_platform)
146
+ # Build gem platform
147
+ #
148
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
149
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
150
+ #
151
+ # @return [String]
152
+ def build_spec_platform(spec, locked_spec)
153
+ spec.platform || locked_spec&.platform || build_spec_generic_platform(spec)
154
+ end
155
+
156
+ # Build gem generic platform
157
+ #
158
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
159
+ #
160
+ # @return [String]
161
+ def build_spec_generic_platform(spec)
162
+ platform = spec.send(:generic_local_platform)
163
+
164
+ case platform
165
+ when String then platform
166
+ when Gem::Platform then platform.os
167
+ end
168
+ end
169
+
170
+ # Build gem source type
171
+ #
172
+ # @param source [Bundler::Source] gem source type
173
+ #
174
+ # @return [Integer] internal gem source type
175
+ def build_spec_gem_source_type(source)
176
+ case source
177
+ when Bundler::Source::Metadata
178
+ GEM_SOURCES_TYPES[:local]
179
+ when Bundler::Source::Rubygems
180
+ GEM_SOURCES_TYPES[:gemfile_source]
181
+ when Bundler::Source::Git
182
+ GEM_SOURCES_TYPES[:gemfile_git]
183
+ when Bundler::Source::Path
184
+ GEM_SOURCES_TYPES[:gemfile_path]
185
+ else
186
+ raise ArgumentError, "unknown source #{source.class}"
187
+ end
123
188
  end
124
189
 
190
+ # Build gem source
191
+ #
192
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
193
+ #
194
+ # @return [Hash]
125
195
  def build_spec_source(spec)
126
- if @direct_dependencies.key?(spec.name)
127
- dep_spec = @direct_dependencies[spec.name]
196
+ source = source_for_spec(spec)
128
197
 
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
198
+ {
199
+ 'type' => build_spec_gem_source_type(source),
200
+ 'value' => source_name_from_source(source)
201
+ }
202
+ end
203
+
204
+ # Figure out source for gem
205
+ #
206
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
207
+ #
208
+ # @return [Bundler::Source] gem source type
209
+ def source_for_spec(spec)
210
+ if @direct_dependencies.key?(spec.name)
211
+ @direct_dependencies[spec.name].source || @main_source
134
212
  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
213
+ spec.source
140
214
  end
141
215
  end
142
216
 
217
+ # Build gem source name
218
+ #
219
+ # @param source [Bundler::Source] gem source type
220
+ #
221
+ # @return [String]
143
222
  def source_name_from_source(source)
144
- source_name(source.remotes.first)
223
+ case source
224
+ when Bundler::Source::Metadata
225
+ ''
226
+ when Bundler::Source::Rubygems
227
+ source_name(source.remotes.first)
228
+ when Bundler::Source::Git
229
+ source.uri
230
+ when Bundler::Source::Path
231
+ source.path
232
+ else
233
+ raise ArgumentError, "unknown source #{source.class}"
234
+ end
145
235
  end
146
236
 
237
+ # @param name [Bundler::URI]
238
+ #
239
+ # @return [String] cleaned source name
147
240
  def source_name(name)
148
241
  name.to_s[0...-1]
149
242
  end
150
243
 
244
+ # Build sources used in the Gemfile
245
+ #
246
+ # @return [Array<Hash>]
151
247
  def build_sources
152
248
  sources = @definition.send(:sources).rubygems_sources
153
- hash = []
249
+ hash = {}
154
250
 
155
251
  sources.each do |source|
156
- type = source.remotes.count > 1 ? SOURCES[:multiple_primary] : SOURCES[:valid]
252
+ type = build_source_type(source.remotes)
157
253
 
158
254
  source.remotes.each do |src|
159
- hash << { 'name' => source_name(src), 'type' => type }
255
+ hash[source_name(src)] = type
160
256
  end
161
257
  end
162
258
 
163
- hash
259
+ hash.map { |name, type| { 'name' => name, 'type' => type } }
260
+ end
261
+
262
+ # Build gem source type
263
+ #
264
+ # @param remotes [Array<Bundler::URI>]
265
+ #
266
+ # @return [Integer] internal source type
267
+ def build_source_type(remotes)
268
+ remotes.count > 1 ? SOURCES_TYPES[:multiple_primary] : SOURCES_TYPES[:valid]
164
269
  end
165
270
 
166
271
  # Checks if we should skip a source
167
272
  #
168
- # @param source [Bundler::Source::Git, Bundler::Source::Rubygems]
273
+ # @param source [Bundler::Source] gem source type
169
274
  #
170
275
  # @return [Boolean] true if we should skip this source, false otherwise
171
276
  def skip?(source)
172
- return true if git?(source)
173
277
  return true if me?(source)
174
278
 
175
279
  false
176
280
  end
177
281
 
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
282
  # Checks if it's a self source, this happens for repositories that are a gem
188
283
  #
189
- # @param source [Bundler::Source::Path,Bundler::Source::Git,Bundler::Source::Rubygems]
284
+ # @param source [Bundler::Source] gem source type
190
285
  #
191
286
  # @return [Boolean] true if it's a self source, false otherwise
192
287
  def me?(source)