Generate a Rust SDK from your OpenAPI spec

Octri reads your OpenAPI document and writes a Rust crate with serde models and typed errors, publishes it to crates.io under your own crate name, and rebuilds it every time the spec changes.

Install from crates.io
cargo add acme-api

The client they actually get

What your users write once the Rust package is installed.

Two call-site shapes come out of the same spec. class-namespaced is the default: a client object whose operations are grouped by resource, which is the surface every widely used SDK ships and the one your own reference docs already assume. functions is the override: no client object, one configure call, and each operation called directly. It is a single setting, per language, and nothing else about the client changes with it.

Every call returns a Result, so ? works the way it does in the rest of your users’ code, and a failure they forget to handle is a compiler warning. Optional parameters are Option values in spec order.

Published with your crates.io token, and docs.rs builds the rustdoc for every release on its own, so the reference page your users read updates with the crate.

Client object (default)
use acme_api::{Acme, ClientAuthConfig, ClientConfig};

let acme = Acme::new(ClientConfig {
    base_url: Some("https://api.acme.com/v1".to_string()),
    auth: Some(ClientAuthConfig {
        bearer: Some(std::env::var("ACME_TOKEN")?),
        ..Default::default()
    }),
    ..Default::default()
});

// The closure sees each item. Return false to stop paging early.
acme.users.list_paginated(Some(100), None, None, |user| {
    println!("{}", user.id);
    true
})?;

let invoice = acme.invoices.create("cus_123".to_string(), 4900, None)?;
Free functions
use acme_api::{ClientAuthConfig, ClientConfig, SdkConfig};
use acme_api::methods::users;

SdkConfig::configure(ClientConfig {
    base_url: Some("https://api.acme.com/v1".to_string()),
    auth: Some(ClientAuthConfig {
        bearer: Some(std::env::var("ACME_TOKEN")?),
        ..Default::default()
    }),
    ..Default::default()
});

users::list_users_paginated(Some(100), None, None, |user| {
    println!("{}", user.id);
    true
})?;

The package at a glance

What a developer checks before adding the Rust package to a project.

Runs on
Stable Rust, edition 2021
Transport
ureq, blocking, so no async runtime is required
Runtime dependencies
ureq, serde, serde_json and base64
Models
Structs deriving Serialize, Deserialize, Clone and Debug
Pagination
Closure that returns false to stop
Ships to
crates.io, with rustdoc on docs.rs

When a call fails

SdkError is an enum, so a match names validation, HTTP, network and timeout failures as separate arms, and kind() maps the status codes callers branch on most to variants.

Typed errors
use acme_api::{SdkError, SdkHttpErrorKind};

match acme.invoices.create("cus_123".to_string(), 4900, None) {
    Ok(invoice) => println!("created {}", invoice.id),
    Err(SdkError::Http(err)) if matches!(err.kind(), SdkHttpErrorKind::RateLimit) => {
        // 429 after the built-in retries. Back off at the caller.
    }
    Err(SdkError::Http(err)) => {
        eprintln!("status {} request {:?}", err.status_code, err.request_id);
    }
    Err(other) => return Err(other.into()),
}

What you decide for Rust

Rust has no engine setting. The operations block on ureq, and an optional async feature is there for the request layer.

  • Client style

    class-namespaced (default), class, namespaced, functions

    A client struct with a public field per resource, so acme.users.list() reads like the clients Rust users already know. Functions are module-level, under acme_api::methods.

  • Argument style

    positional (default), object

    Required parameters by value and optional ones as Option, in spec order. Object moves them into a params struct, which is kinder to callers once an operation has more than three or four.

  • Doc comments

    full (default), minimal

    Full becomes rustdoc, which is the page your users read on docs.rs. Rust developers expect that page to be complete, so keep it on full.

Method naming and namespacing work the same in every language: operations are grouped by your spec’s tags or by path, and the namespace word is dropped from the method name unless you ask to keep the operationId whole.

crates.io is permanent and unscoped

Crate names are global and first come, first served, with no organisation scopes to fall back on. A name cannot be renamed or reused once published, and a version can be yanked but never replaced. Pick the crate name before the first release, and check it is free on crates.io before you announce it anywhere.

crates.io crate name
acme-api
  • Not possible: Rename the crate
  • Not possible: Replace a published version
  • Still possible: Yank a version

The same in every generated client

The parts nobody wants to hand-write, and the parts a hand-written client usually skips.

  • Retries with exponential backoff and jitter, on the statuses that mean "try again", with a per-attempt timeout.
  • Idempotency keys on the methods that need them, so a retried write does not become two.
  • Auth wired in from your spec’s security scheme, read from wherever your users keep secrets.
  • Typed errors split by cause: a failed constraint, an HTTP status, a network failure and a timeout are four different things.
  • Pagination that follows whichever contract your spec declares, cursor, offset, page number or next URL, without prefetching the collection.
  • Server-sent events as a native stream, for the operations that stream.
  • Error reporting to Octri’s monitoring, pre-wired and switched off until a consumer opts in.
  1. Point it at your spec

    Upload an OpenAPI or Swagger document, paste a URL, or connect the repository it lives in so a merge to main updates it.

  2. Read the audit

    The spec is scored out of 10 on what a generator can do with it, with every missing schema and colliding operationId named. 9 of the fourteen rules carry a button that writes the fix.

  3. Shape the client

    Client style, argument style, namespacing, auth, retries and pagination, set once for the project and overridden per language or per endpoint.

  4. Generate and read it

    The package is generated, compiled and handed to you as source you can read before anyone installs it.

  5. Publish, then keep publishing

    Release to the registry under your own account. Every later spec change regenerates the client, and the version number comes from your own document.

Questions

The operation methods block, on ureq, so a consumer can call your API from a CLI or a build script without pulling in an async runtime. tokio and reqwest sit behind an optional async feature in Cargo.toml and are only compiled when someone enables it.