dependabot-common 0.392.0 → 0.394.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/clients/azure.rb +42 -22
  3. data/lib/dependabot/clients/bitbucket.rb +52 -26
  4. data/lib/dependabot/clients/bitbucket_with_retries.rb +11 -12
  5. data/lib/dependabot/clients/codecommit.rb +73 -53
  6. data/lib/dependabot/clients/github_resource_parser.rb +70 -0
  7. data/lib/dependabot/clients/github_with_retries.rb +63 -23
  8. data/lib/dependabot/clients/gitlab_with_retries.rb +19 -17
  9. data/lib/dependabot/clients/json_response_parser.rb +103 -0
  10. data/lib/dependabot/command_helpers.rb +79 -11
  11. data/lib/dependabot/credential.rb +12 -3
  12. data/lib/dependabot/errors.rb +4 -1
  13. data/lib/dependabot/file_fetchers/base.rb +199 -151
  14. data/lib/dependabot/file_filtering.rb +0 -2
  15. data/lib/dependabot/file_updaters/vendor_updater.rb +8 -2
  16. data/lib/dependabot/git_tag_details.rb +8 -14
  17. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +211 -110
  18. data/lib/dependabot/metadata_finders/base/commit_response_parser.rb +68 -0
  19. data/lib/dependabot/metadata_finders/base/commits_finder.rb +44 -38
  20. data/lib/dependabot/package/npm_package_manager_config.rb +72 -0
  21. data/lib/dependabot/package/npm_registry_package.rb +186 -0
  22. data/lib/dependabot/pull_request_creator/azure.rb +1 -1
  23. data/lib/dependabot/pull_request_creator/codecommit.rb +10 -12
  24. data/lib/dependabot/pull_request_creator/github.rb +55 -35
  25. data/lib/dependabot/pull_request_creator/gitlab.rb +37 -11
  26. data/lib/dependabot/pull_request_creator/labeler.rb +65 -15
  27. data/lib/dependabot/pull_request_creator/pr_name_prefixer.rb +179 -78
  28. data/lib/dependabot/pull_request_updater/azure.rb +63 -11
  29. data/lib/dependabot/pull_request_updater/github.rb +44 -34
  30. data/lib/dependabot/pull_request_updater/gitlab.rb +23 -12
  31. data/lib/dependabot/pull_request_updater.rb +3 -3
  32. data/lib/dependabot/shared_helpers.rb +77 -32
  33. data/lib/dependabot/update_checkers/base.rb +9 -5
  34. data/lib/dependabot/update_checkers/version_filters.rb +5 -0
  35. data/lib/dependabot/update_checkers/vulnerability_audit.rb +282 -0
  36. data/lib/dependabot/workspace/git.rb +35 -4
  37. data/lib/dependabot.rb +1 -1
  38. metadata +10 -4
@@ -83,19 +83,19 @@ module Dependabot
83
83
  commit: T.nilable(String),
84
84
  path: T.nilable(String)
85
85
  )
86
- # See PR 9344: should .returns(Seahorse::Client::Response)
87
- # but it not extend Delegator, unblocking until shim or
88
- # another fix is implemented
89
- .returns(T.untyped)
86
+ .returns(Seahorse::Client::Response)
90
87
  end
91
88
  def fetch_repo_contents(repo, commit = nil, path = nil)
92
89
  actual_path = path
93
90
  actual_path = "/" if path.to_s.empty?
94
91
 
