aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--err.go41
-rw-r--r--lulu.go105
-rw-r--r--lulu_test.go83
3 files changed, 190 insertions, 39 deletions
diff --git a/err.go b/err.go
new file mode 100644
index 0000000..e05d1ff
--- /dev/null
+++ b/err.go
@@ -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)
+}
diff --git a/lulu.go b/lulu.go
index 2346749..af425f0 100644
--- a/lulu.go
+++ b/lulu.go
@@ -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)