dependabot-common 0.387.0 → 0.389.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1af9eae7cb945bd312cf8a9fee63b2f4e42adcbc3d137208e2043b659052013d
4
- data.tar.gz: f815b9a6961d5ecbd9c77ffdf296a9a048dbfadf21fae5ddebc4bbd8db6fbd2b
3
+ metadata.gz: b9781413c952e486c9417d63fc70301e7ba10d4283555831df79a6e9b5f2067d
4
+ data.tar.gz: f54e1d3bda0a1126fefa4246c4a7f0f30951bf5995711b4b376fdd152a81352b
5
5
  SHA512:
6
- metadata.gz: 34737076727da04d065d0163c93fe1058cfbb3a86d79d2cfef2be4cf126d2fd4d335e204087d3c337e2d97f5ec0220878a1110e139f553805c4d36574e33cb5d
7
- data.tar.gz: 5885a478a01996a6f382b3c2bedb29ff59ca557c0316582f14e9136bd3881dac04d20874862bc76a93a16119653a85c868d3d08c65e8cabb9fff1ae257124323
6
+ metadata.gz: 48591da2ac8712a8f2db6f9a8cd8c7c5debe954fa1470d7e6a625f18d5249e668cf5d48a338813c4c9ffe080d43628bf168d1dc81e463fa8753fc4edb819568e
7
+ data.tar.gz: 276ee23ea7c643fb0daac14664c83dea4c4fd4070bda5d616278d94eb01cad4722bd32d022667e5f9affe802dcb986c1131dbcb4c5a5eebef359854f0b83d136
@@ -0,0 +1,61 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sawyer"
5
+ require "sorbet-runtime"
6
+ require "time"
7
+
8
+ module Dependabot
9
+ module Clients
10
+ # Typed view over the release fields returned by Octokit.
11
+ class GithubRelease < T::ImmutableStruct
12
+ extend T::Sig
13
+
14
+ const :id, T.nilable(Integer)
15
+ const :name, T.nilable(String)
16
+ const :tag_name, String
17
+ const :body, T.nilable(String)
18
+ const :html_url, T.nilable(String)
19
+ const :prerelease, T::Boolean
20
+ const :published_at, T.nilable(Time)
21
+
22
+ sig { params(resource: Sawyer::Resource).returns(T.nilable(GithubRelease)) }
23
+ def self.from_resource(resource)
24
+ tag_name = T.cast(resource[:tag_name], Object)
25
+ return unless tag_name.is_a?(String)
26
+
27
+ id = T.cast(resource[:id], Object)
28
+ prerelease = T.cast(resource[:prerelease], Object)
29
+
30
+ new(
31
+ id: id.is_a?(Integer) ? id : nil,
32
+ name: string_value(resource, :name),
33
+ tag_name: tag_name,
34
+ body: string_value(resource, :body),
35
+ html_url: string_value(resource, :html_url),
36
+ prerelease: prerelease == true,
37
+ published_at: time_value(resource, :published_at)
38
+ )
39
+ end
40
+
41
+ sig { params(resource: Sawyer::Resource, key: Symbol).returns(T.nilable(String)) }
42
+ def self.string_value(resource, key)
43
+ value = T.cast(resource[key], Object)
44
+ value.is_a?(String) ? value : nil
45
+ end
46
+ private_class_method :string_value
47
+
48
+ sig { params(resource: Sawyer::Resource, key: Symbol).returns(T.nilable(Time)) }
49
+ def self.time_value(resource, key)
50
+ value = T.cast(resource[key], Object)
51
+ return value if value.is_a?(Time)
52
+ return unless value.is_a?(String)
53
+
54
+ Time.parse(value)
55
+ rescue ArgumentError
56
+ nil
57
+ end
58
+ private_class_method :time_value
59
+ end
60
+ end
61
+ end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -9,9 +9,17 @@ module Dependabot
9
9
  class Dependency
10
10
  extend T::Sig
11
11
 