95
- cc_client.get_folder(
96
- repository_name: repo,
97
- commit_specifier: commit,
98
- folder_path: actual_path
92
+ T.cast(
93
+ cc_client.get_folder(
94
+ repository_name: repo,
95
+ commit_specifier: commit,
96
+ folder_path: actual_path
97
+ ),
98
+ Seahorse::Client::Response
99
99
  )
100
100
  end
101
101
 
@@ -121,12 +121,15 @@ module Dependabot
121
121
  params(
122
122
  branch_name: String
123
123
  )
124
- .returns(String)
124
+ .returns(Seahorse::Client::Response)
125
125
  end
126
126
  def branch(branch_name)
127
- cc_client.get_branch(
128
- repository_name: source.unscoped_repo,
129
- branch_name: branch_name
127
+ T.cast(
128
+ cc_client.get_branch(
129
+ repository_name: source.unscoped_repo,
130
+ branch_name: branch_name
131
+ ),
132
+ Seahorse::Client::Response
130
133
  )
131
134
  end
132
135
 
@@ -183,18 +186,24 @@ module Dependabot
183
186
  repo: String,
184
187
  branch_name: String
185
188
  )
186
- .returns(Aws::CodeCommit::Types::Commit)
189
+ .returns(Seahorse::Client::Response)
187
190
  end
188
191
  def commits(repo, branch_name = T.must(source.branch))
189
192
  retrieved_commits = fetch_commits(repo, branch_name, 5)
190
193
 
191
- result = @cc_client.batch_get_commits(
192
- commit_ids: retrieved_commits,
193
- repository_name: repo
194
+ result = T.cast(
195
+ @cc_client.batch_get_commits(
196
+ commit_ids: retrieved_commits,
197
+ repository_name: repo
198
+ ),
199
+ Seahorse::Client::Response
194
200
  )
195
201
 
196
202
  # sort the results by date
197
- result.commits.sort! { |a, b| b.author.date <=> a.author.date }
203
+ output = T.cast(result.data, Aws::CodeCommit::Types::BatchGetCommitsOutput)
204
+ commits = output.commits
205
+ commits&.sort_by! { |commit| commit.author&.date || "" }
206
+ commits&.reverse!
198
207
  result
199
208
  end
200
209
 
@@ -204,7 +213,7 @@ module Dependabot
204
213
  state: String,
205
214
  branch: String
206
215
  )
207
- .returns(T::Array[Aws::CodeCommit::Types::PullRequest])
216
+ .returns(T::Array[Seahorse::Client::Response])
208
217
  end
209
218
  def pull_requests(repo, state, branch)
210
219
  pull_request_ids = @cc_client.list_pull_requests(
@@ -212,18 +221,20 @@ module Dependabot
212
221
  pull_request_status: state
213
222
  ).pull_request_ids
214
223
 
215
- result = []
224
+ result = T.let([], T::Array[Seahorse::Client::Response])
216
225
  # list_pull_requests only gets us the pull request id
217
226
  # get_pull_request has all the info we need
218
227
  pull_request_ids.each do |id|
219
- pr_hash = @cc_client.get_pull_request(
220
- pull_request_id: id
228
+ pr_hash = T.cast(
229
+ @cc_client.get_pull_request(
230
+ pull_request_id: id
231
+ ),
232
+ Seahorse::Client::Response
221
233
  )
234
+ output = T.cast(pr_hash.data, Aws::CodeCommit::Types::GetPullRequestOutput)
222
235
  # only include PRs from the referenced branch
223
- if pr_hash.pull_request.pull_request_targets[0]
224
- .source_reference.include? branch
225
- result << pr_hash
226
- end
236
+ source_reference = T.must(output.pull_request.pull_request_targets[0]).source_reference
237
+ result << pr_hash if source_reference.delete_prefix("refs/heads/") == branch.delete_prefix("refs/heads/")
227
238
  end
228
239
  result
229
240
  end
@@ -234,13 +245,16 @@ module Dependabot
234
245
  branch_name: String,
235
246
  commit_id: String
236
247
  )
237
- .returns(Aws::CodeCommit::Types::BranchInfo)
248
+ .returns(Seahorse::Client::Response)
238
249
  end
239
250
  def create_branch(repo, branch_name, commit_id)
240
- cc_client.create_branch(
241
- repository_name: repo,
242
- branch_name: branch_name,
243
- commit_id: commit_id
251
+ T.cast(
252
+ cc_client.create_branch(
253
+ repository_name: repo,
254
+ branch_name: branch_name,
255
+ commit_id: commit_id
256
+ ),
257
+ Seahorse::Client::Response
244
258
  )
245
259
  end
246
260
 
@@ -252,7 +266,7 @@ module Dependabot
252
266
  commit_message: String,
253
267
  files: T::Array[Dependabot::DependencyFile]
254
268
  )
255
- .returns(Aws::CodeCommit::Types::CreateCommitOutput)
269
+ .returns(Seahorse::Client::Response)
256
270
  end
257
271
  def create_commit(
258
272
  branch_name,
@@ -261,19 +275,22 @@ module Dependabot
261
275
  commit_message,
262
276
  files
263
277
  )
264
- cc_client.create_commit(
265
- repository_name: source.unscoped_repo,
266
- branch_name: branch_name,
267
- parent_commit_id: base_commit,
268
- author_name: author_name,
269
- commit_message: commit_message,
270
- put_files: files.map do |file|
271
- {
272
- file_path: file.path,
273
- file_mode: "NORMAL",
274
- file_content: file.content
275
- }
276
- end
278
+ T.cast(
279
+ cc_client.create_commit(
280
+ repository_name: source.unscoped_repo,
281
+ branch_name: branch_name,
282
+ parent_commit_id: base_commit,
283
+ author_name: author_name,
284
+ commit_message: commit_message,
285
+ put_files: files.map do |file|
286
+ {
287
+ file_path: file.path,
288
+ file_mode: "NORMAL",
289
+ file_content: file.content
290
+ }
291
+ end
292
+ ),
293
+ Seahorse::Client::Response
277
294
  )
278
295
  end
279
296
 
@@ -284,7 +301,7 @@ module Dependabot
284
301
  source_branch: String,
285
302
  pr_description: String
286
303
  )
287
- .returns(T.nilable(Aws::CodeCommit::Types::CreatePullRequestOutput))
304
+ .returns(Seahorse::Client::Response)
288
305
  end
289
306
  def create_pull_request(
290
307
  pr_name,
@@ -292,14 +309,17 @@ module Dependabot
292
309
  source_branch,
293
310
  pr_description
294
311
  )
295
- cc_client.create_pull_request(
296
- title: pr_name,
297
- description: pr_description,
298
- targets: [
299
- { repository_name: source.unscoped_repo,
300
- source_reference: target_branch,
301
- destination_reference: source_branch }
302
- ]
312
+ T.cast(
313
+ cc_client.create_pull_request(
314
+ title: pr_name,
315
+ description: pr_description,
316
+ targets: [
317
+ { repository_name: source.unscoped_repo,
318
+ source_reference: target_branch,
319
+ destination_reference: source_branch }
320
+ ]
321
+ ),
322
+ Seahorse::Client::Response
303
323
  )
304
324
  end
305
325
 
@@ -0,0 +1,70 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/errors"
5
+ require "sorbet-runtime"
6
+
7
+ module Dependabot
8
+ module Clients
9
+ module GithubResourceParser
10
+ extend T::Sig
11
+
12
+ private
13
+
14
+ sig { params(resource: Sawyer::Resource, key: Symbol, context: String).returns(String) }
15
+ def github_string(resource, key, context)
16
+ value = T.cast(resource[key], Object)
17
+ return value if value.is_a?(String)
18
+
19
+ raise_bad_github_response("#{context} #{key} must be a string")
20
+ end
21
+
22
+ sig { params(resource: Sawyer::Resource, key: Symbol, context: String).returns(T.nilable(String)) }
23
+ def github_optional_string(resource, key, context)
24
+ value = T.cast(resource[key], Object)
25
+ return if value.nil?
26
+ return value if value.is_a?(String)
27
+
28
+ raise_bad_github_response("#{context} #{key} must be a string or nil")
29
+ end
30
+
31
+ sig { params(resource: Sawyer::Resource, key: Symbol, context: String).returns(Integer) }
32
+ def github_integer(resource, key, context)
33
+ value = T.cast(resource[key], Object)
34
+ return value if value.is_a?(Integer)
35
+
36
+ raise_bad_github_response("#{context} #{key} must be an integer")
37
+ end
38
+
39
+ sig { params(resource: Sawyer::Resource, key: Symbol, context: String).returns(T::Boolean) }
40
+ def github_boolean(resource, key, context)
41
+ value = T.cast(resource[key], Object)
42
+ return value if value == true
43
+ return false if value.nil? || value == false
44
+
45
+ raise_bad_github_response("#{context} #{key} must be a boolean or nil")
46
+ end
47
+
48
+ sig { params(resource: Sawyer::Resource, key: Symbol, context: String).returns(Sawyer::Resource) }
49
+ def github_resource(resource, key, context)
50
+ value = T.cast(resource[key], Object)
51
+ return value if value.is_a?(Sawyer::Resource)
52
+
53
+ raise_bad_github_response("#{context} #{key} must be an object")
54
+ end
55
+
56
+ sig { params(message: String).returns(T.noreturn) }
57
+ def raise_bad_github_response(message)
58
+ Kernel.raise Dependabot::PrivateSourceBadResponse.new(
59
+ github_response_source,
60
+ "Malformed GitHub response: #{message}"
61
+ )
62
+ end
63
+
64
+ sig { returns(String) }
65
+ def github_response_source
66
+ Kernel.raise NotImplementedError
67
+ end
68
+ end
69
+ end
70
+ end
@@ -3,15 +3,17 @@
3
3
 
4
4
  require "octokit"
5
5
  require "sorbet-runtime"
6
+ require "delegate"
6
7
  require "dependabot/credential"
7
8
 
8
9
  module Dependabot
9
10
  module Clients
10
- class GithubWithRetries
11
+ class GithubWithRetries < SimpleDelegator
11
12
  extend T::Sig
12
13
 
13
14
  DEFAULT_OPEN_TIMEOUT_IN_SECONDS = 2
14
15
  DEFAULT_READ_TIMEOUT_IN_SECONDS = 5
16
+ ClientOptions = T.type_alias { T::Hash[Symbol, Object] }
15
17
 
16
18
  sig { returns(Integer) }
17
19
  def self.open_timeout_in_seconds
@@ -32,7 +34,7 @@ module Dependabot
32
34
  }
33
35
  }
