use std::borrow::Cow;
use reqwest::{
Client, Request,
header::{HeaderMap, HeaderValue},
multipart::{Form, Part},
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::json;
use super::{
Endpoint,
errors::{ApiError, ApiErrorCode},
};
use crate::{
resources::{AppResource, DatabaseResource, WorkspaceResource},
settings::SETTINGS,
types::{
AccountInfo, AppDomain, Database, DatabaseType, RuntimeStatsListItem,
ServiceStatus, Snapshot, SnapshotScope, UploadedApp, WorkspaceInfo,
},
};
#[derive(Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "lowercase")]
pub enum ApiResponse<T> {
Success {
#[serde(skip_serializing_if = "Option::is_none")]
response: Option<T>,
},
Error {
code: ApiErrorCode,
},
}
impl<T> ApiResponse<T> {
pub fn into_result_t(self) -> Result<T, ApiError> {
match self {
ApiResponse::Error { code } => Err(ApiError::Api { code }),
ApiResponse::Success { response } => {
response.ok_or_else(|| panic!("Expected response data"))
}
}
}
pub fn into_bool_result(self) -> Result<bool, ApiError> {
match self {
ApiResponse::Success { .. } => Ok(true),
ApiResponse::Error { code } => Err(ApiError::Api { code }),
}
}
}
#[cfg(test)]
mod tests {
use super::ApiResponse;
use crate::http::errors::{ApiError, ApiErrorCode};
#[test]
fn into_result_t_success_returns_inner_value() {
let resp: ApiResponse<u32> =
ApiResponse::Success { response: Some(42) };
assert_eq!(resp.into_result_t().unwrap(), 42);
}
#[test]
fn into_result_t_error_returns_api_error() {
let resp: ApiResponse<u32> = ApiResponse::Error {
code: ApiErrorCode::NotFound,
};
assert!(matches!(
resp.into_result_t(),
Err(ApiError::Api {
code: ApiErrorCode::NotFound
})
));
}
#[test]
fn into_bool_result_success_returns_true() {
let resp: ApiResponse<()> = ApiResponse::Success { response: None };
assert_eq!(resp.into_bool_result().unwrap(), true);
}
#[test]
fn into_bool_result_error_returns_api_error() {
let resp: ApiResponse<()> = ApiResponse::Error {
code: ApiErrorCode::RateLimit,
};
assert!(matches!(
resp.into_bool_result(),
Err(ApiError::Api {
code: ApiErrorCode::RateLimit
})
));
}
#[test]
fn api_client_default_is_same_as_new() {
unsafe { std::env::set_var("API_TOKEN", "test") };
let client = crate::http::ApiClient::default();
assert!(!client.base_url.is_empty());
}
}
impl Default for ApiClient {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct ApiClient {
pub base_url: String,
pub(crate) http_client: Client,
}
impl ApiClient {
pub fn new() -> ApiClient {
let mut headers = HeaderMap::new();
headers.append(
"Authorization",
HeaderValue::from_str(&SETTINGS.api_token).unwrap(),
);
let client: Client = Client::builder()
.default_headers(headers)
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.http1_only()
.build()
.unwrap();
ApiClient {
base_url: "https://api.squarecloud.app/v2".to_string(),
http_client: client,
}
}
pub(crate) fn url(&self, path: &str) -> String {
format!(
"{}/{}",
self.base_url.trim_end_matches('/'),
path.trim_start_matches('/')
)
}
pub(crate) async fn execute_request<T: DeserializeOwned>(
&self,
request: Request,
) -> Result<ApiResponse<T>, reqwest::Error> {
let response = self.http_client.execute(request).await?;
let response: ApiResponse<T> = response.json().await?;
Ok(response)
}
pub(crate) async fn request_endpoint<T: DeserializeOwned>(
&self,
endpoint: Endpoint,
) -> Result<ApiResponse<T>, reqwest::Error> {
let mut req = self
.http_client
.request(endpoint.method, self.url(&endpoint.path));
if let Some(body) = endpoint.json_body {
req = req.json(&body);
}
let response: ApiResponse<T> = req.send().await?.json().await?;
Ok(response)
}
pub fn app(&self, id: &str) -> AppResource {
AppResource::new(self.clone(), id)
}
pub fn workspace(&self, id: &str) -> WorkspaceResource {
WorkspaceResource::new(self.clone(), id)
}
pub fn database(&self, id: &str) -> DatabaseResource {
DatabaseResource::new(self.clone(), id)
}
pub async fn service_status(&self) -> Result<ServiceStatus, ApiError> {
let endpoint = Endpoint::service_status();
let req = self
.http_client
.request(endpoint.method, self.url(&endpoint.path));
Ok(req.send().await?.json().await?)
}
pub async fn me(&self) -> Result<AccountInfo, ApiError> {
self.request_endpoint::<AccountInfo>(Endpoint::me())
.await?
.into_result_t()
}
pub async fn upload_app(
&self,
bytes: impl Into<Cow<'static, [u8]>>,
) -> Result<UploadedApp, ApiError> {
let endpoint = Endpoint::upload_app();
let form = Form::new().part(
"file",
Part::bytes(bytes)
.file_name("app.zip")
.mime_str("application/zip")
.unwrap(),
);
let request = endpoint
.request_builder(&self.http_client, &self.base_url)
.multipart(form)
.build()?;
self.execute_request(request).await?.into_result_t()
}
pub async fn all_domains(&self) -> Result<Vec<AppDomain>, ApiError> {
self.request_endpoint(Endpoint::app_domains())
.await?
.into_result_t()
}
pub async fn all_apps_status(
&self,
) -> Result<Vec<RuntimeStatsListItem>, ApiError> {
self.request_endpoint(Endpoint::all_apps_status())
.await?
.into_result_t()
}
pub async fn create_database(
&self,
name: String,
memory: u32,
r#type: DatabaseType,
version: String,
) -> Result<Database, ApiError> {
let endpoint = Endpoint::create_database();
let request = endpoint
.request_builder(&self.http_client, &self.base_url)
.json(&json!({
"name": name,
"memory": memory,
"type": r#type,
"version": version}))
.build()?;
self.execute_request(request).await?.into_result_t()
}
pub async fn all_database_status(
&self,
) -> Result<Vec<RuntimeStatsListItem>, ApiError> {
self.request_endpoint(Endpoint::all_database_status())
.await?
.into_result_t()
}
pub async fn create_workspace(
&self,
name: String,
) -> Result<WorkspaceInfo, ApiError> {
let endpoint = Endpoint::create_workspace();
let request = endpoint
.request_builder(&self.http_client, &self.base_url)
.json(&json!({"name": name}))
.build()?;
self.execute_request(request).await?.into_result_t()
}
pub async fn all_snapshots(
&self,
scope: Option<SnapshotScope>,
) -> Result<Vec<Snapshot>, ApiError> {
self.request_endpoint(Endpoint::list_all_snapshots(scope))
.await?
.into_result_t()
}
pub async fn all_workspaces(
&self,
) -> Result<Vec<WorkspaceInfo>, ApiError> {
self.request_endpoint(Endpoint::list_workspaces())
.await?
.into_result_t()
}
}