// 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) { var rec InteriorValidationRecord err := c.postDecode(validateInteriorPath, payload, http.StatusCreated, &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) { path, err := url.JoinPath(validateInteriorPath, fmt.Sprint(id)) if err != nil { return InteriorValidationRecord{}, pkgErr(err) } var rec InteriorValidationRecord if err := c.getDecode(path, &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} var dims CoverDimensions err := c.postDecode(coverDimensionsPath, payload, http.StatusCreated, &dims) if 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} var rec CoverValidationRecord err := c.postDecode(validateCoverPath, payload, http.StatusCreated, &rec) if 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) { path, err := url.JoinPath(validateCoverPath, fmt.Sprint(id)) if err != nil { return CoverValidationRecord{}, pkgErr(err) } var rec CoverValidationRecord if err := c.getDecode(path, &rec); err != nil { return CoverValidationRecord{}, pkgErr(err) } return rec, nil } func (c *Client) getDecode(path string, v any) error { url, err := url.JoinPath(ApiUrl, path) if err != nil { return err } resp, err := c.c.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return errResp{resp} } return decodeResponse(resp, v) } func (c *Client) postDecode(path string, payload any, wantStatus int, v any) error { resp, err := c.post(path, payload) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != wantStatus { return errResp{resp} } return decodeResponse(resp, v) } 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 }