packageurl-ruby 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e48cdeff19115a2dbca64c93e81d7cd4de3a45946927157eeba40388de8ec04f
4
+ data.tar.gz: e94e269df648bade8528f547cfaca06c7e29d8fa23b9f318122d313665da796f
5
+ SHA512:
6
+ metadata.gz: 92f391f396e54dd47e408cac234d5e4982d5ec91418d0ddb1198d07bc7d58cbea59e7323221f973f54bed72ce78e351c861bdd075ff83940d17cb5290da68fc3
7
+ data.tar.gz: 4d8e28fbfad4fed559ac1592ba5dc1d38b76f653508409b209b9febd03b5927bef281a36aa830945971bc75eb49a2cf1bc2e45ec2413c2b5444bb8e2f0eca446
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ["2.7", "3.0"]
15
+
16
+ env:
17
+ RUBYOPT: "-W:no-experimental"
18
+
19
+ steps:
20
+ - uses: actions/checkout@v2
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby-version }}
25
+ bundler-cache: true
26
+ - name: Run tests
27
+ run: bundle exec rspec
28
+ - name: Perform type check
29
+ run: bundle exec steep check
30
+ - name: Lint
31
+ run: bundle exec rubocop
32
+ - name: Check documentation coverage
33
+ run: bundle exec yard stats --list-undoc
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.vscode/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+ NewCops: enable
4
+ Metrics/AbcSize:
5
+ Enabled: false
6
+ Metrics/BlockLength:
7
+ Exclude:
8
+ - "Rakefile"
9
+ - "**/*.rake"
10
+ - "spec/**/*.rb"
11
+ Metrics/CyclomaticComplexity:
12
+ Enabled: false
13
+ Metrics/ClassLength:
14
+ Max: 500
15
+ Metrics/MethodLength:
16
+ Max: 100
17
+ Metrics/ParameterLists:
18
+ Max: 7
19
+ Metrics/PerceivedComplexity:
20
+ Max: 15
21
+ Style/ConditionalAssignment:
22
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [v0.0.1] - 2021-12-09
11
+
12
+ Initial release.
13
+
14
+ [unreleased]: https://github.com/package-url/packageurl-ruby/releases/tag/v0.1.0...main
15
+ [v0.1.0]: https://github.com/package-url/packageurl-ruby/releases/tag/v0.1.0
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'bundler', '~> 2.0'
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rspec', '~> 3.0'
10
+ gem 'rubocop', '~> 1.23.0'
11
+ gem 'rubocop-rake', '~> 0.6.0'
12
+ gem 'rubocop-rspec', '~> 2.6.0'
13
+ gem 'steep', '~> 0.46.0'
14
+ gem 'yard', '~> 0.9.0'
data/Gemfile.lock ADDED
@@ -0,0 +1,100 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ packageurl-ruby (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activesupport (6.1.4.1)
10
+ concurrent-ruby (~> 1.0, >= 1.0.2)
11
+ i18n (>= 1.6, < 2)
12
+ minitest (>= 5.1)
13
+ tzinfo (~> 2.0)
14
+ zeitwerk (~> 2.3)
15
+ ast (2.4.2)
16
+ concurrent-ruby (1.1.9)
17
+ diff-lcs (1.4.4)
18
+ ffi (1.15.4)
19
+ i18n (1.8.11)
20
+ concurrent-ruby (~> 1.0)
21
+ language_server-protocol (3.16.0.3)
22
+ listen (3.7.0)
23
+ rb-fsevent (~> 0.10, >= 0.10.3)
24
+ rb-inotify (~> 0.9, >= 0.9.10)
25
+ minitest (5.14.4)
26
+ parallel (1.21.0)
27
+ parser (3.0.3.0)
28
+ ast (~> 2.4.1)
29
+ rainbow (3.0.0)
30
+ rake (13.0.6)
31
+ rb-fsevent (0.11.0)
32
+ rb-inotify (0.10.1)
33
+ ffi (~> 1.0)
34
+ rbs (1.7.1)
35
+ regexp_parser (2.1.1)
36
+ rexml (3.2.5)
37
+ rspec (3.10.0)
38
+ rspec-core (~> 3.10.0)
39
+ rspec-expectations (~> 3.10.0)
40
+ rspec-mocks (~> 3.10.0)
41
+ rspec-core (3.10.1)
42
+ rspec-support (~> 3.10.0)
43
+ rspec-expectations (3.10.1)
44
+ diff-lcs (>= 1.2.0, < 2.0)
45
+ rspec-support (~> 3.10.0)
46
+ rspec-mocks (3.10.2)
47
+ diff-lcs (>= 1.2.0, < 2.0)
48
+ rspec-support (~> 3.10.0)
49
+ rspec-support (3.10.3)
50
+ rubocop (1.23.0)
51
+ parallel (~> 1.10)
52
+ parser (>= 3.0.0.0)
53
+ rainbow (>= 2.2.2, < 4.0)
54
+ regexp_parser (>= 1.8, < 3.0)
55
+ rexml
56
+ rubocop-ast (>= 1.12.0, < 2.0)
57
+ ruby-progressbar (~> 1.7)
58
+ unicode-display_width (>= 1.4.0, < 3.0)
59
+ rubocop-ast (1.13.0)
60
+ parser (>= 3.0.1.1)
61
+ rubocop-rake (0.6.0)
62
+ rubocop (~> 1.0)
63
+ rubocop-rspec (2.6.0)
64
+ rubocop (~> 1.19)
65
+ ruby-progressbar (1.11.0)
66
+ steep (0.46.0)
67
+ activesupport (>= 5.1)
68
+ language_server-protocol (>= 3.15, < 4.0)
69
+ listen (~> 3.0)
70
+ parallel (>= 1.0.0)
71
+ parser (>= 3.0)
72
+ rainbow (>= 2.2.2, < 4.0)
73
+ rbs (>= 1.2.0)
74
+ terminal-table (>= 2, < 4)
75
+ terminal-table (3.0.2)
76
+ unicode-display_width (>= 1.1.1, < 3)
77
+ tzinfo (2.0.4)
78
+ concurrent-ruby (~> 1.0)
79
+ unicode-display_width (2.1.0)
80
+ yard (0.9.26)
81
+ zeitwerk (2.5.1)
82
+
83
+ PLATFORMS
84
+ x86_64-darwin-19
85
+ x86_64-darwin-20
86
+ x86_64-linux
87
+
88
+ DEPENDENCIES
89
+ bundler (~> 2.0)
90
+ packageurl-ruby!
91
+ rake (~> 13.0)
92
+ rspec (~> 3.0)
93
+ rubocop (~> 1.23.0)
94
+ rubocop-rake (~> 0.6.0)
95
+ rubocop-rspec (~> 2.6.0)
96
+ steep (~> 0.46.0)
97
+ yard (~> 0.9.0)
98
+
99
+ BUNDLED WITH
100
+ 2.2.32
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # packageurl-ruby
2
+
3
+ ![CI][ci badge]
4
+
5
+ A Ruby implementation of the [package url specification][purl-spec].
6
+
7
+ ## Requirements
8
+
9
+ - Ruby 2.7+
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'packageurl-ruby'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ ```console
22
+ $ bundle install
23
+ ```
24
+
25
+ Or install it yourself as:
26
+
27
+ ```console
28
+ $ gem install packageurl-ruby
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```ruby
34
+ require 'packageurl-ruby'
35
+
36
+ purl = PackageURL.parse("pkg:gem/rails@6.1.4")
37
+ purl.type # "gem"
38
+ purl.name # "rails"
39
+ purl.version # "6.1.4"
40
+
41
+ # supports pattern matching with hashes and arrays
42
+ case purl
43
+ in type: 'gem', name: 'rails'
44
+ puts 'Yay! You’re on Rails!'
45
+ in ['pkg', 'gem', *]
46
+ puts '🦊🗯 "Ruby is easy to read"'
47
+ end
48
+ ```
49
+
50
+ ## Development
51
+
52
+ After checking out the repo, run `bin/setup` to install dependencies.
53
+ Then, run `rake spec` to run the tests.
54
+ You can also run `bin/console` for an interactive prompt
55
+ that will allow you to experiment.
56
+
57
+ To install this gem onto your local machine,
58
+ run `bundle exec rake install`.
59
+ To release a new version,
60
+ update the version number in `version.rb`,
61
+ and then run `bundle exec rake release`,
62
+ which will create a git tag for the version,
63
+ push git commits and the created tag,
64
+ and push the `.gem` file to [rubygems.org](https://rubygems.org).
65
+
66
+ [ci badge]: https://github.com/mattt/packageurl-ruby/workflows/CI/badge.svg
67
+ [purl-spec]: https://github.com/package-url/purl-spec
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ namespace :steep do
9
+ task :check do
10
+ system 'steep check'
11
+ end
12
+ end
13
+
14
+ task default: %i[spec steep:check]
data/Steepfile ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :lib do
4
+ signature 'sig'
5
+
6
+ check 'lib'
7
+
8
+ library 'uri'
9
+
10
+ configure_code_diagnostics do |config|
11
+ config[Steep::Diagnostic::Ruby::UnsupportedSyntax] = :hint
12
+ config[Steep::Diagnostic::Ruby::MethodDefinitionMissing] = :hint
13
+ end
14
+ end
15
+
16
+ target :test do
17
+ signature 'sig'
18
+
19
+ check 'test'
20
+
21
+ library 'uri'
22
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'package_url'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PackageURL
4
+ # :nodoc:
5
+ VERSION = '0.1.0'
6
+ end
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'package_url/version'
4
+
5
+ require 'uri'
6
+
7
+ # A package URL, or _purl_, is a URL string used to
8
+ # identify and locate a software package in a mostly universal and uniform way
9
+ # across programing languages, package managers, packaging conventions, tools,
10
+ # APIs and databases.
11
+ #
12
+ # A purl is a URL composed of seven components:
13
+ #
14
+ # ```
15
+ # scheme:type/namespace/name@version?qualifiers#subpath
16
+ # ```
17
+ #
18
+ # For example,
19
+ # the package URL for this Ruby package at version 0.1.0 is
20
+ # `pkg:ruby/mattt/packageurl-ruby@0.1.0`.
21
+ class PackageURL
22
+ # Raised when attempting to parse an invalid package URL string.
23
+ # @see #parse
24
+ class InvalidPackageURL < ArgumentError; end
25
+
26
+ # The URL scheme, which has a constant value of `"pkg"`.
27
+ def scheme
28
+ 'pkg'
29
+ end
30
+
31
+ # The package type or protocol, such as `"gem"`, `"npm"`, and `"github"`.
32
+ attr_reader :type
33
+
34
+ # A name prefix, specific to the type of package.
35
+ # For example, an npm scope, a Docker image owner, or a GitHub user.
36
+ attr_reader :namespace
37
+
38
+ # The name of the package.
39
+ attr_reader :name
40
+
41
+ # The version of the package.
42
+ attr_reader :version
43
+
44
+ # Extra qualifying data for a package, specific to the type of package.
45
+ # For example, the operating system or architecture.
46
+ attr_reader :qualifiers
47
+
48
+ # An extra subpath within a package, relative to the package root.
49
+ attr_reader :subpath
50
+
51
+ # Constructs a package URL from its components
52
+ # @param type [String] The package type or protocol.
53
+ # @param namespace [String] A name prefix, specific to the type of package.
54
+ # @param name [String] The name of the package.
55
+ # @param version [String] The version of the package.
56
+ # @param qualifiers [Hash] Extra qualifying data for a package, specific to the type of package.
57
+ # @param subpath [String] An extra subpath within a package, relative to the package root.
58
+ def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil)
59
+ raise ArgumentError, 'type is required' if type.nil? || type.empty?
60
+ raise ArgumentError, 'name is required' if name.nil? || name.empty?
61
+
62
+ @type = type.downcase
63
+ @namespace = namespace
64
+ @name = name
65
+ @version = version
66
+ @qualifiers = qualifiers
67
+ @subpath = subpath
68
+ end
69
+
70
+ # Creates a new PackageURL from a string.
71
+ # @param [String] string The package URL string.
72
+ # @raise [InvalidPackageURL] If the string is not a valid package URL.
73
+ # @return [PackageURL]
74
+ def self.parse(string)
75
+ components = {}
76
+
77
+ # Split the purl string once from right on '#'
78
+ # - The left side is the remainder
79
+ # - Strip the right side from leading and trailing '/'
80
+ # - Split this on '/'
81
+ # - Discard any empty string segment from that split
82
+ # - Discard any '.' or '..' segment from that split
83
+ # - Percent-decode each segment
84
+ # - UTF-8-decode each segment if needed in your programming language
85
+ # - Join segments back with a '/'
86
+ # - This is the subpath
87
+ case string.rpartition('#')
88
+ in String => remainder, separator, String => subpath unless separator.empty?
89
+ components[:subpath] = subpath.split('/').select do |segment|
90
+ !segment.empty? && segment != '.' && segment != '..'
91
+ end.compact.join('/')
92
+
93
+ string = remainder
94
+ else
95
+ components[:subpath] = nil
96
+ end
97
+
98
+ # Split the remainder once from right on '?'
99
+ # - The left side is the remainder
100
+ # - The right side is the qualifiers string
101
+ # - Split the qualifiers on '&'. Each part is a key=value pair
102
+ # - For each pair, split the key=value once from left on '=':
103
+ # - The key is the lowercase left side
104
+ # - The value is the percent-decoded right side
105
+ # - UTF-8-decode the value if needed in your programming language
106
+ # - Discard any key/value pairs where the value is empty
107
+ # - If the key is checksums,
108
+ # split the value on ',' to create a list of checksums
109
+ # - This list of key/value is the qualifiers object
110
+ case string.rpartition('?')
111
+ in String => remainder, separator, String => qualifiers unless separator.empty?
112
+ components[:qualifiers] = {}
113
+
114
+ qualifiers.split('&').each do |pair|
115
+ case pair.partition('=')
116
+ in String => key, separator, String => value unless separator.empty?
117
+ key = key.downcase
118
+ value = URI.decode_www_form_component(value)
119
+ next if value.empty?
120
+
121
+ case key
122
+ when 'checksums'
123
+ components[:qualifiers][key] = value.split(',')
124
+ else
125
+ components[:qualifiers][key] = value
126
+ end
127
+ else
128
+ next
129
+ end
130
+ end
131
+
132
+ string = remainder
133
+ else
134
+ components[:qualifiers] = nil
135
+ end
136
+
137
+ # Split the remainder once from left on ':'
138
+ # - The left side lowercased is the scheme
139
+ # - The right side is the remainder
140
+ case string.partition(':')
141
+ in 'pkg', separator, String => remainder unless separator.empty?
142
+ string = remainder
143
+ else
144
+ raise InvalidPackageURL, 'invalid or missing "pkg:" URL scheme'
145
+ end
146
+
147
+ # Strip the remainder from leading and trailing '/'
148
+ # - Split this once from left on '/'
149
+ # - The left side lowercased is the type
150
+ # - The right side is the remainder
151
+ string = string.delete_suffix('/')
152
+ case string.partition('/')
153
+ in String => type, separator, remainder unless separator.empty?
154
+ components[:type] = type
155
+
156
+ string = remainder
157
+ else
158
+ raise InvalidPackageURL, 'invalid or missing package type'
159
+ end
160
+
161
+ # Split the remainder once from right on '@'
162
+ # - The left side is the remainder
163
+ # - Percent-decode the right side. This is the version.
164
+ # - UTF-8-decode the version if needed in your programming language
165
+ # - This is the version
166
+ case string.rpartition('@')
167
+ in String => remainder, separator, String => version unless separator.empty?
168
+ components[:version] = URI.decode_www_form_component(version)
169
+
170
+ string = remainder
171
+ else
172
+ components[:version] = nil
173
+ end
174
+
175
+ # Split the remainder once from right on '/'
176
+ # - The left side is the remainder
177
+ # - Percent-decode the right side. This is the name
178
+ # - UTF-8-decode this name if needed in your programming language
179
+ # - Apply type-specific normalization to the name if needed
180
+ # - This is the name
181
+ case string.rpartition('/')
182
+ in String => remainder, separator, String => name unless separator.empty?
183
+ components[:name] = URI.decode_www_form_component(name)
184
+
185
+ # Split the remainder on '/'
186
+ # - Discard any empty segment from that split
187
+ # - Percent-decode each segment
188
+ # - UTF-8-decode the each segment if needed in your programming language
189
+ # - Apply type-specific normalization to each segment if needed
190
+ # - Join segments back with a '/'
191
+ # - This is the namespace
192
+ components[:namespace] = remainder.split('/').map { |s| URI.decode_www_form_component(s) }.compact.join('/')
193
+ in _, _, String => name
194
+ components[:name] = URI.decode_www_form_component(name)
195
+ components[:namespace] = nil
196
+ end
197
+
198
+ new(type: components[:type],
199
+ name: components[:name],
200
+ namespace: components[:namespace],
201
+ version: components[:version],
202
+ qualifiers: components[:qualifiers],
203
+ subpath: components[:subpath])
204
+ end
205
+
206
+ # Returns a hash containing the
207
+ # scheme, type, namespace, name, version, qualifiers, and subpath components
208
+ # of the package URL.
209
+ def to_h
210
+ {
211
+ scheme: scheme,
212
+ type: @type,
213
+ namespace: @namespace,
214
+ name: @name,
215
+ version: @version,
216
+ qualifiers: @qualifiers,
217
+ subpath: @subpath
218
+ }
219
+ end
220
+
221
+ # Returns a string representation of the package URL.
222
+ # Package URL representations are created according to the instructions from
223
+ # https://github.com/package-url/purl-spec/blob/0b1559f76b79829e789c4f20e6d832c7314762c5/PURL-SPECIFICATION.rst#how-to-build-purl-string-from-its-components.
224
+ def to_s
225
+ # Start a purl string with the "pkg:" scheme as a lowercase ASCII string
226
+ purl = 'pkg:'
227
+
228
+ # Append the type string to the purl as a lowercase ASCII string
229
+ # Append '/' to the purl
230
+
231
+ purl += @type
232
+ purl += '/'
233
+
234
+ # If the namespace is not empty:
235
+ # - Strip the namespace from leading and trailing '/'
236
+ # - Split on '/' as segments
237
+ # - Apply type-specific normalization to each segment if needed
238
+ # - UTF-8-encode each segment if needed in your programming language
239
+ # - Percent-encode each segment
240
+ # - Join the segments with '/'
241
+ # - Append this to the purl
242
+ # - Append '/' to the purl
243
+ # - Strip the name from leading and trailing '/'
244
+ # - Apply type-specific normalization to the name if needed
245
+ # - UTF-8-encode the name if needed in your programming language
246
+ # - Append the percent-encoded name to the purl
247
+ #
248
+ # If the namespace is empty:
249
+ # - Apply type-specific normalization to the name if needed
250
+ # - UTF-8-encode the name if needed in your programming language
251
+ # - Append the percent-encoded name to the purl
252
+ case @namespace
253
+ in String => namespace unless namespace.empty?
254
+ segments = []
255
+ @namespace.delete_prefix('/').delete_suffix('/').split('/').each do |segment|
256
+ next if segment.empty?
257
+
258
+ segments << URI.encode_www_form_component(segment)
259
+ end
260
+ purl += segments.join('/')
261
+
262
+ purl += '/'
263
+ purl += URI.encode_www_form_component(@name.delete_prefix('/').delete_suffix('/'))
264
+ else
265
+ purl += URI.encode_www_form_component(@name)
266
+ end
267
+
268
+ # If the version is not empty:
269
+ # - Append '@' to the purl
270
+ # - UTF-8-encode the version if needed in your programming language
271
+ # - Append the percent-encoded version to the purl
272
+ case @version
273
+ in String => version unless version.empty?
274
+ purl += '@'
275
+ purl += URI.encode_www_form_component(@version)
276
+ else
277
+ nil
278
+ end
279
+
280
+ # If the qualifiers are not empty and not composed only of key/value pairs
281
+ # where the value is empty:
282
+ # - Append '?' to the purl
283
+ # - Build a list from all key/value pair:
284
+ # - discard any pair where the value is empty.
285
+ # - UTF-8-encode each value if needed in your programming language
286
+ # - If the key is checksums and this is a list of checksums
287
+ # join this list with a ',' to create this qualifier value
288
+ # - create a string by joining the lowercased key,
289
+ # the equal '=' sign and the percent-encoded value to create a qualifier
290
+ # - sort this list of qualifier strings lexicographically
291
+ # - join this list of qualifier strings with a '&' ampersand
292
+ # - Append this string to the purl
293
+ case @qualifiers
294
+ in Hash => qualifiers unless qualifiers.empty?
295
+ list = []
296
+ qualifiers.each do |key, value|
297
+ next if value.empty?
298
+
299
+ case [key, value]
300
+ in 'checksums', Array => checksums
301
+ list << "#{key.downcase}=#{checksums.join(',')}"
302
+ else
303
+ list << "#{key.downcase}=#{URI.encode_www_form_component(value)}"
304
+ end
305
+ end
306
+
307
+ unless list.empty?
308
+ purl += '?'
309
+ purl += list.sort.join('&')
310
+ end
311
+ else
312
+ nil
313
+ end
314
+
315
+ # If the subpath is not empty and not composed only of
316
+ # empty, '.' and '..' segments:
317
+ # - Append '#' to the purl
318
+ # - Strip the subpath from leading and trailing '/'
319
+ # - Split this on '/' as segments
320
+ # - Discard empty, '.' and '..' segments
321
+ # - Percent-encode each segment
322
+ # - UTF-8-encode each segment if needed in your programming language
323
+ # - Join the segments with '/'
324
+ # - Append this to the purl
325
+ case @subpath
326
+ in String => subpath unless subpath.empty?
327
+ segments = []
328
+ subpath.delete_prefix('/').delete_suffix('/').split('/').each do |segment|
329
+ next if segment.empty? || segment == '.' || segment == '..'
330
+
331
+ segments << URI.encode_www_form_component(segment)
332
+ end
333
+
334
+ unless segments.empty?
335
+ purl += '#'
336
+ purl += segments.join('/')
337
+ end
338
+ else
339
+ nil
340
+ end
341
+
342
+ purl
343
+ end
344
+
345
+ # Returns an array containing the
346
+ # scheme, type, namespace, name, version, qualifiers, and subpath components
347
+ # of the package URL.
348
+ def deconstruct
349
+ [scheme, @type, @namespace, @name, @version, @qualifiers, @subpath]
350
+ end
351
+
352
+ # Returns a hash containing the
353
+ # scheme, type, namespace, name, version, qualifiers, and subpath components
354
+ # of the package URL.
355
+ def deconstruct_keys(_keys)
356
+ to_h
357
+ end
358
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/package_url/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'packageurl-ruby'
7
+ spec.version = PackageURL::VERSION
8
+ spec.authors = ['Mattt']
9
+ spec.email = ['mattt@me.com']
10
+
11
+ spec.summary = 'Ruby implementation of the package url spec'
12
+ spec.description = <<-DESCRIPTION
13
+ A package URL, or purl, is a URL string used to
14
+ identify and locate a software package in a mostly universal and uniform way
15
+ across programing languages, package managers, packaging conventions,
16
+ tools, APIs and databases.
17
+ DESCRIPTION
18
+
19
+ spec.homepage = 'https://github.com/package-url/packageurl-ruby'
20
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
21
+
22
+ spec.metadata['homepage_uri'] = spec.homepage
23
+ spec.metadata['source_code_uri'] = spec.homepage
24
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
30
+ end
31
+ spec.bindir = 'exe'
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ['lib']
34
+
35
+ # For more information and examples about making a new gem, checkout our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ spec.metadata = {
38
+ 'rubygems_mfa_required' => 'true'
39
+ }
40
+ end
@@ -0,0 +1,44 @@
1
+ # A package URL, or _purl_, is a URL string used to identify and locate a software package
2
+ # in a mostly universal and uniform way across
3
+ # programing languages, package managers, packaging conventions, tools, APIs and databases.
4
+ #
5
+ # A purl is a URL composed of seven components:
6
+ #
7
+ # ```
8
+ # scheme:type/namespace/name@version?qualifiers#subpath
9
+ # ```
10
+ #
11
+ # For example,
12
+ # the package URL for this Ruby package at version 0.1.0 is
13
+ # `pkg:ruby/mattt/packageurl-ruby@0.1.0`.
14
+ class PackageURL
15
+ VERSION: String
16
+
17
+ def scheme: () -> String
18
+ attr_reader type: String
19
+ attr_reader namespace: String?
20
+ attr_reader name: String?
21
+ attr_reader version: String?
22
+ attr_reader qualifiers: Hash[String, String]?
23
+ attr_reader subpath: String?
24
+
25
+ def initialize: (type: String `type`,
26
+ ?namespace: String? namespace,
27
+ name: String name,
28
+ ?version: String? version,
29
+ ?qualifiers: Hash[String, String]? qualifiers,
30
+ ?subpath: String? subpath) -> void
31
+
32
+ def self.parse: (String string) -> PackageURL?
33
+
34
+ def to_h: () -> { scheme: String, type: String, namespace: String?, name: String?, version: String?, qualifiers: Hash[String, String]?, subpath: String? }
35
+
36
+ # Returns a string representation of the package URL.
37
+ # Package URL representations are created according to the instructions provided at
38
+ # https://github.com/package-url/purl-spec/blob/0b1559f76b79829e789c4f20e6d832c7314762c5/PURL-SPECIFICATION.rst#how-to-build-purl-string-from-its-components.
39
+ def to_s: () -> String
40
+
41
+ def deconstruct: () -> Array[String | Hash[String, String] | nil]
42
+
43
+ def deconstruct_keys: (Array[Symbol] keys) -> { scheme: String, type: String, namespace: String?, name: String?, version: String?, qualifiers: Hash[String, String]?, subpath: String? }
44
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: packageurl-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mattt
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+ A package URL, or purl, is a URL string used to
15
+ identify and locate a software package in a mostly universal and uniform way
16
+ across programing languages, package managers, packaging conventions,
17
+ tools, APIs and databases.
18
+ email:
19
+ - mattt@me.com
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - ".github/workflows/ci.yml"
25
+ - ".gitignore"
26
+ - ".rspec"
27
+ - ".rubocop.yml"
28
+ - ".ruby-version"
29
+ - CHANGELOG.md
30
+ - Gemfile
31
+ - Gemfile.lock
32
+ - README.md
33
+ - Rakefile
34
+ - Steepfile
35
+ - bin/console
36
+ - bin/setup
37
+ - lib/package_url.rb
38
+ - lib/package_url/version.rb
39
+ - packageurl-ruby.gemspec
40
+ - sig/package_url.rbs
41
+ homepage: https://github.com/package-url/packageurl-ruby
42
+ licenses: []
43
+ metadata:
44
+ rubygems_mfa_required: 'true'
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.7.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.2.15
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Ruby implementation of the package url spec
64
+ test_files: []