purl 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 519631a904ead8452084959edb20711e684c0fc9c0b0a848ff6775f86bc31675
4
- data.tar.gz: cb82893be40a50ae28651b78ed293dd0303b6cc926b81985fe256d32f0b0fea7
3
+ metadata.gz: 6c6c3fd2d17a4601a4c296b18ce67d41c423a5893aeb2b79cd484c0028863abd
4
+ data.tar.gz: 2c5e92ca69e3bb8442df158f34944fad645319cdbac5fb45875c7c8521be5d0c
5
5
  SHA512:
6
- metadata.gz: 5ef9defb2759c124020aa5d6ba88eb281e0bd1fbf256cafdd3ba7c4e17bca09aec0286fe2851b74e12313062a5d3e38a76db85376b60d3f645e9f4720e0cb2b8
7
- data.tar.gz: 6399a94cf6bd5bd92fa6e5da30f7f9c6e056982728bc5828ead26e9d8c9c17b5beedbe4bdababebc0bee701bdeaf27f4ffa7b84e54a451d45e7dea7c9575c9b7
6
+ metadata.gz: 34e7c52f43f92146d605e06338f25b5d12f80f495e320901aed32206dc3e743029be9b51eb5bcd1f84ef46ca450a76ac45fa7c74da5b2506eb3d53b82db6c840
7
+ data.tar.gz: 7941f3f3cb65695448599c590e75ef06198fdafe4739e7587e0f7518f6e58fc948896452c38136a3c1a1e6e0d52acac4b766c2a5f2dd22d0a33f8b2e1b61feb1
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.1] - 2025-08-04
11
+
12
+ ### Fixed
13
+ - Remove arbitrary business logic validation, follow PURL spec for namespace requirements
14
+
15
+ ## [1.3.0] - 2025-07-29
16
+
17
+ ### Added
18
+ - RFC 6570 URI templates for registry URL generation
19
+ - Advanced URL templating capabilities for dynamic registry URL construction
20
+
21
+ ### Enhanced
22
+ - Registry URL generation now supports more flexible URL patterns
23
+ - Improved templating system for custom registry configurations
24
+
10
25
  ## [1.2.0] - 2025-07-27
11
26
 
12
27
  ### Added
@@ -74,13 +74,13 @@ module Purl
74
74
  def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil)
75
75
  @type = validate_and_normalize_type(type)
76
76
  @name = validate_name(name)
77
- @namespace = validate_namespace(namespace) if namespace
77
+ @namespace = validate_namespace(namespace)
78
78
  @version = validate_version(version) if version
79
79
  @qualifiers = validate_qualifiers(qualifiers) if qualifiers
80
80
  @subpath = validate_subpath(subpath) if subpath
81
81
 
82
- # Type-specific validation
83
- validate_type_specific_rules
82
+ # Apply post-validation normalization that depends on other components
83
+ apply_post_validation_normalization
84
84
  end
85
85
 
86
86
  # Parse a PURL string into a PackageURL object
@@ -407,7 +407,7 @@ module Purl
407
407
  # PyPI names are case-insensitive and _ should be normalized to -
408
408
  name_str.downcase.gsub("_", "-")
409
409
  when "mlflow"
410
- # MLflow name normalization is deferred until after qualifiers are set
410
+ # MLflow name normalization happens after qualifiers are validated
411
411
  name_str
412
412
  when "composer"
413
413
  # Composer names should be lowercase
@@ -418,6 +418,16 @@ module Purl
418
418
  end
419
419
 
420
420
  def validate_namespace(namespace)
421
+ # Check namespace requirements from spec
422
+ if namespace_required_for_type?(@type) && namespace.nil?
423
+ raise ValidationError.new(
424
+ "#{@type.capitalize} PURLs require a namespace per the type definition",
425
+ component: :namespace,
426
+ value: namespace,
427
+ rule: "#{@type.downcase} packages need namespace"
428
+ )
429
+ end
430
+
421
431
  return nil if namespace.nil?
422
432
 
423
433
  namespace_str = namespace.to_s.strip
@@ -516,111 +526,35 @@ module Purl
516
526
  subpath_str
517
527
  end
518
528
 
519
- def validate_type_specific_rules
520
- case @type.downcase
521
- when "conan"
522
- validate_conan_specific_rules
523
- when "cran"
524
- validate_cran_specific_rules
525
- when "swift"
526
- validate_swift_specific_rules
527
- when "cpan"
528
- validate_cpan_specific_rules
529
- when "mlflow"
530
- validate_mlflow_specific_rules
529
+ def apply_post_validation_normalization
530
+ # MLflow names are case sensitive or insensitive based on repository per spec
531
+ if @type&.downcase == "mlflow" && @qualifiers && @qualifiers["repository_url"] && @qualifiers["repository_url"].include?("azuredatabricks")
532
+ # Databricks MLflow is case insensitive - normalize to lowercase per spec
533
+ @name = @name.downcase
531
534
  end
