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.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +167 -0
- data/README.md +159 -23
- data/Rakefile +171 -0
- data/SECURITY.md +164 -0
- data/lib/purl/errors.rb +56 -2
- data/lib/purl/package_url.rb +148 -2
- data/lib/purl/registry_url.rb +274 -40
- data/lib/purl/version.rb +1 -1
- data/lib/purl.rb +143 -5
- data/purl-types.json +242 -17
- data/schemas/purl-types.schema.json +154 -0
- data/schemas/test-suite-data.schema.json +134 -0
- metadata +9 -2
data/lib/purl/registry_url.rb
CHANGED
|
@@ -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:
|
|
30
|
-
reverse_regex:
|
|
31
|
-
pattern: build_generation_lambda(type, config),
|
|
32
|
-
reverse_parser:
|
|
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
|
-
|
|
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
|
-
"#{
|
|
97
|
+
"#{base_url}/#{purl.namespace}/#{purl.name}"
|
|
42
98
|
else
|
|
43
|
-
"#{
|
|
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
|
-
"#{
|
|
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
|
-
"#{
|
|
117
|
+
"#{base_url}/#{purl.namespace}/#{purl.name}"
|
|
62
118
|
else
|
|
63
|
-
"#{
|
|
119
|
+
"#{base_url}/#{purl.name}"
|
|
64
120
|
end
|
|
65
121
|
end
|
|
66
122
|
when "pypi"
|
|
67
|
-
->(purl) { "#{
|
|
123
|
+
->(purl) { "#{base_url}/#{purl.name}/" }
|
|
68
124
|
when "hackage"
|
|
69
125
|
->(purl) do
|
|
70
126
|
if purl.version
|
|
71
|
-
"#{
|
|
127
|
+
"#{base_url}/#{purl.name}-#{purl.version}"
|
|
72
128
|
else
|
|
73
|
-
"#{
|
|
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
|
-
"#{
|
|
135
|
+
"#{base_url}/#{purl.name}@#{purl.version}"
|
|
80
136
|
else
|
|
81
|
-
"#{
|
|
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
|
-
"#{
|
|
143
|
+
"#{base_url}/#{purl.namespace}/#{purl.name}"
|
|
88
144
|
else
|
|
89
|
-
"#{
|
|
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
|
-
"#{
|
|
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) { "#{
|
|
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
|
-
#
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ? "#{
|
|
445
|
+
@purl.version ? "#{registry_url}/v/#{@purl.version}" : registry_url
|
|
276
446
|
when "pypi"
|
|
277
|
-
@purl.version ? "#{
|
|
447
|
+
@purl.version ? "#{registry_url}#{@purl.version}/" : registry_url
|
|
278
448
|
when "gem"
|
|
279
|
-
@purl.version ? "#{
|
|
449
|
+
@purl.version ? "#{registry_url}/versions/#{@purl.version}" : registry_url
|
|
280
450
|
when "maven"
|
|
281
|
-
@purl.version ? "#{
|
|
451
|
+
@purl.version ? "#{registry_url}/#{@purl.version}" : registry_url
|
|
282
452
|
when "nuget"
|
|
283
|
-
@purl.version ? "#{
|
|
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
|
-
|
|
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
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
|
-
|
|
30
|
-
|
|
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
|