From b57f03381e4bccba2963cebabe51a9cf32bd96dd Mon Sep 17 00:00:00 2001 From: Sam Anthony Date: Thu, 7 May 2026 12:44:21 -0400 Subject: implement POST /validate-cover --- cover.go | 29 ++++++++++++++++++- cover_test.go | 60 +++++++++++++++++++++++++++++++++++++++ err.go | 28 ++++++++++++++++-- lulu.go | 91 +++++++++++++++++++++++++++++++++++++++-------------------- 4 files changed, 174 insertions(+), 34 deletions(-) diff --git a/cover.go b/cover.go index 30be2d6..e5845a7 100644 --- a/cover.go +++ b/cover.go @@ -6,7 +6,7 @@ import ( "strconv" ) -//go:generate go run github.com/yawnak/string-enumer -t Unit --text -o ./cover_gen.go . +//go:generate go run github.com/yawnak/string-enumer -t Unit -t CoverValidationStatus --text -o ./cover_gen.go . // Unit is a unit of length measurement. type Unit string @@ -17,6 +17,17 @@ const ( Inches Unit = "inch" ) +// CoverValidationStatus is the status of a validation job for a cover +// file that is running on the server. +type CoverValidationStatus string + +const ( + CoverStatusNull CoverValidationStatus = "NULL" // file validation is not started yet + CoverStatusNormalizing CoverValidationStatus = "NORMALIZING" // file validation is still running + CoverStatusNormalized CoverValidationStatus = "NORMALIZED" // file validation finished without any errors + CoverStatusError CoverValidationStatus = "ERROR" // file is invalid, list of errors is included in the response +) + // coverDimensionsReq is the json body of a /cover-dimensions/ request. type coverDimensionsReq struct { PkgId PkgId `json:"pod_package_id"` @@ -24,6 +35,13 @@ type coverDimensionsReq struct { Unit Unit `json:"unit"` } +// validateCoverReq is the json body of a /validate-cover/ request. +type validateCoverReq struct { + SrcUrl string `json:"source_url"` + PkgId PkgId `json:"pod_package_id"` + NPages uint `json:"interior_page_count"` +} + type CoverDimensions struct { Width, Height float64 Unit Unit @@ -53,3 +71,12 @@ func (cd *CoverDimensions) UnmarshalJSON(data []byte) error { cd.Unit = alias.Unit return nil } + +// CoverValidationRecord contains the validation status of a cover file. +type CoverValidationRecord struct { + Id uint + SrcUrl string `json:"source_url"` + NPages uint `json:"page_count"` + Errors string + Status CoverValidationStatus +} diff --git a/cover_test.go b/cover_test.go index a0042c1..9cbab17 100644 --- a/cover_test.go +++ b/cover_test.go @@ -55,3 +55,63 @@ func TestCoverDimensions(t *testing.T) { require.NoError(t, err) require.Equal(t, CoverDimensions{920, 666, Points}, dims) } + +func TestMarshalValidateCoverReq(t *testing.T) { + t.Parallel() + want := `{ + "source_url": "https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1", + "pod_package_id": "0600X0900.BW.STD.PB.060UW444.MXX", + "interior_page_count": 210 + }` + req := validateCoverReq{ + "https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1", + PkgId{ + UsTrade, + Mono, + Standard, + Perfect, + P60UncoatedWhite, + Matte, + NoLinen, + NoFoil, + }, + 210, + } + requireMarshalJsonEq(t, want, req) +} + +func TestUnmarshalCoverValidationRecord(t *testing.T) { + t.Parallel() + data := `{ + "id": 1, + "source_url": "https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1", + "page_count": 210, + "errors": null, + "status": "NORMALIZING" + }` + want := CoverValidationRecord{ + 1, + "https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1", + 210, + "", + CoverStatusNormalizing, + } + requireUnmarshalJsonEq(t, want, data) +} + +func TestValidateCover(t *testing.T) { + c := newClient(t) + mfg := PkgId{ + UsTrade, + Mono, + Standard, + Perfect, + P60UncoatedWhite, + Gloss, + NoLinen, + NoFoil, + } + id, err := c.ValidateCover(coverUrl, mfg, 210) + require.NoError(t, err) + require.NotZero(t, id) +} diff --git a/err.go b/err.go index e05d1ff..cb79500 100644 --- a/err.go +++ b/err.go @@ -6,6 +6,28 @@ import ( "net/http" ) +// pkgErr formats an error to be returned to the package user. +func pkgErr(err error) error { + return fmt.Errorf("lulu: %w", err) +} + +// pkgErrf formats an error with a message to be returned to the package user. +func pkgErrf(err error, format string, a ...any) error { + return fmt.Errorf("lulu: %s: %w", + fmt.Sprintf(format, a...), + err) +} + +type errEncReq struct { + payload any + path string + error +} + +func (e errEncReq) Error() string { + return fmt.Sprintf("error encoding request body %v for %s: %v", e.payload, e.path, e.error) +} + type errResp struct { *http.Response } @@ -14,7 +36,7 @@ func (e errResp) Error() string { resp := e.Response req := resp.Request body, _ := io.ReadAll(resp.Body) - return fmt.Sprintf("lulu: %s %s: %s: %s", req.Method, req.URL, resp.Status, body) + return fmt.Sprintf("%s %s: %s: %s", req.Method, req.URL, resp.Status, body) } type errReadResp struct { @@ -24,7 +46,7 @@ type errReadResp struct { func (e errReadResp) Error() string { req := e.Response.Request - return fmt.Sprintf("lulu: %s %s: error reading response body: %v", + return fmt.Sprintf("%s %s: error reading response body: %v", req.Method, req.URL, e.error) } @@ -36,6 +58,6 @@ type errDecResp struct { func (e errDecResp) Error() string { req := e.Response.Request - return fmt.Sprintf("lulu: %s %s: error decoding response body %q: %v", + return fmt.Sprintf("%s %s: error decoding response body %q: %v", req.Method, req.URL, string(e.body), e.error) } diff --git a/lulu.go b/lulu.go index 1fdf7a3..621cfc1 100644 --- a/lulu.go +++ b/lulu.go @@ -37,7 +37,7 @@ type Client struct { func NewClient(ctx context.Context, key, secret string) (*Client, error) { tokenUrl, err := url.JoinPath(ApiUrl, tokenPath) if err != nil { - return nil, err + return nil, pkgErrf(err, "error creating client") } cfg := &clientcredentials.Config{ @@ -55,7 +55,11 @@ func NewClient(ctx context.Context, key, secret string) (*Client, error) { // // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create func (c *Client) ValidateInterior(srcUrl string, mfg PkgId) (uint, error) { - return c.validateInterior(validateInteriorReq{srcUrl, mfg}) + id, err := c.validateInterior(validateInteriorReq{srcUrl, mfg}) + if err != nil { + return 0, pkgErr(err) + } + return id, nil } // ValidateInteriorBasic is like ValidateInterior but without the @@ -63,22 +67,17 @@ func (c *Client) ValidateInterior(srcUrl string, mfg PkgId) (uint, error) { // // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create func (c *Client) ValidateInteriorBasic(srcUrl string) (uint, error) { - return c.validateInterior(validateInteriorBasicReq{srcUrl}) -} - -func (c *Client) validateInterior(payload any) (uint, error) { - body, err := json.Marshal(payload) + id, err := c.validateInterior(validateInteriorBasicReq{srcUrl}) if err != nil { - return 0, fmt.Errorf("lulu: error encoding request body %v for %s: %w", payload, validateInteriorPath, err) + return 0, pkgErr(err) } + return id, nil +} - url, err := url.JoinPath(ApiUrl, validateInteriorPath) - if err != nil { - return 0, fmt.Errorf("lulu: %w", err) - } - resp, err := c.c.Post(url, "application/json", bytes.NewBuffer(body)) +func (c *Client) validateInterior(payload any) (uint, error) { + resp, err := c.post(validateInteriorPath, payload) if err != nil { - return 0, fmt.Errorf("lulu: %w", err) + return 0, err } defer resp.Body.Close() @@ -98,21 +97,23 @@ func (c *Client) validateInterior(payload any) (uint, error) { func (c *Client) GetInteriorValidation(id uint) (InteriorValidationRecord, error) { url, err := url.JoinPath(ApiUrl, validateInteriorPath, fmt.Sprint(id)) if err != nil { - return InteriorValidationRecord{}, fmt.Errorf("lulu: %w", err) + return InteriorValidationRecord{}, pkgErr(err) } resp, err := c.c.Get(url) if err != nil { - return InteriorValidationRecord{}, fmt.Errorf("lulu: %w", err) + return InteriorValidationRecord{}, pkgErr(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return InteriorValidationRecord{}, errResp{resp} + return InteriorValidationRecord{}, pkgErr(errResp{resp}) } var rec InteriorValidationRecord - err = decodeResponse(resp, &rec) - return rec, err + if err := decodeResponse(resp, &rec); err != nil { + return InteriorValidationRecord{}, pkgErr(err) + } + return rec, nil } // CoverDimensions calculates the required dimensions of the cover for a @@ -122,28 +123,58 @@ func (c *Client) GetInteriorValidation(id uint) (InteriorValidationRecord, error // https://api.lulu.com/docs/#tag/Files-validation/operation/Cover-Dimensions_create func (c *Client) CoverDimensions(mfg PkgId, npages uint, unit Unit) (CoverDimensions, error) { payload := coverDimensionsReq{mfg, npages, unit} - body, err := json.Marshal(payload) + resp, err := c.post(coverDimensionsPath, payload) if err != nil { - return CoverDimensions{}, fmt.Errorf("lulu: error encoding request body %v for %s: %w", payload, coverDimensionsPath, err) + return CoverDimensions{}, pkgErr(err) } + defer resp.Body.Close() - url, err := url.JoinPath(ApiUrl, coverDimensionsPath) - if err != nil { - return CoverDimensions{}, fmt.Errorf("lulu: %w", err) + if resp.StatusCode != http.StatusCreated { + return CoverDimensions{}, errResp{resp} } - resp, err := c.c.Post(url, "application/json", bytes.NewBuffer(body)) + + var dims CoverDimensions + if err := decodeResponse(resp, &dims); err != nil { + return CoverDimensions{}, pkgErr(err) + } + return dims, nil +} + +// ValidateCover 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. +// +// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Cover_create +func (c *Client) ValidateCover(srcUrl string, mfg PkgId, npages uint) (uint, error) { + payload := validateCoverReq{srcUrl, mfg, npages} + resp, err := c.post(validateCoverPath, payload) if err != nil { - return CoverDimensions{}, fmt.Errorf("lulu: %w", err) + return 0, pkgErr(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - return CoverDimensions{}, errResp{resp} + return 0, errResp{resp} } - var dims CoverDimensions - err = decodeResponse(resp, &dims) - return dims, err + var rec CoverValidationRecord + if err := decodeResponse(resp, &rec); err != nil { + return 0, pkgErr(err) + } + return rec.Id, nil +} + +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} + } + url, err := url.JoinPath(ApiUrl, path) + if err != nil { + return nil, err + } + return c.c.Post(url, "application/json", bytes.NewBuffer(body)) } func decodeResponse(resp *http.Response, v any) error { -- cgit v1.2.3