34
36
  }.freeze,
35
- T::Hash[Symbol, T.untyped]
37
+ ClientOptions
36
38
  )
37
39
 
38
40
  RETRYABLE_ERRORS = T.let(
@@ -88,30 +90,35 @@ module Dependabot
88
90
 
89
91
  sig { params(repo: String, branch: String).returns(String) }
90
92
  def fetch_commit(repo, branch)
91
- response = T.unsafe(ref(repo, "heads/#{branch}"))
93
+ response = ref(repo, "heads/#{branch}")
92
94
 
93
- raise Octokit::NotFound if response.is_a?(Array)
95
+ Kernel.raise Octokit::NotFound if response.is_a?(Array)
94
96
 
95
- response.object.sha
97
+ object = T.cast(response[:object], Sawyer::Resource)
98
+ T.cast(object[:sha], String)
96
99
  end
97
100
 
98
101
  sig { params(repo: String).returns(String) }
99
102
  def fetch_default_branch(repo)
100
- T.unsafe(repository(repo)).default_branch
103
+ T.cast(repository(repo)[:default_branch], String)
104
+ end
105
+
106
+ sig { params(repo: String, path: String, ref: String).returns(String) }
107
+ def raw_contents(repo, path:, ref:)
108
+ T.cast(
109
+ contents(repo, path: path, ref: ref, accept: "application/vnd.github.v3.raw"),
110
+ String
111
+ )
101
112
  end
102
113
 
103
114
  ############
104
115
  # Proxying #
105
116
  ############
106
117
 
107
- sig { params(max_retries: T.nilable(Integer), args: T.untyped).void }
118
+ sig { params(max_retries: T.nilable(Integer), args: Object).void }
108
119
  def initialize(max_retries: 3, **args)
109
120
  args = DEFAULT_CLIENT_ARGS.merge(args)
110
-
111
- access_tokens = args.delete(:access_tokens) || []
112
- access_tokens << args[:access_token] if args[:access_token]
113
- access_tokens << nil if access_tokens.empty?
114
- access_tokens.uniq!
121
+ access_tokens = extract_access_tokens(args)
115
122
 
116
123
  # Explicitly set the proxy if one is set in the environment
117
124
  # as Faraday's find_proxy is very slow.
@@ -135,30 +142,29 @@ module Dependabot
135
142
  end,
136
143
  T::Array[Octokit::Client]
137
144
  )
