From 2baa89c08af379abfb393c52c1b3940c3a41632a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 07:16:51 +0000 Subject: [PATCH] Add functional options pattern with WithHTTPClient option Adds an Option type and WithHTTPClient function to allow callers to inject a custom *http.Client for timeouts, proxies, or instrumentation (e.g. OpenTelemetry). Fully backward-compatible via variadic options. Fixes #18 https://claude.ai/code/session_01TQxWtmbL5KCKpHRb2GLQ5q --- client.go | 48 ++++++++++++++++++++++++++++++++++++------------ client_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index ca28c02..11a2970 100644 --- a/client.go +++ b/client.go @@ -39,6 +39,18 @@ var userAgent = fmt.Sprintf( var errAPIKey = errors.New("apiKey cannot be an empty string") +// Option is a functional option for configuring the Client. +type Option func(*Client) + +// WithHTTPClient sets the underlying *http.Client used by the Client. This +// allows callers to configure custom timeouts, proxies, transports, or +// instrument the client with tracing (e.g. OpenTelemetry). +func WithHTTPClient(httpClient *http.Client) Option { + return func(c *Client) { + c.c = httpClient + } +} + // Client is the struct to represent the functionality presented by the // https://ipdata.co API. type Client struct { @@ -47,34 +59,46 @@ type Client struct { k string // api key } -// NewClient takes an API key and returns a Client that uses the default -// endpoint (https://api.ipdata.co/). -func NewClient(apiKey string) (Client, error) { +// NewClient takes an API key and optional Options, and returns a Client that +// uses the default endpoint (https://api.ipdata.co/). +func NewClient(apiKey string, opts ...Option) (Client, error) { if len(apiKey) == 0 { return Client{}, errAPIKey } - return Client{ + c := Client{ c: newHTTPClient(), e: apiEndpoint, k: apiKey, - }, nil + } + + for _, opt := range opts { + opt(&c) + } + + return c, nil } -// NewEUClient takes an API key and returns a Client that uses the EU endpoint -// (https://eu-api.ipdata.co/). This ensures that all requests are routed -// through EU data centers only (Frankfurt, Paris, Ireland), which can be -// useful for GDPR compliance. -func NewEUClient(apiKey string) (Client, error) { +// NewEUClient takes an API key and optional Options, and returns a Client that +// uses the EU endpoint (https://eu-api.ipdata.co/). This ensures that all +// requests are routed through EU data centers only (Frankfurt, Paris, Ireland), +// which can be useful for GDPR compliance. +func NewEUClient(apiKey string, opts ...Option) (Client, error) { if len(apiKey) == 0 { return Client{}, errAPIKey } - return Client{ + c := Client{ c: newHTTPClient(), e: euAPIEndpoint, k: apiKey, - }, nil + } + + for _, opt := range opts { + opt(&c) + } + + return c, nil } type apiErr struct { diff --git a/client_test.go b/client_test.go index 882416e..3d4c72d 100644 --- a/client_test.go +++ b/client_test.go @@ -304,6 +304,40 @@ func TestNewEUClient(t *testing.T) { } } +func TestNewClient_WithHTTPClient(t *testing.T) { + custom := &http.Client{Timeout: 5 * time.Second} + + c, err := NewClient("testAPIkey", WithHTTPClient(custom)) + if err != nil { + t.Fatalf("NewClient() unexpected error: %v", err) + } + + if c.c != custom { + t.Fatal("expected custom http.Client to be used") + } + + if c.e != "https://api.ipdata.co/" { + t.Fatalf("c.e = %q, want %q", c.e, "https://api.ipdata.co/") + } +} + +func TestNewEUClient_WithHTTPClient(t *testing.T) { + custom := &http.Client{Timeout: 5 * time.Second} + + c, err := NewEUClient("testAPIkey", WithHTTPClient(custom)) + if err != nil { + t.Fatalf("NewEUClient() unexpected error: %v", err) + } + + if c.c != custom { + t.Fatal("expected custom http.Client to be used") + } + + if c.e != "https://eu-api.ipdata.co/" { + t.Fatalf("c.e = %q, want %q", c.e, "https://eu-api.ipdata.co/") + } +} + const tjFlagURL = "https://ipdata.co/flags/us.png" func Test_client_Lookup(t *testing.T) {