purl 1.0.0 → 1.1.1

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.
@@ -24,29 +24,85 @@ module Purl
24
24
  end
25
25
 
26
26
  def self.build_pattern_config(type, config)
27
+ # Get the default registry for this type from parent config
28
+ type_config = load_types_config["types"][type]
29
+ default_registry = type_config["default_registry"]
30
+
31
+ # Build full URLs from templates if we have a default registry
32
+ route_patterns = []
33
+ if default_registry
34
+ # Add all template variations
35
+ if config["path_template"]
36
+ route_patterns << default_registry + config["path_template"]
37
+ end
38
+ if config["namespace_path_template"]
39
+ route_patterns << default_registry + config["namespace_path_template"]
40
+ end
41
+ if config["version_path_template"]
42
+ route_patterns << default_registry + config["version_path_template"]
43
+ end
44
+ if config["namespace_version_path_template"]
45
+ route_patterns << default_registry + config["namespace_version_path_template"]
46
+ end
47
+ end
48
+ # Fall back to legacy route_patterns if available
49
+ route_patterns = config["route_patterns"] if route_patterns.empty? && config["route_patterns"]
50
+
51
+ # Build reverse regex from template or use legacy format
52
+ reverse_regex = nil
53
+ if config["reverse_regex"]
54
+ if config["reverse_regex"].start_with?("/") && default_registry
55
+ # Domain-agnostic pattern - combine with default registry domain
56
+ domain_pattern = default_registry.sub(/^https?:\/\//, '').gsub('.', '\\.')
57
+ reverse_regex = Regexp.new("^https?://#{domain_pattern}" + config["reverse_regex"])
58
+ else
59
+ # Legacy full pattern
60
+ reverse_regex = Regexp.new(config["reverse_regex"])
61
+ end
62
+ end
63
+
27
64
  {
28
- base_url: config["base_url"],
29
- route_patterns: config["route_patterns"] || [],
30
- reverse_regex: config["reverse_regex"] ? Regexp.new(config["reverse_regex"]) : nil,
31
- pattern: build_generation_lambda(type, config),
32
- reverse_parser: config["reverse_regex"] ? build_reverse_parser(type, config) : nil
65
+ base_url: config["base_url"] || (default_registry ? default_registry + config["path_template"]&.split('/:').first : nil),
66
+ route_patterns: route_patterns,
67
+ reverse_regex: reverse_regex,
68
+ pattern: build_generation_lambda(type, config, default_registry),
69
+ reverse_parser: reverse_regex ? build_reverse_parser(type, config) : nil
33
70
  }
34
71
  end
35
72
 
36
- def self.build_generation_lambda(type, config)
73
+ # Load types config (needed for accessing default_registry)
74
+ def self.load_types_config
75
+ @types_config ||= begin
76
+ config_path = File.join(__dir__, "..", "..", "purl-types.json")
77
+ require "json"
78
+ JSON.parse(File.read(config_path))
79
+ end
80
+ end
81
+
82
+ def self.build_generation_lambda(type, config, default_registry = nil)
83
+ # Use base_url from config, or build from default_registry + path_template base
84
+ if config["base_url"]
85
+ base_url = config["base_url"]
86
+ elsif default_registry && config["path_template"]
87
+ # Extract the base path from the template (everything before first :parameter)
88
+ base_path = config["path_template"].split('/:').first
89
+ base_url = default_registry + base_path
90
+ else
91
+ return nil
92
+ end
37
93
  case type
38
94
  when "npm"
39
95
  ->(purl) do
40
96
  if purl.namespace
41
- "#{config["base_url"]}/#{purl.namespace}/#{purl.name}"
97
+ "#{base_url}/#{purl.namespace}/#{purl.name}"
42
98
  else
43
- "#{config["base_url"]}/#{purl.name}"
99
+ "#{base_url}/#{purl.name}"
44
100
  end
45
101
  end
46
102
  when "composer", "maven", "swift"
47
103
  ->(purl) do
48
104
  if purl.namespace
49
- "#{config["base_url"]}/#{purl.namespace}/#{purl.name}"
105
+ "#{base_url}/#{purl.namespace}/#{purl.name}"
50
106
  else
51
107
  raise MissingRegistryInfoError.new(
52
108
  "#{type.capitalize} packages require a namespace",
@@ -58,42 +114,42 @@ module Purl
58
114
  when "golang"
59
115
  ->(purl) do
60
116
  if purl.namespace
61
- "#{config["base_url"]}/#{purl.namespace}/#{purl.name}"
117
+ "#{base_url}/#{purl.namespace}/#{purl.name}"
62
118
  else
63
- "#{config["base_url"]}/#{purl.name}"
119
+ "#{base_url}/#{purl.name}"
64
120
  end
65
121
  end
66
122
  when "pypi"
67
- ->(purl) { "#{config["base_url"]}/#{purl.name}/" }
123
+ ->(purl) { "#{base_url}/#{purl.name}/" }
68
124
  when "hackage"
69
125
  ->(purl) do
70
126
  if purl.version
71
- "#{config["base_url"]}/#{purl.name}-#{purl.version}"
127
+ "#{base_url}/#{purl.name}-#{purl.version}"
72
128
  else
73
- "#{config["base_url"]}/#{purl.name}"
129
+ "#{base_url}/#{purl.name}"
74
130
  end
75
131
  end
76
132
  when "deno"
77
133
  ->(purl) do
78
134
  if purl.version
79
- "#{config["base_url"]}/#{purl.name}@#{purl.version}"
135
+ "#{base_url}/#{purl.name}@#{purl.version}"
80
136
  else
81
- "#{config["base_url"]}/#{purl.name}"
137
+ "#{base_url}/#{purl.name}"
82
138
  end
83
139
  end
84
140
  when "clojars"
85
141
  ->(purl) do
86
142
  if purl.namespace
87
- "#{config["base_url"]}/#{purl.namespace}/#{purl.name}"
143
+ "#{base_url}/#{purl.namespace}/#{purl.name}"
88
144
  else
89
- "#{config["base_url"]}/#{purl.name}"
145
+ "#{base_url}/#{purl.name}"
90
146
  end
91
147
  end
92
148
  when "elm"
93
149
  ->(purl) do
94
150
  if purl.namespace
95
151
  version = purl.version || "latest"
96
- "#{config["base_url"]}/#{purl.namespace}/#{purl.name}/#{version}"
152
+ "#{base_url}/#{purl.namespace}/#{purl.name}/#{version}"
97
153
  else
98
154
  raise MissingRegistryInfoError.new(
99
155
  "Elm packages require a namespace",
@@ -103,7 +159,7 @@ module Purl
103
159
  end
104
160
  end
105
161
  else
106
- ->(purl) { "#{config["base_url"]}/#{purl.name}" }
162
+ ->(purl) { "#{base_url}/#{purl.name}" }
107
163
  end
108
164
  end
109
165
 
@@ -177,6 +233,67 @@ module Purl
177
233
  version = match[3] unless match[3] == "latest"
178
234
  { type: type, namespace: namespace, name: name, version: version }
179
235
  end
236
+ when "cocoapods"
237
+ ->(match) do
238
+ name = match[1]
239
+ { type: type, namespace: nil, name: name, version: nil }
240
+ end
241
+ when "composer"
242
+ ->(match) do
243
+ namespace = match[1]
244
+ name = match[2]
245
+ { type: type, namespace: namespace, name: name, version: nil }
246
+ end
247
+ when "conda"
248
+ ->(match) do
249
+ name = match[1]
250
+ { type: type, namespace: nil, name: name, version: nil }
251
+ end
252
+ when "cpan"
253
+ ->(match) do
254
+ name = match[1]
255
+ { type: type, namespace: nil, name: name, version: nil }
256
+ end
257
+ when "hex"
258
+ ->(match) do
259
+ name = match[1]
260
+ { type: type, namespace: nil, name: name, version: nil }
261
+ end
262
+ when "nuget"
263
+ ->(match) do
264
+ name = match[1]
265
+ version = match[2] # from /version pattern
266
+ { type: type, namespace: nil, name: name, version: version }
267
+ end
268
+ when "pub"
269
+ ->(match) do
270
+ name = match[1]
271
+ { type: type, namespace: nil, name: name, version: nil }
272
+ end
273
+ when "swift"
274
+ ->(match) do
275
+ namespace = match[1]
276
+ name = match[2]
277
+ { type: type, namespace: namespace, name: name, version: nil }
278
+ end
279
+ when "bioconductor"
280
+ ->(match) do
281
+ name = match[1]
282
+ { type: type, namespace: nil, name: name, version: nil }
283
+ end
284
+ when "clojars"
285
+ ->(match) do
286
+ if match[1] && match[2]
287
+ # Has namespace: clojars.org/namespace/name
288
+ namespace = match[1]
289
+ name = match[2]
290
+ else
291
+ # No namespace: clojars.org/name
292
+ namespace = nil
293
+ name = match[1] || match[2]
294
+ end
295
+ { type: type, namespace: namespace, name: name, version: nil }
296
+ end
180
297
  else
181
298
  ->(match) do
182
299
  { type: type, namespace: nil, name: match[1], version: nil }
@@ -187,8 +304,8 @@ module Purl
187
304
  # Registry patterns loaded from JSON configuration
188
305
  REGISTRY_PATTERNS = load_registry_patterns.freeze
189
306
 
190
- def self.generate(purl)
191
- new(purl).generate
307
+ def self.generate(purl, base_url: nil)
308
+ new(purl).generate(base_url: base_url)
192
309
  end
193
310
 
194
311
  def self.supported_types
@@ -199,9 +316,49 @@ module Purl
199
316
  REGISTRY_PATTERNS.key?(type.to_s.downcase)
200
317
  end
201
318
 
202
- def self.from_url(registry_url)
203
- # Try to parse the registry URL back into a PURL
204
- REGISTRY_PATTERNS.each do |type, config|
319
+ def self.from_url(registry_url, type: nil)
320
+ # If type is specified, try that specific type first with domain-agnostic parsing
321
+ if type
322
+ normalized_type = type.to_s.downcase
323
+ config = REGISTRY_PATTERNS[normalized_type]
324
+
325
+ if config && config[:reverse_regex] && config[:reverse_parser]
326
+ # Create a domain-agnostic version of the regex by replacing the base domain
327
+ original_regex = config[:reverse_regex].source
328
+
329
+ # For simplified JSON patterns that start with /, create domain-agnostic regex
330
+ domain_agnostic_regex = nil
331
+ if original_regex.start_with?("/")
332
+ # Domain-agnostic pattern - match any domain with this path
333
+ domain_agnostic_regex = Regexp.new("^https?://[^/]+" + original_regex)
334
+ else
335
+ # Legacy full regex pattern
336
+ if original_regex =~ /\^https?:\/\/[^\/]+(.+)$/
337
+ path_pattern = $1
338
+ # Create domain-agnostic regex that matches any domain with the same path structure
339
+ domain_agnostic_regex = Regexp.new("^https?://[^/]+" + path_pattern)
340
+ end
341
+ end
342
+
343
+ if domain_agnostic_regex
344
+ match = registry_url.match(domain_agnostic_regex)
345
+ if match
346
+ parsed_data = config[:reverse_parser].call(match)
347
+ return PackageURL.new(
348
+ type: parsed_data[:type],
349
+ namespace: parsed_data[:namespace],
350
+ name: parsed_data[:name],
351
+ version: parsed_data[:version]
352
+ )
353
+ end
354
+ end
355
+ end
356
+
357
+ # If specified type didn't work, fall through to normal domain-matching logic
358
+ end
359
+
360
+ # Try to parse the registry URL back into a PURL using domain matching
361
+ REGISTRY_PATTERNS.each do |registry_type, config|
205
362
  next unless config[:reverse_regex] && config[:reverse_parser]
206
363
 
207
364
  match = registry_url.match(config[:reverse_regex])
@@ -216,8 +373,15 @@ module Purl
216
373
  end
217
374
  end
218
375
 
376
+ error_message = if type
377
+ "Unable to parse registry URL: #{registry_url} as type '#{type}'. " +
378
+ "URL structure doesn't match expected pattern for this type."
379
+ else
380
+ "Unable to parse registry URL: #{registry_url}. No matching pattern found."
381
+ end
382
+
219
383
  raise UnsupportedTypeError.new(
220
- "Unable to parse registry URL: #{registry_url}. No matching pattern found.",
384
+ error_message,
221
385
  supported_types: REGISTRY_PATTERNS.keys.select { |k| REGISTRY_PATTERNS[k][:reverse_regex] }
222
386
  )
223
387
  end
@@ -247,7 +411,7 @@ module Purl
247
411
  @purl = purl
248
412
  end
249
413
 
250
- def generate
414
+ def generate(base_url: nil)
251
415
  pattern_config = REGISTRY_PATTERNS[@purl.type.downcase]
252
416
 
253
417
  unless pattern_config
@@ -259,7 +423,13 @@ module Purl
259
423
  end
260
424
 
261
425
  begin
262
- pattern_config[:pattern].call(@purl)
426
+ if base_url
427
+ # Use custom base URL with the same URL structure
428
+ generate_with_custom_base_url(base_url, pattern_config)
429
+ else
430
+ # Use default base URL
431
+ pattern_config[:pattern].call(@purl)
432
+ end
263
433
  rescue MissingRegistryInfoError
264
434
  raise
265
435
  rescue => e
@@ -267,23 +437,87 @@ module Purl
267
437
  end
268
438
  end
269
439
 
270
- def generate_with_version
271
- base_url = generate
440
+ def generate_with_version(base_url: nil)
441
+ registry_url = generate(base_url: base_url)
272
442
 
273
443
  case @purl.type.downcase
274
444
  when "npm"
275
- @purl.version ? "#{base_url}/v/#{@purl.version}" : base_url
445
+ @purl.version ? "#{registry_url}/v/#{@purl.version}" : registry_url
276
446
  when "pypi"
277
- @purl.version ? "#{base_url}#{@purl.version}/" : base_url
447
+ @purl.version ? "#{registry_url}#{@purl.version}/" : registry_url
278
448
  when "gem"
279
- @purl.version ? "#{base_url}/versions/#{@purl.version}" : base_url
449
+ @purl.version ? "#{registry_url}/versions/#{@purl.version}" : registry_url
280
450
  when "maven"
281
- @purl.version ? "#{base_url}/#{@purl.version}" : base_url
451
+ @purl.version ? "#{registry_url}/#{@purl.version}" : registry_url
282
452
  when "nuget"
283
- @purl.version ? "#{base_url}/#{@purl.version}" : base_url
453
+ @purl.version ? "#{registry_url}/#{@purl.version}" : registry_url
284
454
  else
285
455
  # For other types, just return the base URL since version-specific URLs vary
286
- base_url
456
+ registry_url
457
+ end
458
+ end
459
+
460
+ private
461
+
462
+ def generate_with_custom_base_url(custom_base_url, pattern_config)
463
+
464
+ # Replace the base URL in the pattern lambda
465
+ case @purl.type.downcase
466
+ when "npm"
467
+ if @purl.namespace
468
+ "#{custom_base_url}/#{@purl.namespace}/#{@purl.name}"
469
+ else
470
+ "#{custom_base_url}/#{@purl.name}"
471
+ end
472
+ when "composer", "maven", "swift"
473
+ if @purl.namespace
474
+ "#{custom_base_url}/#{@purl.namespace}/#{@purl.name}"
475
+ else
476
+ raise MissingRegistryInfoError.new(
477
+ "#{@purl.type.capitalize} packages require a namespace",
478
+ type: @purl.type,
479
+ missing: "namespace"
480
+ )
481
+ end
482
+ when "golang"
483
+ if @purl.namespace
484
+ "#{custom_base_url}/#{@purl.namespace}/#{@purl.name}"
485
+ else
486
+ "#{custom_base_url}/#{@purl.name}"
487
+ end
488
+ when "pypi"
489
+ "#{custom_base_url}/#{@purl.name}/"
490
+ when "hackage"
491
+ if @purl.version
492
+ "#{custom_base_url}/#{@purl.name}-#{@purl.version}"
493
+ else
494
+ "#{custom_base_url}/#{@purl.name}"
495
+ end
496
+ when "deno"
497
+ if @purl.version
498
+ "#{custom_base_url}/#{@purl.name}@#{@purl.version}"
499
+ else
500
+ "#{custom_base_url}/#{@purl.name}"
501
+ end
502
+ when "clojars"
503
+ if @purl.namespace
504
+ "#{custom_base_url}/#{@purl.namespace}/#{@purl.name}"
505
+ else
506
+ "#{custom_base_url}/#{@purl.name}"
507
+ end
508
+ when "elm"
509
+ if @purl.namespace
510
+ version = @purl.version || "latest"
511
+ "#{custom_base_url}/#{@purl.namespace}/#{@purl.name}/#{version}"
512
+ else
513
+ raise MissingRegistryInfoError.new(
514
+ "Elm packages require a namespace",
515
+ type: @purl.type,
516
+ missing: "namespace"
517
+ )
518
+ end
519
+ else
520
+ "#{custom_base_url}/#{@purl.name}"
287
521
  end
288
522
  end
289
523
 
@@ -294,12 +528,12 @@ module Purl
294
528
 
295
529
  # Add registry URL generation methods to PackageURL
296
530
  class PackageURL
297
- def registry_url
298
- RegistryURL.generate(self)
531
+ def registry_url(base_url: nil)
532
+ RegistryURL.generate(self, base_url: base_url)
299
533
  end
300
534
 
301
- def registry_url_with_version
302
- RegistryURL.new(self).generate_with_version
535
+ def registry_url_with_version(base_url: nil)
536
+ RegistryURL.new(self).generate_with_version(base_url: base_url)
303
537
  end
304
538
 
305
539
  def supports_registry_url?
data/lib/purl/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Purl
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.1"
5
5
  end
data/lib/purl.rb CHANGED
@@ -5,7 +5,25 @@ require_relative "purl/errors"
5
5
  require_relative "purl/package_url"
6
6
  require_relative "purl/registry_url"
7
7
 
8
+ # The main PURL (Package URL) module providing functionality to parse,
9
+ # validate, and generate package URLs according to the PURL specification.
10
+ #
11
+ # A Package URL is a mostly universal standard to reference a software package
12
+ # in a uniform way across many tools, programming languages and ecosystems.
13
+ #
14
+ # @example Basic usage
15
+ # purl = Purl.parse("pkg:gem/rails@7.0.0")
16
+ # puts purl.type # "gem"
17
+ # puts purl.name # "rails"
18
+ # puts purl.version # "7.0.0"
19
+ #
20
+ # @example Registry URL conversion
21
+ # purl = Purl.from_registry_url("https://rubygems.org/gems/rails")
22
+ # puts purl.to_s # "pkg:gem/rails"
23
+ #
24
+ # @see https://github.com/package-url/purl-spec PURL Specification
8
25
  module Purl
26
+ # Base error class for all PURL-related errors
9
27
  class Error < StandardError; end
10
28
 
11
29
  # Load PURL types configuration from JSON file
@@ -21,41 +39,95 @@ module Purl
21
39
  KNOWN_TYPES = load_types_config["types"].keys.sort.freeze
22
40
 
23
41
  # Convenience method for parsing PURL strings
42
+ #
43
+ # @param purl_string [String] a PURL string starting with "pkg:"
44
+ # @return [PackageURL] parsed package URL object
45
+ # @raise [InvalidSchemeError] if string doesn't start with "pkg:"
46
+ # @raise [MalformedUrlError] if string is malformed
47
+ #
48
+ # @example
49
+ # purl = Purl.parse("pkg:gem/rails@7.0.0")
50
+ # puts purl.name # "rails"
24
51
  def self.parse(purl_string)
25
52
  PackageURL.parse(purl_string)
26
53
  end
27
54
 
28
55
  # Convenience method for parsing registry URLs back to PURLs
29
- def self.from_registry_url(registry_url)
30
- RegistryURL.from_url(registry_url)
56
+ # @param registry_url [String] The registry URL to parse
57
+ # @param type [String, Symbol, nil] Optional type hint for custom domains
58
+ def self.from_registry_url(registry_url, type: nil)
59
+ RegistryURL.from_url(registry_url, type: type)
31
60
  end
32
61
 
33
62
  # Returns all known PURL types
63
+ #
64
+ # @return [Array<String>] sorted array of known PURL type names
65
+ #
66
+ # @example
67
+ # types = Purl.known_types
68
+ # puts types.include?("gem") # true
34
69
  def self.known_types
35
70
  KNOWN_TYPES.dup
36
71
  end
37
72
 
38
73
  # Returns types that have registry URL support
74
+ #
75
+ # @return [Array<String>] sorted array of types that can generate registry URLs
76
+ #
77
+ # @example
78
+ # types = Purl.registry_supported_types
79
+ # puts types.include?("npm") # true if npm has registry support
39
80
  def self.registry_supported_types
40
81
  RegistryURL.supported_types
41
82
  end
42
83
 
43
84
  # Returns types that support reverse parsing from registry URLs
85
+ #
86
+ # @return [Array<String>] sorted array of types that can parse registry URLs back to PURLs
87
+ #
88
+ # @example
89
+ # types = Purl.reverse_parsing_supported_types
90
+ # puts types.include?("gem") # true if gem has reverse parsing support
44
91
  def self.reverse_parsing_supported_types
45
92
  RegistryURL.supported_reverse_types
46
93
  end
47
94
 
48
95
  # Check if a type is known/valid
96
+ #
97
+ # @param type [String, Symbol] the type to check
98
+ # @return [Boolean] true if type is known, false otherwise
99
+ #
100
+ # @example
101
+ # Purl.known_type?("gem") # true
102
+ # Purl.known_type?("unknown") # false
49
103
  def self.known_type?(type)
50
104
  KNOWN_TYPES.include?(type.to_s.downcase)
51
105
  end
52
106
 
53
- # Get type information including registry support
107
+ # Get comprehensive type information including registry support
108
+ #
109
+ # @param type [String, Symbol] the type to get information for
110
+ # @return [Hash] hash containing type information with keys:
111
+ # - +:type+: normalized type name
112
+ # - +:known+: whether type is known
113
+ # - +:description+: human-readable description
114
+ # - +:default_registry+: default registry URL
115
+ # - +:examples+: array of example PURLs
116
+ # - +:registry_url_generation+: whether registry URL generation is supported
117
+ # - +:reverse_parsing+: whether reverse parsing is supported
118
+ # - +:route_patterns+: array of URL patterns for this type
119
+ #
120
+ # @example
121
+ # info = Purl.type_info("gem")
122
+ # puts info[:description] # "Ruby gems from RubyGems.org"
54
123
  def self.type_info(type)
55
124
  normalized_type = type.to_s.downcase
56
125
  {
57
126
  type: normalized_type,
58
127
  known: known_type?(normalized_type),
128
+ description: type_description(normalized_type),
129
+ default_registry: default_registry(normalized_type),
130
+ examples: type_examples(normalized_type),
59
131
  registry_url_generation: RegistryURL.supports?(normalized_type),
60
132
  reverse_parsing: RegistryURL.supported_reverse_types.include?(normalized_type),
61
133
  route_patterns: RegistryURL.route_patterns_for(normalized_type)
@@ -63,6 +135,13 @@ module Purl
63
135
  end
64
136
 
65
137
  # Get comprehensive information about all types
138
+ #
139
+ # @return [Hash<String, Hash>] hash mapping type names to their information
140
+ # @see #type_info for structure of individual type information
141
+ #
142
+ # @example
143
+ # all_info = Purl.all_type_info
144
+ # gem_info = all_info["gem"]
66
145
  def self.all_type_info
67
146
  result = {}
68
147
 
@@ -82,6 +161,10 @@ module Purl
82
161
  end
83
162
 
84
163
  # Get type configuration from JSON
164
+ #
165
+ # @param type [String, Symbol] the type to get configuration for
166
+ # @return [Hash, nil] configuration hash or nil if type not found
167
+ # @api private
85
168
  def self.type_config(type)
86
169
  config = load_types_config["types"][type.to_s.downcase]
87
170
  return nil unless config
@@ -89,13 +172,39 @@ module Purl
89
172
  config.dup # Return a copy to prevent modification
90
173
  end
91
174
 
92
- # Get description for a type
175
+ # Get human-readable description for a type
176
+ #
177
+ # @param type [String, Symbol] the type to get description for
178
+ # @return [String, nil] description string or nil if not available
179
+ #
180
+ # @example
181
+ # desc = Purl.type_description("gem")
182
+ # puts desc # "Ruby gems from RubyGems.org"
93
183
  def self.type_description(type)
94
184
  config = type_config(type)
95
185
  config ? config["description"] : nil
96
186
  end
97
187
 
188
+ # Get example PURLs for a type
189
+ #
190
+ # @param type [String, Symbol] the type to get examples for
191
+ # @return [Array<String>] array of example PURL strings
192
+ #
193
+ # @example
194
+ # examples = Purl.type_examples("gem")
195
+ # puts examples.first # "pkg:gem/rails@7.0.0"
196
+ def self.type_examples(type)
197
+ config = type_config(type)
198
+ return [] unless config
199
+
200
+ config["examples"] || []
201
+ end
202
+
98
203
  # Get registry configuration for a type
204
+ #
205
+ # @param type [String, Symbol] the type to get registry config for
206
+ # @return [Hash, nil] registry configuration hash or nil if not available
207
+ # @api private
99
208
  def self.registry_config(type)
100
209
  config = type_config(type)
101
210
  return nil unless config
@@ -103,7 +212,35 @@ module Purl
103
212
  config["registry_config"]
104
213
  end
105
214
 
215
+ # Get default registry URL for a type
216
+ #
217
+ # @param type [String, Symbol] the type to get default registry for
218
+ # @return [String, nil] default registry URL or nil if not available
219
+ #
220
+ # @example
221
+ # registry = Purl.default_registry("gem")
222
+ # puts registry # "https://rubygems.org"
223
+ def self.default_registry(type)
224
+ config = type_config(type)
225
+ return nil unless config
226
+
227
+ config["default_registry"]
228
+ end
229
+
106
230
  # Get metadata about the types configuration
231
+ #
232
+ # @return [Hash] metadata hash with keys:
233
+ # - +:version+: configuration version
234
+ # - +:description+: configuration description
235
+ # - +:source+: source of the configuration
236
+ # - +:last_updated+: when configuration was last updated
237
+ # - +:total_types+: total number of types
238
+ # - +:registry_supported_types+: number of types with registry support
239
+ # - +:types_with_default_registry+: number of types with default registry
240
+ #
241
+ # @example
242
+ # metadata = Purl.types_config_metadata
243
+ # puts "Total types: #{metadata[:total_types]}"
107
244
  def self.types_config_metadata
108
245
  config = load_types_config
109
246
  {
@@ -112,7 +249,8 @@ module Purl
112
249
  source: config["source"],
113
250
  last_updated: config["last_updated"],
114
251
  total_types: config["types"].keys.length,
115
- registry_supported_types: config["types"].select { |_, v| v["registry_config"] }.keys.length
252
+ registry_supported_types: config["types"].select { |_, v| v["registry_config"] }.keys.length,
253
+ types_with_default_registry: config["types"].select { |_, v| v["default_registry"] }.keys.length
116
254
  }
117
255
  end
118
256
  end