145
+ super(T.must(@clients.last))
138
146
  end
139
147
 
140
148
  # TODO: Create all the methods that are called on the client
141
149
  sig do
142
150
  params(
143
151
  method_name: T.any(Symbol, String),
144
- args: T.untyped,
145
- block: T.nilable(T.proc.returns(T.untyped))
152
+ args: Object,
153
+ block: T.nilable(Proc)
146
154
  )
147
- .returns(T.untyped)
155
+ .returns(T.anything)
148
156
  end
149
157
  def method_missing(method_name, *args, &block)
158
+ original_args = args
150
159
  untried_clients = @clients.dup
151
160
  client = untried_clients.pop
152
161
 
153
162
  begin
154
- if client.respond_to?(method_name)
155
- mutatable_args = args.map(&:dup)
156
- T.unsafe(client).public_send(method_name, *mutatable_args, &block)
157
- else
158
- super
159
- end
163
+ __setobj__(T.must(client))
164
+ args = original_args.map(&:dup)
165
+ super
160
166
  rescue Octokit::NotFound, Octokit::Unauthorized, Octokit::Forbidden
161
- raise unless (client = untried_clients.pop)
167
+ Kernel.raise unless (client = untried_clients.pop)
162
168
 
163
169
  retry
164
170
  end
@@ -172,7 +178,41 @@ module Dependabot
172
178
  .returns(T::Boolean)
