Skip to content

Fix API compatibility issues and update response structures #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
defaultBaseURL = "https://api.printix.net"
defaultAuthURL = "https://auth.printix.net/oauth/token"
testAuthURL = "https://auth.testenv.printix.net/oauth/token"
submitEndpoint = "/cloudprint/tenants/%s/printers/%s/jobs"
submitEndpoint = "/cloudprint/tenants/%s/printers/%s/queues/%s/submit"
completeUploadEndpoint = "/cloudprint/completeUpload"
printersEndpoint = "/cloudprint/tenants/%s/printers"
jobsEndpoint = "/cloudprint/tenants/%s/jobs"
Expand All @@ -38,6 +38,7 @@ type Client struct {
testMode bool
rateLimitRemain int
rateLimitReset time.Time
userIdentifier string
}

// Option is a function that configures the client.
Expand Down Expand Up @@ -79,14 +80,22 @@ func WithAuthURL(authURL string) Option {
}
}

// WithUserIdentifier sets the user identifier for print jobs.
func WithUserIdentifier(userIdentifier string) Option {
return func(c *Client) {
c.userIdentifier = userIdentifier
}
}

// New creates a new Printix client.
func New(clientID, clientSecret string, opts ...Option) *Client {
c := &Client{
httpClient: &http.Client{Timeout: 30 * time.Second},
baseURL: defaultBaseURL,
authURL: defaultAuthURL,
clientID: clientID,
clientSecret: clientSecret,
httpClient: &http.Client{Timeout: 30 * time.Second},
baseURL: defaultBaseURL,
authURL: defaultAuthURL,
clientID: clientID,
clientSecret: clientSecret,
userIdentifier: "API Client",
}

for _, opt := range opts {
Expand Down Expand Up @@ -179,7 +188,7 @@ func (c *Client) doRequestWithHeaders(ctx context.Context, method, endpoint stri
if body != nil {
req.Header.Set("Content-Type", "application/json")
}

// Add custom headers
for key, value := range customHeaders {
req.Header.Set(key, value)
Expand Down
123 changes: 87 additions & 36 deletions print.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,72 @@ import (
"net/http"
"net/url"
"os"
"time"
)

// UserMapping represents user mapping for print job assignment.
type UserMapping struct {
Key string `json:"key"` // AzureObjectId, AzureUPN, SAMAccountName, OnPremImmutableId, OnPremUpn, Email
Value string `json:"value"` // Value to filter for
}

// PrintJob represents a print job submission.
type PrintJob struct {
PrinterID string `json:"-"` // Not sent in body, used in URL
Title string `json:"title,omitempty"`
User string `json:"user,omitempty"`
PDL string `json:"PDL,omitempty"`
// v1.1 properties
Color *bool `json:"color,omitempty"`
Duplex string `json:"duplex,omitempty"` // NONE, SHORT_EDGE, LONG_EDGE
PageOrientation string `json:"page_orientation,omitempty"` // PORTRAIT, LANDSCAPE, AUTO
Copies *int `json:"copies,omitempty"`
MediaSize string `json:"media_size,omitempty"`
Scaling string `json:"scaling,omitempty"` // NOSCALE, SHRINK, FIT
TestMode bool `json:"-"` // Not sent to API
UseV11 bool `json:"-"` // Use v1.1 API
QueueID string `json:"-"` // Not sent in body, used in URL
Title string `json:"-"` // Not sent in body, used in URL query
User string `json:"-"` // Not sent in body, used in URL query
PDL string `json:"-"` // Not sent in body, used in URL query
// v1.1 properties (sent in body)
Color *bool `json:"color,omitempty"`
Duplex string `json:"duplex,omitempty"` // NONE, SHORT_EDGE, LONG_EDGE
PageOrientation string `json:"page_orientation,omitempty"` // PORTRAIT, LANDSCAPE, AUTO
Copies *int `json:"copies,omitempty"`
MediaSize string `json:"media_size,omitempty"`
Scaling string `json:"scaling,omitempty"` // NOSCALE, SHRINK, FIT
UserMapping *UserMapping `json:"userMapping,omitempty"`
// Control fields
ReleaseImmediately *bool `json:"-"` // Not sent in body, used in URL query
TestMode bool `json:"-"` // Not sent in body, used in URL query
UseV11 bool `json:"-"` // Use v1.1 API
}

// SubmitResponse represents the response from submitting a print job.
type SubmitResponse struct {
Response
Job struct {
ID string `json:"id"`
CreateTime int64 `json:"createTime"`
UpdateTime int64 `json:"updateTime"`
CreateTime string `json:"createTime"` // ISO format timestamp
UpdateTime string `json:"updateTime"` // ISO format timestamp
Status string `json:"status"`
OwnerID string `json:"ownerId"`
ContentType string `json:"contentType"`
Title string `json:"title"`
Links struct {
Self struct {
Href string `json:"href"`
} `json:"self"`
Printer struct {
Href string `json:"href"`
} `json:"printer"`
ChangeOwner struct {
Href string `json:"href"`
Templated bool `json:"templated"`
} `json:"changeOwner"`
} `json:"_links"`
} `json:"job"`
UploadLinks []struct {
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Type string `json:"type"` // "Azure" or "GCP"
Type string `json:"type,omitempty"` // "Azure" or "GCP" - not always present
} `json:"uploadLinks"`
Links struct {
Self struct {
Href string `json:"href"`
} `json:"self"`
UploadCompleted struct {
Href string `json:"href"`
} `json:"uploadCompleted"`
ChangeOwner struct {
Href string `json:"href"`
Templated bool `json:"templated"`
} `json:"changeOwner"`
} `json:"_links"`
}

Expand All @@ -62,11 +84,13 @@ type CompleteUploadRequest struct {

// PrintOptions represents print job options.
type PrintOptions struct {
Copies int `json:"copies,omitempty"`
Color bool `json:"color,omitempty"`
Duplex string `json:"duplex,omitempty"` // "none", "long-edge", "short-edge"
PageRange string `json:"pageRange,omitempty"`
Orientation string `json:"orientation,omitempty"` // "portrait", "landscape"
Copies int `json:"copies,omitempty"` // Number of copies (positive integer)
Color bool `json:"color,omitempty"` // true for color, false for monochrome
Duplex string `json:"duplex,omitempty"` // "none", "long-edge", "short-edge"
Orientation string `json:"orientation,omitempty"` // "portrait", "landscape"
MediaSize string `json:"mediaSize,omitempty"` // Paper size: A0-A5, B4-B5, LETTER, LEGAL, etc.
Scaling string `json:"scaling,omitempty"` // "NOSCALE", "SHRINK", "FIT"
PageRange string `json:"pageRange,omitempty"` // Page range (not used in v1.1 API)
}

// Submit creates a new print job.
Expand All @@ -75,7 +99,10 @@ func (c *Client) Submit(ctx context.Context, job *PrintJob) (*SubmitResponse, er
return nil, fmt.Errorf("tenant ID is required for job submission")
}

endpoint := fmt.Sprintf(submitEndpoint, c.tenantID, job.PrinterID)
if job.QueueID == "" {
return nil, fmt.Errorf("queue ID is required for job submission")
}
endpoint := fmt.Sprintf(submitEndpoint, c.tenantID, job.PrinterID, job.QueueID)

// Add query parameters
params := url.Values{}
Expand All @@ -91,6 +118,12 @@ func (c *Client) Submit(ctx context.Context, job *PrintJob) (*SubmitResponse, er
if c.testMode || job.TestMode {
params.Set("test", "true")
}
// Handle releaseImmediately parameter (default is true)
if job.ReleaseImmediately != nil && !*job.ReleaseImmediately {
params.Set("releaseImmediately", "false")
} else if job.ReleaseImmediately == nil {
params.Set("releaseImmediately", "true")
}
Comment on lines +122 to +126

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic for handling the releaseImmediately query parameter is incomplete. It misses the case where job.ReleaseImmediately is explicitly set to true. In that scenario, the parameter is not added to the query string at all, which is a bug.

Suggested change
if job.ReleaseImmediately != nil && !*job.ReleaseImmediately {
params.Set("releaseImmediately", "false")
} else if job.ReleaseImmediately == nil {
params.Set("releaseImmediately", "true")
}
if job.ReleaseImmediately != nil && !*job.ReleaseImmediately {
params.Set("releaseImmediately", "false")
} else {
params.Set("releaseImmediately", "true")
}


if len(params) > 0 {
endpoint += "?" + params.Encode()
Expand All @@ -103,7 +136,6 @@ func (c *Client) Submit(ctx context.Context, job *PrintJob) (*SubmitResponse, er
if job.UseV11 || job.Color != nil || job.Duplex != "" || job.PageOrientation != "" ||
job.Copies != nil || job.MediaSize != "" || job.Scaling != "" {
headers["version"] = "1.1"
headers["Content-Type"] = "application/json"

// Build v1.1 request body
v11Body := make(map[string]any)
Expand All @@ -125,10 +157,14 @@ func (c *Client) Submit(ctx context.Context, job *PrintJob) (*SubmitResponse, er
if job.Scaling != "" {
v11Body["scaling"] = job.Scaling
}

if len(v11Body) > 0 {
requestBody = v11Body
if job.UserMapping != nil {
v11Body["userMapping"] = job.UserMapping
} else {
v11Body["userMapping"] = nil
}
Comment on lines +160 to 164

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This if/else block for setting userMapping in the v1.1 request body is more verbose than necessary. Since job.UserMapping is a pointer, it can be assigned directly to the map.

v11Body["userMapping"] = job.UserMapping


// Always send body for v1.1, even if empty
requestBody = v11Body
}

resp, err := c.doRequestWithHeaders(ctx, http.MethodPost, endpoint, requestBody, headers)
Expand Down Expand Up @@ -163,9 +199,8 @@ func (c *Client) UploadDocument(ctx context.Context, uploadLink string, headers
req.Header.Set(k, v)
}

// Use a separate HTTP client for cloud storage (no auth needed)
storageClient := &http.Client{Timeout: 60 * time.Second}
resp, err := storageClient.Do(req)
// Use the configured HTTP client for cloud storage uploads
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("uploading document: %w", err)
}
Expand Down Expand Up @@ -205,7 +240,7 @@ func (c *Client) CompleteUpload(ctx context.Context, completeURL string) error {
}

// PrintFile prints a file using Printix.
func (c *Client) PrintFile(ctx context.Context, printerID, title, filePath string, options *PrintOptions) error {
func (c *Client) PrintFile(ctx context.Context, printerID, queueID, title, filePath string, options *PrintOptions) error {
// Read the file
data, err := os.ReadFile(filePath)
if err != nil {
Expand All @@ -230,8 +265,9 @@ func (c *Client) PrintFile(ctx context.Context, printerID, title, filePath strin
// Create print job
job := &PrintJob{
PrinterID: printerID,
QueueID: queueID,
Title: title,
User: "MTS API",
User: c.userIdentifier,
PDL: pdl,
TestMode: c.testMode,
}
Expand Down Expand Up @@ -261,6 +297,13 @@ func (c *Client) PrintFile(ctx context.Context, printerID, title, filePath strin
case "landscape":
job.PageOrientation = "LANDSCAPE"
}
// Add new v1.1 options
if options.MediaSize != "" {
job.MediaSize = options.MediaSize
}
if options.Scaling != "" {
job.Scaling = options.Scaling
}
}

// Submit the job
Expand Down Expand Up @@ -288,12 +331,13 @@ func (c *Client) PrintFile(ctx context.Context, printerID, title, filePath strin
}

// PrintData prints raw data using Printix.
func (c *Client) PrintData(ctx context.Context, printerID, title string, data []byte, pdl string, options *PrintOptions) error {
func (c *Client) PrintData(ctx context.Context, printerID, queueID, title string, data []byte, pdl string, options *PrintOptions) error {
// Create print job
job := &PrintJob{
PrinterID: printerID,
QueueID: queueID,
Title: title,
User: "MTS API",
User: c.userIdentifier,
PDL: pdl,
TestMode: c.testMode,
}
Expand Down Expand Up @@ -323,6 +367,13 @@ func (c *Client) PrintData(ctx context.Context, printerID, title string, data []
case "landscape":
job.PageOrientation = "LANDSCAPE"
}
// Add new v1.1 options
if options.MediaSize != "" {
job.MediaSize = options.MediaSize
}
if options.Scaling != "" {
job.Scaling = options.Scaling
}
}

// Submit the job
Expand Down
Loading