aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-05-07 12:44:21 -0400
committerSam Anthony <sam@samanthony.xyz>2026-05-07 12:44:21 -0400
commitb57f03381e4bccba2963cebabe51a9cf32bd96dd (patch)
treed19815ddb5c6d495b3123bf888060dd88866a182
parentf2ee817ba1423b10b382f6b759a19184db8eeacc (diff)
downloadlulu-b57f03381e4bccba2963cebabe51a9cf32bd96dd.zip
implement POST /validate-cover
-rw-r--r--cover.go29
-rw-r--r--cover_test.go60
-rw-r--r--err.go28
-rw-r--r--lulu.go91
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 {