535
+ # Other MLflow repositories (like Azure ML) are case sensitive - no normalization needed
532
536
  end
533
537
 
534
- def validate_conan_specific_rules
535
- # For conan packages, if a namespace is present WITHOUT any qualifiers at all,
536
- # it's ambiguous. However, any qualifiers (including build settings) make it unambiguous.
537
- # According to the official spec, user/channel are only required if the package was published with them.
538
- if @namespace && (@qualifiers.nil? || @qualifiers.empty?)
539
- raise ValidationError.new(
540
- "Conan PURLs with namespace require qualifiers to be unambiguous",
541
- component: :qualifiers,
542
- value: @qualifiers,
543
- rule: "conan packages with namespace need qualifiers for disambiguation"
544
- )
545
- end
538
+ def namespace_required_for_type?(type)
539
+ return false unless type
546
540
 
547
- # If channel qualifier is present without namespace, user qualifier is also needed (test case 31)
548
- # But if namespace is present, channel alone can be valid (test case 29)
549
- if @qualifiers && @qualifiers["channel"] && @qualifiers["user"].nil? && @namespace.nil?
550
- raise ValidationError.new(
551
- "Conan PURLs with 'channel' qualifier require 'user' qualifier to be unambiguous",
552
- component: :qualifiers,
553
- value: @qualifiers,
554
- rule: "conan packages with channel need user qualifier"
555
- )
556
- end
557
- end
558
-
559
- def validate_cran_specific_rules
560
- # CRAN packages require a version to be unambiguous
561
- if @version.nil?
562
- raise ValidationError.new(
563
- "CRAN PURLs require a version to be unambiguous",
564
- component: :version,
565
- value: @version,
566
- rule: "cran packages need version"
567
- )
568
- end
569
- end
570
-
571
- def validate_swift_specific_rules
572
- # Swift packages require a namespace to be unambiguous
573
- if @namespace.nil?
574
- raise ValidationError.new(
575
- "Swift PURLs require a namespace to be unambiguous",
576
- component: :namespace,
577
- value: @namespace,
578
- rule: "swift packages need namespace"
579
- )
580
- end
541
+ # Read from purl-types.json (included in gem)
542
+ types_data = self.class.purl_types_data
543
+ type_config = types_data.dig("types", type.downcase)
544
+ return false unless type_config
581
545
 
582
- # Swift packages require a version to be unambiguous
583
- if @version.nil?
584
- raise ValidationError.new(
585
- "Swift PURLs require a version to be unambiguous",
586
- component: :version,
587
- value: @version,
588
- rule: "swift packages need version"
589
- )
590
- end
591
- end
592
-
593
- def validate_mlflow_specific_rules
594
- # MLflow names are case sensitive or insensitive based on repository
595
- if @qualifiers && @qualifiers["repository_url"] && @qualifiers["repository_url"].include?("azuredatabricks")
596
- # Azure Databricks MLflow is case insensitive - normalize to lowercase
597
- @name = @name.downcase
598
- end
599
- # Other MLflow repositories are case sensitive - no normalization needed
546
+ # Check namespace_requirement field
547
+ type_config["namespace_requirement"] == "required"
600
548
  end
601
549
 
602
- def validate_cpan_specific_rules
603
- # CPAN has complex rules about module vs distribution names
604
- # These test cases are checking for specific invalid patterns
605
-
606
- # Case 51: "Perl-Version" should be invalid (module name like distribution name)
607
- if @name == "Perl-Version"
608
- raise ValidationError.new(
609
- "CPAN module name 'Perl-Version' conflicts with distribution naming",
610
- component: :name,
611
- value: @name,
612
- rule: "cpan module vs distribution name conflict"
613
- )
614
- end
615
-
616
- # Case 52: namespace with distribution-like name should be invalid
617
- if @namespace == "GDT" && @name == "URI::PackageURL"
618
- raise ValidationError.new(
619
- "CPAN distribution name 'GDT/URI::PackageURL' has invalid format",
620
- component: :name,
621
- value: "#{@namespace}/#{@name}",
622
- rule: "cpan distribution vs module name conflict"
623
- )
550
+ def self.purl_types_data
551
+ @purl_types_data ||= begin
552
+ require "json"
553
+ types_file = File.join(File.dirname(__FILE__), "..", "..", "purl-types.json")
554
+ JSON.parse(File.read(types_file))
555
+ rescue
556
+ # Fallback to empty structure if file can't be read
557
+ {"types" => {}}
624
558
  end
