purl 1.1.0 → 1.1.2
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/CHANGELOG.md +51 -0
- data/README.md +5 -1
- data/Rakefile +256 -1
- data/lib/purl/errors.rb +56 -2
- data/lib/purl/package_url.rb +150 -12
- data/lib/purl/version.rb +1 -1
- data/lib/purl.rb +119 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2e3a83fa55008ae8b31be8d3e5c7306556e4b7d4371e6db46927deae1edee3c
|
|
4
|
+
data.tar.gz: 5a092dfa8e630db8ca28064b1e101a62320d601708b38351a92b2f5431d692dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8db461d6066ce37a6fb9c50c373e2dd6739fddb123ac0627c14f645dadc0b610b24e30a6d0dbcda28148f103870ba8cbc28e34efb65ab026b463752753fcd6d
|
|
7
|
+
data.tar.gz: 5420d53af149d4c1ef1fd8337f232bf4f03f420fcaa3a2de32a38751505167c0eb3d431b382ad803f29ed8b2230f80789cbcdb9c15b4e43a0af5b69fcb554031
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.2] - 2025-07-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Comprehensive benchmarking rake tasks for performance analysis
|
|
14
|
+
- `rake benchmark:parse` - PURL parsing performance benchmarks
|
|
15
|
+
- `rake benchmark:types` - Package type parsing comparison
|
|
16
|
+
- `rake benchmark:registry` - Registry URL generation benchmarks
|
|
17
|
+
- `rake benchmark:all` - Run all benchmarks
|
|
18
|
+
|
|
19
|
+
### Improved
|
|
20
|
+
- **26% improvement in parsing throughput** (~175K PURLs/second)
|
|
21
|
+
- **8% improvement in string conversion performance** (~315K conversions/second)
|
|
22
|
+
- **7% improvement in object creation** (~280K objects/second)
|
|
23
|
+
- Optimized string operations in parse method with conditional regex application
|
|
24
|
+
- Reduced string allocations in `to_s` method using array joining
|
|
25
|
+
- Cached compiled regexes with `.freeze` for better performance
|
|
26
|
+
- Lower memory allocation pressure in high-throughput scenarios
|
|
27
|
+
|
|
28
|
+
## [1.1.1] - 2025-07-25
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- Comprehensive RDoc documentation for all classes and methods
|
|
32
|
+
- RDoc task in Rakefile with proper configuration
|
|
33
|
+
- API documentation link in README
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Add bigdecimal gem dependency to resolve potential loading issues
|
|
37
|
+
- Improve JSON schema loading error handling
|
|
38
|
+
|
|
39
|
+
## [1.1.0] - 2025-07-25
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- JSON schema validation for configuration files (`purl-types.json` and `test-suite-data.json`)
|
|
43
|
+
- New rake tasks: `spec:validate_schemas` and `spec:validate_examples`
|
|
44
|
+
- Examples for all package types in configuration
|
|
45
|
+
- Default registry URLs for package types
|
|
46
|
+
- Enhanced reverse parsing support for additional package types
|
|
47
|
+
- `with` method for creating modified PURL objects (immutable pattern)
|
|
48
|
+
- FUNDING.yml for project sponsorship support
|
|
49
|
+
|
|
50
|
+
### Enhanced
|
|
51
|
+
- Improved README documentation with custom registry examples
|
|
52
|
+
- Enhanced test coverage for new functionality
|
|
53
|
+
- Better compliance test output formatting
|
|
54
|
+
- Comprehensive package type examples and validation
|
|
55
|
+
|
|
56
|
+
### Documentation
|
|
57
|
+
- Updated documentation to remove emoji and enhance readability
|
|
58
|
+
- Added comprehensive examples for custom registry usage
|
|
59
|
+
- Enhanced API documentation throughout
|
|
60
|
+
|
|
10
61
|
## [1.0.0] - 2025-01-24
|
|
11
62
|
|
|
12
63
|
### Added
|
data/README.md
CHANGED
|
@@ -8,7 +8,11 @@ This library features comprehensive error handling with namespaced error types,
|
|
|
8
8
|
[](https://rubygems.org/gems/purl)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
|
|
11
|
-
**[Available on RubyGems](https://rubygems.org/gems/purl)**
|
|
11
|
+
**[Available on RubyGems](https://rubygems.org/gems/purl)** | **[API Documentation](https://rdoc.info/github/andrew/purl)**
|
|
12
|
+
|
|
13
|
+
## Related Libraries
|
|
14
|
+
|
|
15
|
+
- **[Vers](https://github.com/andrew/vers)** - A Ruby library for working with version ranges that supports the VERS specification
|
|
12
16
|
|
|
13
17
|
## Features
|
|
14
18
|
|
data/Rakefile
CHANGED
|
@@ -2,9 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "minitest/test_task"
|
|
5
|
+
require "rdoc/task"
|
|
5
6
|
|
|
6
7
|
Minitest::TestTask.create
|
|
7
8
|
|
|
9
|
+
RDoc::Task.new do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = "doc"
|
|
11
|
+
rdoc.title = "PURL - Package URL Library"
|
|
12
|
+
rdoc.main = "README.md"
|
|
13
|
+
rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
|
|
14
|
+
rdoc.options << "--line-numbers"
|
|
15
|
+
rdoc.options << "--all"
|
|
16
|
+
rdoc.options << "--charset=UTF-8"
|
|
17
|
+
end
|
|
18
|
+
|
|
8
19
|
task default: :test
|
|
9
20
|
|
|
10
21
|
namespace :spec do
|
|
@@ -396,7 +407,14 @@ namespace :spec do
|
|
|
396
407
|
desc "Validate JSON files against their schemas"
|
|
397
408
|
task :validate_schemas do
|
|
398
409
|
require "json"
|
|
399
|
-
|
|
410
|
+
|
|
411
|
+
begin
|
|
412
|
+
require "json-schema"
|
|
413
|
+
rescue LoadError => e
|
|
414
|
+
puts "❌ json-schema gem not available: #{e.message}"
|
|
415
|
+
puts " Install with: gem install json-schema"
|
|
416
|
+
exit 1
|
|
417
|
+
end
|
|
400
418
|
|
|
401
419
|
puts "🔍 Validating JSON files against schemas..."
|
|
402
420
|
puts "=" * 50
|
|
@@ -544,3 +562,240 @@ namespace :spec do
|
|
|
544
562
|
end
|
|
545
563
|
end
|
|
546
564
|
end
|
|
565
|
+
|
|
566
|
+
namespace :benchmark do
|
|
567
|
+
desc "Run PURL parsing benchmarks"
|
|
568
|
+
task :parse do
|
|
569
|
+
require "benchmark"
|
|
570
|
+
require "json"
|
|
571
|
+
require_relative "lib/purl"
|
|
572
|
+
|
|
573
|
+
puts "🚀 PURL Parsing Benchmarks"
|
|
574
|
+
puts "=" * 50
|
|
575
|
+
|
|
576
|
+
# Load sample PURLs from purl-types.json
|
|
577
|
+
purl_types_data = JSON.parse(File.read(File.join(__dir__, "purl-types.json")))
|
|
578
|
+
sample_purls = []
|
|
579
|
+
|
|
580
|
+
purl_types_data["types"].each do |type_name, type_config|
|
|
581
|
+
examples = type_config["examples"]
|
|
582
|
+
sample_purls.concat(examples) if examples&.is_a?(Array)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Add some complex PURLs for stress testing
|
|
586
|
+
complex_purls = [
|
|
587
|
+
"pkg:npm/@babel/core@7.20.0?arch=x64&dev=true#lib/index.js",
|
|
588
|
+
"pkg:maven/org.apache.commons/commons-lang3@3.12.0?classifier=sources",
|
|
589
|
+
"pkg:composer/symfony/console@5.4.0?extra=test&dev=true#src/Application.php",
|
|
590
|
+
"pkg:gem/rails@7.0.0?platform=ruby&env=production#app/controllers/application_controller.rb"
|
|
591
|
+
]
|
|
592
|
+
sample_purls.concat(complex_purls)
|
|
593
|
+
|
|
594
|
+
puts "📊 Sample size: #{sample_purls.length} PURLs"
|
|
595
|
+
puts "📦 Package types: #{purl_types_data['types'].keys.length}"
|
|
596
|
+
puts
|
|
597
|
+
|
|
598
|
+
# Benchmark parsing
|
|
599
|
+
puts "🔍 Parsing Performance:"
|
|
600
|
+
parsing_time = Benchmark.realtime do
|
|
601
|
+
sample_purls.each { |purl| Purl.parse(purl) }
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
puts " Total time: #{(parsing_time * 1000).round(2)}ms"
|
|
605
|
+
puts " Average per PURL: #{(parsing_time * 1000 / sample_purls.length).round(3)}ms"
|
|
606
|
+
puts " PURLs per second: #{(sample_purls.length / parsing_time).round(0)}"
|
|
607
|
+
puts
|
|
608
|
+
|
|
609
|
+
# Benchmark creation
|
|
610
|
+
puts "🔧 Object Creation Performance:"
|
|
611
|
+
creation_time = Benchmark.realtime do
|
|
612
|
+
1000.times do
|
|
613
|
+
Purl::PackageURL.new(
|
|
614
|
+
type: "gem",
|
|
615
|
+
namespace: "rails",
|
|
616
|
+
name: "rails",
|
|
617
|
+
version: "7.0.0",
|
|
618
|
+
qualifiers: {"arch" => "x64"},
|
|
619
|
+
subpath: "app/models/user.rb"
|
|
620
|
+
)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
puts " 1000 objects: #{(creation_time * 1000).round(2)}ms"
|
|
625
|
+
puts " Average per object: #{(creation_time * 1000 / 1000).round(3)}ms"
|
|
626
|
+
puts " Objects per second: #{(1000 / creation_time).round(0)}"
|
|
627
|
+
puts
|
|
628
|
+
|
|
629
|
+
# Benchmark to_s conversion
|
|
630
|
+
puts "🔤 String Conversion Performance:"
|
|
631
|
+
test_purl = Purl.parse("pkg:npm/@babel/core@7.20.0?arch=x64#lib/index.js")
|
|
632
|
+
|
|
633
|
+
string_time = Benchmark.realtime do
|
|
634
|
+
10000.times { test_purl.to_s }
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
puts " 10,000 conversions: #{(string_time * 1000).round(2)}ms"
|
|
638
|
+
puts " Average per conversion: #{(string_time * 1000 / 10000).round(4)}ms"
|
|
639
|
+
puts " Conversions per second: #{(10000 / string_time).round(0)}"
|
|
640
|
+
puts
|
|
641
|
+
|
|
642
|
+
# Memory usage estimation
|
|
643
|
+
puts "💾 Memory Usage Estimation:"
|
|
644
|
+
purl_objects = sample_purls.map { |purl| Purl.parse(purl) }
|
|
645
|
+
|
|
646
|
+
# Rough estimation based on object count and typical Ruby object overhead
|
|
647
|
+
estimated_memory = purl_objects.length * 200 # ~200 bytes per PURL object estimate
|
|
648
|
+
puts " #{purl_objects.length} PURL objects: ~#{estimated_memory} bytes"
|
|
649
|
+
puts " Average per object: ~200 bytes"
|
|
650
|
+
puts
|
|
651
|
+
|
|
652
|
+
# Test different complexity levels
|
|
653
|
+
puts "🎯 Complexity Benchmarks:"
|
|
654
|
+
|
|
655
|
+
complexity_tests = {
|
|
656
|
+
"Simple" => "pkg:gem/rails@7.0.0",
|
|
657
|
+
"With namespace" => "pkg:npm/@babel/core@7.0.0",
|
|
658
|
+
"With qualifiers" => "pkg:cargo/rand@0.7.2?arch=x86_64&os=linux",
|
|
659
|
+
"With subpath" => "pkg:maven/org.springframework/spring-core@5.3.0#org/springframework/core/SpringVersion.class",
|
|
660
|
+
"Full complexity" => "pkg:npm/@babel/core@7.20.0?arch=x64&dev=true&os=linux#lib/parser/index.js"
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
complexity_tests.each do |level, purl_string|
|
|
664
|
+
time = Benchmark.realtime do
|
|
665
|
+
1000.times { Purl.parse(purl_string) }
|
|
666
|
+
end
|
|
667
|
+
puts " #{level.ljust(15)}: #{(time * 1000 / 1000).round(4)}ms per parse"
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
puts
|
|
671
|
+
puts "✅ Benchmark completed!"
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
desc "Compare parsing performance across package types"
|
|
675
|
+
task :types do
|
|
676
|
+
require "benchmark"
|
|
677
|
+
require "json"
|
|
678
|
+
require_relative "lib/purl"
|
|
679
|
+
|
|
680
|
+
puts "📊 Package Type Parsing Comparison"
|
|
681
|
+
puts "=" * 50
|
|
682
|
+
|
|
683
|
+
purl_types_data = JSON.parse(File.read(File.join(__dir__, "purl-types.json")))
|
|
684
|
+
|
|
685
|
+
# Benchmark each type with its examples
|
|
686
|
+
type_benchmarks = {}
|
|
687
|
+
|
|
688
|
+
purl_types_data["types"].each do |type_name, type_config|
|
|
689
|
+
examples = type_config["examples"]
|
|
690
|
+
next unless examples&.is_a?(Array) && examples.any?
|
|
691
|
+
|
|
692
|
+
time = Benchmark.realtime do
|
|
693
|
+
100.times do
|
|
694
|
+
examples.each { |purl| Purl.parse(purl) }
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
avg_time_per_purl = time / (100 * examples.length)
|
|
699
|
+
type_benchmarks[type_name] = {
|
|
700
|
+
time: avg_time_per_purl,
|
|
701
|
+
examples_count: examples.length
|
|
702
|
+
}
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Sort by performance (fastest first)
|
|
706
|
+
sorted_benchmarks = type_benchmarks.sort_by { |_, data| data[:time] }
|
|
707
|
+
|
|
708
|
+
puts "🏆 Performance Rankings (fastest to slowest):"
|
|
709
|
+
puts " Rank Type Avg Time/Parse Examples"
|
|
710
|
+
puts " " + "-" * 45
|
|
711
|
+
|
|
712
|
+
sorted_benchmarks.each_with_index do |(type, data), index|
|
|
713
|
+
rank = (index + 1).to_s.rjust(2)
|
|
714
|
+
time_str = "#{(data[:time] * 1000).round(4)}ms".rjust(10)
|
|
715
|
+
examples_str = data[:examples_count].to_s.rjust(8)
|
|
716
|
+
|
|
717
|
+
puts " #{rank}. #{type.ljust(12)} #{time_str} #{examples_str}"
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
fastest = sorted_benchmarks.first
|
|
721
|
+
slowest = sorted_benchmarks.last
|
|
722
|
+
|
|
723
|
+
puts
|
|
724
|
+
puts "📈 Performance Summary:"
|
|
725
|
+
puts " Fastest: #{fastest[0]} (#{(fastest[1][:time] * 1000).round(4)}ms)"
|
|
726
|
+
puts " Slowest: #{slowest[0]} (#{(slowest[1][:time] * 1000).round(4)}ms)"
|
|
727
|
+
puts " Ratio: #{(slowest[1][:time] / fastest[1][:time]).round(1)}x difference"
|
|
728
|
+
puts
|
|
729
|
+
puts "✅ Type comparison completed!"
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
desc "Benchmark registry URL generation"
|
|
733
|
+
task :registry do
|
|
734
|
+
require "benchmark"
|
|
735
|
+
require "json"
|
|
736
|
+
require_relative "lib/purl"
|
|
737
|
+
|
|
738
|
+
puts "🌐 Registry URL Generation Benchmarks"
|
|
739
|
+
puts "=" * 50
|
|
740
|
+
|
|
741
|
+
# Get PURLs that support registry URL generation
|
|
742
|
+
registry_purls = []
|
|
743
|
+
Purl.registry_supported_types.each do |type|
|
|
744
|
+
examples = Purl.type_examples(type)
|
|
745
|
+
registry_purls.concat(examples) if examples.any?
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
puts "📊 Testing with #{registry_purls.length} registry-supported PURLs"
|
|
749
|
+
puts
|
|
750
|
+
|
|
751
|
+
# Parse all PURLs first
|
|
752
|
+
parsed_purls = registry_purls.map { |purl| Purl.parse(purl) }
|
|
753
|
+
|
|
754
|
+
# Benchmark registry URL generation
|
|
755
|
+
puts "🔗 URL Generation Performance:"
|
|
756
|
+
url_time = Benchmark.realtime do
|
|
757
|
+
parsed_purls.each { |purl| purl.registry_url }
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
puts " Total time: #{(url_time * 1000).round(2)}ms"
|
|
761
|
+
puts " Average per URL: #{(url_time * 1000 / parsed_purls.length).round(3)}ms"
|
|
762
|
+
puts " URLs per second: #{(parsed_purls.length / url_time).round(0)}"
|
|
763
|
+
puts
|
|
764
|
+
|
|
765
|
+
# Benchmark versioned URL generation
|
|
766
|
+
puts "🏷️ Versioned URL Performance:"
|
|
767
|
+
versioned_time = Benchmark.realtime do
|
|
768
|
+
parsed_purls.each { |purl| purl.registry_url_with_version }
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
puts " Total time: #{(versioned_time * 1000).round(2)}ms"
|
|
772
|
+
puts " Average per URL: #{(versioned_time * 1000 / parsed_purls.length).round(3)}ms"
|
|
773
|
+
puts " URLs per second: #{(parsed_purls.length / versioned_time).round(0)}"
|
|
774
|
+
puts
|
|
775
|
+
|
|
776
|
+
# Compare parsing vs URL generation
|
|
777
|
+
parsing_time = Benchmark.realtime do
|
|
778
|
+
registry_purls.each { |purl| Purl.parse(purl) }
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
puts "⚖️ Performance Comparison:"
|
|
782
|
+
puts " Parsing: #{(parsing_time * 1000 / registry_purls.length).round(3)}ms per PURL"
|
|
783
|
+
puts " URL generation: #{(url_time * 1000 / parsed_purls.length).round(3)}ms per PURL"
|
|
784
|
+
puts " Versioned URLs: #{(versioned_time * 1000 / parsed_purls.length).round(3)}ms per PURL"
|
|
785
|
+
|
|
786
|
+
ratio = url_time / parsing_time
|
|
787
|
+
puts " URL gen vs parsing: #{ratio.round(2)}x #{ratio > 1 ? 'slower' : 'faster'}"
|
|
788
|
+
|
|
789
|
+
puts
|
|
790
|
+
puts "✅ Registry URL benchmarks completed!"
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
desc "Run all benchmarks"
|
|
794
|
+
task all: [:parse, :types, :registry] do
|
|
795
|
+
puts
|
|
796
|
+
puts "🎉 All benchmarks completed!"
|
|
797
|
+
puts " Use 'rake benchmark:parse' for parsing performance"
|
|
798
|
+
puts " Use 'rake benchmark:types' for type comparison"
|
|
799
|
+
puts " Use 'rake benchmark:registry' for URL generation"
|
|
800
|
+
end
|
|
801
|
+
end
|
data/lib/purl/errors.rb
CHANGED
|
@@ -5,9 +5,31 @@ module Purl
|
|
|
5
5
|
class Error < StandardError; end
|
|
6
6
|
|
|
7
7
|
# Validation errors for PURL components
|
|
8
|
+
#
|
|
9
|
+
# Contains additional context about which component failed validation
|
|
10
|
+
# and what rule was violated.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# begin
|
|
14
|
+
# PackageURL.new(type: "123invalid", name: "test")
|
|
15
|
+
# rescue ValidationError => e
|
|
16
|
+
# puts e.component # :type
|
|
17
|
+
# puts e.rule # "cannot start with number"
|
|
18
|
+
# end
|
|
8
19
|
class ValidationError < Error
|
|
9
|
-
|
|
20
|
+
# @return [Symbol, nil] the PURL component that failed validation
|
|
21
|
+
attr_reader :component
|
|
22
|
+
|
|
23
|
+
# @return [Object, nil] the value that failed validation
|
|
24
|
+
attr_reader :value
|
|
25
|
+
|
|
26
|
+
# @return [String, nil] the validation rule that was violated
|
|
27
|
+
attr_reader :rule
|
|
10
28
|
|
|
29
|
+
# @param message [String] error message
|
|
30
|
+
# @param component [Symbol, nil] component that failed validation
|
|
31
|
+
# @param value [Object, nil] value that failed validation
|
|
32
|
+
# @param rule [String, nil] validation rule that was violated
|
|
11
33
|
def initialize(message, component: nil, value: nil, rule: nil)
|
|
12
34
|
super(message)
|
|
13
35
|
@component = component
|
|
@@ -19,40 +41,71 @@ module Purl
|
|
|
19
41
|
# Parsing errors for malformed PURL strings
|
|
20
42
|
class ParseError < Error; end
|
|
21
43
|
|
|
22
|
-
# Specific validation errors
|
|
44
|
+
# Specific validation errors for PURL components
|
|
45
|
+
|
|
46
|
+
# Raised when a PURL type is invalid
|
|
23
47
|
class InvalidTypeError < ValidationError; end
|
|
48
|
+
|
|
49
|
+
# Raised when a PURL name is invalid
|
|
24
50
|
class InvalidNameError < ValidationError; end
|
|
51
|
+
|
|
52
|
+
# Raised when a PURL namespace is invalid
|
|
25
53
|
class InvalidNamespaceError < ValidationError; end
|
|
54
|
+
|
|
55
|
+
# Raised when a PURL qualifier is invalid
|
|
26
56
|
class InvalidQualifierError < ValidationError; end
|
|
57
|
+
|
|
58
|
+
# Raised when a PURL version is invalid
|
|
27
59
|
class InvalidVersionError < ValidationError; end
|
|
60
|
+
|
|
61
|
+
# Raised when a PURL subpath is invalid
|
|
28
62
|
class InvalidSubpathError < ValidationError; end
|
|
29
63
|
|
|
30
64
|
# Parsing-specific errors
|
|
65
|
+
|
|
66
|
+
# Raised when a PURL string doesn't start with "pkg:"
|
|
31
67
|
class InvalidSchemeError < ParseError; end
|
|
68
|
+
|
|
69
|
+
# Raised when a PURL string is malformed
|
|
32
70
|
class MalformedUrlError < ParseError; end
|
|
33
71
|
|
|
34
72
|
# Registry URL generation errors
|
|
73
|
+
#
|
|
74
|
+
# Contains additional context about which type caused the error.
|
|
35
75
|
class RegistryError < Error
|
|
76
|
+
# @return [String, nil] the PURL type that caused the error
|
|
36
77
|
attr_reader :type
|
|
37
78
|
|
|
79
|
+
# @param message [String] error message
|
|
80
|
+
# @param type [String, nil] PURL type that caused the error
|
|
38
81
|
def initialize(message, type: nil)
|
|
39
82
|
super(message)
|
|
40
83
|
@type = type
|
|
41
84
|
end
|
|
42
85
|
end
|
|
43
86
|
|
|
87
|
+
# Raised when trying to generate registry URLs for unsupported types
|
|
44
88
|
class UnsupportedTypeError < RegistryError
|
|
89
|
+
# @return [Array<String>] list of supported types
|
|
45
90
|
attr_reader :supported_types
|
|
46
91
|
|
|
92
|
+
# @param message [String] error message
|
|
93
|
+
# @param type [String, nil] unsupported type
|
|
94
|
+
# @param supported_types [Array<String>] list of supported types
|
|
47
95
|
def initialize(message, type: nil, supported_types: [])
|
|
48
96
|
super(message, type: type)
|
|
49
97
|
@supported_types = supported_types
|
|
50
98
|
end
|
|
51
99
|
end
|
|
52
100
|
|
|
101
|
+
# Raised when required registry information is missing
|
|
53
102
|
class MissingRegistryInfoError < RegistryError
|
|
103
|
+
# @return [String, nil] the missing information (e.g., "namespace")
|
|
54
104
|
attr_reader :missing
|
|
55
105
|
|
|
106
|
+
# @param message [String] error message
|
|
107
|
+
# @param type [String, nil] PURL type
|
|
108
|
+
# @param missing [String, nil] what information is missing
|
|
56
109
|
def initialize(message, type: nil, missing: nil)
|
|
57
110
|
super(message, type: type)
|
|
58
111
|
@missing = missing
|
|
@@ -60,5 +113,6 @@ module Purl
|
|
|
60
113
|
end
|
|
61
114
|
|
|
62
115
|
# Legacy compatibility - matches packageurl-ruby's exception name
|
|
116
|
+
# @deprecated Use {ParseError} instead
|
|
63
117
|
InvalidPackageURL = ParseError
|
|
64
118
|
end
|
data/lib/purl/package_url.rb
CHANGED
|
@@ -3,12 +3,74 @@
|
|
|
3
3
|
require "uri"
|
|
4
4
|
|
|
5
5
|
module Purl
|
|
6
|
+
# Represents a Package URL (PURL) - a mostly universal standard to reference
|
|
7
|
+
# a software package in a uniform way across many tools, programming languages
|
|
8
|
+
# and ecosystems.
|
|
9
|
+
#
|
|
10
|
+
# A PURL has the following components:
|
|
11
|
+
# - +type+: the package type (e.g., "gem", "npm", "maven")
|
|
12
|
+
# - +namespace+: optional namespace/scope (e.g., "@babel" for npm)
|
|
13
|
+
# - +name+: the package name (required)
|
|
14
|
+
# - +version+: optional version
|
|
15
|
+
# - +qualifiers+: optional key-value pairs
|
|
16
|
+
# - +subpath+: optional path within the package
|
|
17
|
+
#
|
|
18
|
+
# @example Creating a PackageURL
|
|
19
|
+
# purl = PackageURL.new(
|
|
20
|
+
# type: "gem",
|
|
21
|
+
# name: "rails",
|
|
22
|
+
# version: "7.0.0"
|
|
23
|
+
# )
|
|
24
|
+
# puts purl.to_s # "pkg:gem/rails@7.0.0"
|
|
25
|
+
#
|
|
26
|
+
# @example Parsing a PURL string
|
|
27
|
+
# purl = PackageURL.parse("pkg:npm/@babel/core@7.0.0")
|
|
28
|
+
# puts purl.namespace # "@babel"
|
|
29
|
+
# puts purl.name # "core"
|
|
30
|
+
#
|
|
31
|
+
# @see https://github.com/package-url/purl-spec PURL Specification
|
|
6
32
|
class PackageURL
|
|
7
|
-
|
|
33
|
+
# @return [String] the package type (e.g., "gem", "npm", "maven")
|
|
34
|
+
attr_reader :type
|
|
35
|
+
|
|
36
|
+
# @return [String, nil] the package namespace/scope
|
|
37
|
+
attr_reader :namespace
|
|
38
|
+
|
|
39
|
+
# @return [String] the package name
|
|
40
|
+
attr_reader :name
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] the package version
|
|
43
|
+
attr_reader :version
|
|
44
|
+
|
|
45
|
+
# @return [Hash<String, String>, nil] key-value qualifier pairs
|
|
46
|
+
attr_reader :qualifiers
|
|
47
|
+
|
|
48
|
+
# @return [String, nil] subpath within the package
|
|
49
|
+
attr_reader :subpath
|
|
8
50
|
|
|
9
|
-
VALID_TYPE_CHARS = /\A[a-zA-Z0-9\.\+\-]+\z
|
|
10
|
-
VALID_QUALIFIER_KEY_CHARS = /\A[a-zA-Z0-9\.\-_]+\z
|
|
51
|
+
VALID_TYPE_CHARS = /\A[a-zA-Z0-9\.\+\-]+\z/.freeze
|
|
52
|
+
VALID_QUALIFIER_KEY_CHARS = /\A[a-zA-Z0-9\.\-_]+\z/.freeze
|
|
11
53
|
|
|
54
|
+
# Create a new PackageURL instance
|
|
55
|
+
#
|
|
56
|
+
# @param type [String, Symbol] the package type (required)
|
|
57
|
+
# @param name [String] the package name (required)
|
|
58
|
+
# @param namespace [String, nil] optional namespace/scope
|
|
59
|
+
# @param version [String, nil] optional version
|
|
60
|
+
# @param qualifiers [Hash, nil] optional key-value qualifier pairs
|
|
61
|
+
# @param subpath [String, nil] optional subpath within package
|
|
62
|
+
#
|
|
63
|
+
# @raise [InvalidTypeError] if type is invalid
|
|
64
|
+
# @raise [InvalidNameError] if name is invalid
|
|
65
|
+
# @raise [ValidationError] if any component fails type-specific validation
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# purl = PackageURL.new(
|
|
69
|
+
# type: "npm",
|
|
70
|
+
# namespace: "@babel",
|
|
71
|
+
# name: "core",
|
|
72
|
+
# version: "7.0.0"
|
|
73
|
+
# )
|
|
12
74
|
def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil)
|
|
13
75
|
@type = validate_and_normalize_type(type)
|
|
14
76
|
@name = validate_name(name)
|
|
@@ -21,12 +83,31 @@ module Purl
|
|
|
21
83
|
validate_type_specific_rules
|
|
22
84
|
end
|
|
23
85
|
|
|
86
|
+
# Parse a PURL string into a PackageURL object
|
|
87
|
+
#
|
|
88
|
+
# @param purl_string [String] PURL string starting with "pkg:"
|
|
89
|
+
# @return [PackageURL] parsed package URL object
|
|
90
|
+
# @raise [InvalidSchemeError] if string doesn't start with "pkg:"
|
|
91
|
+
# @raise [MalformedUrlError] if string is malformed
|
|
92
|
+
# @raise [ValidationError] if parsed components fail validation
|
|
93
|
+
#
|
|
94
|
+
# @example Basic parsing
|
|
95
|
+
# purl = PackageURL.parse("pkg:gem/rails@7.0.0")
|
|
96
|
+
# puts purl.type # "gem"
|
|
97
|
+
# puts purl.name # "rails"
|
|
98
|
+
# puts purl.version # "7.0.0"
|
|
99
|
+
#
|
|
100
|
+
# @example Complex parsing with all components
|
|
101
|
+
# purl = PackageURL.parse("pkg:npm/@babel/core@7.0.0?arch=x64#lib/index.js")
|
|
102
|
+
# puts purl.namespace # "@babel"
|
|
103
|
+
# puts purl.qualifiers # {"arch" => "x64"}
|
|
104
|
+
# puts purl.subpath # "lib/index.js"
|
|
24
105
|
def self.parse(purl_string)
|
|
25
106
|
raise InvalidSchemeError, "PURL must start with 'pkg:'" unless purl_string.start_with?("pkg:")
|
|
26
107
|
|
|
27
108
|
# Remove the pkg: prefix and any leading slashes (they're not significant)
|
|
28
109
|
remainder = purl_string[4..-1]
|
|
29
|
-
remainder = remainder.sub(/\A\/+/, "")
|
|
110
|
+
remainder = remainder.sub(/\A\/+/, "") if remainder.start_with?("/")
|
|
30
111
|
|
|
31
112
|
# Split off qualifiers (query string) first
|
|
32
113
|
if remainder.include?("?")
|
|
@@ -134,18 +215,25 @@ module Purl
|
|
|
134
215
|
)
|
|
135
216
|
end
|
|
136
217
|
|
|
218
|
+
# Convert the PackageURL to its canonical string representation
|
|
219
|
+
#
|
|
220
|
+
# @return [String] canonical PURL string
|
|
221
|
+
#
|
|
222
|
+
# @example
|
|
223
|
+
# purl = PackageURL.new(type: "gem", name: "rails", version: "7.0.0")
|
|
224
|
+
# puts purl.to_s # "pkg:gem/rails@7.0.0"
|
|
137
225
|
def to_s
|
|
138
|
-
|
|
226
|
+
parts = ["pkg:", type.downcase]
|
|
139
227
|
|
|
140
228
|
if namespace
|
|
141
229
|
# Encode namespace parts, but preserve the structure
|
|
142
230
|
namespace_parts = namespace.split("/").map do |part|
|
|
143
231
|
URI.encode_www_form_component(part)
|
|
144
232
|
end
|
|
145
|
-
|
|
233
|
+
parts << "/" << namespace_parts.join("/")
|
|
146
234
|
end
|
|
147
235
|
|
|
148
|
-
|
|
236
|
+
parts << "/" << URI.encode_www_form_component(name)
|
|
149
237
|
|
|
150
238
|
if version
|
|
151
239
|
# Special handling for version encoding - don't encode colon in certain contexts
|
|
@@ -156,7 +244,7 @@ module Purl
|
|
|
156
244
|
else
|
|
157
245
|
URI.encode_www_form_component(version)
|
|
158
246
|
end
|
|
159
|
-
|
|
247
|
+
parts << "@" << encoded_version
|
|
160
248
|
end
|
|
161
249
|
|
|
162
250
|
if subpath
|
|
@@ -165,7 +253,7 @@ module Purl
|
|
|
165
253
|
normalized_subpath = self.class.normalize_subpath(subpath)
|
|
166
254
|
if normalized_subpath
|
|
167
255
|
subpath_parts = normalized_subpath.split("/").map { |part| URI.encode_www_form_component(part) }
|
|
168
|
-
|
|
256
|
+
parts << "#" << subpath_parts.join("/")
|
|
169
257
|
end
|
|
170
258
|
end
|
|
171
259
|
|
|
@@ -177,12 +265,21 @@ module Purl
|
|
|
177
265
|
encoded_value = value.to_s # Don't encode values to match canonical form
|
|
178
266
|
"#{encoded_key}=#{encoded_value}"
|
|
179
267
|
end
|
|
180
|
-
|
|
268
|
+
parts << "?" << query_parts.join("&")
|
|
181
269
|
end
|
|
182
270
|
|
|
183
|
-
|
|
271
|
+
parts.join
|
|
184
272
|
end
|
|
185
273
|
|
|
274
|
+
# Convert the PackageURL to a hash representation
|
|
275
|
+
#
|
|
276
|
+
# @return [Hash<Symbol, Object>] hash with component keys and values
|
|
277
|
+
#
|
|
278
|
+
# @example
|
|
279
|
+
# purl = PackageURL.new(type: "gem", name: "rails", version: "7.0.0")
|
|
280
|
+
# hash = purl.to_h
|
|
281
|
+
# # => {:type=>"gem", :namespace=>nil, :name=>"rails", :version=>"7.0.0",
|
|
282
|
+
# # :qualifiers=>nil, :subpath=>nil}
|
|
186
283
|
def to_h
|
|
187
284
|
{
|
|
188
285
|
type: type,
|
|
@@ -194,28 +291,69 @@ module Purl
|
|
|
194
291
|
}
|
|
195
292
|
end
|
|
196
293
|
|
|
294
|
+
# Compare two PackageURL objects for equality
|
|
295
|
+
#
|
|
296
|
+
# Two PURLs are equal if their canonical string representations are identical.
|
|
297
|
+
#
|
|
298
|
+
# @param other [Object] object to compare with
|
|
299
|
+
# @return [Boolean] true if equal, false otherwise
|
|
300
|
+
#
|
|
301
|
+
# @example
|
|
302
|
+
# purl1 = PackageURL.parse("pkg:gem/rails@7.0.0")
|
|
303
|
+
# purl2 = PackageURL.parse("pkg:gem/rails@7.0.0")
|
|
304
|
+
# puts purl1 == purl2 # true
|
|
197
305
|
def ==(other)
|
|
198
306
|
return false unless other.is_a?(PackageURL)
|
|
199
307
|
|
|
200
308
|
to_s == other.to_s
|
|
201
309
|
end
|
|
202
310
|
|
|
311
|
+
# Generate hash code for the PackageURL
|
|
312
|
+
#
|
|
313
|
+
# @return [Integer] hash code based on canonical string representation
|
|
203
314
|
def hash
|
|
204
315
|
to_s.hash
|
|
205
316
|
end
|
|
206
317
|
|
|
207
318
|
# Pattern matching support for Ruby 2.7+
|
|
319
|
+
#
|
|
320
|
+
# Allows destructuring PackageURL in pattern matching.
|
|
321
|
+
#
|
|
322
|
+
# @return [Array] array of [type, namespace, name, version, qualifiers, subpath]
|
|
323
|
+
#
|
|
324
|
+
# @example Ruby 2.7+ pattern matching
|
|
325
|
+
# case purl
|
|
326
|
+
# in ["gem", nil, name, version, nil, nil]
|
|
327
|
+
# puts "Simple gem: #{name} v#{version}"
|
|
328
|
+
# end
|
|
208
329
|
def deconstruct
|
|
209
330
|
[type, namespace, name, version, qualifiers, subpath]
|
|
210
331
|
end
|
|
211
332
|
|
|
333
|
+
# Pattern matching support for Ruby 2.7+ (hash patterns)
|
|
334
|
+
#
|
|
335
|
+
# @param keys [Array<Symbol>, nil] keys to extract, or nil for all keys
|
|
336
|
+
# @return [Hash<Symbol, Object>] hash with requested keys
|
|
337
|
+
#
|
|
338
|
+
# @example Ruby 2.7+ hash pattern matching
|
|
339
|
+
# case purl
|
|
340
|
+
# in {type: "gem", name:, version:}
|
|
341
|
+
# puts "Gem #{name} version #{version}"
|
|
342
|
+
# end
|
|
212
343
|
def deconstruct_keys(keys)
|
|
213
344
|
return to_h.slice(*keys) if keys
|
|
214
345
|
to_h
|
|
215
346
|
end
|
|
216
347
|
|
|
217
348
|
# Create a new PackageURL with modified attributes
|
|
218
|
-
#
|
|
349
|
+
#
|
|
350
|
+
# @param changes [Hash] attributes to change
|
|
351
|
+
# @return [PackageURL] new PackageURL instance with changes applied
|
|
352
|
+
#
|
|
353
|
+
# @example
|
|
354
|
+
# purl = PackageURL.parse("pkg:gem/rails@7.0.0")
|
|
355
|
+
# new_purl = purl.with(version: "7.1.0", qualifiers: {"arch" => "x64"})
|
|
356
|
+
# puts new_purl.to_s # "pkg:gem/rails@7.1.0?arch=x64"
|
|
219
357
|
def with(**changes)
|
|
220
358
|
current_attrs = to_h
|
|
221
359
|
new_attrs = current_attrs.merge(changes)
|
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,6 +39,15 @@ 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
|
|
@@ -33,26 +60,66 @@ module Purl
|
|
|
33
60
|
end
|
|
34
61
|
|
|
35
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
|
|
36
69
|
def self.known_types
|
|
37
70
|
KNOWN_TYPES.dup
|
|
38
71
|
end
|
|
39
72
|
|
|
40
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
|
|
41
80
|
def self.registry_supported_types
|
|
42
81
|
RegistryURL.supported_types
|
|
43
82
|
end
|
|
44
83
|
|
|
45
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
|
|
46
91
|
def self.reverse_parsing_supported_types
|
|
47
92
|
RegistryURL.supported_reverse_types
|
|
48
93
|
end
|
|
49
94
|
|
|
50
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
|
|
51
103
|
def self.known_type?(type)
|
|
52
104
|
KNOWN_TYPES.include?(type.to_s.downcase)
|
|
53
105
|
end
|
|
54
106
|
|
|
55
|
-
# 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"
|
|
56
123
|
def self.type_info(type)
|
|
57
124
|
normalized_type = type.to_s.downcase
|
|
58
125
|
{
|
|
@@ -68,6 +135,13 @@ module Purl
|
|
|
68
135
|
end
|
|
69
136
|
|
|
70
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"]
|
|
71
145
|
def self.all_type_info
|
|
72
146
|
result = {}
|
|
73
147
|
|
|
@@ -87,6 +161,10 @@ module Purl
|
|
|
87
161
|
end
|
|
88
162
|
|
|
89
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
|
|
90
168
|
def self.type_config(type)
|
|
91
169
|
config = load_types_config["types"][type.to_s.downcase]
|
|
92
170
|
return nil unless config
|
|
@@ -94,13 +172,27 @@ module Purl
|
|
|
94
172
|
config.dup # Return a copy to prevent modification
|
|
95
173
|
end
|
|
96
174
|
|
|
97
|
-
# 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"
|
|
98
183
|
def self.type_description(type)
|
|
99
184
|
config = type_config(type)
|
|
100
185
|
config ? config["description"] : nil
|
|
101
186
|
end
|
|
102
187
|
|
|
103
|
-
# Get
|
|
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"
|
|
104
196
|
def self.type_examples(type)
|
|
105
197
|
config = type_config(type)
|
|
106
198
|
return [] unless config
|
|
@@ -109,6 +201,10 @@ module Purl
|
|
|
109
201
|
end
|
|
110
202
|
|
|
111
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
|
|
112
208
|
def self.registry_config(type)
|
|
113
209
|
config = type_config(type)
|
|
114
210
|
return nil unless config
|
|
@@ -117,6 +213,13 @@ module Purl
|
|
|
117
213
|
end
|
|
118
214
|
|
|
119
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"
|
|
120
223
|
def self.default_registry(type)
|
|
121
224
|
config = type_config(type)
|
|
122
225
|
return nil unless config
|
|
@@ -125,6 +228,19 @@ module Purl
|
|
|
125
228
|
end
|
|
126
229
|
|
|
127
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]}"
|
|
128
244
|
def self.types_config_metadata
|
|
129
245
|
config = load_types_config
|
|
130
246
|
{
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: purl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -41,6 +41,7 @@ metadata:
|
|
|
41
41
|
allowed_push_host: https://rubygems.org
|
|
42
42
|
homepage_uri: https://github.com/andrew/purl
|
|
43
43
|
source_code_uri: https://github.com/andrew/purl
|
|
44
|
+
changelog_uri: https://github.com/andrew/purl/blob/main/CHANGELOG.md
|
|
44
45
|
rdoc_options: []
|
|
45
46
|
require_paths:
|
|
46
47
|
- lib
|