use std::time::Duration;
use chrono::{DateTime, Utc};
pub use ironflow_types::{ApiMeta, ApiResponse};
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use reqwest::{Client, RequestBuilder, Response};
use serde::Serialize;
use serde_json::{from_str, json};
use uuid::Uuid;
pub use crate::builder::{ArtifactDownload, ClientBuilder};
use crate::error::{ApiErrorEnvelope, Error};
use crate::rate_limit::RateLimiter;
use crate::retry::RetryConfig;
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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_group: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i16>,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct StatsHistoryParams<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub granularity: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_steps: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_group: Option<&'a str>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ListStepsParams<'a> {
pub agent_role: &'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>,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct ListRunLogsFilter<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct ListAuditLogsFilter<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub event_type: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<u32>,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct ListApprovalDelegationsFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub from_user_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to_user_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<u32>,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct ListSignalsFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct IronflowClient {
pub(crate) http: Client,
base_url: String,
pub(crate) retry_config: RetryConfig,
pub(crate) rate_limiter: RateLimiter,
}
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 {
Self::from_config_with_retry(config, RetryConfig::default(), RateLimiter::new())
}
pub fn from_config_with_retry(
config: ClientConfig,
retry_config: RetryConfig,
rate_limiter: RateLimiter,
) -> 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,
retry_config,
rate_limiter,
}
}
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))
}
pub(crate) fn post(&self, path: &str) -> RequestBuilder {
self.http.post(self.url(path))
}
pub(crate) fn put(&self, path: &str) -> RequestBuilder {
self.http.put(self.url(path))
}
pub(crate) fn delete(&self, path: &str) -> RequestBuilder {
self.http.delete(self.url(path))
}
pub(crate) fn patch(&self, path: &str) -> RequestBuilder {
self.http.patch(self.url(path))
}
pub(crate) async fn into_api_error(response: Response) -> Error {
let status = response.status();
let status_code = status.as_u16();
let text = response.text().await.unwrap_or_default();
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,
)
})
}
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_once(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::CancelRunResponse>, Error> {
self.send_envelope(self.post(&format!("/api/v1/runs/{id}/cancel")))
.await
}
pub async fn pause_run(&self, id: Uuid) -> Result<ApiResponse<types::PauseRunResponse>, Error> {
self.send_envelope(self.post(&format!("/api/v1/runs/{id}/pause")))
.await
}
pub async fn resume_run(
&self,
id: Uuid,
) -> Result<ApiResponse<types::ResumeRunResponse>, Error> {
self.send_envelope(self.post(&format!("/api/v1/runs/{id}/resume")))
.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 submit_input<T: Serialize + ?Sized>(
&self,
run_id: Uuid,
step_id: Uuid,
value: &T,
) -> Result<ApiResponse<types::RunResponse>, Error> {
self.send_envelope(
self.post(&format!("/api/v1/runs/{run_id}/steps/{step_id}/input"))
.json(value),
)
.await
}
pub async fn reject_input(
&self,
run_id: Uuid,
step_id: Uuid,
reason: Option<&str>,
) -> Result<ApiResponse<types::RunResponse>, Error> {
self.send_envelope(
self.post(&format!("/api/v1/runs/{run_id}/steps/{step_id}/reject"))
.json(&json!({ "reason": reason })),
)
.await
}
pub async fn retry_run(
&self,
id: Uuid,
force: bool,
) -> Result<ApiResponse<types::RunResponse>, Error> {
let action = if force { "retry?force=true" } else { "retry" };
self.run_action(id, action).await
}
pub async fn replay_run(&self, id: Uuid) -> Result<ApiResponse<types::RunResponse>, Error> {
self.run_action(id, "replay").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 plan_workflow(
&self,
name: &str,
request: &types::PlanWorkflowRequest,
) -> Result<ApiResponse<types::ExecutionPlanResponse>, Error> {
self.send_envelope(
self.post(&format!("/api/v1/workflows/{name}/plan"))
.json(request),
)
.await
}
pub async fn pause_workflow(
&self,
name: &str,
) -> Result<ApiResponse<types::WorkflowPauseResponse>, Error> {
self.send_envelope(self.post(&format!("/api/v1/workflows/{name}/pause")))
.await
}
pub async fn resume_workflow(
&self,
name: &str,
) -> Result<ApiResponse<types::WorkflowPauseResponse>, Error> {
self.send_envelope(self.post(&format!("/api/v1/workflows/{name}/resume")))
.await
}
pub async fn get_stats(&self) -> Result<ApiResponse<types::StatsResponse>, Error> {
self.send_envelope(self.get("/api/v1/stats")).await
}
pub async fn stats_history(
&self,
workflow: Option<&str>,
period: Option<&str>,
granularity: Option<&str>,
) -> Result<ApiResponse<types::StatsHistoryResponse>, Error> {
self.stats_history_with(&StatsHistoryParams {
workflow,
period,
granularity,
..StatsHistoryParams::default()
})
.await
}
pub async fn stats_history_with(
&self,
params: &StatsHistoryParams<'_>,
) -> Result<ApiResponse<types::StatsHistoryResponse>, Error> {
self.send_envelope(self.get("/api/v1/stats/history").query(params))
.await
}
pub async fn agent_role_stats(
&self,
period: Option<&str>,
) -> Result<ApiResponse<types::AgentRoleStatsResponse>, Error> {
let mut request = self.get("/api/v1/stats/agent-roles");
if let Some(period) = period {
request = request.query(&[("period", period)]);
}
self.send_envelope(request).await
}
pub async fn list_steps(
&self,
params: &ListStepsParams<'_>,
) -> Result<ApiResponse<Vec<types::StepResponse>>, Error> {
self.send_envelope(self.get("/api/v1/steps").query(params))
.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_once(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 get_user_groups(
&self,
id: Uuid,
) -> Result<ApiResponse<types::UserGroupsResponse>, Error> {
self.send_envelope(self.get(&format!("/api/v1/users/{id}/groups")))
.await
}
pub async fn update_user_groups(
&self,
id: Uuid,
groups: &[String],
) -> Result<ApiResponse<types::UserGroupsResponse>, Error> {
let request = types::UpdateUserGroupsRequest {
groups: groups.to_vec(),
};
self.send_envelope(
self.put(&format!("/api/v1/users/{id}/groups"))
.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 rotate_secrets(
&self,
request: &types::RotateSecretsRequest,
) -> Result<ApiResponse<types::RotateSecretsResponse>, Error> {
self.send_envelope(self.post("/api/v1/secrets/rotate").json(request))
.await
}
pub async fn secret_key_versions(
&self,
) -> Result<ApiResponse<types::KeyVersionsResponse>, Error> {
self.send_envelope(self.get("/api/v1/secrets/key-versions"))
.await
}
pub async fn get_run_logs(
&self,
run_id: Uuid,
filter: &ListRunLogsFilter<'_>,
) -> Result<ApiResponse<Vec<types::LogEntry>>, Error> {
self.send_envelope(
self.get(&format!("/api/v1/runs/{run_id}/logs"))
.query(filter),
)
.await
}
pub async fn list_audit_logs(&self) -> Result<ApiResponse<Vec<types::AuditLogEntry>>, Error> {
self.list_audit_logs_filtered(&ListAuditLogsFilter::default())
.await
}
pub async fn list_audit_logs_filtered(
&self,
filter: &ListAuditLogsFilter<'_>,
) -> Result<ApiResponse<Vec<types::AuditLogEntry>>, Error> {
self.send_envelope(self.get("/api/v1/audit-logs").query(filter))
.await
}
}