625
559
  end
626
560
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "addressable/template"
4
+
3
5
  module Purl
4
6
  class RegistryURL
5
7
  # Load registry patterns from JSON configuration
@@ -27,8 +29,7 @@ module Purl
27
29
  # Get the default registry for this type from the extended config
28
30
  default_registry = type_config["default_registry"]
29
31
 
30
- # Use route_patterns directly from our extended registry config
31
- route_patterns = config["route_patterns"] || []
32
+ # Route patterns are replaced by URI templates
32
33
 
33
34
  # Build reverse regex from template or use legacy format
34
35
  reverse_regex = nil
@@ -45,10 +46,13 @@ module Purl
45
46
 
46
47
  {
47
48
  base_url: config["base_url"] || (default_registry ? default_registry + config["path_template"]&.split('/:').first : nil),
48
- route_patterns: route_patterns,
49
49
  reverse_regex: reverse_regex,
50
50
  pattern: build_generation_lambda(type, config, default_registry),
51
- reverse_parser: reverse_regex ? build_reverse_parser(type, config) : nil
51
+ reverse_parser: reverse_regex ? build_reverse_parser(type, config) : nil,
52
+ uri_template: config["uri_template"] ? Addressable::Template.new(config["uri_template"]) : nil,
53
+ uri_template_no_namespace: config["uri_template_no_namespace"] ? Addressable::Template.new(config["uri_template_no_namespace"]) : nil,
54
+ uri_template_with_version: config["uri_template_with_version"] ? Addressable::Template.new(config["uri_template_with_version"]) : nil,
55
+ uri_template_with_version_no_namespace: config["uri_template_with_version_no_namespace"] ? Addressable::Template.new(config["uri_template_with_version_no_namespace"]) : nil
52
56
  }
53
57
  end
54
58
 
@@ -376,18 +380,41 @@ module Purl
376
380
  pattern_config = REGISTRY_PATTERNS[type.to_s.downcase]
377
381
  return [] unless pattern_config
378
382
 
379
- pattern_config[:route_patterns] || []
383
+ # Generate route patterns from URI templates
384
+ patterns = []
385
+
386
+ if pattern_config[:uri_template]
387
+ patterns << uri_template_to_route_pattern(pattern_config[:uri_template])
388
+ end
389
+
390
+ if pattern_config[:uri_template_no_namespace]
391
+ patterns << uri_template_to_route_pattern(pattern_config[:uri_template_no_namespace])
392
+ end
393
+
394
+ if pattern_config[:uri_template_with_version]
395
+ patterns << uri_template_to_route_pattern(pattern_config[:uri_template_with_version])
396
+ end
397
+
398
+ if pattern_config[:uri_template_with_version_no_namespace]
399
+ patterns << uri_template_to_route_pattern(pattern_config[:uri_template_with_version_no_namespace])
400
+ end
401
+
402
+ patterns.uniq
380
403
  end
381
404
 
382
405
  def self.all_route_patterns
383
406
  result = {}
384
407
  REGISTRY_PATTERNS.each do |type, config|
385
- if config[:route_patterns]
386
- result[type] = config[:route_patterns]
387
- end
408
+ patterns = route_patterns_for(type)
409
+ result[type] = patterns unless patterns.empty?
388
410
  end
389
411
  result
390
412
  end
413
+
414
+ private_class_method def self.uri_template_to_route_pattern(template)
415
+ # Convert URI template format {variable} to route pattern format :variable
416
+ template.pattern.gsub(/\{([^}]+)\}/, ':\1')
417
+ end
391
418
 
392
419
  def initialize(purl)
393
420
  @purl = purl
@@ -408,8 +435,12 @@ module Purl
408
435
  if base_url
409
436
  # Use custom base URL with the same URL structure
410
437
  generate_with_custom_base_url(base_url, pattern_config)
438
+ elsif pattern_config[:uri_template]
439
+ # Use URI template if available
440
+ template = select_uri_template(pattern_config, include_version: false)
441
+ generate_with_uri_template(template)
411
442
  else
412
- # Use default base URL
443
+ # Fall back to legacy lambda pattern
413
444
  pattern_config[:pattern].call(@purl)
414
445
  end
415
446
  rescue MissingRegistryInfoError
@@ -420,27 +451,121 @@ module Purl
420
451
  end
421
452
 
422
453
  def generate_with_version(base_url: nil)
