diff options
| -rw-r--r-- | err.go | 41 | ||||
| -rw-r--r-- | lulu.go | 105 | ||||
| -rw-r--r-- | lulu_test.go | 83 |
3 files changed, 190 insertions, 39 deletions
@@ -0,0 +1,41 @@ +package lulu + +import ( + "fmt" + "io" + "net/http" +) + +type errResp struct { + *http.Response +} + +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) +} + +type errReadResp struct { + *http.Response + error +} + +func (e errReadResp) Error() string { + req := e.Response.Request + return fmt.Sprintf("lulu: %s %s: error reading response body: %v", + req.Method, req.URL, e.error) +} + +type errDecResp struct { + *http.Response + body []byte + error +} + +func (e errDecResp) Error() string { + req := e.Response.Request + return fmt.Sprintf("lulu: %s %s: error decoding response body %q: %v", + req.Method, req.URL, string(e.body), e.error) +} @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "strconv" "golang.org/x/oauth2/clientcredentials" ) @@ -33,8 +34,8 @@ type Unit string const ( Points Unit = "pt" - Millimeters = "mm" - Inches = "inch" + Millimeters Unit = "mm" + Inches Unit = "inch" ) type ValidationStatus string @@ -77,6 +78,43 @@ type InteriorValidationRecord struct { ValidPkgIds []PkgId `json:"valid_pod_package_ids"` } +// coverDimensionsReq is the json body of a /cover-dimensions/ request. +type coverDimensionsReq struct { + PkgId PkgId `json:"pod_package_id"` + NPages uint `json:"interior_page_count"` + Unit Unit `json:"unit"` +} + +type CoverDimensions struct { + Width, Height float64 + Unit Unit +} + +func (cd *CoverDimensions) UnmarshalJSON(data []byte) error { + s := string(data) + var alias struct { + Width, Height string + Unit Unit + } + if err := json.Unmarshal(data, &alias); err != nil { + return fmt.Errorf("malformed %T: %q: %w", cd, s, err) + } + + w, err := strconv.ParseFloat(alias.Width, 64) + if err != nil { + return fmt.Errorf("malformed %T.Width: %q: %w", cd, s, err) + } + h, err := strconv.ParseFloat(alias.Height, 64) + if err != nil { + return fmt.Errorf("malformed %T.Height: %q: %w", cd, s, err) + } + + cd.Width = w + cd.Height = h + cd.Unit = alias.Unit + return nil +} + type Client struct { c *http.Client } @@ -132,20 +170,12 @@ func (c *Client) validateInterior(payload any) (uint, error) { defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - body, _ := io.ReadAll(resp.Body) - return 0, fmt.Errorf("lulu: POST %s: %s: %s", url, resp.Status, body) + return 0, errResp{resp} } - buf := new(bytes.Buffer) - if _, err := io.Copy(buf, resp.Body); err != nil { - return 0, fmt.Errorf("lulu: POST %s: error reading response body: %w", url, err) - } - dec := json.NewDecoder(buf) var rec InteriorValidationRecord - if err := dec.Decode(&rec); err != nil { - return 0, fmt.Errorf("lulu: POST %s: error decoding response body %q: %w", url, buf, err) - } - return rec.Id, nil + err = decodeResponse(resp, &rec) + return rec.Id, err } // GetInteriorValidation retrieves information about an interior file @@ -164,18 +194,53 @@ func (c *Client) GetInteriorValidation(id uint) (InteriorValidationRecord, error defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return InteriorValidationRecord{}, fmt.Errorf("lulu: GET %s: %s: %s", url, resp.Status, body) + return InteriorValidationRecord{}, errResp{resp} + } + + var rec InteriorValidationRecord + err = decodeResponse(resp, &rec) + return rec, err +} + +// 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) { + payload := coverDimensionsReq{mfg, npages, unit} + body, err := json.Marshal(payload) + if err != nil { + return CoverDimensions{}, fmt.Errorf("lulu: error encoding request body %v for %s: %w", payload, coverDimensionsPath, err) + } + + url, err := url.JoinPath(ApiUrl, coverDimensionsPath) + if err != nil { + return CoverDimensions{}, fmt.Errorf("lulu: %w", err) + } + resp, err := c.c.Post(url, "application/json", bytes.NewBuffer(body)) + if err != nil { + return CoverDimensions{}, fmt.Errorf("lulu: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return CoverDimensions{}, errResp{resp} } + var dims CoverDimensions + err = decodeResponse(resp, &dims) + return dims, err +} + +func decodeResponse(resp *http.Response, v any) error { buf := new(bytes.Buffer) if _, err := io.Copy(buf, resp.Body); err != nil { - return InteriorValidationRecord{}, fmt.Errorf("lulu: GET %s: error reading response body: %w", url, err) + return errReadResp{resp, err} } dec := json.NewDecoder(buf) - var rec InteriorValidationRecord - if err := dec.Decode(&rec); err != nil { - return rec, fmt.Errorf("lulu: GET %s: error decoding response body %q: %w", url, buf, err) + if err := dec.Decode(v); err != nil { + return errDecResp{resp, buf.Bytes(), err} } - return rec, nil + return nil } diff --git a/lulu_test.go b/lulu_test.go index 5f11e99..513d590 100644 --- a/lulu_test.go +++ b/lulu_test.go @@ -46,12 +46,10 @@ func TestMain(m *testing.M) { func TestMarshalValidateInteriorReq(t *testing.T) { t.Parallel() - want := `{ "source_url": "https://example.com/interior.pdf", "pod_package_id": "0850X1100.BW.STD.LW.060UW444.MNG" }` - req := validateInteriorReq{ "https://example.com/interior.pdf", PkgId{ @@ -64,7 +62,6 @@ func TestMarshalValidateInteriorReq(t *testing.T) { NavyLinen, GoldFoil}, } - requireMarshalJsonEq(t, want, req) } @@ -77,26 +74,23 @@ func TestMarshalValidateInteriorBasicReq(t *testing.T) { func TestUnmarshalInteriorValidationRecord(t *testing.T) { t.Parallel() - data := []byte(`{ + data := `{ "id": 1, "source_url": "https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1", "page_count": 210, "errors": null, "status": "VALIDATING", "valid_pod_package_ids": null -}`) - var rec InteriorValidationRecord - err := json.Unmarshal(data, &rec) - require.NoError(t, err) - require.Equal(t, - InteriorValidationRecord{ - 1, - "https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1", - 210, - "", - StatusValidating, - nil}, - rec) +}` + want := InteriorValidationRecord{ + 1, + "https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1", + 210, + "", + StatusValidating, + nil, + } + requireUnmarshalJsonEq(t, want, data) } func TestValidateInterior(t *testing.T) { @@ -134,7 +128,7 @@ func TestGetInteriorValidation(t *testing.T) { id, err := c.ValidateInteriorBasic(interiorUrl) require.NoError(t, err) - // Poll until status is non-null + // Poll until done timeout := 15 * time.Second period := time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -146,11 +140,12 @@ func TestGetInteriorValidation(t *testing.T) { rec, err := c.GetInteriorValidation(id) require.NoError(t, err) if rec.Status.IsFinal() { + require.Equal(t, StatusValidated, rec.Status) + require.Equal(t, id, rec.Id) require.Equal(t, interiorUrl, rec.SrcUrl) require.Equal(t, uint(210), rec.NPages) require.Empty(t, rec.Errors) - require.Equal(t, StatusValidated, rec.Status) require.NotEmpty(t, rec.ValidPkgIds) return } @@ -162,6 +157,56 @@ func TestGetInteriorValidation(t *testing.T) { } } +func TestUnmarshalCoverDimensions(t *testing.T) { + t.Parallel() + requireUnmarshalJsonEq(t, + CoverDimensions{123.4, 567.8, Points}, + `{"width": "123.400", "height": "567.800", "unit": "pt"}`) + requireUnmarshalJsonEq(t, + CoverDimensions{123.4, 567.8, Millimeters}, + `{"width": "123.400", "height": "567.800", "unit": "mm"}`) + requireUnmarshalJsonEq(t, + CoverDimensions{123.4, 567.8, Inches}, + `{"width": "123.400", "height": "567.800", "unit": "inch"}`) +} + +func TestMarshalCoverDimensionsReq(t *testing.T) { + t.Parallel() + requireMarshalJsonEq(t, `{ + "pod_package_id": "0600X0900.BW.STD.PB.060UW444.MXX", + "interior_page_count": 210, + "unit": "pt"}`, + coverDimensionsReq{ + PkgId{ + UsTrade, + Mono, + Standard, + Perfect, + P60UncoatedWhite, + Matte, + NoLinen, + NoFoil}, + 210, + Points}) +} + +func TestCoverDimensions(t *testing.T) { + c := newClient(t) + mfg := PkgId{ + UsTrade, + Mono, + Standard, + Perfect, + P60UncoatedWhite, + Matte, + NoLinen, + NoFoil, + } + dims, err := c.CoverDimensions(mfg, 210, Points) + require.NoError(t, err) + require.Equal(t, CoverDimensions{920, 666, Points}, dims) +} + func newClient(t *testing.T) *Client { t.Helper() c, err := NewClient(t.Context(), clientKey, clientSecret) |