use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct JsonApiResponse<T> {
pub data: Vec<T>,
#[serde(default)]
pub included: Vec<serde_json::Value>,
#[serde(default)]
pub meta: Option<PaginationMeta>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PaginationMeta {
#[serde(default)]
pub offset: Option<u64>,
#[serde(default)]
pub limit: Option<u64>,
#[serde(default)]
pub total: Option<u64>,
}
impl<T> JsonApiResponse<T> {
pub fn has_more(&self) -> bool {
if let Some(meta) = &self.meta
&& let (Some(offset), Some(limit), Some(total)) = (meta.offset, meta.limit, meta.total)
{
return offset + limit < total;
}
false
}
pub fn next_offset(&self) -> Option<u64> {
if let Some(meta) = &self.meta
&& let (Some(offset), Some(limit), Some(total)) = (meta.offset, meta.limit, meta.total)
{
let next = offset + limit;
if next < total {
return Some(next);
}
}
None
}
}
#[derive(Debug, Deserialize)]
pub struct JsonApiSingleResponse<T> {
pub data: T,
#[serde(default)]
pub included: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Project {
#[serde(rename = "type")]
pub resource_type: String,
pub id: String,
pub attributes: ProjectAttributes,
#[serde(default)]
pub relationships: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProjectAttributes {
pub name: String,
#[serde(rename = "description", default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Branch {
#[serde(rename = "type")]
pub resource_type: String,
pub id: String,
pub attributes: BranchAttributes,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BranchAttributes {
pub name: String,
#[serde(rename = "main-for-project", default)]
pub main_for_project: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Run {
#[serde(rename = "type")]
pub resource_type: String,
pub id: String,
pub attributes: RunAttributes,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RunAttributes {
#[serde(default)]
pub status: Option<String>,
#[serde(rename = "date-created", default)]
pub date_created: Option<String>,
#[serde(rename = "date-completed", default)]
pub date_completed: Option<String>,
}
pub struct CommonClient {
http: reqwest::Client,
base_url: String,
}
impl CommonClient {
pub fn new(base_url: &str, jwt: &str) -> crate::error::Result<Self> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {jwt}"))
.map_err(|e| crate::error::PolarisError::Other(format!("invalid header value: {e}")))?,
);
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/vnd.api+json"),
);
let http = reqwest::Client::builder()
.default_headers(headers)
.timeout(std::time::Duration::from_secs(120))
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.map_err(crate::error::PolarisError::Http)?;
Ok(Self {
http,
base_url: base_url.trim_end_matches('/').to_string(),
})
}
pub async fn list_projects(
&self,
name_filter: Option<&str>,
limit: u32,
offset: u32,
) -> crate::error::Result<JsonApiResponse<Project>> {
let mut url = format!(
"{}/api/common/v0/projects?page[limit]={limit}&page[offset]={offset}",
self.base_url
);
if let Some(name) = name_filter {
url.push_str(&format!(
"&filter[project][name][$eq]={}",
urlencoding::encode(name)
));
}
url.push_str("&include[project][]=branches");
let resp = self.http.get(&url).send().await?;
Self::check_response(resp).await
}
pub async fn list_branches(
&self,
project_id: &str,
limit: u32,
offset: u32,
) -> crate::error::Result<JsonApiResponse<Branch>> {
let url = format!(
"{}/api/common/v0/branches?filter[branch][project][id][$eq]={}&page[limit]={limit}&page[offset]={offset}",
self.base_url,
urlencoding::encode(project_id),
);
let resp = self.http.get(&url).send().await?;
Self::check_response(resp).await
}
pub async fn list_runs(
&self,
project_id: &str,
revision_id: Option<&str>,
limit: u32,
offset: u32,
) -> crate::error::Result<JsonApiResponse<Run>> {
let mut url = format!(
"{}/api/common/v0/runs?filter[run][project][id][$eq]={}&page[limit]={limit}&page[offset]={offset}",
self.base_url,
urlencoding::encode(project_id),
);
if let Some(rev) = revision_id {
url.push_str(&format!("&filter[run][revision][id][$eq]={}", urlencoding::encode(rev)));
}
let resp = self.http.get(&url).send().await?;
Self::check_response(resp).await
}
async fn check_response<T: serde::de::DeserializeOwned>(
resp: reqwest::Response,
) -> crate::error::Result<T> {
let status = resp.status();
if !status.is_success() {
let code = status.as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(crate::error::PolarisError::Api {
status: code,
detail: body,
});
}
resp.json::<T>()
.await
.map_err(|e| crate::error::PolarisError::Deserialize(e.to_string()))
}
}