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