// 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 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, pkgErrf(err, "error creating client") } 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) { id, err := c.validateInterior(validateInteriorReq{srcUrl, mfg}) if err != nil { return 0, pkgErr(err) } return id, nil } // 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) { id, err := c.validateInterior(validateInteriorBasicReq{srcUrl}) if err != nil { return 0, pkgErr(err) } return id, nil } func (c *Client) validateInterior(payload any) (uint, error) { resp, err := c.post(validateInteriorPath, payload) if err != nil { return 0, err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return 0, errResp{resp} } var rec InteriorValidationRecord err = decodeResponse(resp, &rec) return rec.Id, err } // 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{}, pkgErr(err) } resp, err := c.c.Get(url) if err != nil { return InteriorValidationRecord{}, pkgErr(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return InteriorValidationRecord{}, pkgErr(errResp{resp}) } var rec InteriorValidationRecord if err := decodeResponse(resp, &rec); err != nil { return InteriorValidationRecord{}, pkgErr(err) } return rec, nil } // 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} resp, err := c.post(coverDimensionsPath, payload) if err != nil { return CoverDimensions{}, pkgErr(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return CoverDimensions{}, errResp{resp} } 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 0, pkgErr(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return 0, errResp{resp} } var rec CoverValidationRecord if err := decodeResponse(resp, &rec); err != nil { return 0, pkgErr(err) } return rec.Id, nil } // GetCoverValidiation retrieves information about a cover file validation job that was started by ValidiateCover(). // // https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Cover_read func (c *Client) GetCoverValidation(id uint) (CoverValidationRecord, error) { url, err := url.JoinPath(ApiUrl, validateCoverPath, fmt.Sprint(id)) if err != nil { return CoverValidationRecord{}, pkgErr(err) } resp, err := c.c.Get(url) if err != nil { return CoverValidationRecord{}, pkgErr(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return CoverValidationRecord{}, pkgErr(errResp{resp}) } var rec CoverValidationRecord if err := decodeResponse(resp, &rec); err != nil { return CoverValidationRecord{}, pkgErr(err) } return rec, 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 { buf := new(bytes.Buffer) if _, err := io.Copy(buf, resp.Body); err != nil { return errReadResp{resp, err} } dec := json.NewDecoder(buf) if err := dec.Decode(v); err != nil { return errDecResp{resp, buf.Bytes(), err} } return nil }