From 2990789d98405291bfbb3e0dce4efca1153120ab Mon Sep 17 00:00:00 2001 From: Sam Anthony Date: Wed, 6 May 2026 21:19:12 -0400 Subject: implement POST /validate-interior --- lulu.go | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) (limited to 'lulu.go') diff --git a/lulu.go b/lulu.go index 3493991..44220b8 100644 --- a/lulu.go +++ b/lulu.go @@ -1 +1,151 @@ +// 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 = "VALIDATING" // file validation is still running + StatusValidated = "VALIDATED" // file validation finished without any errors + StatusNormalizing = "NORMALIZING" // file normalization (next step of validation, available only if pod_package_id is was passed in the payload) is still running + StatusNormalized = "NORMALIZED" // file normalization finished without any errors + StatusError = "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) + } + + dec := json.NewDecoder(resp.Body) + var rec InteriorValidationRecord + err = dec.Decode(&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 int) (InteriorValidationRecord, error) { + return InteriorValidationRecord{}, fmt.Errorf("not implemented") // TODO +} -- cgit v1.2.3