Skip to main content

Crate mdcast_client

Crate mdcast_client 

Source
Expand description

Async HTTP client for mdcast-server, with the content-addressed asset negotiation made invisible.

Hand it a document and a bag of assets. It hashes them, builds the AssetManifest, posts the render, and — if the server answers 409 with blobs it does not hold — uploads exactly those and retries once. You never compute a digest, never build a manifest, and never write a retry loop.

use mdcast_client::{AssetBundle, Client, request};
use mdcast_api::Target;

let client = Client::builder("http://mdcast.internal:8080")
    .token(std::env::var("MDCAST_TOKEN")?)
    .build()?;

let bundle = AssetBundle::new().with("img/chart.svg", std::fs::read("chart.svg")?);
let artifact = client
    .render(request::markdown("# Q3\n\n![](img/chart.svg)", Target::Pdf), &bundle)
    .await?;

std::fs::write(&artifact.filename, &artifact.bytes)?;

That is the whole cold-cache exchange: three requests (409 → upload → 200) behind one await. A warm cache costs one.

§Why this crate does not depend on mdcast

Speaking the protocol needs a handful of serde structs, not a rendering engine — so this crate depends on mdcast_api and an HTTP client, and never on mdcast itself, not even with default-features = false. Depending on it would compile rust-embed (which pulls the whole vendored reveal.js tree and the reference OOXML documents in through a proc macro), clap, tempfile, toml, regex, pulldown-cmark, futures, tracing-subscriber, typst and pandoc’s zip/xml stack into a process whose entire job is to POST some JSON. That cost is exactly what the workspace split exists to prevent (PROJECT_PLAN.md §7.1).

§Module map

ModuleContents
clientClient + ClientBuilder — render, with the 409 retry folded in
blobsClient::check / Client::upload / Client::preflight — the explicit, pessimistic path
bundleAssetBundle — key → bytes, and the hashing that turns it into a manifest
artifactArtifact — bytes, filename, content type
requestconstructors for the two wire request bodies
errorError — transport, 401, 413, a 409 that survived the retry, a server ErrorResponse, a bundle that cannot satisfy its manifest

§Two strategies

Client::render is optimistic: assume the server has the assets, react to a 409 if it does not. That is one request in the steady state and three on a cold cache, and it is the default.

A caller that would rather never discover a large upload mid-render calls Client::preflight first — POST /v1/blobs/check, upload what came back, then render — trading a guaranteed extra round trip for a render that cannot 409.

Re-exports§

pub use artifact::Artifact;
pub use bundle::AssetBundle;
pub use bundle::BlobFetchError;
pub use bundle::sha256;
pub use client::Client;
pub use client::ClientBuilder;
pub use error::Error;
pub use error::Result;
pub use mdcast_api;

Modules§

artifact
What a successful render hands back: the bytes, plus the two things the response headers say about them.
blobs
The blob half of the protocol: the “what are you missing?” probe, the multipart upload that answers it, and the pessimistic combination of the two.
bundle
The caller-facing half of asset negotiation: a bag of key → bytes that knows how to hash itself.
client
The client itself: one call per render, with the blob negotiation folded into it.
error
The one error type every call returns.
request
Constructors for the two wire request bodies.