173
179
  end
174
180
  def respond_to_missing?(method_name, include_private = false)
175
- @clients.first.respond_to?(method_name) || super
181
+ @clients.first.respond_to?(method_name, include_private)
182
+ end
183
+
184
+ private
185
+
186
+ sig { params(args: ClientOptions).returns(T::Array[T.nilable(String)]) }
187
+ def extract_access_tokens(args)
188
+ access_tokens = T.let([], T::Array[T.nilable(String)])
189
+ case (raw_access_tokens = args.delete(:access_tokens))
190
+ when nil
191
+ nil
192
+ when Array
193
+ raw_access_tokens.each do |raw_token|
194
+ token = T.cast(raw_token, Object)
195
+ case token
196
+ when nil, String then access_tokens << token
197
+ else Kernel.raise TypeError, "access_tokens must contain only strings or nil"
198
+ end
199
+ end
200
+ else
201
+ Kernel.raise TypeError, "access_tokens must be an array or nil"
202
+ end
203
+
204
+ access_token = args[:access_token]
205
+ case access_token
206
+ when nil
207
+ nil
208
+ when String
209
+ access_tokens << access_token
210
+ else
211
+ Kernel.raise TypeError, "access_token must be a string or nil"
212
+ end
213
+
214
+ access_tokens << nil if access_tokens.empty?
215
+ access_tokens.uniq
176
216
  end
177
217
  end
178
218
  end
@@ -3,14 +3,17 @@
3
3
 
4
4
  require "gitlab"
5
5
  require "sorbet-runtime"
6
+ require "delegate"
6
7
 
7
8
  require "dependabot/credential"
8
9
 
9
10
  module Dependabot
10
11
  module Clients
