purl 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae14d9c24e0b1f4f8d76b97c59ed34d8e34090d5329ac9358bf40be4e4acdf14
4
- data.tar.gz: 4ff6fa43fd388006d553bae72b4685ef228cff186b135ec335d7a6b724abb034
3
+ metadata.gz: 519631a904ead8452084959edb20711e684c0fc9c0b0a848ff6775f86bc31675
4
+ data.tar.gz: cb82893be40a50ae28651b78ed293dd0303b6cc926b81985fe256d32f0b0fea7
5
5
  SHA512:
6
- metadata.gz: 5f965309ce3f2dddc29065c247082f11541a8ead617c2ad398a7dbd71affd6417e7f09959d88ea59492256dec7039b458507688da9288723bda2d83942f6683f
7
- data.tar.gz: eb9fd5d72ba8a6c40cafe9c736594f76d20ea40e1886af1693d54dbb36b1ba1105ed05b415b6989098b7288df79a6e9f3c3ddb7aa095eb6dad069a7755374f39
6
+ metadata.gz: 5ef9defb2759c124020aa5d6ba88eb281e0bd1fbf256cafdd3ba7c4e17bca09aec0286fe2851b74e12313062a5d3e38a76db85376b60d3f645e9f4720e0cb2b8
7
+ data.tar.gz: 6399a94cf6bd5bd92fa6e5da30f7f9c6e056982728bc5828ead26e9d8c9c17b5beedbe4bdababebc0bee701bdeaf27f4ffa7b84e54a451d45e7dea7c9575c9b7
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "purl-spec"]
2
+ path = purl-spec
3
+ url = https://github.com/package-url/purl-spec.git
data/CHANGELOG.md CHANGED
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2025-07-27
11
+
12
+ ### Added
13
+ - Default registry URLs for 10 additional package types:
14
+ - `golang`: https://pkg.go.dev (Go package discovery site)
15
+ - `luarocks`: https://luarocks.org (Lua package repository)
16
+ - `clojars`: https://clojars.org (Clojure package repository)
17
+ - `elm`: https://package.elm-lang.org (Elm package catalog)
18
+ - `deno`: https://deno.land (Deno module registry)
19
+ - `homebrew`: https://formulae.brew.sh (Homebrew package browser)
20
+ - `bioconductor`: https://bioconductor.org (R bioinformatics packages)
21
+ - `huggingface`: https://huggingface.co (Machine learning models)
22
+ - `swift`: https://swiftpackageindex.com (Swift package index)
23
+ - `conan`: https://conan.io/center (C/C++ package center)
24
+
25
+ ### Enhanced
26
+ - Registry configuration support for newly added package types
27
+ - Updated test suite to validate all new default registries
28
+ - Improved package type coverage with comprehensive registry URL mapping
29
+
30
+ ### Configuration
31
+ - Updated `purl-types.json` to version 1.2.0 with enhanced registry configurations
32
+ - Added specialized registry handling for Go's unique import path structure
33
+
34
+ ## [1.1.2] - 2025-07-25
35
+
36
+ ### Added
37
+ - Comprehensive benchmarking rake tasks for performance analysis
38
+ - `rake benchmark:parse` - PURL parsing performance benchmarks
39
+ - `rake benchmark:types` - Package type parsing comparison
40
+ - `rake benchmark:registry` - Registry URL generation benchmarks
41
+ - `rake benchmark:all` - Run all benchmarks
42
+
43
+ ### Improved
44
+ - **26% improvement in parsing throughput** (~175K PURLs/second)
45
+ - **8% improvement in string conversion performance** (~315K conversions/second)
46
+ - **7% improvement in object creation** (~280K objects/second)
47
+ - Optimized string operations in parse method with conditional regex application
48
+ - Reduced string allocations in `to_s` method using array joining
49
+ - Cached compiled regexes with `.freeze` for better performance
50
+ - Lower memory allocation pressure in high-throughput scenarios
51
+
52
+ ## [1.1.1] - 2025-07-25
53
+
54
+ ### Added
55
+ - Comprehensive RDoc documentation for all classes and methods
56
+ - RDoc task in Rakefile with proper configuration
57
+ - API documentation link in README
58
+
59
+ ### Fixed
60
+ - Add bigdecimal gem dependency to resolve potential loading issues
61
+ - Improve JSON schema loading error handling
62
+
63
+ ## [1.1.0] - 2025-07-25
64
+
65
+ ### Added
66
+ - JSON schema validation for configuration files (`purl-types.json` and `test-suite-data.json`)
67
+ - New rake tasks: `spec:validate_schemas` and `spec:validate_examples`
68
+ - Examples for all package types in configuration
69
+ - Default registry URLs for package types
70
+ - Enhanced reverse parsing support for additional package types
71
+ - `with` method for creating modified PURL objects (immutable pattern)
72
+ - FUNDING.yml for project sponsorship support
73
+
74
+ ### Enhanced
75
+ - Improved README documentation with custom registry examples
76
+ - Enhanced test coverage for new functionality
77
+ - Better compliance test output formatting
78
+ - Comprehensive package type examples and validation
79
+
80
+ ### Documentation
81
+ - Updated documentation to remove emoji and enhance readability
82
+ - Added comprehensive examples for custom registry usage
83
+ - Enhanced API documentation throughout
84
+
10
85
  ## [1.0.0] - 2025-01-24