423
- registry_url = generate(base_url: base_url)
454
+ return generate(base_url: base_url) unless @purl.version
455
+
456
+ pattern_config = REGISTRY_PATTERNS[@purl.type.downcase]
457
+
458
+ if base_url
459
+ # Use custom base URL with version
460
+ generate_with_custom_base_url_and_version(base_url, pattern_config)
461
+ elsif pattern_config[:uri_template_with_version] || pattern_config[:uri_template]
462
+ # Use version-specific URI template if available
463
+ template = select_uri_template(pattern_config, include_version: true)
464
+ generate_with_uri_template(template, include_version: true)
465
+ else
466
+ # Fall back to legacy version handling
467
+ registry_url = generate(base_url: base_url)
468
+
469
+ case @purl.type.downcase
470
+ when "npm"
471
+ "#{registry_url}/v/#{@purl.version}"
472
+ when "pypi"
473
+ "#{registry_url}#{@purl.version}/"
474
+ when "gem"
475
+ "#{registry_url}/versions/#{@purl.version}"
476
+ when "maven"
477
+ "#{registry_url}/#{@purl.version}"
478
+ when "nuget"
479
+ "#{registry_url}/#{@purl.version}"
480
+ else
481
+ registry_url
482
+ end
483
+ end
484
+ end
485
+
486
+ private
487
+
488
+ def select_uri_template(pattern_config, include_version: false)
489
+ if include_version
490
+ if @purl.namespace && pattern_config[:uri_template_with_version]
491
+ pattern_config[:uri_template_with_version]
492
+ elsif !@purl.namespace && pattern_config[:uri_template_with_version_no_namespace]
493
+ pattern_config[:uri_template_with_version_no_namespace]
494
+ elsif pattern_config[:uri_template_with_version]
495
+ pattern_config[:uri_template_with_version]
496
+ elsif @purl.namespace && pattern_config[:uri_template]
497
+ pattern_config[:uri_template]
498
+ elsif !@purl.namespace && pattern_config[:uri_template_no_namespace]
499
+ pattern_config[:uri_template_no_namespace]
500
+ else
501
+ pattern_config[:uri_template]
502
+ end
503
+ else
504
+ if @purl.namespace && pattern_config[:uri_template]
505
+ pattern_config[:uri_template]
506
+ elsif !@purl.namespace && pattern_config[:uri_template_no_namespace]
507
+ pattern_config[:uri_template_no_namespace]
508
+ else
509
+ pattern_config[:uri_template]
510
+ end
511
+ end
512
+ end
513
+
514
+ def generate_with_uri_template(template, include_version: false)
515
+ variables = {
516
+ name: @purl.name
517
+ }
518
+
519
+ # Add namespace if present and required
520
+ if @purl.namespace
521
+ variables[:namespace] = @purl.namespace
522
+ end
523
+
524
+ # Add version if requested and present
525
+ if include_version && @purl.version
526
+ variables[:version] = @purl.version
527
+ end
528
+
529
+ # Handle namespace requirements based on package type
530
+ case @purl.type.downcase
531
+ when "composer", "maven", "swift", "elm"
532
+ unless @purl.namespace
533
+ raise MissingRegistryInfoError.new(
534
+ "#{@purl.type.capitalize} packages require a namespace",
535
+ type: @purl.type,
536
+ missing: "namespace"
537
+ )
538
+ end
539
+ end
540
+
541
+ # Build the URL manually to avoid encoding issues with special characters like @
542
+ result = template.pattern
543
+
544
+ variables.each do |key, value|
545
+ result = result.gsub("{#{key}}", value.to_s)
546
+ end
547
+
548
+ result
549
+ end
550
+
551
+ def generate_with_custom_base_url_and_version(custom_base_url, pattern_config)
552
+ # For now, fall back to the existing custom base URL method and add version
553
+ base_result = generate_with_custom_base_url(custom_base_url, pattern_config)
424
554
 
425
555
  case @purl.type.downcase
426
556
  when "npm"
427
- @purl.version ? "#{registry_url}/v/#{@purl.version}" : registry_url
557
+ "#{base_result}/v/#{@purl.version}"
428
558
  when "pypi"
429
- @purl.version ? "#{registry_url}#{@purl.version}/" : registry_url
559
+ "#{base_result}#{@purl.version}/"
430
560
  when "gem"
431
- @purl.version ? "#{registry_url}/versions/#{@purl.version}" : registry_url
432
- when "maven"
433
- @purl.version ? "#{registry_url}/#{@purl.version}" : registry_url
434
- when "nuget"
435
- @purl.version ? "#{registry_url}/#{@purl.version}" : registry_url
561
+ "#{base_result}/versions/#{@purl.version}"
562
+ when "maven", "nuget"
563
+ "#{base_result}/#{@purl.version}"
436
564
  else
