use std::time::Duration;
pub use ironflow_types::{ApiMeta, ApiResponse};
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use reqwest::{Client, RequestBuilder};
use serde::de::DeserializeOwned;
use uuid::Uuid;
use crate::error::{ApiErrorEnvelope, Error};
use crate::types;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub base_url: String,
pub api_key: String,
pub timeout: Duration,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct ListRunsFilter<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_steps: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
}
#[derive(Debug, Clone)]
pub struct IronflowClient {
pub(crate) http: Client,
base_url: String,
}
impl IronflowClient {
pub fn new(base_url: &str, api_key: &str) -> Self {
let config = ClientConfig {
base_url: base_url.trim_end_matches('/').to_string(),
api_key: api_key.to_string(),
timeout: Duration::from_secs(30),
};
Self::from_config(config)
}
pub fn from_config(config: ClientConfig) -> Self {
let mut headers = HeaderMap::new();
let auth_value = format!("Bearer {}", config.api_key);
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value).expect("invalid API key characters"),
);
let http = Client::builder()
.timeout(config.timeout)
.default_headers(headers)
.build()
.expect("failed to build HTTP client");
Self {
http,
base_url: config.base_url,
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub(crate) fn url(&self, path: &str) -> String {
format!("{}{path}", self.base_url)
}
pub(crate) fn get(&self, path: &str) -> RequestBuilder {
self.http.get(self.url(path))
}
fn post(&self, path: &str) -> RequestBuilder {
self.http.post(self.url(path))
}
fn put(&self, path: &str) -> RequestBuilder {
self.http.put(self.url(path))
}
fn delete(&self, path: &str) -> RequestBuilder {
self.http.delete(self.url(path))
}
fn patch(&self, path: &str) -> RequestBuilder {
self.http.patch(self.url(path))
}
pub(crate) async fn into_api_error(response: reqwest::Response) -> Error {
let status = response.status();
let status_code = status.as_u16();
let text = response.text().await.unwrap_or_default();
serde_json::from_str::<ApiErrorEnvelope>(&text)
.map(|env| Error::api(status_code, env.error.code, env.error.message))
.unwrap_or_else(|_| {
Error::api(
status_code,
status.canonical_reason().unwrap_or("UNKNOWN"),
text,
)
})
}
async fn send_envelope<T: DeserializeOwned>(
&self,
request: RequestBuilder,
) -> Result<ApiResponse<T>, Error> {
let response = request.send().await?;
if !response.status().is_success() {
return Err(Self::into_api_error(response).await);
}
let bytes = response.bytes().await?;
serde_json::from_slice::<ApiResponse<T>>(&bytes)
.map_err(|e| Error::Deserialize(format!("{e}: {}", String::from_utf8_lossy(&bytes))))
}
async fn send_no_content(&self, request: RequestBuilder) -> Result<(), Error> {
let response = request.send().await?;
if response.status().is_success() {
return Ok(());
}
Err(Self::into_api_error(response).await)
}
pub async fn health_check(&self) -> Result<(), Error> {
self.send_no_content(self.get("/api/v1/health-check")).await
}
pub async fn list_runs(&self) -> Result<ApiResponse<Vec<types::RunResponse>>, Error> {
self.list_runs_filtered(&ListRunsFilter::default()).await
}
pub async fn list_runs_filtered(
&self,
filter: &ListRunsFilter<'_>,
) -> Result<ApiResponse<Vec<types::RunResponse>>, Error> {
self.send_envelope(self.get("/api/v1/runs").query(filter))
.await
}
pub async fn create_run(
&self,
request: &types::CreateRunRequest,
) -> Result<ApiResponse<types::RunResponse>, Error> {
self.send_envelope(self.post("/api/v1/runs").json(request))
.await
}
pub async fn create_run_idempotent(
&self,
request: &types::CreateRunRequest,
idempotency_key: &str,
) -> Result<ApiResponse<types::RunResponse>, Error> {
self.send_envelope(
self.post("/api/v1/runs")
.header("Idempotency-Key", idempotency_key)
.json(request),
)
.await
}
pub async fn get_run(&self, id: Uuid) -> Result<ApiResponse<types::RunDetailResponse>, Error> {
self.send_envelope(self.get(&format!("/api/v1/runs/{id}")))
.await
}
async fn run_action(
&self,
id: Uuid,
action: &str,
) -> Result<ApiResponse<types::RunResponse>, Error> {
self.send_envelope(self.post(&format!("/api/v1/runs/{id}/{action}")))
.await
}
pub async fn cancel_run(&self, id: Uuid) -> Result<ApiResponse<types::RunResponse>, Error> {
self.run_action(id, "cancel").await
}
pub async fn approve_run(&self, id: Uuid) -> Result<ApiResponse<types::RunResponse>, Error> {
self.run_action(id, "approve").await
}
pub async fn reject_run(&self, id: Uuid) -> Result<ApiResponse<types::RunResponse>, Error> {
self.run_action(id, "reject").await
}
pub async fn retry_run(&self, id: Uuid) -> Result<ApiResponse<types::RunResponse>, Error> {
self.run_action(id, "retry").await
}
pub async fn list_workflows(&self) -> Result<ApiResponse<Vec<types::WorkflowSummary>>, Error> {
self.send_envelope(self.get("/api/v1/workflows")).await
}
pub async fn get_workflow(
&self,
name: &str,
) -> Result<ApiResponse<types::WorkflowDetailResponse>, Error> {
self.send_envelope(self.get(&format!("/api/v1/workflows/{name}")))
.await
}
pub async fn get_stats(&self) -> Result<ApiResponse<types::StatsResponse>, Error> {
self.send_envelope(self.get("/api/v1/stats")).await
}
pub async fn sign_in(&self, request: &types::SignInRequest) -> Result<(), Error> {
self.send_no_content(self.post("/api/v1/auth/sign-in").json(request))
.await
}
pub async fn sign_out(&self) -> Result<(), Error> {
self.send_no_content(self.post("/api/v1/auth/sign-out"))
.await
}
pub async fn me(&self) -> Result<ApiResponse<types::MeResponse>, Error> {
self.send_envelope(self.get("/api/v1/auth/me")).await
}
pub async fn list_api_keys(&self) -> Result<ApiResponse<Vec<types::ApiKeyResponse>>, Error> {
self.send_envelope(self.get("/api/v1/api-keys")).await
}
pub async fn create_api_key(
&self,
request: &types::CreateApiKeyRequest,
) -> Result<ApiResponse<types::CreateApiKeyResponse>, Error> {
self.send_envelope(self.post("/api/v1/api-keys").json(request))
.await
}
pub async fn available_scopes(&self) -> Result<ApiResponse<Vec<types::ScopeEntry>>, Error> {
self.send_envelope(self.get("/api/v1/api-keys/scopes"))
.await
}
pub async fn delete_api_key(&self, id: Uuid) -> Result<(), Error> {
self.send_no_content(self.delete(&format!("/api/v1/api-keys/{id}")))
.await
}
pub async fn list_users(&self) -> Result<ApiResponse<Vec<types::UserResponse>>, Error> {
self.send_envelope(self.get("/api/v1/users")).await
}
pub async fn create_user(
&self,
request: &types::CreateUserRequest,
) -> Result<ApiResponse<types::UserResponse>, Error> {
self.send_envelope(self.post("/api/v1/users").json(request))
.await
}
pub async fn delete_user(&self, id: Uuid) -> Result<(), Error> {
self.send_no_content(self.delete(&format!("/api/v1/users/{id}")))
.await
}
pub async fn update_role(
&self,
id: Uuid,
request: &types::UpdateRoleRequest,
) -> Result<ApiResponse<types::UserResponse>, Error> {
self.send_envelope(
self.patch(&format!("/api/v1/users/{id}/role"))
.json(request),
)
.await
}
pub async fn list_secrets(&self) -> Result<ApiResponse<Vec<types::SecretResponse>>, Error> {
self.send_envelope(self.get("/api/v1/secrets")).await
}
pub async fn create_secret(
&self,
request: &types::SetSecretRequest,
) -> Result<ApiResponse<types::SecretResponse>, Error> {
self.send_envelope(self.post("/api/v1/secrets").json(request))
.await
}
pub async fn update_secret(
&self,
key: &str,
request: &types::UpdateSecretRequest,
) -> Result<ApiResponse<types::SecretResponse>, Error> {
self.send_envelope(self.put(&format!("/api/v1/secrets/{key}")).json(request))
.await
}
pub async fn delete_secret(&self, key: &str) -> Result<(), Error> {
self.send_no_content(self.delete(&format!("/api/v1/secrets/{key}")))
.await
}
pub async fn list_audit_logs(&self) -> Result<ApiResponse<Vec<types::AuditLogEntry>>, Error> {
self.send_envelope(self.get("/api/v1/audit-logs")).await
}
}