Rust SDK 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]asyncfnmain() -> tofupilot::Result<()>{let client = TofuPilot::new("your-api-key");// Create a test runlet run = client.runs().create().procedure_id("550e8400-e29b-41d4-a716-446655440000").serial_number("SN-001234").part_number("PCB-V1.2").outcome(LogGetOutcome::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 |
| Runs.Attachments | upload, download | - |
| Procedures | list, create, get, update, delete | docs/sdks/procedures |
| Units | list, create, get, update, delete, add_child, remove_child | docs/sdks/units |
| Units.Attachments | upload, download, delete | - |
| 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 |
| 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![LogGetOutcome::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);}Attach files directly to runs or units:
// Upload a file to a runlet id = client.runs().attachments().upload(&run.id,"report.pdf").await?;// Upload a file to a unitlet id = client.units().attachments().upload("SN-0001","calibration.pdf").await?;// Download an attachment
client.runs().attachments().download(&url,"local-report.pdf").await?;// Delete a unit attachment
client.units().attachments().delete("SN-0001",vec![id]).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(LogGetOutcome::Pass).started_at(now - chrono::TimeDelta::minutes(5)).ended_at(now).phases(vec![RunCreatePhases::builder().name("voltage_check").outcome(PhaseGetOutcome::Pass).started_at(now - chrono::TimeDelta::minutes(5)).ended_at(now - chrono::TimeDelta::minutes(3)).measurements(vec![RunCreateMeasurements::builder().name("output_voltage").outcome(Outcome::Pass).measured_value(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| asyncmove{println!("[{}] {} {}", ctx.operation_id, req.method(), req.url());
req
}).on_after_error(|ctx, err| {let msg = format!("[{}] Error: {err}", ctx.operation_id);asyncmove{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 constructorslet 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?;