use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::error::SailError;
use crate::http::{HttpCore, IdempotencyKey, Method, RequestSpec};
use crate::retry::DEFAULT_RETRY_POLICY;
#[derive(Debug, Clone, Serialize)]
pub struct App {
pub id: String,
pub name: String,
pub created_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppOverview {
#[serde(default)]
pub app_id: String,
#[serde(default)]
pub app_name: String,
#[serde(default)]
pub sailbox_count: i64,
#[serde(default)]
pub running_count: i64,
#[serde(default)]
pub paused_count: i64,
#[serde(default)]
pub sleeping_count: i64,
#[serde(default)]
pub failed_count: i64,
#[serde(default)]
pub cpu_requested_vcpu: i64,
#[serde(default)]
pub cpu_used_vcpu: f64,
#[serde(default)]
pub memory_requested_bytes: i64,
#[serde(default)]
pub memory_used_bytes: i64,
#[serde(default)]
pub disk_requested_bytes: i64,
#[serde(default)]
pub disk_used_bytes: i64,
}
pub async fn find_app(
api_http: &HttpCore,
name: &str,
mint_if_missing: bool,
) -> Result<App, SailError> {
let body = json!({ "name": name, "mint_if_missing": mint_if_missing });
let bytes = serde_json::to_vec(&body).map_err(|e| SailError::Internal {
message: format!("failed to serialize request body: {e}"),
})?;
let (status, data) = api_http
.request(&RequestSpec {
method: Method::Post,
path: "/v1/apps/find".to_string(),
query: Vec::new(),
body: Some(bytes),
extra_headers: Vec::new(),
timeout: None,
policy: DEFAULT_RETRY_POLICY,
idempotency_key: IdempotencyKey::Auto,
})
.await?;
if status == 404 {
return Err(SailError::NotFound {
message: format!("App {name:?} not found"),
});
}
raise_app_error(status, &data, "Failed to find app")?;
Ok(App {
id: data
.get("id")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
name: data
.get("name")
.and_then(Value::as_str)
.unwrap_or(name)
.to_string(),
created_at: data.get("created_at").and_then(Value::as_i64).unwrap_or(0),
})
}
pub async fn list_apps(sailbox_http: &HttpCore) -> Result<Vec<AppOverview>, SailError> {
let (status, data) = sailbox_http
.request(&RequestSpec {
method: Method::Get,
path: "/v1/sailboxes/overview".to_string(),
query: Vec::new(),
body: None,
extra_headers: Vec::new(),
timeout: None,
policy: DEFAULT_RETRY_POLICY,
idempotency_key: IdempotencyKey::Auto,
})
.await?;
raise_app_error(status, &data, "Failed to list apps")?;
let Some(rows) = data.get("data").and_then(Value::as_array) else {
return Ok(Vec::new());
};
rows.iter()
.map(|row| {
serde_json::from_value::<AppOverview>(row.clone()).map_err(|e| SailError::Internal {
message: format!("failed to parse app overview: {e}"),
})
})
.collect()
}
fn raise_app_error(status: u16, data: &Value, context: &str) -> Result<(), SailError> {
if status < 300 {
return Ok(());
}
let detail = data
.get("error")
.and_then(|e| e.get("message"))
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.unwrap_or("unknown error");
let message = format!("{context}: {detail}");
if status == 401 || status == 403 {
return Err(SailError::PermissionDenied { message });
}
Err(SailError::Api {
status,
message,
body: data.clone(),
})
}