diff --git a/ecosystems.go b/ecosystems.go index 4059317..4de94d0 100644 --- a/ecosystems.go +++ b/ecosystems.go @@ -9,8 +9,16 @@ import ( "github.com/ecosyste-ms/ecosystems-go" "github.com/ecosyste-ms/ecosystems-go/packages" "github.com/git-pkgs/registries" + "github.com/oapi-codegen/nullable" ) +// nullableValue returns the underlying value of n, or the zero value of T if n +// is null or unspecified. +func nullableValue[T any](n nullable.Nullable[T]) T { + v, _ := n.Get() + return v +} + // EcosystemsClient wraps the ecosyste.ms API client. type EcosystemsClient struct { client *ecosystems.Client @@ -58,30 +66,20 @@ func (c *EcosystemsClient) BulkLookup(ctx context.Context, purls []string) (map[ RegistryURL: registries.DefaultURL(pkg.Ecosystem), Source: "ecosystems", } - if pkg.LatestReleaseNumber != nil { - info.LatestVersion = *pkg.LatestReleaseNumber - } + info.LatestVersion = nullableValue(pkg.LatestReleaseNumber) if len(pkg.NormalizedLicenses) > 0 { info.License = pkg.NormalizedLicenses[0] - } else if pkg.Licenses != nil && *pkg.Licenses != "" { - info.License = *pkg.Licenses - } - if pkg.Description != nil { - info.Description = *pkg.Description + } else if licenses := nullableValue(pkg.Licenses); licenses != "" { + info.License = licenses } - if pkg.Homepage != nil { - info.Homepage = *pkg.Homepage - } - if pkg.RepositoryUrl != nil { - info.Repository = *pkg.RepositoryUrl - } - info.ChangelogFilename = extractChangelogFilename(pkg.RepoMetadata) + info.Description = nullableValue(pkg.Description) + info.Homepage = nullableValue(pkg.Homepage) + info.Repository = nullableValue(pkg.RepositoryURL) + info.ChangelogFilename = extractChangelogFilename(nullableValue(pkg.RepoMetadata)) // Popularity and usage info.Downloads = pkg.Downloads - if pkg.DownloadsPeriod != nil { - info.DownloadsPeriod = *pkg.DownloadsPeriod - } + info.DownloadsPeriod = nullableValue(pkg.DownloadsPeriod) info.DependentPackagesCount = pkg.DependentPackagesCount info.DependentReposCount = pkg.DependentReposCount @@ -96,12 +94,8 @@ func (c *EcosystemsClient) BulkLookup(ctx context.Context, purls []string) (map[ // extractChangelogFilename digs into the ecosyste.ms RepoMetadata to find the // changelog filename at metadata.files.changelog. -func extractChangelogFilename(repoMetadata *map[string]interface{}) string { - if repoMetadata == nil { - return "" - } - meta := *repoMetadata - metadataRaw, ok := meta["metadata"] +func extractChangelogFilename(repoMetadata map[string]interface{}) string { + metadataRaw, ok := repoMetadata["metadata"] if !ok { return "" } @@ -176,26 +170,20 @@ func (c *EcosystemsClient) GetVersion(ctx context.Context, purlStr string) (*Ver func versionInfoFromEcosystems( number string, - publishedAt, integrity, license, status *string, - metadata *map[string]any, + publishedAt, integrity, license, status nullable.Nullable[string], + metadata nullable.Nullable[map[string]any], ) VersionInfo { info := VersionInfo{Number: number} - if publishedAt != nil { - info.PublishedAt, _ = time.Parse(time.RFC3339, *publishedAt) - } - if integrity != nil { - info.Integrity = *integrity + if pub := nullableValue(publishedAt); pub != "" { + info.PublishedAt, _ = time.Parse(time.RFC3339, pub) } - if license != nil { - info.License = *license - } - if status != nil { - info.Status = *status + info.Integrity = nullableValue(integrity) + info.License = nullableValue(license) + if s, err := status.Get(); err == nil { + info.Status = s info.Yanked = info.Status == string(registries.StatusYanked) } - if metadata != nil { - info.Metadata = *metadata - } + info.Metadata = nullableValue(metadata) return info } @@ -235,7 +223,7 @@ func (c *EcosystemsClient) GetDependentsByRepositoryURL(ctx context.Context, rep result = append(result, RepositoryDependents{ PackageName: pkg.Name, Ecosystem: pkg.Ecosystem, - PURL: pkg.Purl, + PURL: pkg.PURL, Dependents: out, }) } @@ -246,32 +234,25 @@ func convertDependentPackage(pkg packages.Package) DependentPackage { out := DependentPackage{ Ecosystem: pkg.Ecosystem, Name: pkg.Name, - PURL: pkg.Purl, + PURL: pkg.PURL, Downloads: pkg.Downloads, DependentReposCount: pkg.DependentReposCount, } - if pkg.RepositoryUrl != nil { - out.Repository = *pkg.RepositoryUrl - } + out.Repository = nullableValue(pkg.RepositoryURL) if out.Repository == "" { - out.Repository = extractRepoHTMLURL(pkg.RepoMetadata) + out.Repository = extractRepoHTMLURL(nullableValue(pkg.RepoMetadata)) } - if pkg.RegistryUrl != nil { - out.RegistryURL = *pkg.RegistryUrl + if registryURL, err := pkg.RegistryURL.Get(); err == nil { + out.RegistryURL = registryURL } else { out.RegistryURL = registries.DefaultURL(pkg.Ecosystem) } - if pkg.LatestReleaseNumber != nil { - out.LatestVersion = *pkg.LatestReleaseNumber - } + out.LatestVersion = nullableValue(pkg.LatestReleaseNumber) return out } -func extractRepoHTMLURL(repoMetadata *map[string]interface{}) string { - if repoMetadata == nil { - return "" - } - if htmlURL, ok := (*repoMetadata)["html_url"].(string); ok { +func extractRepoHTMLURL(repoMetadata map[string]interface{}) string { + if htmlURL, ok := repoMetadata["html_url"].(string); ok { return htmlURL } return "" @@ -283,23 +264,13 @@ func convertMaintainers(maintainers []packages.Maintainer) []Maintainer { } result := make([]Maintainer, 0, len(maintainers)) for _, m := range maintainers { - out := Maintainer{} - if m.Login != nil { - out.Login = *m.Login - } - if m.Name != nil { - out.Name = *m.Name - } - if m.Email != nil { - out.Email = *m.Email - } - if m.HtmlUrl != nil { - out.URL = *m.HtmlUrl - } - if m.Role != nil { - out.Role = *m.Role - } - result = append(result, out) + result = append(result, Maintainer{ + Login: nullableValue(m.Login), + Name: nullableValue(m.Name), + Email: nullableValue(m.Email), + URL: nullableValue(m.HTMLURL), + Role: nullableValue(m.Role), + }) } return result } @@ -311,19 +282,15 @@ func convertAdvisories(advisories []packages.Advisory) []Advisory { result := make([]Advisory, 0, len(advisories)) for _, adv := range advisories { a := Advisory{ - Identifiers: adv.Identifiers, - } - if adv.Title != nil { - a.Title = *adv.Title + Title: nullableValue(adv.Title), + Severity: nullableValue(adv.Severity), + CVSSScore: nullableValue(adv.CVSSScore), + URL: nullableValue(adv.URL), } - if adv.Severity != nil { - a.Severity = *adv.Severity - } - if adv.CvssScore != nil { - a.CVSSScore = *adv.CvssScore - } - if adv.Url != nil { - a.URL = *adv.Url + for _, id := range adv.Identifiers { + if v := nullableValue(id); v != "" { + a.Identifiers = append(a.Identifiers, v) + } } result = append(result, a) } diff --git a/enrichment_test.go b/enrichment_test.go index 47f1603..9864a7a 100644 --- a/enrichment_test.go +++ b/enrichment_test.go @@ -10,6 +10,7 @@ import ( "github.com/ecosyste-ms/ecosystems-go" "github.com/ecosyste-ms/ecosystems-go/packages" + "github.com/oapi-codegen/nullable" ) const testVersionLodash = "4.17.21" @@ -46,7 +47,7 @@ func TestExtractChangelogFilename(t *testing.T) { }, }, } - got := extractChangelogFilename(&meta) + got := extractChangelogFilename(meta) if got != "CHANGELOG.md" { t.Errorf("got %q, want %q", got, "CHANGELOG.md") } @@ -61,7 +62,7 @@ func TestExtractChangelogFilename(t *testing.T) { t.Run("missing metadata key", func(t *testing.T) { meta := map[string]interface{}{"other": "value"} - got := extractChangelogFilename(&meta) + got := extractChangelogFilename(meta) if got != "" { t.Errorf("got %q, want empty", got) } @@ -71,7 +72,7 @@ func TestExtractChangelogFilename(t *testing.T) { meta := map[string]interface{}{ "metadata": map[string]interface{}{"other": "value"}, } - got := extractChangelogFilename(&meta) + got := extractChangelogFilename(meta) if got != "" { t.Errorf("got %q, want empty", got) } @@ -79,7 +80,7 @@ func TestExtractChangelogFilename(t *testing.T) { t.Run("wrong type for metadata", func(t *testing.T) { meta := map[string]interface{}{"metadata": "not a map"} - got := extractChangelogFilename(&meta) + got := extractChangelogFilename(meta) if got != "" { t.Errorf("got %q, want empty", got) } @@ -93,7 +94,7 @@ func TestExtractChangelogFilename(t *testing.T) { }, }, } - got := extractChangelogFilename(&meta) + got := extractChangelogFilename(meta) if got != "" { t.Errorf("got %q, want empty", got) } @@ -132,16 +133,16 @@ func TestAdvisoryMapping(t *testing.T) { } func TestConvertMaintainers(t *testing.T) { - login := "alice" - name := "Alice Example" - email := "alice@example.com" - htmlURL := "https://www.npmjs.com/~alice" - role := "owner" + login := nullable.NewNullableWithValue("alice") + name := nullable.NewNullableWithValue("Alice Example") + email := nullable.NewNullableWithValue("alice@example.com") + htmlURL := nullable.NewNullableWithValue("https://www.npmjs.com/~alice") + role := nullable.NewNullableWithValue("owner") t.Run("populated", func(t *testing.T) { got := convertMaintainers([]packages.Maintainer{ - {Login: &login, Name: &name, Email: &email, HtmlUrl: &htmlURL, Role: &role}, - {Login: &login}, + {Login: login, Name: name, Email: email, HTMLURL: htmlURL, Role: role}, + {Login: login}, }) want := []Maintainer{ {Login: "alice", Name: "Alice Example", Email: "alice@example.com", URL: "https://www.npmjs.com/~alice", Role: "owner"}, @@ -471,14 +472,14 @@ func TestEcosystemsClientGetVersionIncludesMetadata(t *testing.T) { } func TestVersionInfoFromEcosystemsIncludesYankedStatus(t *testing.T) { - status := "yanked" - integrity := "sha256-example" - license := "MIT" - metadata := map[string]any{"reason": "bad release"} + status := nullable.NewNullableWithValue("yanked") + integrity := nullable.NewNullableWithValue("sha256-example") + license := nullable.NewNullableWithValue("MIT") + metadata := nullable.NewNullableWithValue(map[string]any{"reason": "bad release"}) - got := versionInfoFromEcosystems("1.2.3", nil, &integrity, &license, &status, &metadata) + got := versionInfoFromEcosystems("1.2.3", nil, integrity, license, status, metadata) - if got.Number != "1.2.3" || got.Integrity != integrity || got.License != license { + if got.Number != "1.2.3" || got.Integrity != "sha256-example" || got.License != "MIT" { t.Fatalf("version metadata = %+v", got) } if got.Status != "yanked" || !got.Yanked { diff --git a/go.mod b/go.mod index d6fa7da..a9f7b5c 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/git-pkgs/enrichment go 1.25.6 require ( - github.com/ecosyste-ms/ecosystems-go v0.3.0 + github.com/ecosyste-ms/ecosystems-go v0.4.0 github.com/git-pkgs/purl v0.1.15 github.com/git-pkgs/registries v0.6.3 github.com/git-pkgs/spdx v0.1.4 github.com/git-pkgs/vers v0.3.0 github.com/git-pkgs/vulns v0.2.1 + github.com/oapi-codegen/nullable v1.1.0 ) require ( @@ -17,7 +18,7 @@ require ( github.com/git-pkgs/pom v0.1.5 // indirect github.com/github/go-spdx/v2 v2.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/oapi-codegen/runtime v1.4.2 // indirect + github.com/oapi-codegen/runtime v1.6.0 // indirect github.com/package-url/packageurl-go v0.1.6 // indirect github.com/pandatix/go-cvss v0.6.2 // indirect ) diff --git a/go.sum b/go.sum index 1196341..bbe7938 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ecosyste-ms/ecosystems-go v0.3.0 h1:eVTNKQ3PyVNkSAnk1934958Vg4rR7ho5B5MWEQaMLi4= -github.com/ecosyste-ms/ecosystems-go v0.3.0/go.mod h1:bkjiI8WoTFB1Jw0M3h8yn3KXCD/+LdETsQ3m2BNLMHQ= +github.com/ecosyste-ms/ecosystems-go v0.4.0 h1:5A+zF+XWT8sYYYjlc2/tI1SmiDGzbHLyT9CapVc5dGA= +github.com/ecosyste-ms/ecosystems-go v0.4.0/go.mod h1:FVswCrp3DQkur1HjVqfDF/gYrDSEmiFflntcB1G0DbA= github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M= github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4= github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY= @@ -28,8 +28,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= -github.com/oapi-codegen/runtime v1.4.2 h1:GMxFVYLzoYLua+/KvzgSphkyK1lLTReQI9Vf4hvATKE= -github.com/oapi-codegen/runtime v1.4.2/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= +github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/package-url/packageurl-go v0.1.6 h1:YO3p6u1XmCUliivUg/qWphaY8vI6hxSnnPv7Bfg3m5M= github.com/package-url/packageurl-go v0.1.6/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI=