diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-07-29 16:45:00 -0230 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-07-29 16:45:00 -0230 |
| commit | e7bba51bb8bb41c462873c613bc6d4cf402a58d1 (patch) | |
| tree | a94f91656e52aaf5c8edb5aed02679bae8efab11 | |
| parent | 95acc996fdcc6221741a3072633c5753e88d0f07 (diff) | |
| download | lulu-e7bba51bb8bb41c462873c613bc6d4cf402a58d1.zip | |
sandbox and production client constructors
Previously this was a package-level configuration with Sandbox() and
Production() controlling the global ApiUrl variable. Now it's controlled
per-client.
| -rw-r--r-- | cmd/lulu/main.go | 6 | ||||
| -rw-r--r-- | cost_test.go | 2 | ||||
| -rw-r--r-- | cover_test.go | 8 | ||||
| -rw-r--r-- | interior_test.go | 10 | ||||
| -rw-r--r-- | job_test.go | 4 | ||||
| -rw-r--r-- | lulu.go | 93 | ||||
| -rw-r--r-- | lulu_test.go | 5 | ||||
| -rw-r--r-- | print_test.go | 6 |
8 files changed, 68 insertions, 66 deletions
diff --git a/cmd/lulu/main.go b/cmd/lulu/main.go index 0609279..45a5bef 100644 --- a/cmd/lulu/main.go +++ b/cmd/lulu/main.go @@ -24,7 +24,11 @@ func main() { creds, err := readCreds(cli.Sandbox) ctx.FatalIfErrorf(err) - clnt, err := lulu.NewClient(context.Background(), creds) + newClient := lulu.NewClient + if cli.Sandbox { + newClient = lulu.NewSandboxClient + } + clnt, err := newClient(context.Background(), creds) ctx.FatalIfErrorf(err) err = ctx.Run(cli.Globals, clnt) diff --git a/cost_test.go b/cost_test.go index a02c125..fe5da83 100644 --- a/cost_test.go +++ b/cost_test.go @@ -133,7 +133,7 @@ func TestUnmarshalCostResp(t *testing.T) { } func TestCost(t *testing.T) { - c := newClient(t) + c := tNewClient(t) items := costReqSample.Items addr := ShippingAddress{ City: "Lübeck", diff --git a/cover_test.go b/cover_test.go index fe40b69..4659f38 100644 --- a/cover_test.go +++ b/cover_test.go @@ -41,7 +41,7 @@ func TestUnmarshalCoverDimensions(t *testing.T) { } func TestCoverDimensions(t *testing.T) { - c := newClient(t) + c := tNewClient(t) mfg := PkgId{ UsTrade, Mono, @@ -101,7 +101,7 @@ func TestUnmarshalCoverValidation(t *testing.T) { } func TestStartCoverValidation(t *testing.T) { - c := newClient(t) + c := tNewClient(t) mfg := PkgId{ UsTrade, Mono, @@ -118,7 +118,7 @@ func TestStartCoverValidation(t *testing.T) { } func TestGetCoverValidation(t *testing.T) { - c := newClient(t) + c := tNewClient(t) mfg := PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil} id, err := c.StartCoverValidation(coverUrl, mfg, 210) require.NoError(t, err) @@ -135,7 +135,7 @@ func TestGetCoverValidation(t *testing.T) { } func TestValidateCover(t *testing.T) { - c := newClient(t) + c := tNewClient(t) ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() mfg := PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil} diff --git a/interior_test.go b/interior_test.go index f32d16e..eb87924 100644 --- a/interior_test.go +++ b/interior_test.go @@ -52,7 +52,7 @@ func TestUnmarshalInteriorValidation(t *testing.T) { } func TestStartInteriorValidation(t *testing.T) { - c := newClient(t) + c := tNewClient(t) mfg := PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil} id, err := c.StartInteriorValidation(interiorUrl, mfg) require.NoError(t, err) @@ -60,14 +60,14 @@ func TestStartInteriorValidation(t *testing.T) { } func TestStartInteriorValidationBasic(t *testing.T) { - c := newClient(t) + c := tNewClient(t) id, err := c.StartInteriorValidationBasic(interiorUrl) require.NoError(t, err) require.NotZero(t, id) } func TestGetInteriorValidation(t *testing.T) { - c := newClient(t) + c := tNewClient(t) mfg := PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil} id, err := c.StartInteriorValidation(interiorUrl, mfg) require.NoError(t, err) @@ -84,7 +84,7 @@ func TestGetInteriorValidation(t *testing.T) { } func TestValidateInterior(t *testing.T) { - c := newClient(t) + c := tNewClient(t) ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() mfg := PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil} @@ -94,7 +94,7 @@ func TestValidateInterior(t *testing.T) { } func TestValidateInteriorBasic(t *testing.T) { - c := newClient(t) + c := tNewClient(t) ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() val, err := c.ValidateInteriorBasic(ctx, interiorUrl) diff --git a/job_test.go b/job_test.go index 27b371d..cf4fc2c 100644 --- a/job_test.go +++ b/job_test.go @@ -29,7 +29,7 @@ func TestJob(t *testing.T) { shipOpt := Mail items := []Printable{printableSample} - c := newClient(t) + c := tNewClient(t) job1, err := c.Print(contact, jobEid, productionDelay, addr, shipOpt, items) require.NoError(t, err) require.NotZero(t, job1.Id) @@ -47,7 +47,7 @@ func TestJob(t *testing.T) { func TestJobs(t *testing.T) { // Create some jobs - c := newClient(t) + c := tNewClient(t) jobParams := []struct { contact string extid string @@ -15,11 +15,11 @@ import ( ) const ( - SandboxUrl = "https://api.sandbox.lulu.com/" - ProductionUrl = "https://api.lulu.com/" + sandbox = "https://api.sandbox.lulu.com/" + production = "https://api.lulu.com/" - ProductionApiKeyPage = "https://developers.lulu.com/user-profile/api-keys" - SandboxApiKeyPage = "https://developers.sandbox.lulu.com/user-profile/api-keys" + productionKeyPage = "https://developers.lulu.com/user-profile/api-keys" + sandboxKeyPage = "https://developers.sandbox.lulu.com/user-profile/api-keys" PollPeriod = time.Second @@ -31,31 +31,11 @@ const ( printJobsPath = "/print-jobs" ) -// ApiUrl is the location of the API server. It is set to the sandbox -// environment by default; change it to the production environment when -// you are ready to deploy. -var ApiUrl = SandboxUrl - -// Sandbox sets ApiUrl to SandboxApiUrl so that subsequent requests will -// be sent to the sandbox API server. -func Sandbox() { ApiUrl = SandboxUrl } - -// Production sets ApiUrl to ProductionApiUrl so that subsequent requests -// will be sent to the production API server. -func Production() { ApiUrl = ProductionUrl } - -// ApiKeyPage returns the URL of the page where you can generate a -// client-key and client-secret to use for authentication. -func ApiKeyPage() string { - if ApiUrl == SandboxUrl { - return SandboxApiKeyPage - } - return ProductionApiKeyPage -} - type Client struct { - ctx context.Context - c *http.Client + ctx context.Context + c *http.Client + apiUrl string + keyPageUrl string } // Credentials contains the client-key and client-secret used to @@ -64,10 +44,23 @@ type Credentials struct { Key, Secret string } -// NewClient returns a client that will use the given credentials to -// connect to the API server. +// NewClient creates a client that will use creds to connect to the +// production API server. +// +// ctx is used by the OAuth2 client. If you don't care about this, use +// [context.Background]. See [clientcredentials.Config.Client]. func NewClient(ctx context.Context, creds Credentials) (*Client, error) { - tokenUrl, err := url.JoinPath(ApiUrl, tokenPath) + return newClient(ctx, creds, production, productionKeyPage) +} + +// NewClient creates a client that will connect to the sandbox API +// server. See [NewClient]. +func NewSandboxClient(ctx context.Context, creds Credentials) (*Client, error) { + return newClient(ctx, creds, sandbox, sandboxKeyPage) +} + +func newClient(ctx context.Context, creds Credentials, apiUrl, keyPageUrl string) (*Client, error) { + tokenUrl, err := url.JoinPath(apiUrl, tokenPath) if err != nil { return nil, pkgErrf(err, "error creating client") } @@ -77,7 +70,7 @@ func NewClient(ctx context.Context, creds Credentials) (*Client, error) { ClientSecret: creds.Secret, TokenURL: tokenUrl, } - return &Client{ctx, cfg.Client(ctx)}, nil + return &Client{ctx, cfg.Client(ctx), apiUrl, keyPageUrl}, nil } // Context returns the client's context. @@ -85,8 +78,8 @@ func (c *Client) Context() context.Context { return c.ctx } // ValidateInterior starts a server-side validation job for the given // interior file and polls its status until it finishes or the context -// expires. See also: StartInteriorValidation() and -// GetInteriorValidation(). +// expires. See [Client.StartInteriorValidation], [Client.GetInteriorValidation], +// and [PollPeriod]. func (c *Client) ValidateInterior(ctx context.Context, srcUrl string, mfg PkgId) (InteriorValidation, error) { id, err := c.StartInteriorValidation(srcUrl, mfg) if err != nil { @@ -95,7 +88,7 @@ func (c *Client) ValidateInterior(ctx context.Context, srcUrl string, mfg PkgId) return c.pollInteriorValidation(ctx, id, InteriorStatusNormalized) } -// ValidateInteriorBasic is like ValidateInterior but without the +// ValidateInteriorBasic is like [Client.ValidateInterior] but without the // optional pod_package_id. func (c *Client) ValidateInteriorBasic(ctx context.Context, srcUrl string) (InteriorValidation, error) { id, err := c.StartInteriorValidationBasic(srcUrl) @@ -116,7 +109,7 @@ func (c *Client) pollInteriorValidation(ctx context.Context, id uint, wantStatus // StartInteriorValidation starts a server-side validation job for the // interior file located at srcUrl using manufacturing settings given by -// mfg. It returns the ID of the job. Use GetInteriorValidation() to poll +// mfg. It returns the ID of the job. Use [Client.GetInteriorValidation] to poll // the status of the job. // // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create @@ -128,7 +121,7 @@ func (c *Client) StartInteriorValidation(srcUrl string, mfg PkgId) (uint, error) return id, nil } -// StartInteriorValidationBasic is like StartInteriorValidation but +// StartInteriorValidationBasic is like [Client.StartInteriorValidation] but // without the optional pod_package_id. // // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create @@ -177,9 +170,10 @@ func (c *Client) CoverDimensions(mfg PkgId, npages uint, unit Unit) (CoverDimens return dims, nil } -// ValidateCover starts a server-side validation job for the given cover -// file and polls its status until it finishes or the context expires. -// See also: StartCoverValidation() and GetCoverValidation(). +// ValidateCover starts a server-side validation job for the given +// cover file and polls its status until it finishes or the context +// expires. See [Client.StartCoverValidation], +// [Client.GetCoverValidation], and [PollPeriod]. func (c *Client) ValidateCover(ctx context.Context, srcUrl string, mfg PkgId, npages uint) (CoverValidation, error) { id, err := c.StartCoverValidation(srcUrl, mfg, npages) if err != nil { @@ -191,10 +185,11 @@ func (c *Client) ValidateCover(ctx context.Context, srcUrl string, mfg PkgId, np }) } -// StartCoverValidation starts a server-side validation job for the cover -// file located at srcUrl, returning the job ID. mfg is the manufacturing -// settings of the book, and npages is the number of interior pages. Use -// GetCoverValidation() to poll the status of the job. +// StartCoverValidation starts a server-side validation job for the +// cover file located at srcUrl, returning the job ID. mfg is the +// manufacturing settings of the book, and npages is the number of +// interior pages. Use [Client.GetCoverValidation] to poll the status +// of the job. // // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Cover_create func (c *Client) StartCoverValidation(srcUrl string, mfg PkgId, npages uint) (uint, error) { @@ -384,7 +379,7 @@ func (c *Client) getDecode(path string, v any) error { // getQueryDecode sends a GET path?query request and unmarshals the response into v. func (c *Client) getQueryDecode(path string, query url.Values, v any) error { - url, err := url.JoinPath(ApiUrl, path) + url, err := c.url(path) if err != nil { return err } @@ -419,7 +414,7 @@ func (c *Client) post(path string, payload any) (*http.Response, error) { if err != nil { return nil, errEncReq{payload, path, err} } - url, err := url.JoinPath(ApiUrl, path) + url, err := c.url(path) if err != nil { return nil, err } @@ -435,7 +430,7 @@ func (c *Client) putDecode(path string, payload any, v any) error { if err != nil { return errEncReq{payload, path, err} } - url, err := url.JoinPath(ApiUrl, path) + url, err := c.url(path) if err != nil { return err } @@ -458,6 +453,10 @@ func (c *Client) putDecode(path string, payload any, v any) error { return decodeResponse(resp, v) } +func (c *Client) url(path string) (string, error) { + return url.JoinPath(c.apiUrl, path) +} + func decodeResponse(resp *http.Response, v any) error { body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/lulu_test.go b/lulu_test.go index 063d5aa..6791680 100644 --- a/lulu_test.go +++ b/lulu_test.go @@ -29,17 +29,16 @@ var ( func TestMain(m *testing.M) { flag.BoolVar(&Debug, "d", false, "Print debug info to stderr.") flag.Parse() - Sandbox() m.Run() } -func newClient(t *testing.T) *Client { +func tNewClient(t *testing.T) *Client { t.Helper() creds := Credentials{ Key: strings.TrimSpace(clientKey), Secret: strings.TrimSpace(clientSecret), } - c, err := NewClient(t.Context(), creds) + c, err := NewSandboxClient(t.Context(), creds) require.NoError(t, err) return c } diff --git a/print_test.go b/print_test.go index 23d54c8..9115c1a 100644 --- a/print_test.go +++ b/print_test.go @@ -171,7 +171,7 @@ func TestPrint(t *testing.T) { shipOpt := Mail item := printableSample - c := newClient(t) + c := tNewClient(t) startTime := time.Now() job, err := c.Print(contact, jobEid, productionDelay, addr, shipOpt, []Printable{item}) require.NoError(t, err) @@ -205,7 +205,7 @@ func TestReprint(t *testing.T) { printItem := printableSample // Create print job - c := newClient(t) + c := tNewClient(t) job1, err := c.Print(contact, "print", productionDelay, addr, shipOpt, []Printable{printItem}) require.NoError(t, err) id1 := job1.Id @@ -242,7 +242,7 @@ func TestReprint(t *testing.T) { func TestCancel(t *testing.T) { // Print - c := newClient(t) + c := tNewClient(t) contact := MustParseEmailAddress("test@test.com") jobEid := "cancel-test" productionDelay := 120 * time.Minute |