purl 0.1.0 → 1.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.
data/README.md CHANGED
@@ -1,39 +1,435 @@
1
- # Purl
1
+ # Purl - Package URL Parser for Ruby
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ A Ruby library for parsing, validating, and generating Package URLs (PURLs) as defined by the [PURL specification](https://github.com/package-url/purl-spec).
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/purl`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ This library features comprehensive error handling with namespaced error types, bidirectional registry URL conversion, and JSON-based configuration for cross-language compatibility.
6
+
7
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-red.svg)](https://www.ruby-lang.org/)
8
+ [![Gem Version](https://badge.fury.io/rb/purl.svg)](https://rubygems.org/gems/purl)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+
11
+ **[Available on RubyGems](https://rubygems.org/gems/purl)**
12
+
13
+ ## Features
14
+
15
+ - **Comprehensive PURL parsing and validation** with 37 package types (32 official + 5 additional ecosystems)
16
+ - **Better error handling** with namespaced error classes and contextual information
17
+ - **Bidirectional registry URL conversion** - generate registry URLs from PURLs and parse PURLs from registry URLs
18
+ - **Type-specific validation** for conan, cran, and swift packages
19
+ - **Registry URL generation** for 20 package ecosystems (npm, gem, maven, pypi, etc.)
20
+ - **Rails-style route patterns** for registry URL templates
21
+ - **100% compliance** with official PURL specification test suite (59/59 tests passing)
22
+ - **Cross-language compatibility** with JSON-based configuration
23
+ - **Comprehensive documentation** and examples
6
24
 
7
25
  ## Installation
8
26
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
27
+ Add this line to your application's Gemfile:
10
28
 
11
- Install the gem and add to the application's Gemfile by executing:
29
+ ```ruby
30
+ gem 'purl'
31
+ ```
32
+
33
+ And then execute:
12
34
 
13
35
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
36
+ bundle install
15
37
  ```
16
38
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
39
+ Or install it yourself as:
18
40
 
19
41
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
42
+ gem install purl
21
43
  ```
22
44
 
23
45
  ## Usage
24
46
 
25
- TODO: Write usage instructions here
47
+ ### Basic PURL Parsing
48
+
49
+ ```ruby
50
+ require 'purl'
51
+
52
+ # Parse a PURL string
53
+ purl = Purl.parse("pkg:gem/rails@7.0.0")
54
+ puts purl.type # => "gem"
55
+ puts purl.name # => "rails"
56
+ puts purl.version # => "7.0.0"
57
+ puts purl.namespace # => nil
58
+
59
+ # Parse with namespace and qualifiers
60
+ purl = Purl.parse("pkg:npm/@babel/core@7.0.0?arch=x86_64")
61
+ puts purl.type # => "npm"
62
+ puts purl.namespace # => "@babel"
63
+ puts purl.name # => "core"
64
+ puts purl.version # => "7.0.0"
65
+ puts purl.qualifiers # => {"arch" => "x86_64"}
66
+ ```
67
+
68
+ ### Creating PURLs
69
+
70
+ ```ruby
71
+ # Create a PURL object
72
+ purl = Purl::PackageURL.new(
73
+ type: "maven",
74
+ namespace: "org.apache.commons",
75
+ name: "commons-lang3",
76
+ version: "3.12.0"
77
+ )
78
+
79
+ puts purl.to_s # => "pkg:maven/org.apache.commons/commons-lang3@3.12.0"
80
+ ```
81
+
82
+ ### Modifying PURL Objects
83
+
84
+ PURL objects are immutable by design, but you can create new objects with modified attributes using the `with` method:
85
+
86
+ ```ruby
87
+ # Create original PURL
88
+ original = Purl::PackageURL.new(
89
+ type: "npm",
90
+ namespace: "@babel",
91
+ name: "core",
92
+ version: "7.20.0",
93
+ qualifiers: { "arch" => "x64" }
94
+ )
95
+
96
+ # Create new PURL with updated version
97
+ updated = original.with(version: "7.21.0")
98
+ puts updated.to_s # => "pkg:npm/@babel/core@7.21.0?arch=x64"
99
+
100
+ # Update qualifiers
101
+ with_new_qualifiers = original.with(
102
+ qualifiers: { "arch" => "arm64", "os" => "linux" }
103
+ )
104
+ puts with_new_qualifiers.to_s # => "pkg:npm/@babel/core@7.20.0?arch=arm64&os=linux"
105
+
106
+ # Update multiple attributes at once
107
+ fully_updated = original.with(
108
+ version: "8.0.0",
109
+ qualifiers: { "dev" => "true" },
110
+ subpath: "lib/index.js"
111
+ )
112
+ puts fully_updated.to_s # => "pkg:npm/@babel/core@8.0.0#lib/index.js?dev=true"
113
+
114
+ # Original remains unchanged
115
+ puts original.to_s # => "pkg:npm/@babel/core@7.20.0?arch=x64"
116
+ ```
117
+
118
+ ### Registry URL Generation
119
+
120
+ ```ruby
121
+ # Generate registry URLs from PURLs
122
+ purl = Purl.parse("pkg:gem/rails@7.0.0")
123
+ puts purl.registry_url # => "https://rubygems.org/gems/rails"
124
+ puts purl.registry_url_with_version # => "https://rubygems.org/gems/rails/versions/7.0.0"
125
+
126
+ # Check if registry URL generation is supported
127
+ puts purl.supports_registry_url? # => true
128
+
129
+ # NPM with scoped packages
130
+ purl = Purl.parse("pkg:npm/@babel/core@7.0.0")
131
+ puts purl.registry_url # => "https://www.npmjs.com/package/@babel/core"
132
+ ```
133
+
134
+ ### Reverse Parsing: Registry URLs to PURLs
135
+
136
+ ```ruby
137
+ # Parse registry URLs back to PURLs
138
+ purl = Purl.from_registry_url("https://rubygems.org/gems/rails/versions/7.0.0")
139
+ puts purl.to_s # => "pkg:gem/rails@7.0.0"
140
+
141
+ # Works with various registries
142
+ purl = Purl.from_registry_url("https://www.npmjs.com/package/@babel/core")
143
+ puts purl.to_s # => "pkg:npm/@babel/core"
144
+
145
+ purl = Purl.from_registry_url("https://pypi.org/project/django/4.0.0/")
146
+ puts purl.to_s # => "pkg:pypi/django@4.0.0"
147
+ ```
148
+
149
+ ### Custom Registry Domains
150
+
151
+ You can parse registry URLs from custom domains or generate URLs for private registries:
152
+
153
+ ```ruby
154
+ # Parse from custom domain (specify type to help with parsing)
155
+ purl = Purl.from_registry_url("https://npm.company.com/package/@babel/core", type: "npm")
156
+ puts purl.to_s # => "pkg:npm/@babel/core"
157
+
158
+ # Generate URLs for custom registries
159
+ purl = Purl.parse("pkg:gem/rails@7.0.0")
160
+ custom_url = purl.registry_url(base_url: "https://gems.internal.com/gems")
161
+ puts custom_url # => "https://gems.internal.com/gems/rails"
162
+
163
+ # With version-specific URLs
164
+ with_version = purl.registry_url_with_version(base_url: "https://gems.internal.com/gems")
165
+ puts with_version # => "https://gems.internal.com/gems/rails/versions/7.0.0"
166
+
167
+ # Works with all supported package types
168
+ composer_purl = Purl.parse("pkg:composer/symfony/console@5.4.0")
169
+ private_composer = composer_purl.registry_url(base_url: "https://packagist.company.com/packages")
170
+ puts private_composer # => "https://packagist.company.com/packages/symfony/console"
171
+ ```
172
+
173
+ ### Route Patterns
174
+
175
+ ```ruby
176
+ # Get route patterns for a package type (Rails-style)
177
+ patterns = Purl::RegistryURL.route_patterns_for("gem")
178
+ # => ["https://rubygems.org/gems/:name", "https://rubygems.org/gems/:name/versions/:version"]
179
+
180
+ # Get all route patterns
181
+ all_patterns = Purl::RegistryURL.all_route_patterns
182
+ puts all_patterns["npm"]
183
+ # => ["https://www.npmjs.com/package/:namespace/:name", "https://www.npmjs.com/package/:name", ...]
184
+ ```
185
+
186
+ ### Working with Qualifiers
187
+
188
+ Qualifiers are key-value pairs that provide additional metadata about packages:
189
+
190
+ ```ruby
191
+ # Create PURL with qualifiers
192
+ purl = Purl::PackageURL.new(
193
+ type: "apk",
194
+ name: "curl",
195
+ version: "7.83.0-r0",
196
+ qualifiers: {
197
+ "distro" => "alpine-3.16",
198
+ "arch" => "x86_64",
199
+ "repository_url" => "https://dl-cdn.alpinelinux.org"
200
+ }
201
+ )
202
+ puts purl.to_s # => "pkg:apk/curl@7.83.0-r0?arch=x86_64&distro=alpine-3.16&repository_url=https://dl-cdn.alpinelinux.org"
203
+
204
+ # Access qualifiers
205
+ puts purl.qualifiers["distro"] # => "alpine-3.16"
206
+ puts purl.qualifiers["arch"] # => "x86_64"
207
+
208
+ # Parse PURL with qualifiers
209
+ parsed = Purl.parse("pkg:rpm/httpd@2.4.53?distro=fedora-36&arch=x86_64")
210
+ puts parsed.qualifiers # => {"distro" => "fedora-36", "arch" => "x86_64"}
211
+
212
+ # Add qualifiers to existing PURL
213
+ with_qualifiers = purl.with(qualifiers: purl.qualifiers.merge("signed" => "true"))
214
+ ```
215
+
216
+ ### Package Type Information
217
+
218
+ ```ruby
219
+ # Get all known PURL types
220
+ puts Purl.known_types.length # => 37
221
+ puts Purl.known_types.include?("gem") # => true
222
+
223
+ # Check type support
224
+ puts Purl.known_type?("gem") # => true
225
+ puts Purl.registry_supported_types # => ["cargo", "gem", "maven", "npm", ...]
226
+ puts Purl.reverse_parsing_supported_types # => ["bioconductor", "cargo", "clojars", ...]
227
+
228
+ # Get default registry for a type
229
+ puts Purl.default_registry("gem") # => "https://rubygems.org"
230
+ puts Purl.default_registry("npm") # => "https://registry.npmjs.org"
231
+ puts Purl.default_registry("golang") # => nil (no default)
232
+
233
+ # Get official examples for a type
234
+ puts Purl.type_examples("gem") # => ["pkg:gem/rails@7.0.4", "pkg:gem/bundler@2.3.26", ...]
235
+ puts Purl.type_examples("npm") # => ["pkg:npm/lodash@4.17.21", "pkg:npm/@babel/core@7.20.0", ...]
236
+ puts Purl.type_examples("unknown") # => []
237
+
238
+ # Get detailed type information
239
+ info = Purl.type_info("gem")
240
+ puts info[:known] # => true
241
+ puts info[:description] # => "RubyGems"
242
+ puts info[:default_registry] # => "https://rubygems.org"
243
+ puts info[:examples] # => ["pkg:gem/rails@7.0.4", ...]
244
+ puts info[:registry_url_generation] # => true
245
+ puts info[:reverse_parsing] # => true
246
+ puts info[:route_patterns] # => ["https://rubygems.org/gems/:name", ...]
247
+ ```
248
+
249
+ ### Error Handling
250
+
251
+ ```ruby
252
+ # Detailed error types with context
253
+ begin
254
+ Purl.parse("invalid-purl")
255
+ rescue Purl::InvalidSchemeError => e
256
+ puts "Scheme error: #{e.message}"
257
+ rescue Purl::ParseError => e
258
+ puts "Parse error: #{e.message}"
259
+ puts "Component: #{e.component}"
260
+ puts "Value: #{e.value}"
261
+ end
262
+
263
+ # Type-specific validation errors
264
+ begin
265
+ Purl::PackageURL.new(type: "swift", name: "Alamofire") # Swift requires namespace
266
+ rescue Purl::ValidationError => e
267
+ puts e.message # => "Swift PURLs require a namespace to be unambiguous"
268
+ end
269
+ ```
270
+
271
+ ### Supported Package Types
272
+
273
+ The library supports 37 package types (32 official + 5 additional ecosystems):
274
+
275
+ **Registry URL Generation (20 types):**
276
+ - `bioconductor` (R/Bioconductor) - bioconductor.org
277
+ - `cargo` (Rust) - crates.io
278
+ - `clojars` (Clojure) - clojars.org
279
+ - `cocoapods` (iOS) - cocoapods.org
280
+ - `composer` (PHP) - packagist.org
281
+ - `conda` (Python) - anaconda.org
282
+ - `cpan` (Perl) - metacpan.org
283
+ - `deno` (Deno) - deno.land/x
284
+ - `elm` (Elm) - package.elm-lang.org
285
+ - `gem` (Ruby) - rubygems.org
286
+ - `golang` (Go) - pkg.go.dev
287
+ - `hackage` (Haskell) - hackage.haskell.org
288
+ - `hex` (Elixir) - hex.pm
289
+ - `homebrew` (macOS) - formulae.brew.sh
290
+ - `maven` (Java) - mvnrepository.com
291
+ - `npm` (Node.js) - npmjs.com
292
+ - `nuget` (.NET) - nuget.org
293
+ - `pub` (Dart) - pub.dev
294
+ - `pypi` (Python) - pypi.org
295
+ - `swift` (Swift) - swiftpackageindex.com
296
+
297
+ **Reverse Parsing (20 types):**
298
+ - `bioconductor`, `cargo`, `clojars`, `cocoapods`, `composer`, `conda`, `cpan`, `deno`, `elm`, `gem`, `golang`, `hackage`, `hex`, `homebrew`, `maven`, `npm`, `nuget`, `pub`, `pypi`, `swift`
299
+
300
+ **All 37 Supported Types:**
301
+ `alpm`, `apk`, `bioconductor`, `bitbucket`, `bitnami`, `cargo`, `clojars`, `cocoapods`, `composer`, `conan`, `conda`, `cpan`, `cran`, `deb`, `deno`, `docker`, `elm`, `gem`, `generic`, `github`, `golang`, `hackage`, `hex`, `homebrew`, `huggingface`, `luarocks`, `maven`, `mlflow`, `npm`, `nuget`, `oci`, `pub`, `pypi`, `qpkg`, `rpm`, `swid`, `swift`
302
+
303
+ ## Specification Compliance
304
+
305
+ - **100% compliance** with the official PURL specification test suite (59/59 tests passing)
306
+ - **All 32 official package types** plus 5 additional ecosystem types supported
307
+ - **Type-specific validation** for conan, cran, swift, cpan, and mlflow packages
308
+ - **Proper error handling** for invalid PURLs that should be rejected
309
+
310
+ ## JSON Configuration
311
+
312
+ Package types and registry patterns are stored in `purl-types.json` for easy contribution and cross-language compatibility:
313
+
314
+ ```json
315
+ {
316
+ "version": "1.0.0",
317
+ "description": "PURL types and registry URL patterns for package ecosystems",
318
+ "source": "https://github.com/package-url/purl-spec/blob/main/PURL-TYPES.rst",
319
+ "last_updated": "2025-07-24",
320
+ "types": {
321
+ "gem": {
322
+ "description": "RubyGems",
323
+ "default_registry": "https://rubygems.org",
324
+ "registry_config": {
325
+ "path_template": "/gems/:name",
326
+ "version_path_template": "/gems/:name/versions/:version",
327
+ "reverse_regex": "/gems/([^/?#]+)(?:/versions/([^/?#]+))?",
328
+ "components": {
329
+ "namespace": false,
330
+ "version_in_url": true,
331
+ "version_path": "/versions/"
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
339
+ **Key Configuration Improvements:**
340
+ - **Domain-agnostic patterns**: `path_template` without hardcoded domains enables custom registries
341
+ - **Flexible URL generation**: Combine `default_registry` + `path_template` for any domain
342
+ - **Cleaner JSON**: Reduced duplication and easier maintenance
343
+ - **Cross-registry compatibility**: Same URL structure works with public and private registries
344
+
345
+ ## JSON Schema Validation
346
+
347
+ The library includes JSON schemas for validation and documentation:
348
+
349
+ - **`schemas/purl-types.schema.json`** - Schema for the PURL types configuration file
350
+ - **`schemas/test-suite-data.schema.json`** - Schema for the official test suite data
351
+
352
+ These schemas provide:
353
+ - **Structure validation** - Ensure JSON files conform to expected format
354
+ - **Documentation** - Self-documenting configuration with descriptions
355
+ - **IDE support** - Enable autocomplete and validation in editors
356
+ - **CI/CD integration** - Validate configuration in automated pipelines
357
+
358
+ Validate JSON files against their schemas:
359
+ ```bash
360
+ rake spec:validate_schemas
361
+ ```
26
362
 
27
363
  ## Development
28
364
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
365
+ After checking out the repo, run `bin/setup` to install dependencies. Then:
366
+
367
+ ```bash
368
+ # Run tests
369
+ rake test
370
+
371
+ # Run specification compliance tests
372
+ rake spec:compliance
373
+
374
+ # Update test cases from official PURL spec
375
+ rake spec:update
376
+
377
+ # Show type information
378
+ rake spec:types
379
+
380
+ # Verify types against official specification
381
+ rake spec:verify_types
382
+ ```
383
+
384
+ ### Rake Tasks
30
385
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
386
+ - `rake spec:update` - Fetch latest test cases from official PURL spec repository
387
+ - `rake spec:compliance` - Run compliance tests against official test suite
388
+ - `rake spec:types` - Show information about all PURL types and their support
389
+ - `rake spec:verify_types` - Verify our types list against official specification
390
+ - `rake spec:validate_schemas` - Validate JSON files against their schemas
391
+ - `rake spec:debug` - Show detailed info about failing test cases
392
+
393
+ ## Funding
394
+
395
+ If you find this project useful, please consider supporting its development:
396
+
397
+ - [GitHub Sponsors](https://github.com/sponsors/andrew)
398
+ - [Ko-fi](https://ko-fi.com/andrewnez)
399
+ - [Buy Me a Coffee](https://www.buymeacoffee.com/andrewnez)
400
+
401
+ Your support helps maintain and improve this library for the entire Ruby community.
32
402
 
33
403
  ## Contributing
34
404
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/purl. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/purl/blob/main/CODE_OF_CONDUCT.md).
405
+ Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
406
+
407
+ 1. Fork it
408
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
409
+ 3. Make your changes
410
+ 4. Add tests for your changes
411
+ 5. Ensure all tests pass (`rake test`)
412
+ 6. Commit your changes (`git commit -am 'Add some feature'`)
413
+ 7. Push to the branch (`git push origin my-new-feature`)
414
+ 8. Create new Pull Request
415
+
416
+ ### Adding New Package Types
417
+
418
+ To add support for a new package type:
419
+
420
+ 1. Update `purl-types.json` with the new type configuration
421
+ 2. Add registry URL patterns if applicable
422
+ 3. Add type-specific validation rules if needed in `lib/purl/package_url.rb`
423
+ 4. Add tests for the new functionality
424
+
425
+ ## License
426
+
427
+ This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
428
+
429
+ ## Changelog
430
+
431
+ See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes and releases.
36
432
 
37
433
  ## Code of Conduct
38
434
 
39
- Everyone interacting in the Purl project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/purl/blob/main/CODE_OF_CONDUCT.md).
435
+ Everyone interacting in the Purl project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).