From e5f68df27973c3c8e8ae98e36ef796cccfefe1b4 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Mon, 8 Jun 2026 11:06:43 +0100 Subject: [PATCH] Add WithFrom, WithAPIKey, WithBatchSize options Thread ecosyste.ms identity and batch-size knobs through to the underlying ecosystems-go client. Identifying the client (WithFrom or WithAPIKey) moves it out of the shared rate-limit pool, which the upstream issue at git-pkgs/git-pkgs#231 traced to opaque HTTP/2 stream rejections. WithBatchSize lets callers stay below stricter server-side limits. The options are ignored when running in direct registries mode, since the registries client has no equivalent knobs. --- ecosystems.go | 17 ++++++++++++++--- enrichment_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ factory.go | 32 +++++++++++++++++++++++++++++++- go.mod | 4 ++-- go.sum | 14 ++++++++------ hybrid.go | 8 ++++---- 6 files changed, 105 insertions(+), 16 deletions(-) diff --git a/ecosystems.go b/ecosystems.go index 695e6bc..334bc14 100644 --- a/ecosystems.go +++ b/ecosystems.go @@ -16,11 +16,22 @@ type EcosystemsClient struct { // NewEcosystemsClient creates a client that uses the ecosyste.ms API. func NewEcosystemsClient() (*EcosystemsClient, error) { - return newEcosystemsClient(defaultUserAgent) + return newEcosystemsClient(options{userAgent: defaultUserAgent}) } -func newEcosystemsClient(userAgent string) (*EcosystemsClient, error) { - client, err := ecosystems.NewClient(userAgent) +func newEcosystemsClient(o options) (*EcosystemsClient, error) { + clientOpts := []ecosystems.Option{} + if o.from != "" { + clientOpts = append(clientOpts, ecosystems.WithFrom(o.from)) + } + if o.apiKey != "" { + clientOpts = append(clientOpts, ecosystems.WithAPIKey(o.apiKey)) + } + if o.batchSize > 0 { + clientOpts = append(clientOpts, ecosystems.WithBatchSize(o.batchSize)) + } + + client, err := ecosystems.NewClient(o.userAgent, clientOpts...) if err != nil { return nil, err } diff --git a/enrichment_test.go b/enrichment_test.go index 5cd775a..0a13a44 100644 --- a/enrichment_test.go +++ b/enrichment_test.go @@ -479,6 +479,52 @@ func TestNewClientWithUserAgent(t *testing.T) { } } +func TestBuildOptions(t *testing.T) { + o := buildOptions([]Option{ + WithUserAgent("ua"), + WithFrom("dev@example.com"), + WithAPIKey("secret"), + WithBatchSize(25), + }) + if o.userAgent != "ua" { + t.Errorf("userAgent = %q, want %q", o.userAgent, "ua") + } + if o.from != "dev@example.com" { + t.Errorf("from = %q, want %q", o.from, "dev@example.com") + } + if o.apiKey != "secret" { + t.Errorf("apiKey = %q, want %q", o.apiKey, "secret") + } + if o.batchSize != 25 { + t.Errorf("batchSize = %d, want %d", o.batchSize, 25) + } +} + +func TestBuildOptionsDefaults(t *testing.T) { + o := buildOptions(nil) + if o.userAgent != defaultUserAgent { + t.Errorf("userAgent = %q, want %q", o.userAgent, defaultUserAgent) + } + if o.from != "" || o.apiKey != "" || o.batchSize != 0 { + t.Errorf("expected zero values for from/apiKey/batchSize, got %+v", o) + } +} + +func TestNewEcosystemsClientWithAllOptions(t *testing.T) { + c, err := newEcosystemsClient(options{ + userAgent: "git-pkgs/test", + from: "dev@example.com", + apiKey: "secret", + batchSize: 25, + }) + if err != nil { + t.Fatalf("newEcosystemsClient: %v", err) + } + if c == nil || c.client == nil { + t.Fatal("expected non-nil client") + } +} + func TestDepsDevBulkLookup(t *testing.T) { callCount := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/factory.go b/factory.go index 489c0bb..985b64c 100644 --- a/factory.go +++ b/factory.go @@ -13,6 +13,9 @@ type Option func(*options) type options struct { userAgent string + from string + apiKey string + batchSize int } // WithUserAgent sets the User-Agent header for API requests. @@ -22,6 +25,33 @@ func WithUserAgent(ua string) Option { } } +// WithFrom sets the From header (email address) for ecosyste.ms API +// requests. Identifying the client moves it out of the shared +// rate-limit pool, which reduces stream-level rejections. +// Ignored by the direct registries client. +func WithFrom(email string) Option { + return func(o *options) { + o.from = email + } +} + +// WithAPIKey sets the bearer token sent on ecosyste.ms API requests. +// Ignored by the direct registries client. +func WithAPIKey(key string) Option { + return func(o *options) { + o.apiKey = key + } +} + +// WithBatchSize sets the per-request batch size for ecosyste.ms bulk +// lookups. Values <= 0 or above the upstream maximum fall back to the +// upstream default. Ignored by the direct registries client. +func WithBatchSize(size int) Option { + return func(o *options) { + o.batchSize = size + } +} + func buildOptions(opts []Option) options { o := options{userAgent: defaultUserAgent} for _, opt := range opts { @@ -44,7 +74,7 @@ func NewClient(opts ...Option) (Client, error) { //nolint:ireturn // returns dif if directMode() { return newRegistriesClient(o.userAgent), nil } - return newHybridClient(o.userAgent) + return newHybridClient(o) } // directMode checks if direct registry mode is enabled. diff --git a/go.mod b/go.mod index 7b848f2..33b6df2 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/git-pkgs/enrichment go 1.25.6 require ( - github.com/ecosyste-ms/ecosystems-go v0.1.1 + github.com/ecosyste-ms/ecosystems-go v0.2.0 github.com/git-pkgs/purl v0.1.12 github.com/git-pkgs/registries v0.6.1 github.com/git-pkgs/vers v0.2.6 @@ -16,6 +16,6 @@ require ( github.com/git-pkgs/spdx v0.1.4 // 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.1.2 // indirect + github.com/oapi-codegen/runtime v1.4.1 // indirect github.com/package-url/packageurl-go v0.1.6 // indirect ) diff --git a/go.sum b/go.sum index 9a85af4..9832cb1 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.1.1 h1:YYiBK9TCCTeE+BtmpN2FssaRFcmF+T0v4LrupIOjehQ= -github.com/ecosyste-ms/ecosystems-go v0.1.1/go.mod h1:VczXs1CO9nL8XbL1NwvgmwIaqzMsAxcsXnTpRtwi9gU= +github.com/ecosyste-ms/ecosystems-go v0.2.0 h1:Nhpg54C+St8Sd/mf8bNJmQqx35ZgHw33TfFoxaXMQI8= +github.com/ecosyste-ms/ecosystems-go v0.2.0/go.mod h1:CCdzT1iAZirbEZAbFSnWpK88eKKaIWex7gjtZ0UudXA= 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.4 h1:C6st+XSbF75eKuwfdkDZZtYHoTcaWRIEQYar5VtszUo= @@ -24,8 +24,10 @@ github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWo github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= -github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +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.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo= +github.com/oapi-codegen/runtime v1.4.1/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -33,7 +35,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hybrid.go b/hybrid.go index 1b15674..f0dadc5 100644 --- a/hybrid.go +++ b/hybrid.go @@ -15,17 +15,17 @@ type HybridClient struct { // NewHybridClient creates a client that routes based on PURL qualifiers. func NewHybridClient() (*HybridClient, error) { - return newHybridClient(defaultUserAgent) + return newHybridClient(options{userAgent: defaultUserAgent}) } -func newHybridClient(userAgent string) (*HybridClient, error) { - eco, err := newEcosystemsClient(userAgent) +func newHybridClient(o options) (*HybridClient, error) { + eco, err := newEcosystemsClient(o) if err != nil { return nil, err } return &HybridClient{ ecosystems: eco, - registries: newRegistriesClient(userAgent), + registries: newRegistriesClient(o.userAgent), }, nil }