diff options
| -rw-r--r-- | cmd/lulu/cancel.go | 2 | ||||
| -rw-r--r-- | cmd/lulu/cd.go | 2 | ||||
| -rw-r--r-- | cmd/lulu/cost.go | 2 | ||||
| -rw-r--r-- | cmd/lulu/job.go | 2 | ||||
| -rw-r--r-- | cmd/lulu/jobs.go | 2 | ||||
| -rw-r--r-- | cmd/lulu/main.go | 4 | ||||
| -rw-r--r-- | cmd/lulu/print.go | 2 | ||||
| -rw-r--r-- | cmd/lulu/vc.go | 6 | ||||
| -rw-r--r-- | cmd/lulu/vi.go | 12 | ||||
| -rw-r--r-- | cover_test.go | 2 | ||||
| -rw-r--r-- | interior_test.go | 4 | ||||
| -rw-r--r-- | lulu.go | 411 | ||||
| -rw-r--r-- | lulu_test.go | 2 |
13 files changed, 241 insertions, 212 deletions
diff --git a/cmd/lulu/cancel.go b/cmd/lulu/cancel.go index 4e5d3b1..38b95b7 100644 --- a/cmd/lulu/cancel.go +++ b/cmd/lulu/cancel.go @@ -6,6 +6,6 @@ type CancelCmd struct { Id uint64 `arg help:"ID of the job to cancel"` } -func (cmd CancelCmd) Run(clnt *lulu.Client) error { +func (cmd CancelCmd) Run(clnt lulu.Client) error { return clnt.Cancel(cmd.Id) } diff --git a/cmd/lulu/cd.go b/cmd/lulu/cd.go index 8af670e..3d3a57f 100644 --- a/cmd/lulu/cd.go +++ b/cmd/lulu/cd.go @@ -12,7 +12,7 @@ type CoverDimensionsCmd struct { Unit lulu.Unit `short:"u" default:"pt" help:"${unit_help}"` } -func (cmd CoverDimensionsCmd) Run(clnt *lulu.Client) error { +func (cmd CoverDimensionsCmd) Run(clnt lulu.Client) error { dims, err := clnt.CoverDimensions(cmd.Mfg, cmd.NPages, cmd.Unit) if err != nil { return err diff --git a/cmd/lulu/cost.go b/cmd/lulu/cost.go index 1ef655d..575ea38 100644 --- a/cmd/lulu/cost.go +++ b/cmd/lulu/cost.go @@ -19,7 +19,7 @@ type CostCmd struct { Items []CostLineItem `required help:"${cost_item_help}"` } -func (cmd *CostCmd) Run(cli *kong.Kong, clnt *lulu.Client) error { +func (cmd *CostCmd) Run(cli *kong.Kong, clnt lulu.Client) error { items := make([]lulu.CostLineItem, len(cmd.Items)) for i := range cmd.Items { items[i] = cmd.Items[i].CostLineItem diff --git a/cmd/lulu/job.go b/cmd/lulu/job.go index b12fbe7..a4c7fa9 100644 --- a/cmd/lulu/job.go +++ b/cmd/lulu/job.go @@ -13,7 +13,7 @@ type JobCmd struct { Id uint64 `arg help:"ID of the job to retrieve"` } -func (cmd JobCmd) Run(clnt *lulu.Client) error { +func (cmd JobCmd) Run(clnt lulu.Client) error { job, err := clnt.Job(cmd.Id) if err != nil { return err diff --git a/cmd/lulu/jobs.go b/cmd/lulu/jobs.go index 98d656b..eb23762 100644 --- a/cmd/lulu/jobs.go +++ b/cmd/lulu/jobs.go @@ -18,7 +18,7 @@ type JobsCmd struct { Status lulu.OrderStatus `help:"${order_status_help}"` } -func (cmd JobsCmd) Run(clnt *lulu.Client) error { +func (cmd JobsCmd) Run(clnt lulu.Client) error { jobs, err := clnt.Jobs(cmd.queries()...) if err != nil { return err diff --git a/cmd/lulu/main.go b/cmd/lulu/main.go index 45a5bef..bd17b77 100644 --- a/cmd/lulu/main.go +++ b/cmd/lulu/main.go @@ -19,7 +19,6 @@ func main() { NoExpandSubcommands: true, }), ) - lulu.Debug = cli.Debug creds, err := readCreds(cli.Sandbox) @@ -30,7 +29,8 @@ func main() { } clnt, err := newClient(context.Background(), creds) ctx.FatalIfErrorf(err) + ctx.BindTo(clnt, (*lulu.Client)(nil)) // use BindTo to make kong work with interfaces - err = ctx.Run(cli.Globals, clnt) + err = ctx.Run(cli.Globals) ctx.FatalIfErrorf(err) } diff --git a/cmd/lulu/print.go b/cmd/lulu/print.go index cbd536e..23d3752 100644 --- a/cmd/lulu/print.go +++ b/cmd/lulu/print.go @@ -19,7 +19,7 @@ type PrintCmd struct { Items []Printable `required placeholder:"PRINTABLE" help:"${printable_help}"` } -func (cmd PrintCmd) Run(clnt *lulu.Client) error { +func (cmd PrintCmd) Run(clnt lulu.Client) error { items := make([]lulu.Printable, len(cmd.Items)) for i := range cmd.Items { items[i] = lulu.Printable(cmd.Items[i]) diff --git a/cmd/lulu/vc.go b/cmd/lulu/vc.go index 5a2a84f..78d9670 100644 --- a/cmd/lulu/vc.go +++ b/cmd/lulu/vc.go @@ -18,10 +18,10 @@ type ValidateCoverCmd struct { Timeout time.Duration `short:"t" default:"${default_timeout}"` } -func (cmd ValidateCoverCmd) Run(cli *kong.Kong, clnt *lulu.Client) error { - ctx, cancel := context.WithTimeout(clnt.Context(), cmd.Timeout) +func (cmd ValidateCoverCmd) Run(cli *kong.Kong, clnt lulu.Client) error { + ctx, cancel := context.WithTimeout(context.Background(), cmd.Timeout) defer cancel() - val, err := clnt.ValidateCover(ctx, cmd.Url.String(), cmd.Mfg, cmd.NPages) + val, err := lulu.ValidateCover(ctx, clnt, cmd.Url.String(), cmd.Mfg, cmd.NPages) if err != nil { return err } diff --git a/cmd/lulu/vi.go b/cmd/lulu/vi.go index 27e26ab..38f9d35 100644 --- a/cmd/lulu/vi.go +++ b/cmd/lulu/vi.go @@ -26,9 +26,9 @@ type ValidateInteriorBasicCmd struct { Url *url.URL `arg help:"Location of interior file"` } -func (cmd ValidateInteriorBasicCmd) Run(cli *kong.Kong, clnt *lulu.Client, timeout time.Duration) error { +func (cmd ValidateInteriorBasicCmd) Run(cli *kong.Kong, clnt lulu.Client, timeout time.Duration) error { return validateInterior(cli, clnt, timeout, func(ctx context.Context) (lulu.InteriorValidation, error) { - return clnt.ValidateInteriorBasic(ctx, cmd.Url.String()) + return lulu.ValidateInteriorBasic(ctx, clnt, cmd.Url.String()) }, lulu.InteriorStatusValidated) } @@ -37,14 +37,14 @@ type ValidateInteriorFullCmd struct { Mfg lulu.PkgId `arg help:"${mfg_help}"` } -func (cmd ValidateInteriorFullCmd) Run(cli *kong.Kong, clnt *lulu.Client, timeout time.Duration) error { +func (cmd ValidateInteriorFullCmd) Run(cli *kong.Kong, clnt lulu.Client, timeout time.Duration) error { return validateInterior(cli, clnt, timeout, func(ctx context.Context) (lulu.InteriorValidation, error) { - return clnt.ValidateInterior(ctx, cmd.Url.String(), cmd.Mfg) + return lulu.ValidateInterior(ctx, clnt, cmd.Url.String(), cmd.Mfg) }, lulu.InteriorStatusNormalized) } -func validateInterior(cli *kong.Kong, clnt *lulu.Client, timeout time.Duration, validate func(ctx context.Context) (lulu.InteriorValidation, error), wantStatus lulu.InteriorValidationStatus) error { - ctx, cancel := context.WithTimeout(clnt.Context(), timeout) +func validateInterior(cli *kong.Kong, clnt lulu.Client, timeout time.Duration, validate func(ctx context.Context) (lulu.InteriorValidation, error), wantStatus lulu.InteriorValidationStatus) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() val, err := validate(ctx) if err != nil { diff --git a/cover_test.go b/cover_test.go index 4659f38..7e4c64f 100644 --- a/cover_test.go +++ b/cover_test.go @@ -139,7 +139,7 @@ func TestValidateCover(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() mfg := PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil} - val, err := c.ValidateCover(ctx, coverUrl, mfg, 210) + val, err := ValidateCover(ctx, c, coverUrl, mfg, 210) require.NoError(t, err) validateCoverValidation(t, val) } diff --git a/interior_test.go b/interior_test.go index eb87924..faa0986 100644 --- a/interior_test.go +++ b/interior_test.go @@ -88,7 +88,7 @@ func TestValidateInterior(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() mfg := PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil} - val, err := c.ValidateInterior(ctx, interiorUrl, mfg) + val, err := ValidateInterior(ctx, c, interiorUrl, mfg) require.NoError(t, err) validateInteriorValidation(t, val, InteriorStatusNormalized) } @@ -97,7 +97,7 @@ func TestValidateInteriorBasic(t *testing.T) { c := tNewClient(t) ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() - val, err := c.ValidateInteriorBasic(ctx, interiorUrl) + val, err := ValidateInteriorBasic(ctx, c, interiorUrl) require.NoError(t, err) validateInteriorValidation(t, val, InteriorStatusValidated) } @@ -31,7 +31,159 @@ const ( printJobsPath = "/print-jobs" ) -type Client struct { +// A Client connects to the Lulu API server. +type Client interface { + // 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 + // [Client.GetInteriorValidation] to poll the status of the job. + // + // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create + StartInteriorValidation(srcUrl string, mfg PkgId) (uint, error) + + // StartInteriorValidationBasic is like + // [Client.StartInteriorValidation] but without the optional + // pod_package_id. + // + // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create + StartInteriorValidationBasic(srcUrl string) (uint, error) + + // GetInteriorValidation retrieves information about an interior file + // validation job that was started by ValidateInterior(). + // + // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_read + GetInteriorValidation(id uint) (InteriorValidation, error) + + // CoverDimensions calculates the required dimensions of the cover for + // a book with the given manufacturing settings and number of pages. + // The returned dimensions are given in the specified units of + // measurement. + // + // https://api.lulu.com/docs/#tag/Files-validation/operation/Cover-Dimensions_create + CoverDimensions(mfg PkgId, npages uint, unit Unit) (CoverDimensions, error) + + // 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 + StartCoverValidation(srcUrl string, mfg PkgId, npages uint) (uint, error) + + // GetCoverValidiation retrieves information about a cover file + // validation job that was started by ValidateCover(). + // + // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Cover_read + GetCoverValidation(id uint) (CoverValidation, error) + + // Cost calculates the cost of a hypothetical print order without + // actually creating a print job. + // + // https://api.lulu.com/docs/#tag/Print-Job-Cost-Calculations/operation/Print-Job-cost-calculations_create + Cost(items []CostLineItem, addr ShippingAddress, shipOpt ShippingLevel) (Cost, ShippingAddressValidation, error) + + // Print creates a new print job. + // + // contact: Email address that should be contacted if + // questions regarding the Print-Job arise. Lulu recommends to + // use the email of a person who is responsible for placing + // the Print-Job like a developer or business owner. + // + // externalId: Arbitrary string to identify and connect a + // print job to your systems. Set it to an order number, a + // purchase order or whatever else works for your particular + // use case. + // + // productionDelay: Delay before a newly created Print-Job is + // sent to production. Must be + // [[MinProductionDelay],[MaxProductionDelay]]. This gives + // some time for the order to be cancelled before the books + // are printed. Once production has started, orders cannot be + // canceled anymore. + // + // addr: The postal address of the customer that the order + // will be sent to. + // + // shipOpt: Shipping method. + // + // items: List of books to print. + // + // https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_create + Print(contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []Printable) (PrintJob, error) + + // Reprint creates a print job, printing books whose PrintableIds are + // known from (one or more) prior calls to [Client.Print]. + // + // https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_reprint + Reprint(contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []Reprintable) (PrintJob, error) + + // Job retrieves the print job with the given ID, which is a + // [PrintJob.Id] returned by [Client.Print] or [Client.Reprint]. + // + // https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_read + Job(id uint64) (PrintJob, error) + + // Jobs retrieves a list of all print jobs that have been created, + // filtered by a set of optional query parameters. + // + // https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_list + Jobs(queries ...PrintJobQuery) ([]PrintJob, error) + + // Cancel cancels the print job with the given ID. jobId is from a + // [PrintJob] returned by [Client.Print] or [Client.Reprint]. + // + // https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_status_cancel + Cancel(jobId uint64) error +} + +// ValidateInterior starts a server-side validation job for the given +// interior file and polls its status until it finishes or the context +// expires. See [Client.StartInteriorValidation], [Client.GetInteriorValidation], +// and [PollPeriod]. +func ValidateInterior(ctx context.Context, c Client, srcUrl string, mfg PkgId) (InteriorValidation, error) { + id, err := c.StartInteriorValidation(srcUrl, mfg) + if err != nil { + return InteriorValidation{}, err + } + return pollInteriorValidation(ctx, c, id, InteriorStatusNormalized) +} + +// ValidateInteriorBasic is like [Client.ValidateInterior] but without the +// optional pod_package_id. +func ValidateInteriorBasic(ctx context.Context, c Client, srcUrl string) (InteriorValidation, error) { + id, err := c.StartInteriorValidationBasic(srcUrl) + if err != nil { + return InteriorValidation{}, err + } + return pollInteriorValidation(ctx, c, id, InteriorStatusValidated) +} + +func pollInteriorValidation(ctx context.Context, c Client, id uint, wantStatus InteriorValidationStatus) (InteriorValidation, error) { + return poll(ctx, func() (InteriorValidation, bool, error) { + val, err := c.GetInteriorValidation(id) + done := val.Status == wantStatus || + val.Status == InteriorStatusError + return val, done, err + }) +} + +// 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 ValidateCover(ctx context.Context, c Client, srcUrl string, mfg PkgId, npages uint) (CoverValidation, error) { + id, err := c.StartCoverValidation(srcUrl, mfg, npages) + if err != nil { + return CoverValidation{}, err + } + return poll(ctx, func() (CoverValidation, bool, error) { + val, err := c.GetCoverValidation(id) + return val, val.Status.IsFinal(), err + }) +} + +type client struct { ctx context.Context c *http.Client apiUrl string @@ -49,17 +201,17 @@ type Credentials struct { // // 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) { +func NewClient(ctx context.Context, creds Credentials) (Client, error) { 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) { +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) { +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") @@ -70,50 +222,10 @@ func newClient(ctx context.Context, creds Credentials, apiUrl, keyPageUrl string ClientSecret: creds.Secret, TokenURL: tokenUrl, } - return &Client{ctx, cfg.Client(ctx), apiUrl, keyPageUrl}, nil -} - -// Context returns the client's context. -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 [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 { - return InteriorValidation{}, err - } - return c.pollInteriorValidation(ctx, id, InteriorStatusNormalized) -} - -// 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) - if err != nil { - return InteriorValidation{}, err - } - return c.pollInteriorValidation(ctx, id, InteriorStatusValidated) -} - -func (c *Client) pollInteriorValidation(ctx context.Context, id uint, wantStatus InteriorValidationStatus) (InteriorValidation, error) { - return poll(ctx, func() (InteriorValidation, bool, error) { - val, err := c.GetInteriorValidation(id) - done := val.Status == wantStatus || - val.Status == InteriorStatusError - return val, done, err - }) + return &client{ctx, cfg.Client(ctx), apiUrl, keyPageUrl}, nil } -// 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 [Client.GetInteriorValidation] to poll -// the status of the job. -// -// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create -func (c *Client) StartInteriorValidation(srcUrl string, mfg PkgId) (uint, error) { +func (c *client) StartInteriorValidation(srcUrl string, mfg PkgId) (uint, error) { id, err := c.startInteriorValidation(validateInteriorReq{srcUrl, mfg}) if err != nil { return 0, pkgErr(err) @@ -121,11 +233,7 @@ func (c *Client) StartInteriorValidation(srcUrl string, mfg PkgId) (uint, error) return id, nil } -// StartInteriorValidationBasic is like [Client.StartInteriorValidation] but -// without the optional pod_package_id. -// -// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create -func (c *Client) StartInteriorValidationBasic(srcUrl string) (uint, error) { +func (c *client) StartInteriorValidationBasic(srcUrl string) (uint, error) { id, err := c.startInteriorValidation(validateInteriorBasicReq{srcUrl}) if err != nil { return 0, pkgErr(err) @@ -133,17 +241,13 @@ func (c *Client) StartInteriorValidationBasic(srcUrl string) (uint, error) { return id, nil } -func (c *Client) startInteriorValidation(payload any) (uint, error) { +func (c *client) startInteriorValidation(payload any) (uint, error) { var val InteriorValidation err := c.postDecode(validateInteriorPath, payload, http.StatusCreated, &val) return val.Id, err } -// GetInteriorValidation retrieves information about an interior file -// validation job that was started by ValidateInterior(). -// -// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_read -func (c *Client) GetInteriorValidation(id uint) (InteriorValidation, error) { +func (c *client) GetInteriorValidation(id uint) (InteriorValidation, error) { path, err := url.JoinPath(validateInteriorPath, fmt.Sprint(id)) if err != nil { return InteriorValidation{}, pkgErr(err) @@ -155,12 +259,7 @@ func (c *Client) GetInteriorValidation(id uint) (InteriorValidation, error) { return val, nil } -// CoverDimensions calculates the required dimensions of the cover for a -// book with the given manufacturing settings and number of pages. The -// returned dimensions are given in the specified units of measurement. -// -// https://api.lulu.com/docs/#tag/Files-validation/operation/Cover-Dimensions_create -func (c *Client) CoverDimensions(mfg PkgId, npages uint, unit Unit) (CoverDimensions, error) { +func (c *client) CoverDimensions(mfg PkgId, npages uint, unit Unit) (CoverDimensions, error) { payload := coverDimensionsReq{mfg, npages, unit} var dims CoverDimensions err := c.postDecode(coverDimensionsPath, payload, http.StatusCreated, &dims) @@ -170,29 +269,7 @@ 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 [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 { - return CoverValidation{}, err - } - return poll(ctx, func() (CoverValidation, bool, error) { - val, err := c.GetCoverValidation(id) - return val, val.Status.IsFinal(), err - }) -} - -// 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) { +func (c *client) StartCoverValidation(srcUrl string, mfg PkgId, npages uint) (uint, error) { payload := validateCoverReq{srcUrl, mfg, npages} var val CoverValidation err := c.postDecode(validateCoverPath, payload, http.StatusCreated, &val) @@ -202,11 +279,7 @@ func (c *Client) StartCoverValidation(srcUrl string, mfg PkgId, npages uint) (ui return val.Id, nil } -// GetCoverValidiation retrieves information about a cover file -// validation job that was started by ValidateCover(). -// -// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Cover_read -func (c *Client) GetCoverValidation(id uint) (CoverValidation, error) { +func (c *client) GetCoverValidation(id uint) (CoverValidation, error) { path, err := url.JoinPath(validateCoverPath, fmt.Sprint(id)) if err != nil { return CoverValidation{}, pkgErr(err) @@ -218,11 +291,7 @@ func (c *Client) GetCoverValidation(id uint) (CoverValidation, error) { return val, nil } -// Cost calculates the cost of a hypothetical print order without -// actually creating a print job. -// -// https://api.lulu.com/docs/#tag/Print-Job-Cost-Calculations/operation/Print-Job-cost-calculations_create -func (c *Client) Cost(items []CostLineItem, addr ShippingAddress, shipOpt ShippingLevel) (Cost, ShippingAddressValidation, error) { +func (c *client) Cost(items []CostLineItem, addr ShippingAddress, shipOpt ShippingLevel) (Cost, ShippingAddressValidation, error) { payload := costReq{items, addr, shipOpt} var resp costResp err := c.postDecode(printJobCostPath, payload, http.StatusCreated, &resp) @@ -232,83 +301,7 @@ func (c *Client) Cost(items []CostLineItem, addr ShippingAddress, shipOpt Shippi return resp.cost(), resp.AddressValidation, nil } -// Jobs retrieves a list of all print jobs that have been created, -// filtered by a set of optional query parameters. -// -// https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_list -func (c *Client) Jobs(queries ...PrintJobQuery) ([]PrintJob, error) { - var q printJobQueries - q.apply(queries...) - qvals := q.vals() - - var jobs []PrintJob - var cnt uint - for page := 1; ; page++ { - qvals.Set("page", fmt.Sprint(page)) - var resp jobsResp - if err := c.getQueryDecode(printJobsPath, qvals, &resp); err != nil { - return jobs, pkgErr(err) - } - - cnt = resp.Count - if len(resp.Results) == 0 && resp.Next != "" { - return jobs, pkgErr(fmt.Errorf("no results on this page, but server returned a next page: %s", resp.Next)) - } - if len(resp.Results) > 0 { - jobs = append(jobs, resp.Results...) - } - if len(resp.Results) == 0 || len(resp.Next) == 0 { - break - } - } - if uint(len(jobs)) != cnt { - return jobs, pkgErr(fmt.Errorf("expected %d jobs, got %d", cnt, len(jobs))) - } - return jobs, nil -} - -// Job retrieves the print job with the given ID. -// -// https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_read -func (c *Client) Job(id uint64) (PrintJob, error) { - var job PrintJob - path, err := url.JoinPath(printJobsPath, fmt.Sprint(id)) - if err != nil { - return job, pkgErr(err) - } - if err := c.getDecode(path, &job); err != nil { - return job, pkgErr(err) - } - return job, nil -} - -// Print creates a new print job. -// -// contact: Email address that should be contacted if questions -// regarding the Print-Job arise. Lulu recommends to use the -// email of a person who is responsible for placing the Print-Job -// like a developer or business owner. -// -// externalId: Arbitrary string to identify and connect a print -// job to your systems. Set it to an order number, a purchase -// order or whatever else works for your particular use case. -// -// productionDelay: Delay before a newly created Print-Job is -// sent to production. Minimum is 60 minutes, maximum is 2880 -// minutes (=48 hours). As most cancellation requests occur right -// after an order has been placed, it makes sense to wait for -// some time before sending an order to production. Once -// production has started, orders cannot be canceled anymore. -// -// addr: The postal address of the customer that the order will -// be sent to. -// -// shipOpt: Shipping method. -// -// items: List of books to print. -// -// https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_create -func (c *Client) Print(contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []Printable) (PrintJob, error) { +func (c *client) Print(contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []Printable) (PrintJob, error) { job, err := print(c, contact, externalId, productionDelay, addr, shipOpt, items) if err != nil { err = pkgErr(err) @@ -316,11 +309,7 @@ func (c *Client) Print(contact EmailAddress, externalId string, productionDelay return job, err } -// Reprint creates a print job, printing books whose PrintableIds are -// known from a prior order. See also: Print(). -// -// https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_reprint -func (c *Client) Reprint(contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []Reprintable) (PrintJob, error) { +func (c *client) Reprint(contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []Reprintable) (PrintJob, error) { job, err := print(c, contact, externalId, productionDelay, addr, shipOpt, items) if err != nil { err = pkgErr(err) @@ -328,7 +317,7 @@ func (c *Client) Reprint(contact EmailAddress, externalId string, productionDela return job, err } -func print[P Printable | Reprintable](c *Client, contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []P) (PrintJob, error) { +func print[P Printable | Reprintable](c *client, contact EmailAddress, externalId string, productionDelay time.Duration, addr ShippingAddress, shipOpt ShippingLevel, items []P) (PrintJob, error) { if err := verifyProductionDelay(productionDelay); err != nil { return PrintJob{}, err } @@ -353,11 +342,51 @@ func verifyProductionDelay(delay time.Duration) error { return nil } -// Cancel cancels the print job with the given ID. -// -// https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_status_cancel -func (c *Client) Cancel(id uint64) error { - path, err := url.JoinPath(printJobsPath, fmt.Sprint(id), "status") +func (c *client) Job(id uint64) (PrintJob, error) { + var job PrintJob + path, err := url.JoinPath(printJobsPath, fmt.Sprint(id)) + if err != nil { + return job, pkgErr(err) + } + if err := c.getDecode(path, &job); err != nil { + return job, pkgErr(err) + } + return job, nil +} + +func (c *client) Jobs(queries ...PrintJobQuery) ([]PrintJob, error) { + var q printJobQueries + q.apply(queries...) + qvals := q.vals() + + var jobs []PrintJob + var cnt uint + for page := 1; ; page++ { + qvals.Set("page", fmt.Sprint(page)) + var resp jobsResp + if err := c.getQueryDecode(printJobsPath, qvals, &resp); err != nil { + return jobs, pkgErr(err) + } + + cnt = resp.Count + if len(resp.Results) == 0 && resp.Next != "" { + return jobs, pkgErr(fmt.Errorf("no results on this page, but server returned a next page: %s", resp.Next)) + } + if len(resp.Results) > 0 { + jobs = append(jobs, resp.Results...) + } + if len(resp.Results) == 0 || len(resp.Next) == 0 { + break + } + } + if uint(len(jobs)) != cnt { + return jobs, pkgErr(fmt.Errorf("expected %d jobs, got %d", cnt, len(jobs))) + } + return jobs, nil +} + +func (c *client) Cancel(jobId uint64) error { + path, err := url.JoinPath(printJobsPath, fmt.Sprint(jobId), "status") if err != nil { return pkgErr(err) } @@ -367,18 +396,18 @@ func (c *Client) Cancel(id uint64) error { return pkgErr(err) } if status.Status != OrderCanceled { - return pkgErr(fmt.Errorf("cancel job %d: expected %s, got %s", id, OrderCanceled, status.Status)) + return pkgErr(fmt.Errorf("cancel job %d: expected %s, got %s", jobId, OrderCanceled, status.Status)) } return nil } // getDecode sends a GET request and unmarshals the response. -func (c *Client) getDecode(path string, v any) error { +func (c *client) getDecode(path string, v any) error { return c.getQueryDecode(path, url.Values{}, v) } // 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 { +func (c *client) getQueryDecode(path string, query url.Values, v any) error { url, err := c.url(path) if err != nil { return err @@ -397,7 +426,7 @@ func (c *Client) getQueryDecode(path string, query url.Values, v any) error { } // postDecode sends a POST request and unmarshals the response. -func (c *Client) postDecode(path string, payload any, wantStatus int, v any) error { +func (c *client) postDecode(path string, payload any, wantStatus int, v any) error { resp, err := c.post(path, payload) if err != nil { return err @@ -409,7 +438,7 @@ func (c *Client) postDecode(path string, payload any, wantStatus int, v any) err return decodeResponse(resp, v) } -func (c *Client) post(path string, payload any) (*http.Response, error) { +func (c *client) post(path string, payload any) (*http.Response, error) { body, err := json.Marshal(payload) if err != nil { return nil, errEncReq{payload, path, err} @@ -425,7 +454,7 @@ func (c *Client) post(path string, payload any) (*http.Response, error) { } // putDecode sends a PUT request and unmarshals the response. -func (c *Client) putDecode(path string, payload any, v any) error { +func (c *client) putDecode(path string, payload any, v any) error { body, err := json.Marshal(payload) if err != nil { return errEncReq{payload, path, err} @@ -453,7 +482,7 @@ func (c *Client) putDecode(path string, payload any, v any) error { return decodeResponse(resp, v) } -func (c *Client) url(path string) (string, error) { +func (c *client) url(path string) (string, error) { return url.JoinPath(c.apiUrl, path) } diff --git a/lulu_test.go b/lulu_test.go index 6791680..17935e9 100644 --- a/lulu_test.go +++ b/lulu_test.go @@ -32,7 +32,7 @@ func TestMain(m *testing.M) { m.Run() } -func tNewClient(t *testing.T) *Client { +func tNewClient(t *testing.T) Client { t.Helper() creds := Credentials{ Key: strings.TrimSpace(clientKey), |