From 0865e2e03651dcc7be91846a6e52125e56bcde31 Mon Sep 17 00:00:00 2001 From: Sam Anthony Date: Wed, 13 May 2026 18:28:09 -0400 Subject: poll for file validation status --- lulu.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 34 deletions(-) (limited to 'lulu.go') 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() + } } } -- cgit v1.2.3