// Package lulu is a client library for the Lulu book printing API. package lulu import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "golang.org/x/oauth2/clientcredentials" ) const ( SandboxUrl = "https://api.sandbox.lulu.com/" ProductionUrl = "https://api.lulu.com/" tokenPath = "/auth/realms/glasstree/protocol/openid-connect/token" validateInteriorPath = "/validate-interior" coverDimensionsPath = "/cover-dimensions" validateCoverPath = "/validate-cover" ) // ApiUrl is the location of the API server. It is set to the sandbox // environment by default; change it to the production environment when // you are ready to deploy. var ApiUrl = SandboxUrl // Unit is a unit of length measurement. type Unit string const ( Points Unit = "pt" Millimeters = "mm" Inches = "inch" ) type ValidationStatus string const ( StatusNull ValidationStatus = "NULL" // file validation is not started yet StatusValidating ValidationStatus = "VALIDATING" // file validation is still running StatusValidated ValidationStatus = "VALIDATED" // file validation finished without any errors StatusNormalizing ValidationStatus = "NORMALIZING" // file normalization (next step of validation, available only if pod_package_id is was passed in the payload) is still running StatusNormalized ValidationStatus = "NORMALIZED" // file normalization finished without any errors StatusError ValidationStatus = "ERROR" // file is invalid, list of errors is included in the response ) func (s ValidationStatus) IsFinal() bool { switch s { case StatusValidated, StatusNormalized, StatusError: return true } return false } // validateInteriorReq is the json body of a /validate-interior/ request. type validateInteriorReq struct { SrcUrl string `json:"source_url"` PkgId PkgId `json:"pod_package_id"` } // validateInteriorReq is the json body of a /validate-interior/ request without the optional pod_package_id. type validateInteriorBasicReq struct { SrcUrl string `json:"source_url"` } // InteriorValidationRecord contains the validation status of an interior file. type InteriorValidationRecord struct { Id uint SrcUrl string `json:"source_url"` NPages uint `json:"page_count"` Errors string Status ValidationStatus ValidPkgIds []PkgId `json:"valid_pod_package_ids"` } type Client struct { c *http.Client } // NewClient returns a client that will use the given client-key and // client-secret to connect to the API server. func NewClient(ctx context.Context, key, secret string) (*Client, error) { tokenUrl, err := url.JoinPath(ApiUrl, tokenPath) if err != nil { return nil, err } cfg := &clientcredentials.Config{ ClientID: key, ClientSecret: secret, TokenURL: tokenUrl, } 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. // // 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}) } // ValidateInteriorBasic is like ValidateInterior 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) { return c.validateInterior(validateInteriorBasicReq{srcUrl}) } func (c *Client) validateInterior(payload any) (uint, error) { body, err := json.Marshal(payload) if err != nil { return 0, fmt.Errorf("lulu: error encoding request body %v for %s: %w", payload, validateInteriorPath, err) } 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)) if err != nil { return 0, fmt.Errorf("lulu: %w", err) } 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) } 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 } // GetInteriorValidation retrieves information about an interior file // 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) { url, err := url.JoinPath(ApiUrl, validateInteriorPath, fmt.Sprint(id)) if err != nil { return InteriorValidationRecord{}, fmt.Errorf("lulu: %w", err) } resp, err := c.c.Get(url) if err != nil { return InteriorValidationRecord{}, fmt.Errorf("lulu: %w", err) } 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) } 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) } 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) } return rec, nil }