11
- class GitlabWithRetries
12
+ class GitlabWithRetries < SimpleDelegator
12
13
  extend T::Sig
13
14
 
15
+ ClientOptions = T.type_alias { T::Hash[Symbol, Object] }
16
+
14
17
  RETRYABLE_ERRORS = T.let(
15
18
  [Gitlab::Error::BadGateway].freeze,
16
19
  T::Array[T.class_of(Gitlab::Error::ResponseError)]
@@ -67,22 +70,24 @@ module Dependabot
67
70
 
68
71
  sig { params(repo: String, branch: String).returns(String) }
69
72
  def fetch_commit(repo, branch)
70
- T.unsafe(branch(repo, branch)).commit.id
73
+ commit = T.cast(branch(repo, branch)["commit"], Gitlab::ObjectifiedHash)
74
+ T.cast(commit["id"], String)
71
75
  end
72
76
 
73
77
  sig { params(repo: String).returns(String) }
74
78
  def fetch_default_branch(repo)
75
- T.unsafe(project(repo)).default_branch
79
+ T.cast(project(repo)["default_branch"], String)
76
80
  end
77
81
 
78
82
  ############
79
83
  # Proxying #
80
84
  ############
81
85
 
82
- sig { params(max_retries: T.nilable(Integer), args: T.untyped).void }
86
+ sig { params(max_retries: T.nilable(Integer), args: Object).void }
83
87
  def initialize(max_retries: 3, **args)
84
88
  @max_retries = T.let(max_retries || 3, Integer)
85
89
  @client = T.let(::Gitlab::Client.new(args), ::Gitlab::Client)
90
+ super(@client)
86
91
  end
87
92
 
88
93
  # Create commit in gitlab repo with correctly mapped file actions
@@ -99,7 +104,7 @@ module Dependabot
99
104
  branch_name: String,
100
105
  commit_message: String,
101
106
  files: T::Array[Dependabot::DependencyFile],
102
- options: T.untyped
107
+ options: Object
103
108
  )
104
109
  .returns(Gitlab::ObjectifiedHash)
105
110
  end
@@ -117,19 +122,16 @@ module Dependabot
117
122
  sig do
118
123
  params(
119
124
  method_name: T.any(Symbol, String),
120
- args: T.untyped,
121
- block: T.nilable(T.proc.returns(T.untyped))
125
+ args: Object,
126
+ block: T.nilable(Proc)
122
127
  )
123
- .returns(T.untyped)
128
+ .returns(T.anything)
124
129
  end
125
130
  def method_missing(method_name, *args, &block)
131
+ original_args = args
126
132
  retry_connection_failures do
127
- if @client.respond_to?(method_name)
128
- mutatable_args = args.map(&:dup)
129
- T.unsafe(@client).public_send(method_name, *mutatable_args, &block)
130
- else
131
- super
132
- end
133
+ args = original_args.map(&:dup)
134
+ super
133
135
  end
134
136
  end
135
137
 
@@ -141,7 +143,7 @@ module Dependabot
141
143
  .returns(T::Boolean)
142
144
  end
143
145
  def respond_to_missing?(method_name, include_private = false)
144
- @client.respond_to?(method_name) || super
146
+ @client.respond_to?(method_name, include_private)
145
147
  end
146
148
 
147
149
  sig do
@@ -156,7 +158,7 @@ module Dependabot
156
158
  yield
157
159
  rescue *RETRYABLE_ERRORS
158
160
  retry_attempt += 1
159
- retry_attempt <= @max_retries ? retry : raise
161
+ retry_attempt <= @max_retries ? retry : Kernel.raise
160
162
  end
161
163
  end
162
164
 
@@ -166,7 +168,7 @@ module Dependabot
166
168
  #
167
169
  # @param [Array<Dependabot::DependencyFile>] files
168
170
  # @return [Array<Hash>]
