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_json::from_str;
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>,
}
#[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)]
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::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,
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 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_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 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
}
}