12
+ Metadata = T.type_alias { T::Hash[Symbol, Object] }
13
+ ProductionCheck = T.type_alias do
14
+ T.proc.params(groups: T::Array[String]).returns(T::Boolean)
15
+ end
16
+ RequirementInput = T.type_alias { DependencyRequirement::Input }
17
+ ObjectHash = T.type_alias { DependencyRequirement::ObjectHash }
18
+ RequirementSource = T.type_alias { DependencyRequirement::Source }
19
+
12
20
  @production_checks = T.let(
13
21
  {},
14
- T::Hash[String, T.proc.params(arg0: T::Array[T.untyped]).returns(T::Boolean)]
22
+ T::Hash[String, ProductionCheck]
15
23
  )
16
24
  @display_name_builders = T.let({}, T::Hash[String, T.proc.params(arg0: String).returns(String)])
17
25
  @name_normalisers = T.let({}, T::Hash[String, T.proc.params(arg0: String).returns(String)])
@@ -19,9 +27,13 @@ module Dependabot
19
27
  {},
20
28
  T::Hash[String, T.proc.params(arg0: Dependency).returns(T.nilable(String))]
21
29
  )
30
+ IDENTITY_NAME_NORMALISER = T.let(
31
+ ->(name) { name },
32
+ T.proc.params(name: String).returns(String)
33
+ )
22
34
 
23
35
  sig do
24
- params(package_manager: String).returns(T.proc.params(arg0: T::Array[T.untyped]).returns(T::Boolean))
36
+ params(package_manager: String).returns(ProductionCheck)
25
37
  end
26
38
  def self.production_check_for_package_manager(package_manager)
27
39
  production_check = @production_checks[package_manager]
@@ -33,9 +45,9 @@ module Dependabot
33
45
  sig do
34
46
  params(
35
47
  package_manager: String,
36
- production_check: T.proc.params(arg0: T::Array[T.untyped]).returns(T::Boolean)
48
+ production_check: ProductionCheck
37
49
  )
38
- .returns(T.proc.params(arg0: T::Array[T.untyped]).returns(T::Boolean))
50
+ .returns(ProductionCheck)
39
51
  end
40
52
  def self.register_production_check(package_manager, production_check)
41
53
  @production_checks[package_manager] = production_check
@@ -53,7 +65,7 @@ module Dependabot
53
65
 
54
66
  sig { params(package_manager: String).returns(T.nilable(T.proc.params(arg0: String).returns(String))) }
55
67
  def self.name_normaliser_for_package_manager(package_manager)
56
- @name_normalisers[package_manager] || ->(name) { name }
68
+ @name_normalisers[package_manager] || IDENTITY_NAME_NORMALISER
57
69
  end
58
70
 
59
71
  sig do
@@ -106,10 +118,10 @@ module Dependabot
106
118
  sig { returns(T.nilable(String)) }
107
119
  attr_accessor :directory
108
120
 
109
- sig { returns(T.nilable(T::Array[T::Hash[Symbol, T.untyped]])) }
121
+ sig { returns(T.nilable(T::Array[Metadata])) }
110
122
  attr_reader :subdependency_metadata
111
123
 
112
- sig { returns(T::Hash[Symbol, T.untyped]) }
124
+ sig { returns(Metadata) }
113
125
  attr_reader :metadata
114
126
 
115
127
  # Attribution metadata for group membership tracking
@@ -129,16 +141,16 @@ module Dependabot
129
141
  sig do
130
142
  params(
131
143
  name: String,
132
- requirements: T::Array[T::Hash[T.any(Symbol, String), T.untyped]],
144
+ requirements: T::Array[RequirementInput],
133
145
  package_manager: String,
134
146
  # TODO: Make version a Dependabot::Version everywhere
135
147
  version: T.nilable(T.any(String, Dependabot::Version)),
136
148
  previous_version: T.nilable(String),
137
- previous_requirements: T.nilable(T::Array[T::Hash[T.any(Symbol, String), T.untyped]]),
149
+ previous_requirements: T.nilable(T::Array[RequirementInput]),
138
150
  directory: T.nilable(String),
139
- subdependency_metadata: T.nilable(T::Array[T::Hash[T.any(Symbol, String), String]]),
151
+ subdependency_metadata: T.nilable(T::Array[ObjectHash]),
140
152
  removed: T::Boolean,
141
- metadata: T.nilable(T::Hash[T.any(Symbol, String), String])
153
+ metadata: T.nilable(ObjectHash)
142
154
  ).void