169
- sig { params(files: T::Array[Dependabot::DependencyFile]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
171
+ sig { params(files: T::Array[Dependabot::DependencyFile]).returns(T::Array[T::Hash[Symbol, Object]]) }
170
172
  def file_actions(files)
171
173
  files.map do |file|
172
174
  {
@@ -0,0 +1,103 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ require "dependabot/errors"
7
+ require "sorbet-runtime"
8
+
9
+ module Dependabot
10
+ module Clients
11
+ module JsonResponseParser
12
+ extend T::Sig
13
+
14
+ JsonObject = T.type_alias { T::Hash[String, Object] }
15
+ JsonObjects = T.type_alias { T::Array[JsonObject] }
16
+
17
+ private
18
+
19
+ sig { params(body: String, context: String).returns(JsonObject) }
20
+ def parse_json_object(body, context)
21
+ value = T.cast(JSON.parse(body), Object)
22
+ return T.let(value, JsonObject) if value.is_a?(Hash)
23
+
24
+ raise_bad_response(context, "expected an object")
25
+ rescue JSON::ParserError => e
26
+ raise_bad_response(context, e.message)
27
+ end
28
+
29
+ sig { params(object: JsonObject, key: String, context: String).returns(JsonObject) }
30
+ def object_field(object, key, context)
31
+ value = object.fetch(key)
32
+ return T.let(value, JsonObject) if value.is_a?(Hash)
33
+
34
+ raise_bad_response(context, "#{key} must be an object")
35
+ rescue KeyError => e
36
+ raise_bad_response(context, e.message)
37
+ end
38
+
39
+ sig { params(object: JsonObject, key: String, context: String).returns(String) }
40
+ def string_field(object, key, context)
41
+ string_value(object.fetch(key), context)
42
+ rescue KeyError => e
43
+ raise_bad_response(context, e.message)
44
+ end
45
+
46
+ sig { params(value: Object, context: String).returns(String) }
47
+ def string_value(value, context)
48
+ return value if value.is_a?(String)
49
+
50
+ raise_bad_response(context, "expected a string")
51
+ end
52
+
53
+ sig { params(object: JsonObject, key: String, context: String).returns(JsonObjects) }
54
+ def object_array_field(object, key, context)
55
+ object_array(object.fetch(key), context)
56
+ rescue KeyError => e
57
+ raise_bad_response(context, e.message)
58
+ end
59
+
60
+ sig { params(value: Object, context: String).returns(JsonObjects) }
61
+ def object_array(value, context)
62
+ raise_bad_response(context, "expected an array") unless value.is_a?(Array)
63
+
64
+ value.map do |raw_entry|
65
+ entry = T.cast(raw_entry, Object)
66
+ next T.let(entry, JsonObject) if entry.is_a?(Hash)
67
+
68
+ raise_bad_response(context, "array entries must be objects")
69
+ end
70
+ end
71
+
72
+ sig { params(object: JsonObject, key: String, context: String).returns(JsonObject) }
73
+ def first_object_field(object, key, context)
74
+ value = object_array_field(object, key, context).first
75
+ return value if value
76
+
77
+ raise_bad_response(context, "#{key} must contain at least one object")
78
+ end
79
+
80
+ sig { params(object: JsonObject, context: String, paths: T::Array[T::Array[String]]).void }
81
+ def validate_string_paths(object, context, paths)
82
+ paths.each do |path|
83
+ current = T.must(path[0...-1]).reduce(object) { |value, key| object_field(value, key, context) }
84
+ string_field(current, T.must(path.last), context)
85
+ end
86
+ end
87
+
88
+ sig { params(context: String, message: String).returns(T.noreturn) }
89
+ def raise_bad_response(context, message)
90
+ provider, source = response_identity
91
+ Kernel.raise Dependabot::PrivateSourceBadResponse.new(
92
+ source,
93
+ "Malformed #{provider} response for #{context}: #{message}"
94
+ )
95
+ end
96
+
97
+ sig { returns([String, String]) }
98
+ def response_identity
99
+ Kernel.raise NotImplementedError
100
+ end
101
+ end
102
+ end
103
+ end