Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
version: 2
updates:
- package-ecosystem: gitsubmodule
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
cooldown:
default-days: 7

- package-ecosystem: gomod
directory: /
schedule:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ permissions: {}
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
go-version: ['1.26', '1.27']
Expand All @@ -19,6 +21,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: true

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
Expand All @@ -33,6 +36,8 @@ jobs:

lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "testdata/purl-spec"]
path = testdata/purl-spec
url = https://github.com/package-url/purl-spec.git
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,27 @@ fmt.Println(version) // 1.2.3

## Type Configuration

Type information comes from an embedded copy of [purl-types.json](https://github.com/andrew/purl/blob/main/purl-types.json) which includes default registries, namespace requirements, and URI templates for registry URL generation.
Type information is embedded in `types.json`. Type names and namespace requirements are checked against the pinned [purl-spec](https://github.com/package-url/purl-spec) submodule. Default registries and URL templates are maintained locally.

The local types `bioconductor`, `clojars`, `deno`, and `elm` are retained. The legacy `homebrew` type keeps its existing PURLs and registry behavior; use `brew` explicitly for the registered type. Recognition of a type does not imply full support for its normalization or validation rules.

```go
purl.KnownTypes() // []string of all known PURL types
purl.IsKnownType("npm") // true
purl.DefaultRegistry("npm") // https://registry.npmjs.org
```

## Development

Initialize the spec submodule before running tests:

```sh
git submodule update --init
go test ./...
```

Dependabot checks for spec updates weekly. The metadata test reports missing types, namespace differences, and unregistered types without a local allowance. Update `types.json` when those checks fail, preserving local registry settings.

## License

[MIT](LICENSE).
2 changes: 2 additions & 0 deletions defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func TestIsDefaultRegistry(t *testing.T) {

// Types with no default registry
{"apk", "https://example.com", false},
{"vcpkg", "https://github.com/azure-sdk/vcpkg", false},
{"julia", "https://github.com/HolyLab/HolyLabRegistry", false},
{"deb", "https://example.com", false},

// Unknown type
Expand Down
1 change: 1 addition & 0 deletions makepurl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func TestBuildPURLString(t *testing.T) {
{"golang normalization", "golang", "GitHub.com/Foo/Bar", "v1.0.0", "", "pkg:golang/github.com/foo/bar@v1.0.0"},
{"mlflow databricks normalization", "mlflow", "TrafficSigns", "1.0", "https://adb-123.4.azuredatabricks.net/api/2.0/mlflow", "pkg:mlflow/trafficsigns@1.0?repository_url=https:%2F%2Fadb-123.4.azuredatabricks.net%2Fapi%2F2.0%2Fmlflow"},
{"mlflow azureml preserves case", "mlflow", "TrafficSigns", "1.0", "https://westus2.api.azureml.ms/mlflow/v1.0", "pkg:mlflow/TrafficSigns@1.0?repository_url=https:%2F%2Fwestus2.api.azureml.ms%2Fmlflow%2Fv1.0"},
{"vcpkg github registry", "vcpkg", "zlib", "1.3.1", "https://github.com/azure-sdk/vcpkg", "pkg:vcpkg/zlib@1.3.1?repository_url=https:%2F%2Fgithub.com%2Fazure-sdk%2Fvcpkg"},
{"helm https repo", "helm", "postgresql", "12.1.15", "https://charts.bitnami.com/bitnami", "pkg:helm/postgresql@12.1.15"},
{"helm oci repo", "helm", "redis", "17.3.7", "oci://registry-1.docker.io/bitnamicharts", "pkg:helm/redis@17.3.7"},
{"helm local alias", "helm", "metrics-server", "3.12.2", "@internal", "pkg:helm/metrics-server@3.12.2"},
Expand Down
125 changes: 125 additions & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package purl_test

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/git-pkgs/purl"
)

func TestSpecTypeMetadata(t *testing.T) {
var types []string
readSpecJSON(t, "purl-types-index.json", &types)
if len(types) == 0 {
t.Fatal("spec type index is empty")
}
registered := make(map[string]bool, len(types))
for _, name := range types {
if registered[name] {
t.Errorf("duplicate spec type %q", name)
}
registered[name] = true
t.Run(name, func(t *testing.T) {
checkSpecTypeMetadata(t, name)
})
}

localTypes := map[string]string{
"bioconductor": "Bioconductor registry support",
"clojars": "Clojars registry support",
"deno": "Deno registry support",
"elm": "Elm registry support",
"homebrew": "Legacy type retained alongside brew for compatibility",
}
for _, name := range purl.KnownTypes() {
if !registered[name] && localTypes[name] == "" {
t.Errorf("unregistered type %q needs an explicit local type allowance", name)
}
}
for name := range localTypes {
if registered[name] || !purl.IsKnownType(name) {
t.Errorf("remove stale local type allowance for %q", name)
}
}
}

func checkSpecTypeMetadata(t *testing.T, name string) {
t.Helper()
var definition struct {
Type string `json:"type"`
Namespace struct {
Requirement string `json:"requirement"`
} `json:"namespace_definition"`
}
readSpecJSON(t, filepath.Join("types", name+"-definition.json"), &definition)
if definition.Type != name {
t.Fatalf("definition type = %q, want %q", definition.Type, name)
}
requirement := definition.Namespace.Requirement
switch requirement {
case "required", "optional", "prohibited":
default:
t.Fatalf("unknown namespace requirement %q", requirement)
}
cfg := purl.TypeInfo(name)
if cfg == nil {
t.Fatalf("spec type %q is missing from types.json", name)
}
if !purl.IsKnownType(name) || !purl.IsValidEcosystem(name) {
t.Errorf("spec type %q is not recognized", name)
}
if cfg.NamespaceRequirement != requirement {
t.Errorf("namespace requirement = %q, spec requires %q", cfg.NamespaceRequirement, requirement)
}
if cfg.NamespaceRequired() != (requirement == "required") || cfg.NamespaceProhibited() != (requirement == "prohibited") {
t.Errorf("namespace helpers disagree with spec requirement %q", requirement)
}
}

func readSpecJSON(t *testing.T, name string, value any) {
t.Helper()
data, err := os.ReadFile(filepath.Join("testdata", "purl-spec", name))
if err != nil {
t.Fatalf("read spec: %v; initialize it with git submodule update --init", err)
}
if err := json.Unmarshal(data, value); err != nil {
t.Fatalf("decode spec %s: %v", name, err)
}
}

func TestHomebrewCompatibility(t *testing.T) {
const registry = "https://formulae.brew.sh"
const packageURL = registry + "/formula/wget"
const want = "pkg:homebrew/wget@1.21.3"
for _, got := range []string{
purl.MakePURL("homebrew", "wget", "1.21.3").String(),
purl.MakePURLString("homebrew", "wget", "1.21.3"),
purl.BuildPURLString("homebrew", "wget", "1.21.3", registry),
} {
if got != want {
t.Errorf("homebrew builder = %q, want %q", got, want)
}
}
p, err := purl.Parse(want)
if err != nil {
t.Fatal(err)
}
if got, err := p.RegistryURL(); err != nil || got != packageURL {
t.Errorf("RegistryURL() = %q, %v", got, err)
}
p, err = purl.ParseRegistryURL(packageURL)
if err != nil {
t.Fatal(err)
}
if got := p.String(); got != "pkg:homebrew/wget" {
t.Errorf("ParseRegistryURL() = %q, want legacy homebrew type", got)
}
if !purl.IsKnownType("brew") || !purl.IsKnownType("homebrew") {
t.Error("both brew and homebrew must remain recognized")
}
if got := purl.MakePURLString("brew", "wget", "1.21.3"); got != "pkg:brew/wget@1.21.3" {
t.Errorf("explicit brew builder = %q", got)
}
}
1 change: 1 addition & 0 deletions testdata/purl-spec
Submodule purl-spec added at ec6d7e
Loading
Loading