aboutsummaryrefslogtreecommitdiffstats
path: root/lulu.go
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-05-13 18:28:09 -0400
committerSam Anthony <sam@samanthony.xyz>2026-05-13 18:28:09 -0400
commit0865e2e03651dcc7be91846a6e52125e56bcde31 (patch)
treec5abe0a595e1ad26db0e4e7195ad963ca845e202 /lulu.go
parent48f85453ffbe36f6c428a5d9a23b74122686b64c (diff)
downloadlulu-0865e2e03651dcc7be91846a6e52125e56bcde31.zip
poll for file validation status
Diffstat (limited to 'lulu.go')
-rw-r--r--lulu.go139
1 files changed, 105 insertions, 34 deletions
diff --git a/lulu.go b/lulu.go
index 04e34ab..a4e12cb 100644
--- a/lulu.go
+++ b/lulu.go
@@ -7,10 +7,8 @@ import (
"encoding/json"
"fmt"
"io"
- "log"
"net/http"
"net/url"
- "os"
"time"
"golang.org/x/oauth2/clientcredentials"
@@ -20,6 +18,11 @@ const (
SandboxUrl = "https://api.sandbox.lulu.com/"
ProductionUrl = "https://api.lulu.com/"
+ ProductionApiKeyPage = "https://developers.lulu.com/user-profile/api-keys"
+ SandboxApiKeyPage = "https://developers.sandbox.lulu.com/user-profile/api-keys"
+
+ PollPeriod = time.Second
+
tokenPath = "/auth/realms/glasstree/protocol/openid-connect/token"
validateInteriorPath = "/validate-interior"
coverDimensionsPath = "/cover-dimensions"
@@ -33,10 +36,22 @@ const (
// you are ready to deploy.
var ApiUrl = SandboxUrl
-var (
- Debug = false // print debug info to stdout
- debugLog = log.New(os.Stderr, "DEBUG ", log.LstdFlags|log.Llongfile)
-)
+// Sandbox sets ApiUrl to SandboxApiUrl so that subsequent requests will
+// be sent to the sandbox API server.
+func Sandbox() { ApiUrl = SandboxUrl }
+
+// Production sets ApiUrl to ProductionApiUrl so that subsequent requests
+// will be sent to the production API server.
+func Production() { ApiUrl = ProductionUrl }
+
+// ApiKeyPage returns the URL of the page where you can generate a
+// client-key and client-secret to use for authentication.
+func ApiKeyPage() string {
+ if ApiUrl == SandboxUrl {
+ return SandboxApiKeyPage
+ }
+ return ProductionApiKeyPage
+}
type Client struct {
c *http.Client
@@ -58,34 +73,63 @@ func NewClient(ctx context.Context, key, secret string) (*Client, error) {
return &Client{cfg.Client(ctx)}, nil
}
-// ValidateInterior 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 GetInteriorValidation() to poll the
-// status of the job.
+// ValidateInterior starts a server-side validation job for the given
+// interior file and polls its status until it finishes or the context
+// expires. See also: StartInteriorValidation() and
+// GetInteriorValidation().
+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)
+}
+
+// ValidateInteriorBasic is like 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)
+}
+
+func (c *Client) pollInteriorValidation(ctx context.Context, id uint) (InteriorValidation, error) {
+ return poll(ctx, func() (InteriorValidation, bool, error) {
+ val, err := c.GetInteriorValidation(id)
+ return val, val.Status.IsFinal(), err
+ })
+}
+
+// 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 GetInteriorValidation() to poll
+// the status of the job.
//
// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create
-func (c *Client) ValidateInterior(srcUrl string, mfg PkgId) (uint, error) {
- id, err := c.validateInterior(validateInteriorReq{srcUrl, mfg})
+func (c *Client) StartInteriorValidation(srcUrl string, mfg PkgId) (uint, error) {
+ id, err := c.startInteriorValidation(validateInteriorReq{srcUrl, mfg})
if err != nil {
return 0, pkgErr(err)
}
return id, nil
}
-// ValidateInteriorBasic is like ValidateInterior but without the
-// optional pod_package_id.
+// StartInteriorValidationBasic is like StartInteriorValidation but
+// without the optional pod_package_id.
//
// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Interior_create
-func (c *Client) ValidateInteriorBasic(srcUrl string) (uint, error) {
- id, err := c.validateInterior(validateInteriorBasicReq{srcUrl})
+func (c *Client) StartInteriorValidationBasic(srcUrl string) (uint, error) {
+ id, err := c.startInteriorValidation(validateInteriorBasicReq{srcUrl})
if err != nil {
return 0, pkgErr(err)
}
return id, nil
}
-func (c *Client) validateInterior(payload any) (uint, error) {
- var rec InteriorValidationRecord
+func (c *Client) startInteriorValidation(payload any) (uint, error) {
+ var rec InteriorValidation
err := c.postDecode(validateInteriorPath, payload, http.StatusCreated, &rec)
return rec.Id, err
}
@@ -94,14 +138,14 @@ func (c *Client) validateInterior(payload any) (uint, error) {
// 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) (InteriorValidationRecord, error) {
+func (c *Client) GetInteriorValidation(id uint) (InteriorValidation, error) {
path, err := url.JoinPath(validateInteriorPath, fmt.Sprint(id))
if err != nil {
- return InteriorValidationRecord{}, pkgErr(err)
+ return InteriorValidation{}, pkgErr(err)
}
- var rec InteriorValidationRecord
+ var rec InteriorValidation
if err := c.getDecode(path, &rec); err != nil {
- return InteriorValidationRecord{}, pkgErr(err)
+ return InteriorValidation{}, pkgErr(err)
}
return rec, nil
}
@@ -121,15 +165,29 @@ func (c *Client) CoverDimensions(mfg PkgId, npages uint, unit Unit) (CoverDimens
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
+// ValidateCover starts a server-side validation job for the given cover
+// file and polls its status until it finishes or the context expires.
+// See also: StartCoverValidation() and GetCoverValidation().
+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
// 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) {
+func (c *Client) StartCoverValidation(srcUrl string, mfg PkgId, npages uint) (uint, error) {
payload := validateCoverReq{srcUrl, mfg, npages}
- var rec CoverValidationRecord
+ var rec CoverValidation
err := c.postDecode(validateCoverPath, payload, http.StatusCreated, &rec)
if err != nil {
return 0, pkgErr(err)
@@ -138,17 +196,17 @@ func (c *Client) ValidateCover(srcUrl string, mfg PkgId, npages uint) (uint, err
}
// GetCoverValidiation retrieves information about a cover file
-// validation job that was started by ValidiateCover().
+// 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) (CoverValidationRecord, error) {
+func (c *Client) GetCoverValidation(id uint) (CoverValidation, error) {
path, err := url.JoinPath(validateCoverPath, fmt.Sprint(id))
if err != nil {
- return CoverValidationRecord{}, pkgErr(err)
+ return CoverValidation{}, pkgErr(err)
}
- var rec CoverValidationRecord
+ var rec CoverValidation
if err := c.getDecode(path, &rec); err != nil {
- return CoverValidationRecord{}, pkgErr(err)
+ return CoverValidation{}, pkgErr(err)
}
return rec, nil
}
@@ -339,8 +397,21 @@ func decodeResponse(resp *http.Response, v any) error {
return nil
}
-func debugf(format string, a ...any) {
- if Debug {
- debugLog.Printf(format, a...)
+func poll[T any](ctx context.Context, f func() (v T, done bool, err error)) (T, error) {
+ timer := time.NewTimer(0)
+ for {
+ select {
+ case <-timer.C:
+ v, done, err := f()
+ if err != nil {
+ return v, err
+ } else if done {
+ return v, nil
+ }
+ timer.Reset(PollPeriod)
+ case <-ctx.Done():
+ var v T
+ return v, ctx.Err()
+ }
}
}