packageurl-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []