aboutsummaryrefslogtreecommitdiffstats
path: root/lulu.go
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-05-06 21:19:12 -0400
committerSam Anthony <sam@samanthony.xyz>2026-05-06 21:19:12 -0400
commit2990789d98405291bfbb3e0dce4efca1153120ab (patch)
treed7bdc19b28909bdc2d5abf24f2c3575d199463ae /lulu.go
parentb6b81ac135e77f7856efb07389a6ac42cc137a6e (diff)
downloadlulu-2990789d98405291bfbb3e0dce4efca1153120ab.zip
implement POST /validate-interior
Diffstat (limited to 'lulu.go')
-rw-r--r--lulu.go150
1 files changed, 150 insertions, 0 deletions
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
+}