143
155
  end
144
156
  def initialize(
@@ -174,14 +186,16 @@ module Dependabot
174
186
  )
175
187
  @package_manager = package_manager
176
188
  @directory = directory
189
+ @subdependency_metadata = T.let(nil, T.nilable(T::Array[Metadata]))
177
190
  unless top_level? || subdependency_metadata == []
178
191
  @subdependency_metadata = T.let(
179
192
  subdependency_metadata&.map { |h| symbolize_keys(h) },
180
- T.nilable(T::Array[T::Hash[Symbol, T.untyped]])
193
+ T.nilable(T::Array[Metadata])
181
194
  )
182
195
  end
183
196
  @removed = removed
184
- @metadata = T.let(symbolize_keys(metadata || {}), T::Hash[Symbol, T.untyped])
197
+ @metadata = T.let(symbolize_keys(metadata || {}), Metadata)
198
+ @numeric_version = T.let(nil, T.nilable(Dependabot::Version))
185
199
  check_values
186
200
  end
187
201
  # rubocop:enable Metrics/PerceivedComplexity
@@ -203,7 +217,7 @@ module Dependabot
203
217
  @numeric_version ||= T.let(version_class.new(T.must(version)), T.nilable(Dependabot::Version))
204
218
  end
205
219
 
206
- sig { returns(T::Hash[String, T.untyped]) }
220
+ sig { returns(T::Hash[String, Object]) }
207
221
  def to_h