437
- # For other types, just return the base URL since version-specific URLs vary
438
- registry_url
565
+ base_result
439
566
  end
440
567
  end
441
568
 
442
- private
443
-
444
569
  def generate_with_custom_base_url(custom_base_url, pattern_config)
445
570
 
446
571
  # Replace the base URL in the pattern lambda
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.2.0"
4
+ VERSION = "1.3.1"
5
5
  end
data/purl-types.json CHANGED
@@ -50,10 +50,8 @@
50
50
  ],
51
51
  "registry_config": {
52
52
  "base_url": "https://crates.io/crates",
53
- "route_patterns": [
54
- "https://crates.io/crates/:name"
55
- ],
56
53
  "reverse_regex": "^https://crates\\.io/crates/([^/?#]+)",
54
+ "uri_template": "https://crates.io/crates/{name}",
57
55
  "components": {
58
56
  "namespace": false,
59
57
  "version_in_url": false
@@ -70,10 +68,8 @@
70
68
  ],
71
69
  "registry_config": {
72
70
  "base_url": "https://cocoapods.org/pods",
73
- "route_patterns": [
74
- "https://cocoapods.org/pods/:name"
75
- ],
76
71
  "reverse_regex": "^https://cocoapods\\.org/pods/([^/?#]+)",
72
+ "uri_template": "https://cocoapods.org/pods/{name}",
77
73
  "components": {
78
74
  "namespace": false,
79
75
  "version_in_url": false
@@ -83,6 +79,7 @@
83
79
  "composer": {
84
80
  "description": "Composer PHP packages",
85
81
  "default_registry": "https://packagist.org",
82
+ "namespace_requirement": "required",
86
83
  "examples": [
87
84
  "pkg:composer/symfony/console@6.1.7",
88
85
  "pkg:composer/laravel/framework@9.42.2",
@@ -90,10 +87,8 @@
90
87
  ],
91
88
  "registry_config": {
92
89
  "base_url": "https://packagist.org/packages",
93
- "route_patterns": [
94
- "https://packagist.org/packages/:namespace/:name"
95
- ],
96
90
  "reverse_regex": "^https://packagist\\.org/packages/([^/?#]+)/([^/?#]+)",
91
+ "uri_template": "https://packagist.org/packages/{namespace}/{name}",
97
92
  "components": {
98
93
  "namespace": true,
99
94
  "namespace_required": true,
@@ -111,10 +106,8 @@
111
106
  ],
112
107
  "registry_config": {
113
108
  "base_url": "https://conan.io/center/recipes",
114
- "route_patterns": [
115
- "https://conan.io/center/recipes/:name"
116
- ],
117
109
  "reverse_regex": "^https://conan\\.io/center/recipes/([^/?#]+)",
110
+ "uri_template": "https://conan.io/center/recipes/{name}",
118
111
  "components": {
119
112
  "namespace": false,
120
113
  "version_in_url": false
@@ -131,10 +124,8 @@
131
124
  ],
132
125
  "registry_config": {
133
126
  "base_url": "https://anaconda.org/conda-forge",
134
- "route_patterns": [
135
- "https://anaconda.org/conda-forge/:name"
136
- ],
137
127
  "reverse_regex": "^https://anaconda\\.org/conda-forge/([^/?#]+)",
128
+ "uri_template": "https://anaconda.org/conda-forge/{name}",
138
129
  "components": {
139
130
  "namespace": false,
140
131
  "version_in_url": false
@@ -151,10 +142,8 @@
151
142
  ],
152
143
  "registry_config": {
153
144
  "base_url": "https://metacpan.org/dist",
154
- "route_patterns": [
155
- "https://metacpan.org/dist/:name"
156
- ],
157
145
  "reverse_regex": "^https://metacpan\\.org/dist/([^/?#]+)",
146
+ "uri_template": "https://metacpan.org/dist/{name}",
158
147
  "components": {
159
148
  "namespace": false,
160
149
  "version_in_url": false
@@ -164,6 +153,7 @@
164
153
  "cran": {
165
154
  "description": "CRAN R packages",
166
155
  "default_registry": "https://cran.r-project.org",
156
+ "namespace_requirement": "prohibited",
167
157
  "examples": [
168
158
  "pkg:cran/ggplot2@3.4.0",
169
159
  "pkg:cran/dplyr@1.0.10",
@@ -193,6 +183,7 @@
193
183
  "gem": {
194
184
  "description": "RubyGems",
195
185
  "default_registry": "https://rubygems.org",
186
+ "namespace_requirement": "prohibited",
196
187
  "examples": [
197
188
  "pkg:gem/ruby-advisory-db-check@0.12.4",
198
189
  "pkg:gem/rails@7.0.4",
@@ -200,11 +191,9 @@
200
191
  ],
201
192
  "registry_config": {
202
193
  "base_url": "https://rubygems.org/gems",
203
- "route_patterns": [
204
- "https://rubygems.org/gems/:name",
205
- "https://rubygems.org/gems/:name/versions/:version"
206
- ],
207
194
  "reverse_regex": "^https://rubygems\\.org/gems/([^/?#]+)(?:/versions/([^/?#]+))?",
195
+ "uri_template": "https://rubygems.org/gems/{name}",
196
+ "uri_template_with_version": "https://rubygems.org/gems/{name}/versions/{version}",
208
197
  "components": {
209
198
  "namespace": false,
210
199
  "version_in_url": true,
@@ -240,11 +229,8 @@
240
229
  ],
241
230
  "registry_config": {
242
231
  "base_url": "https://pkg.go.dev",
243
- "route_patterns": [
244
- "https://pkg.go.dev/:namespace/:name",
245
- "https://pkg.go.dev/:full_path"
246
- ],
247
232
  "reverse_regex": "^https://pkg\\.go\\.dev/(.+)",
233
+ "uri_template": "https://pkg.go.dev/{namespace}/{name}",
248
234
  "components": {
249
235
  "namespace": true,
250
236
  "namespace_required": true,
@@ -263,11 +249,9 @@
263
249
  ],
264
250
  "registry_config": {
265
251
  "base_url": "https://hackage.haskell.org/package",
266
- "route_patterns": [
267
- "https://hackage.haskell.org/package/:name",
268
- "https://hackage.haskell.org/package/:name-:version"
269
- ],
270
252
  "reverse_regex": "^https://hackage\\.haskell\\.org/package/([^/?#-]+)(?:-([^/?#]+))?",
253
+ "uri_template": "https://hackage.haskell.org/package/{name}",
254
+ "uri_template_with_version": "https://hackage.haskell.org/package/{name}-{version}",
271
255
  "components": {
272
256
  "namespace": false,
273
257
  "version_in_url": true,
@@ -285,10 +269,8 @@
285
269
  ],
286
270
  "registry_config": {
287
271
  "base_url": "https://hex.pm/packages",
288
- "route_patterns": [
289
- "https://hex.pm/packages/:name"
290
- ],
291
272
  "reverse_regex": "^https://hex\\.pm/packages/([^/?#]+)",
273
+ "uri_template": "https://hex.pm/packages/{name}",
292
274
  "components": {
293
275
  "namespace": false,
294
276
  "version_in_url": false
@@ -299,16 +281,14 @@
299
281
  "description": "Hugging Face ML models",
300
282
  "default_registry": "https://huggingface.co",
301
283
  "examples": [
302
- "pkg:huggingface/distilbert-base-uncased@043235d6088ecd3dd5fb5ca3592b6913fd516027",
284
+ "pkg:huggingface/huggingface/distilbert-base-uncased@043235d6088ecd3dd5fb5ca3592b6913fd516027",
303
285
  "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https://hub-ci.huggingface.co"
304
286
  ],
305
287
  "registry_config": {
306
288
  "base_url": "https://huggingface.co",
307
- "route_patterns": [
308
- "https://huggingface.co/:namespace/:name",
309
- "https://huggingface.co/:name"
310
- ],
311
289
  "reverse_regex": "^https://huggingface\\.co/(?:([^/?#]+)/)?([^/?#]+)",
290
+ "uri_template": "https://huggingface.co/{namespace}/{name}",
291
+ "uri_template_no_namespace": "https://huggingface.co/{name}",
312
292
  "components": {
313
293
  "namespace": true,
314
294
  "namespace_required": false,
@@ -326,11 +306,9 @@
326
306
  ],
327
307
  "registry_config": {
328
308
  "base_url": "https://luarocks.org/modules",
329
- "route_patterns": [
330
- "https://luarocks.org/modules/:namespace/:name",
331
- "https://luarocks.org/modules/:name"
332
- ],
333
309
  "reverse_regex": "^https://luarocks\\.org/modules/(?:([^/?#]+)/)?([^/?#]+)",
310
+ "uri_template": "https://luarocks.org/modules/{namespace}/{name}",
311
+ "uri_template_no_namespace": "https://luarocks.org/modules/{name}",
334
312
  "components": {
335
313
  "namespace": true,
336
314
  "namespace_required": false,
@@ -341,6 +319,7 @@
341
319
  "maven": {
342
320
  "description": "PURL type for Maven JARs and related artifacts.",
343
321
  "default_registry": "https://repo.maven.apache.org/maven2",
322
+ "namespace_requirement": "required",
344
323
  "examples": [
345
324
  "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
346
325
  "pkg:maven/junit/junit@4.13.2",
@@ -348,11 +327,9 @@
348
327
  ],
349
328
  "registry_config": {
350
329
  "base_url": "https://mvnrepository.com/artifact",
351
- "route_patterns": [
352
- "https://mvnrepository.com/artifact/:namespace/:name",
353
- "https://mvnrepository.com/artifact/:namespace/:name/:version"
354
- ],
355
330
  "reverse_regex": "^https://mvnrepository\\.com/artifact/([^/?#]+)/([^/?#]+)(?:/([^/?#]+))?",
331
+ "uri_template": "https://mvnrepository.com/artifact/{namespace}/{name}",
332
+ "uri_template_with_version": "https://mvnrepository.com/artifact/{namespace}/{name}/{version}",
356
333
  "components": {
357
334
  "namespace": true,
358
335
  "namespace_required": true,
@@ -372,6 +349,7 @@
372
349
  "npm": {
373
350
  "description": "PURL type for npm packages.",
374
351
  "default_registry": "https://registry.npmjs.org",
352
+ "namespace_requirement": "optional",
375
353
  "examples": [
376
354
  "pkg:npm/@babel/core@7.20.0",
377
355
  "pkg:npm/lodash@4.17.21",
@@ -379,13 +357,11 @@
379
357
  ],
380
358
  "registry_config": {
381
359
  "base_url": "https://www.npmjs.com/package",
382
- "route_patterns": [
383
- "https://www.npmjs.com/package/:namespace/:name",
384
- "https://www.npmjs.com/package/:name",
385
- "https://www.npmjs.com/package/:namespace/:name/v/:version",
386
- "https://www.npmjs.com/package/:name/v/:version"
387
- ],
388
360
  "reverse_regex": "^https://(?:www\\.)?npmjs\\.com/package/(?:(@[^/]+)/)?([^/?#]+)(?:/v/([^/?#]+))?",
361
+ "uri_template": "https://www.npmjs.com/package/{namespace}/{name}",
362
+ "uri_template_no_namespace": "https://www.npmjs.com/package/{name}",
363
+ "uri_template_with_version": "https://www.npmjs.com/package/{namespace}/{name}/v/{version}",
364
+ "uri_template_with_version_no_namespace": "https://www.npmjs.com/package/{name}/v/{version}",
389
365
  "components": {
390
366
  "namespace": true,
391
367
  "namespace_required": false,
@@ -405,11 +381,9 @@
405
381
  ],
406
382
  "registry_config": {
407
383
  "base_url": "https://www.nuget.org/packages",
408
- "route_patterns": [
409
- "https://www.nuget.org/packages/:name",
410
- "https://www.nuget.org/packages/:name/:version"
411
- ],
412
384
  "reverse_regex": "^https://(?:www\\.)?nuget\\.org/packages/([^/?#]+)(?:/([^/?#]+))?",
385
+ "uri_template": "https://www.nuget.org/packages/{name}",
386
+ "uri_template_with_version": "https://www.nuget.org/packages/{name}/{version}",
413
387
  "components": {
414
388
  "namespace": false,
415
389
  "version_in_url": true,
@@ -437,10 +411,8 @@
437
411
  ],
438
412
  "registry_config": {
439
413
  "base_url": "https://pub.dev/packages",
440
- "route_patterns": [
441
- "https://pub.dev/packages/:name"
442
- ],
443
414
  "reverse_regex": "^https://pub\\.dev/packages/([^/?#]+)",
415
+ "uri_template": "https://pub.dev/packages/{name}",
444
416
  "components": {
445
417
  "namespace": false,
446
418
  "version_in_url": false
@@ -457,11 +429,9 @@
457
429
  ],
458
430
  "registry_config": {
459
431
  "base_url": "https://pypi.org/project",
460
- "route_patterns": [
461
- "https://pypi.org/project/:name/",
462
- "https://pypi.org/project/:name/:version/"
463
- ],
464
432
  "reverse_regex": "^https://pypi\\.org/project/([^/?#]+)/?(?:([^/?#]+)/?)?",
433
+ "uri_template": "https://pypi.org/project/{name}/",
434
+ "uri_template_with_version": "https://pypi.org/project/{name}/{version}/",
465
435
  "components": {
466
436
  "namespace": false,
467
437
  "version_in_url": true,
@@ -483,7 +453,7 @@
483
453
  "default_registry": null,
484
454
  "examples": [
485
455
  "pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25",
486
- "pkg:rpm/centerim@4.22.10-1.el6?arch=i686&epoch=1&distro=fedora-25"
456
+ "pkg:rpm/fedora/centerim@4.22.10-1.el6?arch=i686&epoch=1&distro=fedora-25"
487
457
  ]
488
458
  },
489
459
  "swid": {
@@ -498,16 +468,15 @@
498
468
  "swift": {
499
469
  "description": "Swift packages",
500
470
  "default_registry": "https://swiftpackageindex.com",
471
+ "namespace_requirement": "required",
501
472
  "examples": [
502
473
  "pkg:swift/github.com/Alamofire/Alamofire@5.6.4",
503
474
  "pkg:swift/github.com/apple/swift-package-manager@1.7.0"
504
475
  ],
505
476
  "registry_config": {
506
477
  "base_url": "https://swiftpackageindex.com",
507
- "route_patterns": [
508
- "https://swiftpackageindex.com/:namespace/:name"
509
- ],
510
478
  "reverse_regex": "^https://swiftpackageindex\\.com/([^/?#]+)/([^/?#]+)",
479
+ "uri_template": "https://swiftpackageindex.com/{namespace}/{name}",
511
480
  "components": {
512
481
  "namespace": true,
513
482
  "namespace_required": true,
@@ -524,11 +493,9 @@
524
493
  ],
525
494
  "registry_config": {
526
495
  "base_url": "https://clojars.org",
527
- "route_patterns": [
528
- "https://clojars.org/:namespace/:name",
529
- "https://clojars.org/:name"
530
- ],
531
496
  "reverse_regex": "^https://clojars\\.org/(?:([^/?#]+)/)?([^/?#]+)",
497
+ "uri_template": "https://clojars.org/{namespace}/{name}",
498
+ "uri_template_no_namespace": "https://clojars.org/{name}",
532
499
  "components": {
533
500
  "namespace": true,
534
501
  "namespace_required": false,
@@ -545,11 +512,9 @@
545
512
  ],
546
513
  "registry_config": {
547
514
  "base_url": "https://package.elm-lang.org/packages",
548
- "route_patterns": [
549
- "https://package.elm-lang.org/packages/:namespace/:name/latest",
550
- "https://package.elm-lang.org/packages/:namespace/:name/:version"
551
- ],
552
515
  "reverse_regex": "^https://package\\.elm-lang\\.org/packages/([^/?#]+)/([^/?#]+)(?:/([^/?#]+))?",
516
+ "uri_template": "https://package.elm-lang.org/packages/{namespace}/{name}/latest",
517
+ "uri_template_with_version": "https://package.elm-lang.org/packages/{namespace}/{name}/{version}",
553
518
  "components": {
554
519
  "namespace": true,
555
520
  "namespace_required": true,
@@ -567,11 +532,9 @@
567
532
  ],
568
533
  "registry_config": {
569
534
  "base_url": "https://deno.land/x",
570
- "route_patterns": [
571
- "https://deno.land/x/:name",
572
- "https://deno.land/x/:name@:version"
573
- ],
574
535
  "reverse_regex": "^https://deno\\.land/x/([^/?#@]+)(?:@([^/?#]+))?",
536
+ "uri_template": "https://deno.land/x/{name}",
537
+ "uri_template_with_version": "https://deno.land/x/{name}@{version}",
575
538
  "components": {
576
539
  "namespace": false,
577
540
  "version_in_url": true,
@@ -588,10 +551,8 @@
588
551
  ],
589
552
  "registry_config": {
590
553
  "base_url": "https://formulae.brew.sh/formula",
591
- "route_patterns": [
592
- "https://formulae.brew.sh/formula/:name"
593
- ],
594
554
  "reverse_regex": "^https://formulae\\.brew\\.sh/formula/([^/?#]+)",
555
+ "uri_template": "https://formulae.brew.sh/formula/{name}",
595
556
  "components": {
596
557
  "namespace": false,
597
558
  "version_in_url": false
@@ -607,10 +568,8 @@
607
568
  ],
608
569
  "registry_config": {
609
570
  "base_url": "https://bioconductor.org/packages",
610
- "route_patterns": [
611
- "https://bioconductor.org/packages/:name"
612
- ],
613
571
  "reverse_regex": "^https://bioconductor\\.org/packages/([^/?#]+)",
572
+ "uri_template": "https://bioconductor.org/packages/{name}",
614
573
  "components": {
615
574
  "namespace": false,
616
575
  "version_in_url": false
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: addressable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.8'
12
26
  description: |-
13
27
  This library features comprehensive error handling with namespaced error types, bidirectional registry URL conversion, and JSON-based configuration for cross-language compatibility.
14
28
  It supports 37 package types (32 official + 5 additional ecosystems) and is fully compliant with the official PURL specification test suite.