Rust client for the TofuPilot REST API. Async, typed, with retries and request lifecycle hooks.
Add to your Cargo.toml:
[dependencies]
tofupilot = "0.1"
tokio = { version = "1", features = ["full"] }use tofupilot::TofuPilot;
use tofupilot::types::*;
#[tokio::main]
async fn main() -> tofupilot::Result<()> {
let client = TofuPilot::new("your-api-key");
// Create a test run
let run = client.runs().create()
.procedure_id("550e8400-e29b-41d4-a716-446655440000")
.serial_number("SN-001234")
.part_number("PCB-V1.2")
.outcome(Outcome::Pass)
.started_at(chrono::Utc::now() - chrono::TimeDelta::minutes(5))
.ended_at(chrono::Utc::now())
.send()
.await?;
println!("Created run: {}", run.id);
Ok(())
}| Resource | Methods | Docs |
|---|---|---|
| Runs | list, create, get, update, delete | docs/sdks/runs |
| Procedures | list, create, get, update, delete | docs/sdks/procedures |
| Units | list, create, get, update, delete, add_child, remove_child | docs/sdks/units |
| Parts | list, create, get, update, delete | docs/sdks/parts |
| Batches | list, create, get, update, delete | docs/sdks/batches |
| Stations | list, create, get, get_current, update, remove | docs/sdks/stations |
| Revisions | create, get, update, delete | docs/sdks/revisions |
| Versions | create, get, delete | docs/sdks/versions |
| Attachments | initialize, finalize, delete, upload_file, upload_bytes, download_file | docs/sdks/attachments |
| User | list | docs/sdks/user |
All model types are documented in docs/models/.
Every API call uses the builder pattern. Required fields are enforced at send time:
// Optional fields are chained before .send()
let runs = client.runs().list()
.outcomes(vec![Outcome::Pass])
.part_numbers(vec!["PCB-V1.2".into()])
.limit(50)
.sort_by(RunListSortBy::StartedAt)
.sort_order(ListSortOrder::Desc)
.send()
.await?;
for run in &runs.data {
println!("{}: {:?}", run.id, run.outcome);
}The SDK provides high-level helpers that handle the three-step upload flow (initialize → PUT → finalize) in a single call:
// Upload from disk
let upload = client.attachments().upload_file("report.pdf").await?;
// Or upload raw bytes
let csv = b"header\nrow1\nrow2";
let upload = client.attachments().upload_bytes("data.csv", csv.to_vec()).await?;
// Link to a run
client.runs().update()
.id(&run.id)
.attachments(vec![upload.id])
.send()
.await?;
// Download a file
client.attachments().download_file(&upload.url, "local-report.pdf").await?;use tofupilot::types::*;
let now = chrono::Utc::now();
let run = client.runs().create()
.procedure_id(proc_id)
.serial_number("SN-001")
.part_number("PCB-V1")
.outcome(Outcome::Pass)
.started_at(now - chrono::TimeDelta::minutes(5))
.ended_at(now)
.phases(vec![RunCreatePhases::builder()
.name("voltage_check")
.outcome(PhasesOutcome::Pass)
.started_at(now - chrono::TimeDelta::minutes(5))
.ended_at(now - chrono::TimeDelta::minutes(3))
.measurements(vec![RunCreateMeasurements::builder()
.name("output_voltage")
.outcome(ValidatorsOutcome::Pass)
.measured_value(serde_json::json!(3.3))
.units("V")
.build()
.unwrap()
])
.build()
.unwrap()
])
.send()
.await?;All API errors are typed:
use tofupilot::Error;
match client.runs().get().id("nonexistent").send().await {
Ok(run) => println!("Found: {}", run.id),
Err(Error::NotFound(e)) => println!("Not found: {}", e.message),
Err(Error::Unauthorized(e)) => println!("Bad API key: {}", e.message),
Err(Error::BadRequest(e)) => {
println!("Validation error: {}", e.message);
for issue in &e.issues {
println!(" - {}", issue.message);
}
}
Err(e) => println!("Other error: {e}"),
}The client automatically retries on 429 (rate limit) and 5xx errors with exponential backoff. Configure via ClientConfig:
use tofupilot::config::ClientConfig;
use std::time::Duration;
let client = TofuPilot::with_config(
ClientConfig::new("your-api-key")
.base_url("https://your-instance.tofupilot.app/api")
.timeout(Duration::from_secs(60))
.max_retries(5),
);Inspect or modify requests and responses:
use tofupilot::Hooks;
let hooks = Hooks::new()
.on_before_request(|ctx, req| async move {
println!("[{}] {} {}", ctx.operation_id, req.method(), req.url());
req
})
.on_after_error(|ctx, err| {
let msg = format!("[{}] Error: {err}", ctx.operation_id);
async move { eprintln!("{msg}"); }
});
let client = TofuPilot::with_config(
ClientConfig::new("your-api-key").hooks(hooks),
);Some fields distinguish between "not sent" and "explicitly null". These use NullableField<T>:
use tofupilot::types::NullableField;
// Has a value (From<T> impl)
let field: NullableField<String> = "hello".to_string().into();
// Convenience constructors
let field = NullableField::value("hello".to_string());
let field: NullableField<String> = NullableField::null();Builder methods handle this automatically — you never need to construct NullableField manually:
client.runs().create()
.procedure_version("1.2.3") // sets Value("1.2.3")
.procedure_version_null() // sets Null
// omitted fields default to AbsentPoint the client at your own TofuPilot instance:
let client = TofuPilot::with_config(
ClientConfig::new("your-api-key")
.base_url("https://your-instance.example.com/api"),
);Override server URL or timeout for individual requests:
let result = client.runs().list()
.server_url("https://staging.tofupilot.app/api")
.timeout(std::time::Duration::from_secs(120))
.send()
.await?;