11
86
 
12
87
  ### Added
data/README.md CHANGED
@@ -10,6 +10,10 @@ This library features comprehensive error handling with namespaced error types,
10
10
 
11
11
  **[Available on RubyGems](https://rubygems.org/gems/purl)** | **[API Documentation](https://rdoc.info/github/andrew/purl)**
12
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
16
+
13
17
  ## Features
14
18
 
15
19
  - **Comprehensive PURL parsing and validation** with 37 package types (32 official + 5 additional ecosystems)
@@ -381,6 +385,25 @@ rake spec:types
381
385
  rake spec:verify_types
382
386
  ```
383
387
 
388
+ ### Testing Against Official Specification
389
+
390
+ This library includes the official [purl-spec](https://github.com/package-url/purl-spec) repository as a git submodule for testing and validation:
391
+
392
+ ```bash
393
+ # Initialize submodule (first time only)
394
+ git submodule update --init --recursive
395
+
396
+ # Update submodule to latest spec
397
+ git submodule update --remote purl-spec
398
+ ```
399
+
400
+ The tests use files from the submodule to:
401
+ - **Schema validation**: Validate our `purl-types.json` against the official schema in `purl-spec/schemas/`
402
+ - **Type compliance**: Ensure our supported types match the official types in `purl-spec/types/`
403
+ - **Test data**: Access official test cases and examples from `purl-spec/tests/`
404
+
405
+ The submodule is automatically updated weekly via Dependabot, ensuring tests stay current with the latest specification changes. When the submodule updates, you can review and merge the PR to adopt new spec requirements.
406
+
384
407
  ### Rake Tasks
385
408
 
386
409
  - `rake spec:update` - Fetch latest test cases from official PURL spec repository
data/Rakefile CHANGED
@@ -562,3 +562,240 @@ namespace :spec do
562
562
  end
563
563
  end
564
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
@@ -48,8 +48,8 @@ module Purl
48
48
  # @return [String, nil] subpath within the package
49
49
  attr_reader :subpath
50
50
 
51
- VALID_TYPE_CHARS = /\A[a-zA-Z0-9\.\+\-]+\z/
52
- 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
53
53
 
54
54
  # Create a new PackageURL instance
55
55
  #
@@ -107,7 +107,7 @@ module Purl
107
107
 
108
108
  # Remove the pkg: prefix and any leading slashes (they're not significant)
109
109
  remainder = purl_string[4..-1]
110
- remainder = remainder.sub(/\A\/+/, "")
110
+ remainder = remainder.sub(/\A\/+/, "") if remainder.start_with?("/")
111
111
 
112
112
  # Split off qualifiers (query string) first
113
113
  if remainder.include?("?")
@@ -223,17 +223,17 @@ module Purl
223
223
  # purl = PackageURL.new(type: "gem", name: "rails", version: "7.0.0")
224
224
  # puts purl.to_s # "pkg:gem/rails@7.0.0"
225
225
  def to_s
226
- result = "pkg:#{type.downcase}"
226
+ parts = ["pkg:", type.downcase]
227
227
 
228
228
  if namespace
229
229
  # Encode namespace parts, but preserve the structure
230
230
  namespace_parts = namespace.split("/").map do |part|
231
231
  URI.encode_www_form_component(part)
232
232
  end
233
- result += "/#{namespace_parts.join("/")}"
233
+ parts << "/" << namespace_parts.join("/")
234
234
  end
235
235
 
236
- result += "/#{URI.encode_www_form_component(name)}"
236
+ parts << "/" << URI.encode_www_form_component(name)
237
237
 
238
238
  if version
239
239
  # Special handling for version encoding - don't encode colon in certain contexts
@@ -244,7 +244,7 @@ module Purl
244
244
  else
245
245
  URI.encode_www_form_component(version)
246
246
  end
247
- result += "@#{encoded_version}"
247
+ parts << "@" << encoded_version
248
248
  end
249
249
 
250
250
  if subpath
@@ -253,7 +253,7 @@ module Purl
253
253
  normalized_subpath = self.class.normalize_subpath(subpath)
254
254
  if normalized_subpath
255
255
  subpath_parts = normalized_subpath.split("/").map { |part| URI.encode_www_form_component(part) }
256
- result += "##{subpath_parts.join("/")}"
256
+ parts << "#" << subpath_parts.join("/")
257
257
  end
258
258
  end
259
259
 
@@ -265,10 +265,10 @@ module Purl
265
265
  encoded_value = value.to_s # Don't encode values to match canonical form
266
266
  "#{encoded_key}=#{encoded_value}"
267
267
  end
268
- result += "?#{query_parts.join("&")}"
268
+ parts << "?" << query_parts.join("&")
269
269
  end
270
270
 
271
- result
271
+ parts.join
272
272
  end
273
273
 
274
274
  # Convert the PackageURL to a hash representation
@@ -532,14 +532,15 @@ module Purl
532
532
  end
533
533
 
534
534
  def validate_conan_specific_rules
535
- # For conan packages, if a namespace is present WITHOUT any qualifiers,
536
- # it's ambiguous (test case 30)
537
- if @namespace && (@qualifiers.nil? || (@qualifiers["user"].nil? && @qualifiers["channel"].nil?))
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?)
538
539
  raise ValidationError.new(
539
- "Conan PURLs with namespace require 'user' and/or 'channel' qualifiers to be unambiguous",
540
+ "Conan PURLs with namespace require qualifiers to be unambiguous",
540
541
  component: :qualifiers,
541
542
  value: @qualifiers,
542
- rule: "conan packages with namespace need user/channel qualifiers"
543
+ rule: "conan packages with namespace need qualifiers for disambiguation"
543
544
  )
544
545
  end
545
546
 
@@ -5,7 +5,7 @@ module Purl
5
5
  # Load registry patterns from JSON configuration
6
6
  def self.load_registry_patterns
7
7
  @registry_patterns ||= begin
8
- # Load JSON config directly to avoid circular dependency
8
+ # Load extended registry configs
9
9
  config_path = File.join(__dir__, "..", "..", "purl-types.json")
10
10
  require "json"
11
11
  config = JSON.parse(File.read(config_path))
@@ -16,37 +16,19 @@ module Purl
16
16
  next unless type_config["registry_config"]
17
17
 
18
18
  registry_config = type_config["registry_config"]
19
- patterns[type] = build_pattern_config(type, registry_config)
19
+ patterns[type] = build_pattern_config(type, registry_config, type_config)
20
20
  end
21
21
 
22
22
  patterns
23
23
  end
24
24
  end
25
25
 
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]
26
+ def self.build_pattern_config(type, config, type_config)
27
+ # Get the default registry for this type from the extended config
29
28
  default_registry = type_config["default_registry"]
30
29
 
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"]
30
+ # Use route_patterns directly from our extended registry config
31
+ route_patterns = config["route_patterns"] || []
50
32
 
51
33
  # Build reverse regex from template or use legacy format
52
34
  reverse_regex = nil
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.1.1"
4
+ VERSION = "1.2.0"
5
5
  end