208
222
  {
209
223
  "name" => name,
@@ -227,7 +241,12 @@ module Dependabot
227
241
  def production?
228
242
  return subdependency_production_check unless top_level?
229
243
 
230
- groups = requirements.flat_map { |r| r.fetch(:groups).map(&:to_s) }
244
+ groups = requirements.flat_map do |requirement|
245
+ requirement_groups = requirement.groups
246
+ raise TypeError, "groups must be an array of strings or symbols" unless requirement_groups
247
+
248
+ requirement_groups.map(&:to_s)
249
+ end
231
250
 
232
251
  self.class
233
252
  .production_check_for_package_manager(package_manager)
@@ -277,7 +296,10 @@ module Dependabot
277
296
  sig { params(requirements: T::Array[Dependabot::DependencyRequirement]).returns(T.nilable(String)) }
278
297
  def docker_digest_from_reqs(requirements)
279
298
  requirements
280
- .filter_map { |r| r.dig(:source, "digest") || r.dig(:source, :digest) }
299
+ .filter_map do |requirement|
300
+ source = requirement.source
301
+ optional_source_string(source, "digest") if source.is_a?(Hash)
302
+ end
281
303
  .first
282
304
  end
283
305
 
@@ -286,7 +308,8 @@ module Dependabot
286
308
  return nil if previous_requirements.nil?
287
309
 
288
310
  previous_refs = T.must(previous_requirements).filter_map do |r|
289
- r.dig(:source, "ref") || r.dig(:source, :ref)
311
+ source = r.source
312
+ optional_source_string(source, "ref") if source.is_a?(Hash)
290
313
  end.uniq
291
314
  previous_refs.first if previous_refs.one?
292
315
  end
@@ -294,7 +317,8 @@ module Dependabot
294
317
  sig { returns(T.nilable(String)) }
295
318
  def new_ref
296
319
  new_refs = requirements.filter_map do |r|
297
- r.dig(:source, "ref") || r.dig(:source, :ref)
320
+ source = r.source
321
+ optional_source_string(source, "ref") if source.is_a?(Hash)
298
322
  end.uniq
299
323
  new_refs.first if new_refs.one?
300
324
  end
@@ -308,10 +332,10 @@ module Dependabot
308
332
  # support this feature will return more than the current version.
309
333
  sig { returns(T::Array[T.nilable(String)]) }
310
334
  def all_versions
311
- all_versions = metadata[:all_versions]
312
- return [version].compact unless all_versions
335
+ versions = metadata_dependencies(:all_versions)
336
+ return [version].compact unless versions
313
337
 
314
- all_versions.filter_map(&:version)
338
+ versions.filter_map(&:version)
315
339
  end
316
340
 
317
341
  # This dependency is being indirectly updated by an update to another
@@ -319,10 +343,63 @@ module Dependabot
319
343
  # surface it to the user in the PR.
320
344
  sig { returns(T.nilable(T::Boolean)) }
321
345
  def informational_only?
322
- metadata[:information_only]
346
+ metadata_boolean(:information_only)
347
+ end
348
+
349
+ sig { params(key: Symbol).returns(T.nilable(String)) }
350
+ def metadata_string(key)
351
+ value = metadata[key]
352
+ return if value.nil?
353
+ return value if value.is_a?(String)
354
+
355
+ raise TypeError, "#{key} metadata must be a string or nil"
323
356
  end
324
357
 
325
- sig { params(other: T.anything).returns(T::Boolean) }
358
+ sig { params(key: Symbol).returns(T.nilable(T::Array[String])) }
359
+ def metadata_string_array(key)
360
+ value = metadata[key]
361
+ return if value.nil?
362
+ raise TypeError, "#{key} metadata must be an array of strings or nil" unless value.is_a?(Array)
363
+
364
+ value.map do |raw_entry|
365
+ entry = T.cast(raw_entry, Object)
366
+ raise TypeError, "#{key} metadata must be an array of strings or nil" unless entry.is_a?(String)
367
+
368
+ entry
369
+ end
370
+ end
371
+
372
+ sig { params(key: Symbol).returns(T.nilable(T::Array[Dependency])) }
373
+ def metadata_dependencies(key)
374
+ value = metadata_array(key)
375
+ return unless value
376
+
377
+ value.map do |raw_dependency|
378
+ raise TypeError, "#{key} metadata must be an array of dependencies" unless raw_dependency.is_a?(Dependency)
379
+
380
+ raw_dependency
381
+ end
382
+ end
383
+
384
+ sig { params(key: Symbol).returns(T.nilable(T::Array[Object])) }
385
+ def metadata_array(key)
386
+ value = metadata[key]
387
+ return if value.nil?
388
+ raise TypeError, "#{key} metadata must be an array" unless value.is_a?(Array)
389
+
390
+ value.map { |entry| T.cast(entry, Object) }
391
+ end
392
+
393
+ sig { params(key: Symbol).returns(T.nilable(T::Boolean)) }
394
+ def metadata_boolean(key)
395
+ value = metadata[key]
396
+ return if value.nil?
397
+ return value if value == true || value == false
398
+
399
+ raise TypeError, "#{key} metadata must be a boolean or nil"
400
+ end
401
+
402
+ sig { params(other: Object).returns(T::Boolean) }
326
403
  def ==(other)
327
404
  case other
328
405
  when Dependency
@@ -337,7 +414,7 @@ module Dependabot
337
414
  to_h.hash
338
415
  end
339
416
 
340
- sig { params(other: T.anything).returns(T::Boolean) }
417
+ sig { params(other: Object).returns(T::Boolean) }
341
418
  def eql?(other)
342
419
  self == other
343
420
  end
@@ -361,19 +438,27 @@ module Dependabot
361
438
  params(
362
439
  allowed_types: T.nilable(T::Array[String])
363
440
  )
364
- .returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped]))
441
+ .returns(T.nilable(ObjectHash))
365
442
  end
366
443
  def source_details(allowed_types: nil)
367
444
  sources = all_sources.uniq.compact
368
- sources.select! { |source| allowed_types.include?(source[:type].to_s) } if allowed_types
445
+ source_hashes = sources.map do |source|
446
+ raise TypeError, "source details must be a hash with string or symbol keys" unless source.is_a?(Hash)
447
+
448
+ source
449
+ end
450
+ if allowed_types
451
+ source_hashes.select! { |source| allowed_types.include?(optional_source_string(source, "type").to_s) }
452
+ end
369
453
 
370
454
  git = allowed_types == ["git"]
371
455
 
372
- if (git && sources.map { |s| s[:url] }.uniq.count > 1) || (!git && sources.count > 1)
373
- raise "Multiple sources! #{sources.join(', ')}"
456
+ if (git && source_hashes.map { |source| optional_source_string(source, "url") }.uniq.count > 1) ||
457
+ (!git && source_hashes.count > 1)
458
+ raise "Multiple sources! #{source_hashes.join(', ')}"
374
459
  end
375
460
 
