caretly 0.1.0

Client for the Caretly API: render ZPL labels to PDF or PNG.
Documentation

caretly

crates.io docs.rs

Rust client for the Caretly API: render ZPL labels to vector PDF or PNG. It has an async client for tokio and a blocking client, sharing the same options, result and error types.

Source: https://github.com/jaapieaapie1/zpl (folder clients/rust).

Install

cargo add caretly

Or in Cargo.toml:

[dependencies]
caretly = "0.1"

Features

Feature Default What it adds
async yes caretly::Client, the async client for tokio.
rustls-tls yes HTTPS through rustls.
blocking no caretly::blocking::Client, no async runtime needed.
native-tls no HTTPS through the platform's TLS library instead of rustls.
gzip no The compress option: gzip the request body.

Blocking only, without tokio in your code:

caretly = { version = "0.1", default-features = false, features = ["blocking", "rustls-tls"] }

Keep one of rustls-tls or native-tls on to reach https:// URLs; without them only http:// (for example a self-hosted server on localhost) works.

Minimum supported Rust version: 1.85. On toolchains older than the newest dependency releases need, let Cargo pick compatible versions with resolver = "3" (or CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback).

Quickstart

Create an API key in the console. Test keys (zpl_test_…) are free and watermark every label; live keys (zpl_live_…) are billed per label. The client reads the key from CARETLY_API_KEY unless you pass one.

Async

use caretly::{Client, Dpmm, Format, RenderOptions};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new()?; // reads CARETLY_API_KEY

    let pdf = client.render_pdf("^XA^FO50,50^A0N,40,40^FDHello^FS^XZ").await?;
    pdf.save("label.pdf")?;
    println!("{} page(s)", pdf.page_count);

    let options = RenderOptions::new().format(Format::Png).page(1).dpmm(Dpmm::D12);
    let png = client.render("^XA^FO50,50^A0N,40,40^FDHello^FS^XZ", options).await?;
    png.save("label.png")?;
    Ok(())
}

Blocking

use caretly::blocking::Client;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder().api_key("zpl_test_...").build()?;
    let pdf = client.render_pdf("^XA^FO50,50^A0N,40,40^FDHello^FS^XZ")?;
    pdf.save("label.pdf")?;
    Ok(())
}

Do not call the blocking client from inside an async runtime; use the async client there.

Several files

Each file starts from a fresh printer state; pages follow the order of the files.

# async fn run(client: caretly::Client) -> Result<(), caretly::Error> {
let result = client
    .render_files([("a.zpl", "^XA^FDa^FS^XZ"), ("b.zpl", "^XA^FDb^FS^XZ")], Default::default())
    .await?;
println!("pages per file: {:?}", result.file_page_counts);
# Ok(()) }

File contents can be &str, String, &[u8] or Vec<u8>.

Client settings

use std::time::Duration;

let client = caretly::Client::builder()
    .api_key("zpl_live_...")              // default: CARETLY_API_KEY
    .base_url("http://localhost:3000")    // default: https://caretly.dev
    .timeout(Duration::from_secs(10))     // per attempt, default: 30 s
    .max_retries(3)                       // default: 2
    .build()?;
# Ok::<(), caretly::Error>(())

An empty API key sends no Authorization header, for self-hosted servers without keys.

Render options

Method Query / header Meaning
format(Format::Pdf | Format::Png) format Output format. Default PDF (every page).
page(n) page For PNG: the page to render, from 1.
dpmm(Dpmm::D6 | D8 | D12 | D24) dpmm Resolution in dots/mm. Default 8.
timings(true) timings=1 Ask for a Server-Timing header.
idempotency_key("order-1234") Idempotency-Key A retry with the same key and content is billed once. Max 255 characters.
compress(true) (feature gzip) Content-Encoding: gzip Gzip the request body.

Unset options are left out of the request, so the server defaults apply. page(0), an empty or too long idempotency key, and an empty file list fail before anything is sent with Error::InvalidArgument. Dpmm::try_from(n) checks a numeric resolution.

Result

RenderResult holds:

Field From Meaning
bytes body The PDF or PNG. save(path) writes it to a file.
content_type Content-Type application/pdf or image/png.
page_count X-Page-Count Total pages.
file_page_counts X-File-Page-Counts Pages per file (multipart only, else empty).
labels_billed X-Labels-Billed Labels billed; 0 for a test key; None without keys.
rate_limit RateLimit-Limit, RateLimit-Remaining Burst size and what is left.
server_timing Server-Timing Raw header, with timings(true).

Errors

Every call returns Result<_, caretly::Error>:

Variant Cause
BadRequest 400: ZPL parse error, invalid parameter, page out of range.
Authentication 401: key missing, invalid, expired or revoked.
PaymentRequired 402: billing inactive, live keys paused.
PayloadTooLarge 413: body over 100 MiB decompressed.
UnsupportedEncoding 415: unsupported Content-Encoding.
RateLimited 429: too many requests (retried).
Overloaded 503: server busy, not billed (retried).
Api Any other status, including other 5xx.
Connection Network failure (retried).
Timeout The timeout elapsed (not retried).
InvalidArgument Client-side validation failed; nothing was sent.

The API variants carry an ApiError with status, message (the response body) and retry_after. Error::status(), Error::retry_after() and Error::api_error() reach them without matching.

# async fn run(client: caretly::Client) {
match client.render_pdf("^XA^FO50,50^FDoops").await {
    Ok(pdf) => println!("{} bytes", pdf.bytes.len()),
    Err(caretly::Error::BadRequest(e)) => eprintln!("invalid ZPL: {}", e.message),
    Err(e) => eprintln!("render failed: {e}"),
}
# }

Retries

A 429, a 503 or a connection error is retried up to max_retries times (default 2). The client waits Retry-After seconds when the server sends it, and otherwise backs off 0.5 s, 1 s, 2 s, 4 s, then 8 s. Other statuses and timeouts are not retried.

When retries are on and you did not set an idempotency key, the client generates one (a UUID v4) per call and sends it on every attempt, so a retried request is never billed twice. Set max_retries(0) to turn retries off.

Examples

CARETLY_API_KEY=zpl_test_... cargo run --example render_pdf
CARETLY_API_KEY=zpl_test_... cargo run --example render_files -- a.zpl b.zpl
CARETLY_API_KEY=zpl_test_... cargo run --example blocking --features blocking

Development

cargo test --all-features
cargo test --no-default-features --features blocking
cargo clippy --all-features --all-targets -- -D warnings
cargo fmt --check

The tests run against a local mock server; they need no network or API key.

Publishing

This crate lives in clients/rust and is not part of the repository's root workspace (its Cargo.toml has its own [workspace] table). From clients/rust:

  1. Update version in Cargo.toml and add an entry to CHANGELOG.md.
  2. Run the checks above, then cargo package --list to review the packaged files.
  3. cargo publish --dry-run
  4. Log in once with cargo login <token> (a token from https://crates.io/settings/tokens with the publish-new / publish-update scope).
  5. cargo publish
  6. Tag the release: git tag clients/rust/v0.1.0 && git push origin clients/rust/v0.1.0.

docs.rs builds the documentation with all features after publishing.

License

MIT, see LICENSE.