1use serde::Serialize;
9use serde_json::{json, Value};
10use time::OffsetDateTime;
11
12use crate::error::SailError;
13use crate::http::{HttpCore, IdempotencyKey, Method, RequestSpec};
14use crate::retry::DEFAULT_RETRY_POLICY;
15
16#[derive(Debug, Clone, Serialize)]
18pub struct App {
19 pub id: String,
21 pub name: String,
23 #[serde(with = "time::serde::rfc3339")]
25 pub created_at: OffsetDateTime,
26}
27
28impl App {
29 fn from_json(row: &Value) -> App {
31 App {
32 id: row
33 .get("id")
34 .and_then(Value::as_str)
35 .unwrap_or("")
36 .to_string(),
37 name: row
38 .get("name")
39 .and_then(Value::as_str)
40 .unwrap_or("")
41 .to_string(),
42 created_at: OffsetDateTime::from_unix_timestamp(
45 row.get("created_at").and_then(Value::as_i64).unwrap_or(0),
46 )
47 .unwrap_or(OffsetDateTime::UNIX_EPOCH),
48 }
49 }
50}
51
52pub(crate) async fn find_app(
54 api_http: &HttpCore,
55 name: &str,
56 mint_if_missing: bool,
57) -> Result<App, SailError> {
58 let body = json!({ "name": name, "mint_if_missing": mint_if_missing });
59 let bytes = serde_json::to_vec(&body).map_err(|e| SailError::Internal {
60 message: format!("failed to serialize request body: {e}"),
61 })?;
62 let (status, data) = api_http
63 .request(&RequestSpec {
64 method: Method::Post,
65 path: "/v1/apps/find".to_string(),
66 query: Vec::new(),
67 body: Some(bytes),
68 extra_headers: Vec::new(),
69 timeout: None,
70 policy: DEFAULT_RETRY_POLICY,
71 idempotency_key: IdempotencyKey::Auto,
72 })
73 .await?;
74 if status == 404 {
76 return Err(SailError::NotFound {
77 message: format!("App {name:?} not found"),
78 });
79 }
80 raise_app_error(status, &data, "Failed to find app")?;
81 Ok(App::from_json(&data))
82}
83
84pub(crate) async fn list_apps(api_http: &HttpCore) -> Result<Vec<App>, SailError> {
89 let (status, data) = api_http
90 .request(&RequestSpec {
91 method: Method::Get,
92 path: "/v1/apps".to_string(),
93 query: Vec::new(),
94 body: None,
95 extra_headers: Vec::new(),
96 timeout: None,
97 policy: DEFAULT_RETRY_POLICY,
98 idempotency_key: IdempotencyKey::Auto,
99 })
100 .await?;
101 raise_app_error(status, &data, "Failed to list apps")?;
102 let Some(rows) = data.get("data").and_then(Value::as_array) else {
103 return Ok(Vec::new());
104 };
105 Ok(rows.iter().map(App::from_json).collect())
106}
107
108fn raise_app_error(status: u16, data: &Value, context: &str) -> Result<(), SailError> {
111 if status < 300 {
112 return Ok(());
113 }
114 let detail = crate::http::api_error_message(data, "unknown error");
115 let message = format!("{context}: {detail}");
116 if status == 401 || status == 403 {
117 return Err(SailError::PermissionDenied { message });
118 }
119 Err(SailError::Api {
120 status,
121 message,
122 body: data.clone(),
123 })
124}