376
- sources.first
461
+ source_hashes.first
377
462
  end
378
463
 
379
464
  sig { returns(T.nilable(String)) }
@@ -381,15 +466,26 @@ module Dependabot
381
466
  details = source_details
382
467
  return "default" if details.nil?
383
468
 
384
- details[:type] || details.fetch("type")
469
+ required_source_string(details, "type")
385
470
  end
386
471
 
387
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
472
+ sig do
473
+ params(
474
+ key: String,
475
+ allowed_types: T.nilable(T::Array[String])
476
+ ).returns(T.nilable(String))
477
+ end
478
+ def source_string(key, allowed_types: nil)
479
+ details = source_details(allowed_types: allowed_types)
480
+ optional_source_string(details, key) if details
481
+ end
482
+
483
+ sig { returns(T::Array[T.nilable(RequirementSource)]) }
388
484
  def all_sources
389
485
  if top_level?
390
- requirements.map { |requirement| requirement.fetch(:source) }
486
+ requirements.map(&:source)
391
487
  elsif subdependency_metadata
392
- T.must(subdependency_metadata).filter_map { |data| data[:source] }
488
+ T.must(subdependency_metadata).filter_map { |data| source_from_metadata(data) }
393
489
  else
394
490
  []
395
491
  end
@@ -461,7 +557,7 @@ module Dependabot
461
557
  "#{optional_keys.join(', ')}."
462
558
  end
463
559
 
464
- return if requirement_fields.flatten.none? { |r| r[:requirement] == "" }
560
+ return if requirement_fields.flatten.none? { |r| r.requirement == "" }
465
561
 
466
562
  raise ArgumentError, "blank strings must not be provided as requirements"
467
563
  end
@@ -476,9 +572,44 @@ module Dependabot
476
572
  end
477
573
  end
478
574
 
