use reqwest::{Client, Method, Response};
use serde_json::{json, Value};
use crate::error::JobcelisError;
pub struct JobcelisClient {
http: Client,
api_key: String,
base_url: String,
auth_token: Option<String>,
}
impl JobcelisClient {
pub fn new(api_key: &str) -> Self {
Self {
http: Client::new(),
api_key: api_key.to_string(),
base_url: "https://jobcelis.com".to_string(),
auth_token: None,
}
}
pub fn with_base_url(api_key: &str, base_url: &str) -> Self {
Self {
http: Client::new(),
api_key: api_key.to_string(),
base_url: base_url.trim_end_matches('/').to_string(),
auth_token: None,
}
}
pub fn set_auth_token(&mut self, token: &str) {
self.auth_token = Some(token.to_string());
}
pub async fn register(&self, email: &str, password: &str, name: Option<&str>) -> Result<Value, JobcelisError> {
let mut body = json!({"email": email, "password": password});
if let Some(n) = name {
body["name"] = json!(n);
}
self.public_post("/api/v1/auth/register", body).await
}
pub async fn login(&self, email: &str, password: &str) -> Result<Value, JobcelisError> {
self.public_post("/api/v1/auth/login", json!({"email": email, "password": password})).await
}
pub async fn refresh_token(&self, refresh_token: &str) -> Result<Value, JobcelisError> {
self.public_post("/api/v1/auth/refresh", json!({"refresh_token": refresh_token})).await
}
pub async fn verify_mfa(&self, token: &str, code: &str) -> Result<Value, JobcelisError> {
self.post("/api/v1/auth/mfa/verify", json!({"token": token, "code": code})).await
}
pub async fn send_event(&self, topic: &str, payload: Value) -> Result<Value, JobcelisError> {
self.post("/api/v1/events", json!({"topic": topic, "payload": payload})).await
}
pub async fn send_events(&self, events: Vec<Value>) -> Result<Value, JobcelisError> {
self.post("/api/v1/events/batch", json!({"events": events})).await
}
pub async fn get_event(&self, event_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/events/{event_id}"), &[]).await
}
pub async fn list_events(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/events", ¶ms).await
}
pub async fn delete_event(&self, event_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/events/{event_id}")).await
}
pub async fn simulate_event(&self, topic: &str, payload: Value) -> Result<Value, JobcelisError> {
self.post("/api/v1/simulate", json!({"topic": topic, "payload": payload})).await
}
pub async fn create_webhook(&self, url: &str, extra: Option<Value>) -> Result<Value, JobcelisError> {
let mut body = json!({"url": url});
if let Some(e) = extra {
if let Value::Object(map) = e {
for (k, v) in map { body[k] = v; }
}
}
self.post("/api/v1/webhooks", body).await
}
pub async fn get_webhook(&self, webhook_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/webhooks/{webhook_id}"), &[]).await
}
pub async fn list_webhooks(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/webhooks", ¶ms).await
}
pub async fn update_webhook(&self, webhook_id: &str, data: Value) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/webhooks/{webhook_id}"), data).await
}
pub async fn delete_webhook(&self, webhook_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/webhooks/{webhook_id}")).await
}
pub async fn webhook_health(&self, webhook_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/webhooks/{webhook_id}/health"), &[]).await
}
pub async fn webhook_templates(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/webhooks/templates", &[]).await
}
pub async fn test_webhook(&self, webhook_id: &str) -> Result<Value, JobcelisError> {
self.post(&format!("/api/v1/webhooks/{webhook_id}/test"), json!({})).await
}
pub async fn list_deliveries(&self, limit: u32, cursor: Option<&str>, status: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
if let Some(s) = status { params.push(("status", s.to_string())); }
self.get("/api/v1/deliveries", ¶ms).await
}
pub async fn retry_delivery(&self, delivery_id: &str) -> Result<Value, JobcelisError> {
self.post(&format!("/api/v1/deliveries/{delivery_id}/retry"), json!({})).await
}
pub async fn list_dead_letters(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/dead-letters", ¶ms).await
}
pub async fn get_dead_letter(&self, dead_letter_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/dead-letters/{dead_letter_id}"), &[]).await
}
pub async fn retry_dead_letter(&self, dead_letter_id: &str) -> Result<Value, JobcelisError> {
self.post(&format!("/api/v1/dead-letters/{dead_letter_id}/retry"), json!({})).await
}
pub async fn resolve_dead_letter(&self, dead_letter_id: &str) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/dead-letters/{dead_letter_id}/resolve"), json!({})).await
}
pub async fn create_replay(&self, topic: &str, from_date: &str, to_date: &str, webhook_id: Option<&str>) -> Result<Value, JobcelisError> {
let mut body = json!({"topic": topic, "from_date": from_date, "to_date": to_date});
if let Some(wh) = webhook_id { body["webhook_id"] = json!(wh); }
self.post("/api/v1/replays", body).await
}
pub async fn list_replays(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/replays", ¶ms).await
}
pub async fn get_replay(&self, replay_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/replays/{replay_id}"), &[]).await
}
pub async fn cancel_replay(&self, replay_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/replays/{replay_id}")).await
}
pub async fn create_job(&self, name: &str, queue: &str, cron_expression: &str, extra: Option<Value>) -> Result<Value, JobcelisError> {
let mut body = json!({"name": name, "queue": queue, "cron_expression": cron_expression});
if let Some(e) = extra {
if let Value::Object(map) = e {
for (k, v) in map { body[k] = v; }
}
}
self.post("/api/v1/jobs", body).await
}
pub async fn list_jobs(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/jobs", ¶ms).await
}
pub async fn get_job(&self, job_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/jobs/{job_id}"), &[]).await
}
pub async fn update_job(&self, job_id: &str, data: Value) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/jobs/{job_id}"), data).await
}
pub async fn delete_job(&self, job_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/jobs/{job_id}")).await
}
pub async fn list_job_runs(&self, job_id: &str, limit: u32) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/jobs/{job_id}/runs"), &[("limit", limit.to_string())]).await
}
pub async fn cron_preview(&self, expression: &str, count: u32) -> Result<Value, JobcelisError> {
self.get("/api/v1/jobs/cron-preview", &[("expression", expression.to_string()), ("count", count.to_string())]).await
}
pub async fn create_pipeline(&self, name: &str, topics: Vec<&str>, steps: Vec<Value>) -> Result<Value, JobcelisError> {
self.post("/api/v1/pipelines", json!({"name": name, "topics": topics, "steps": steps})).await
}
pub async fn list_pipelines(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/pipelines", ¶ms).await
}
pub async fn get_pipeline(&self, pipeline_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/pipelines/{pipeline_id}"), &[]).await
}
pub async fn update_pipeline(&self, pipeline_id: &str, data: Value) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/pipelines/{pipeline_id}"), data).await
}
pub async fn delete_pipeline(&self, pipeline_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/pipelines/{pipeline_id}")).await
}
pub async fn test_pipeline(&self, pipeline_id: &str, payload: Value) -> Result<Value, JobcelisError> {
self.post(&format!("/api/v1/pipelines/{pipeline_id}/test"), payload).await
}
pub async fn create_event_schema(&self, topic: &str, schema: Value) -> Result<Value, JobcelisError> {
self.post("/api/v1/event-schemas", json!({"topic": topic, "schema": schema})).await
}
pub async fn list_event_schemas(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/event-schemas", ¶ms).await
}
pub async fn get_event_schema(&self, schema_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/event-schemas/{schema_id}"), &[]).await
}
pub async fn update_event_schema(&self, schema_id: &str, data: Value) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/event-schemas/{schema_id}"), data).await
}
pub async fn delete_event_schema(&self, schema_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/event-schemas/{schema_id}")).await
}
pub async fn validate_payload(&self, topic: &str, payload: Value) -> Result<Value, JobcelisError> {
self.post("/api/v1/event-schemas/validate", json!({"topic": topic, "payload": payload})).await
}
pub async fn list_sandbox_endpoints(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/sandbox-endpoints", &[]).await
}
pub async fn create_sandbox_endpoint(&self, name: Option<&str>) -> Result<Value, JobcelisError> {
let body = match name {
Some(n) => json!({"name": n}),
None => json!({}),
};
self.post("/api/v1/sandbox-endpoints", body).await
}
pub async fn delete_sandbox_endpoint(&self, endpoint_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/sandbox-endpoints/{endpoint_id}")).await
}
pub async fn list_sandbox_requests(&self, endpoint_id: &str, limit: u32) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/sandbox-endpoints/{endpoint_id}/requests"), &[("limit", limit.to_string())]).await
}
pub async fn events_per_day(&self, days: u32) -> Result<Value, JobcelisError> {
self.get("/api/v1/analytics/events-per-day", &[("days", days.to_string())]).await
}
pub async fn deliveries_per_day(&self, days: u32) -> Result<Value, JobcelisError> {
self.get("/api/v1/analytics/deliveries-per-day", &[("days", days.to_string())]).await
}
pub async fn top_topics(&self, limit: u32) -> Result<Value, JobcelisError> {
self.get("/api/v1/analytics/top-topics", &[("limit", limit.to_string())]).await
}
pub async fn webhook_stats(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/analytics/webhook-stats", &[]).await
}
pub async fn get_project(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/project", &[]).await
}
pub async fn update_project(&self, data: Value) -> Result<Value, JobcelisError> {
self.patch("/api/v1/project", data).await
}
pub async fn list_topics(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/topics", &[]).await
}
pub async fn get_token(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/token", &[]).await
}
pub async fn regenerate_token(&self) -> Result<Value, JobcelisError> {
self.post("/api/v1/token/regenerate", json!({})).await
}
pub async fn list_projects(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/projects", &[]).await
}
pub async fn create_project(&self, name: &str) -> Result<Value, JobcelisError> {
self.post("/api/v1/projects", json!({"name": name})).await
}
pub async fn get_project_by_id(&self, project_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/projects/{project_id}"), &[]).await
}
pub async fn update_project_by_id(&self, project_id: &str, data: Value) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/projects/{project_id}"), data).await
}
pub async fn delete_project(&self, project_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/projects/{project_id}")).await
}
pub async fn set_default_project(&self, project_id: &str) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/projects/{project_id}/default"), json!({})).await
}
pub async fn list_members(&self, project_id: &str) -> Result<Value, JobcelisError> {
self.get(&format!("/api/v1/projects/{project_id}/members"), &[]).await
}
pub async fn add_member(&self, project_id: &str, email: &str, role: Option<&str>) -> Result<Value, JobcelisError> {
let r = role.unwrap_or("member");
self.post(&format!("/api/v1/projects/{project_id}/members"), json!({"email": email, "role": r})).await
}
pub async fn update_member(&self, project_id: &str, member_id: &str, role: &str) -> Result<Value, JobcelisError> {
self.patch(&format!("/api/v1/projects/{project_id}/members/{member_id}"), json!({"role": role})).await
}
pub async fn remove_member(&self, project_id: &str, member_id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/projects/{project_id}/members/{member_id}")).await
}
pub async fn list_pending_invitations(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/invitations/pending", &[]).await
}
pub async fn accept_invitation(&self, invitation_id: &str) -> Result<Value, JobcelisError> {
self.post(&format!("/api/v1/invitations/{invitation_id}/accept"), json!({})).await
}
pub async fn reject_invitation(&self, invitation_id: &str) -> Result<Value, JobcelisError> {
self.post(&format!("/api/v1/invitations/{invitation_id}/reject"), json!({})).await
}
pub async fn list_audit_logs(&self, limit: u32, cursor: Option<&str>) -> Result<Value, JobcelisError> {
let mut params: Vec<(&str, String)> = vec![("limit", limit.to_string())];
if let Some(c) = cursor { params.push(("cursor", c.to_string())); }
self.get("/api/v1/audit-log", ¶ms).await
}
pub async fn export_events(&self, format: &str) -> Result<String, JobcelisError> {
self.get_raw("/api/v1/export/events", &[("format", format.to_string())]).await
}
pub async fn export_deliveries(&self, format: &str) -> Result<String, JobcelisError> {
self.get_raw("/api/v1/export/deliveries", &[("format", format.to_string())]).await
}
pub async fn export_jobs(&self, format: &str) -> Result<String, JobcelisError> {
self.get_raw("/api/v1/export/jobs", &[("format", format.to_string())]).await
}
pub async fn export_audit_log(&self, format: &str) -> Result<String, JobcelisError> {
self.get_raw("/api/v1/export/audit-log", &[("format", format.to_string())]).await
}
pub async fn get_consents(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/me/consents", &[]).await
}
pub async fn accept_consent(&self, purpose: &str) -> Result<Value, JobcelisError> {
self.post(&format!("/api/v1/me/consents/{purpose}/accept"), json!({})).await
}
pub async fn export_my_data(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/me/data", &[]).await
}
pub async fn restrict_processing(&self) -> Result<Value, JobcelisError> {
self.post("/api/v1/me/restrict", json!({})).await
}
pub async fn lift_restriction(&self) -> Result<(), JobcelisError> {
self.do_delete("/api/v1/me/restrict").await
}
pub async fn object_to_processing(&self) -> Result<Value, JobcelisError> {
self.post("/api/v1/me/object", json!({})).await
}
pub async fn restore_consent(&self) -> Result<(), JobcelisError> {
self.do_delete("/api/v1/me/object").await
}
pub async fn list_embed_tokens(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/embed/tokens", &[]).await
}
pub async fn create_embed_token(&self, config: Value) -> Result<Value, JobcelisError> {
self.post("/api/v1/embed/tokens", config).await
}
pub async fn revoke_embed_token(&self, id: &str) -> Result<(), JobcelisError> {
self.do_delete(&format!("/api/v1/embed/tokens/{id}")).await
}
pub async fn get_notification_channel(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/notification-channels", &[]).await
}
pub async fn upsert_notification_channel(&self, config: Value) -> Result<Value, JobcelisError> {
self.put("/api/v1/notification-channels", config).await
}
pub async fn delete_notification_channel(&self) -> Result<(), JobcelisError> {
self.do_delete("/api/v1/notification-channels").await
}
pub async fn test_notification_channel(&self) -> Result<Value, JobcelisError> {
self.post("/api/v1/notification-channels/test", json!({})).await
}
pub async fn get_retention_policy(&self) -> Result<Value, JobcelisError> {
self.get("/api/v1/retention", &[]).await
}
pub async fn update_retention_policy(&self, policy: Value) -> Result<Value, JobcelisError> {
self.patch("/api/v1/retention", policy).await
}
pub async fn preview_purge(&self, params: Value) -> Result<Value, JobcelisError> {
self.post("/api/v1/purge/preview", params).await
}
pub async fn purge_data(&self, params: Value) -> Result<Value, JobcelisError> {
self.post("/api/v1/purge", params).await
}
pub async fn health(&self) -> Result<Value, JobcelisError> {
self.get("/health", &[]).await
}
pub async fn status(&self) -> Result<Value, JobcelisError> {
self.get("/status", &[]).await
}
async fn get(&self, path: &str, params: &[(&str, String)]) -> Result<Value, JobcelisError> {
self.request(Method::GET, path, params, None).await
}
async fn post(&self, path: &str, body: Value) -> Result<Value, JobcelisError> {
self.request(Method::POST, path, &[], Some(body)).await
}
async fn put(&self, path: &str, body: Value) -> Result<Value, JobcelisError> {
self.request(Method::PUT, path, &[], Some(body)).await
}
async fn patch(&self, path: &str, body: Value) -> Result<Value, JobcelisError> {
self.request(Method::PATCH, path, &[], Some(body)).await
}
async fn do_delete(&self, path: &str) -> Result<(), JobcelisError> {
self.request(Method::DELETE, path, &[], None).await?;
Ok(())
}
async fn get_raw(&self, path: &str, params: &[(&str, String)]) -> Result<String, JobcelisError> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.http.request(Method::GET, &url)
.header("x-api-key", &self.api_key)
.query(params);
if let Some(ref token) = self.auth_token {
req = req.header("authorization", format!("Bearer {token}"));
}
let resp = req.send().await?;
let status = resp.status().as_u16();
let body = resp.text().await?;
if status >= 400 {
let detail = serde_json::from_str(&body).unwrap_or(json!(body));
return Err(JobcelisError::Api { status, detail });
}
Ok(body)
}
async fn public_post(&self, path: &str, body: Value) -> Result<Value, JobcelisError> {
let url = format!("{}{}", self.base_url, path);
let resp = self.http.post(&url)
.header("content-type", "application/json")
.json(&body)
.send()
.await?;
self.handle_response(resp).await
}
async fn request(&self, method: Method, path: &str, params: &[(&str, String)], body: Option<Value>) -> Result<Value, JobcelisError> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.http.request(method, &url)
.header("content-type", "application/json")
.header("accept", "application/json")
.header("x-api-key", &self.api_key);
if let Some(ref token) = self.auth_token {
req = req.header("authorization", format!("Bearer {token}"));
}
if !params.is_empty() {
req = req.query(params);
}
if let Some(b) = body {
req = req.json(&b);
}
let resp = req.send().await?;
self.handle_response(resp).await
}
async fn handle_response(&self, resp: Response) -> Result<Value, JobcelisError> {
let status = resp.status().as_u16();
if status == 204 {
return Ok(json!(null));
}
let body = resp.text().await?;
if status >= 400 {
let detail = serde_json::from_str(&body).unwrap_or(json!(body));
return Err(JobcelisError::Api { status, detail });
}
let parsed: Value = serde_json::from_str(&body)?;
Ok(parsed)
}
}