479
- sig { params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
575
+ sig { params(hash: ObjectHash).returns(Metadata) }
480
576
  def symbolize_keys(hash)
481
577
  hash.keys.to_h { |k| [k.to_sym, hash[k]] }
482
578
  end
579
+
580
+ sig { params(metadata: Metadata).returns(T.nilable(RequirementSource)) }
581
+ def source_from_metadata(metadata)
582
+ raw_source = metadata[:source]
583
+ return if raw_source.nil?
584
+ return raw_source if raw_source.is_a?(String)
585
+ unless raw_source.is_a?(Hash)
586
+ raise TypeError, "source must be a string or hash with string or symbol keys, or nil"
587
+ end
588
+
589
+ raw_source.each_key do |raw_key|
590
+ key = T.cast(raw_key, Object)
591
+ next if key.is_a?(String) || key.is_a?(Symbol)
592
+
593
+ raise TypeError, "source must be a string or hash with string or symbol keys, or nil"
594
+ end
595
+ raw_source
596
+ end
597
+
598
+ sig { params(source: ObjectHash, key: String).returns(T.nilable(String)) }
599
+ def optional_source_string(source, key)
600
+ value = source[key.to_sym] || source[key]
601
+ return if value.nil?
602
+ return value if value.is_a?(String)
603
+
604
+ raise TypeError, "source #{key} must be a string or nil"
605
+ end
606
+
607
+ sig { params(source: ObjectHash, key: String).returns(String) }
608
+ def required_source_string(source, key)
609
+ raw_value = source.key?(key.to_sym) ? source[key.to_sym] : source.fetch(key)
610
+ return raw_value if raw_value.is_a?(String)
611
+
612
+ raise TypeError, "source #{key} must be a string"
613
+ end
483
614
  end
484
615
  end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -32,7 +32,7 @@ module Dependabot
32
32
  # metadata, but in most cases this will be empty.
33
33
  sig { override.params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
34
34
  def fetch_subdependencies(dependency)
35
- dependency.metadata.fetch(:depends_on, [])
35
+ dependency.metadata_string_array(:depends_on) || []
36
36
  end
37
37
 
38
38
  # TODO: Delegate this to ecosystem-specific base classes
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -33,18 +33,24 @@ module Dependabot
33
33
  extend T::Sig
34
34
  extend T::Generic
35
35
 
36
- # The values of a requirement entry are heterogeneous and
37
- # ecosystem-specific, so this bridge class is necessarily untyped at
38
- # the Hash level; the typed readers below are the migration path.
39
- # rubocop:disable Sorbet/ForbidTUntyped
36
+ Group = T.type_alias { T.any(String, Symbol) }
37
+ ObjectHash = T.type_alias { T::Hash[T.any(Symbol, String), Object] }
38
+ Requirement = T.type_alias { T.any(String, Symbol) }
39
+ Source = T.type_alias { T.any(String, ObjectHash) }
40
+ Input = T.type_alias { T.any(DependencyRequirement, ObjectHash) }
41
+
40
42
  K = type_member { { fixed: Symbol } }
43
+ # Hash-style access remains dynamic until ecosystem callers migrate to the
44
+ # typed readers. Keeping that compatibility here avoids a flag-day change.
45
+ # rubocop:disable Sorbet/ForbidTUntyped
41
46
  V = type_member { { fixed: T.untyped } }
42
47
  Elem = type_member { { fixed: [Symbol, T.untyped] } }
48
+ # rubocop:enable Sorbet/ForbidTUntyped
43
49
 
44
50
  # Builds a DependencyRequirement from a requirement hash, symbolising
45
51
  # top-level keys. Accepts both plain hashes and existing
46
52
  # DependencyRequirement instances and always returns a new instance.
47
- sig { params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(DependencyRequirement) }
53
+ sig { params(hash: Input).returns(DependencyRequirement) }
48
54
  def self.create(hash)
49
55
  requirement = new
50
56
  requirement.replace(hash.keys.to_h { |k| [k.to_sym, hash[k]] })
@@ -52,16 +58,21 @@ module Dependabot
52
58
  end
53
59
 
54
60
  # The version constraint string, e.g. ">= 1.0, < 2.0". Nil when the
55
- # dependency is pinned by a lockfile rather than a manifest constraint.
56
- sig { returns(T.nilable(String)) }
61
+ # dependency is pinned by a lockfile rather than a manifest constraint,
62
+ # or :unfixable when no valid updated requirement can be generated.
63
+ sig { returns(T.nilable(Requirement)) }
57
64
  def requirement
58
- self[:requirement]
65
+ value = T.cast(self[:requirement], T.nilable(Object))
66
+ return if value.nil?
67
+ return value if value.is_a?(String) || value == :unfixable
68
+
69
+ raise TypeError, "requirement must be a string, :unfixable, or nil"
59
70
  end
60
71
 
61
72
  # The manifest file this requirement was declared in, e.g. "Gemfile".
62
73
  sig { returns(T.nilable(String)) }
63
74
  def file
64
- self[:file]
75
+ optional_string(:file)
65
76
  end
66
77
 
67
78
  # The dependency groups this requirement belongs to, e.g. ["dev"] or
@@ -69,26 +80,79 @@ module Dependabot
69
80
  # Nilable because some requirement entries are constructed with
70
81
  # groups: nil, and the reader must reflect that to stay a drop-in for
71
82
  # the underlying hash access under sorbet-runtime.
72
- sig { returns(T.nilable(T::Array[T.untyped])) }
83
+ sig { returns(T.nilable(T::Array[Group])) }
73
84
  def groups
74
- self[:groups]
85
+ value = T.cast(self[:groups], T.nilable(Object))
86
+ return if value.nil?
87
+ raise TypeError, "groups must be an array of strings or symbols, or nil" unless value.is_a?(Array)
88
+
89
+ value.each do |raw_group|
90
+ group = T.cast(raw_group, Object)
91
+ next if group.is_a?(String) || group.is_a?(Symbol)
92
+
93
+ raise TypeError, "groups must be an array of strings or symbols, or nil"
94
+ end
95
+ value
75
96
  end
76
97
 
77
98
  # The source details for the dependency, e.g.
78
99
  # { type: "git", url: "https://github.com/..." }. Keys may be symbols
79
100
  # or strings depending on whether the requirement was built by a file
80
101
  # parser or deserialised from a job definition.
81
- sig { returns(T.nilable(T::Hash[T.any(Symbol, String), T.untyped])) }
102
+ sig { returns(T.nilable(Source)) }
82
103
  def source
83
- self[:source]
104
+ value = T.cast(self[:source], T.nilable(Object))
105
+ return if value.nil?
106
+ return value if value.is_a?(String)
107
+
108
+ object_hash(value, :source)
84
109
  end
85
110
 
86
111
  # Optional ecosystem-specific metadata about the requirement, e.g.
87
112
  # { property_name: "rails.version" }.
88
- sig { returns(T.nilable(T::Hash[T.any(Symbol, String), T.untyped])) }
113
+ sig { returns(T.nilable(ObjectHash)) }
89
114
  def metadata
90
- self[:metadata]
115
+ optional_object_hash(:metadata)
116
+ end
117
+
118
+ private
119
+
120
+ sig { params(key: Symbol).returns(T.nilable(String)) }
121
+ def optional_string(key)
122
+ value = T.cast(self[key], T.nilable(Object))
123
+ return if value.nil?
124
+ return value if value.is_a?(String)
125
+
126
+ raise TypeError, "#{key} must be a string or nil"
127
+ end
128
+
129
+ sig { params(key: Symbol).returns(T.nilable(ObjectHash)) }
130
+ def optional_object_hash(key)
131
+ value = T.cast(self[key], T.nilable(Object))
132
+ return if value.nil?
133
+
134
+ object_hash(value, key)
135
+ end
136
+
137
+ sig { params(value: Object, key: Symbol).returns(ObjectHash) }
138
+ def object_hash(value, key)
139
+ message = object_hash_message(key)
140
+ raise TypeError, "#{key} must be #{message}" unless value.is_a?(Hash)
141
+
142
+ value.each_key do |raw_nested_key|
143
+ nested_key = T.cast(raw_nested_key, Object)
144
+ next if nested_key.is_a?(String) || nested_key.is_a?(Symbol)
145
+
146
+ raise TypeError, "#{key} must be #{message}"
147
+ end
148
+ value
149
+ end
150
+
151
+ sig { params(key: Symbol).returns(String) }
152
+ def object_hash_message(key)
153
+ return "a string or hash with string or symbol keys, or nil" if key == :source
154
+
155
+ "a hash with string or symbol keys, or nil"
91
156
  end
92
- # rubocop:enable Sorbet/ForbidTUntyped
93
157
  end
94
158
  end
@@ -0,0 +1,51 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ class ErrorDetails < T::ImmutableStruct
8
+ extend T::Sig
9
+
10
+ DetailHash = T.type_alias { T::Hash[T.any(String, Symbol), T.anything] }
11
+ Detail = T.type_alias { T.any(String, DetailHash) }
12
+
13
+ const :error_type, String
14
+ const :error_detail, T.nilable(Detail)
15
+
16
+ sig { params(hash: T::Hash[Symbol, T.anything]).returns(ErrorDetails) }
17
+ def self.from_hash(hash)
18
+ raw_type = T.cast(hash[:"error-type"], Object)
19
+ raise TypeError, "error-type must be a string" unless raw_type.is_a?(String)
20
+
21
+ new(
22
+ error_type: raw_type,
23
+ error_detail: parse_detail(T.cast(hash[:"error-detail"], T.nilable(Object)))
24
+ )
25
+ end
26
+
27
+ sig { returns(T::Hash[Symbol, Object]) }
28
+ def to_h
29
+ result = T.let({ "error-type": error_type }, T::Hash[Symbol, Object])
30
+ result[:"error-detail"] = error_detail if error_detail
31
+ result
32
+ end
33
+
34
+ sig { params(value: T.nilable(Object)).returns(T.nilable(Detail)) }
35
+ def self.parse_detail(value)
36
+ return if value.nil?
37
+ return value if value.is_a?(String)
38
+ raise TypeError, "error-detail must be a string or hash" unless value.is_a?(Hash)
39
+
40
+ result = T.let({}, DetailHash)
41
+ value.each do |raw_key, raw_value|
42
+ key = T.cast(raw_key, Object)
43
+ raise TypeError, "error-detail keys must be strings or symbols" unless key.is_a?(String) || key.is_a?(Symbol)
44
+
45
+ result[key] = T.cast(raw_value, Object)
46
+ end
47
+ result
48
+ end
49
+ private_class_method :parse_detail
50
+ end
51
+ end