use std::time::Duration;
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
use reqwest::{Client as HttpClient, Method, Response, StatusCode};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::errors::{Error, parse_api_error};
use crate::types::QueryOptions;
const DEFAULT_BASE_URL: &str = "https://api.gitverse.ru";
const DEFAULT_API_VERSION: &str = "1";
const DEFAULT_TIMEOUT_SECS: u64 = 30;
const DEFAULT_USER_AGENT: &str = "gitverse-sdk";
#[derive(Debug, Clone)]
pub struct RateLimitInfo {
pub limit: i64,
pub remaining: i64,
pub reset: i64,
pub retry_after: i64,
}
#[derive(Debug, Clone)]
pub struct ApiVersionInfo {
pub version: String,
pub latest_version: String,
pub deprecated: bool,
pub decommissioning: String,
}
#[derive(Default)]
pub struct ClientConfig {
pub base_url: Option<String>,
pub token: String,
pub api_version: Option<String>,
pub timeout_secs: Option<u64>,
pub user_agent: Option<String>,
}
pub struct Client {
base_url: String,
token: String,
api_version: String,
http_client: HttpClient,
user_agent: String,
}
impl Client {
pub fn new(config: ClientConfig) -> Result<Self, Error> {
let base_url = config
.base_url
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
.trim_end_matches('/')
.to_string();
let api_version = config
.api_version
.unwrap_or_else(|| DEFAULT_API_VERSION.to_string());
let user_agent = config
.user_agent
.unwrap_or_else(|| DEFAULT_USER_AGENT.to_string());
let timeout = Duration::from_secs(config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS));
let http_client = HttpClient::builder().timeout(timeout).build()?;
Ok(Self {
base_url,
token: config.token,
api_version,
http_client,
user_agent,
})
}
pub fn set_token(&mut self, token: impl Into<String>) {
self.token = token.into();
}
pub(crate) async fn send_get(
&self,
path: &str,
query: Option<&QueryOptions>,
) -> Result<Response, Error> {
self.request(Method::GET, path, query, Option::<&()>::None)
.await
}
pub(crate) async fn send_post<B: Serialize>(
&self,
path: &str,
body: Option<&B>,
) -> Result<Response, Error> {
self.request(Method::POST, path, None, body).await
}
pub(crate) async fn send_put<B: Serialize>(
&self,
path: &str,
body: Option<&B>,
) -> Result<Response, Error> {
self.request(Method::PUT, path, None, body).await
}
pub(crate) async fn send_patch<B: Serialize>(
&self,
path: &str,
body: Option<&B>,
) -> Result<Response, Error> {
self.request(Method::PATCH, path, None, body).await
}
pub(crate) async fn send_delete<B: Serialize>(
&self,
path: &str,
body: Option<&B>,
) -> Result<Response, Error> {
self.request(Method::DELETE, path, None, body).await
}
async fn request<B: Serialize>(
&self,
method: Method,
path: &str,
query: Option<&QueryOptions>,
body: Option<&B>,
) -> Result<Response, Error> {
let clean_path = path.trim_start_matches('/');
let url = format!("{}/{}", self.base_url, clean_path);
let mut request = self.http_client.request(method, &url);
let mut headers = HeaderMap::new();
let ua = HeaderValue::from_str(&self.user_agent).map_err(|_| {
Error::Api(crate::errors::ApiError {
status_code: StatusCode::INTERNAL_SERVER_ERROR,
message: format!("invalid User-Agent value: {:?}", self.user_agent),
documentation_url: String::new(),
request_id: String::new(),
})
})?;
headers.insert(USER_AGENT, ua);
let accept = HeaderValue::from_str(&format!(
"application/vnd.gitverse.object+json; version={}",
self.api_version
))
.map_err(|_| {
Error::Api(crate::errors::ApiError {
status_code: StatusCode::INTERNAL_SERVER_ERROR,
message: format!("invalid API version value: {:?}", self.api_version),
documentation_url: String::new(),
request_id: String::new(),
})
})?;
headers.insert(ACCEPT, accept);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
if !self.token.is_empty() {
let auth = HeaderValue::from_str(&format!("Bearer {}", self.token)).map_err(|_| {
Error::Api(crate::errors::ApiError {
status_code: StatusCode::INTERNAL_SERVER_ERROR,
message: "invalid token: contains non-ASCII characters".to_string(),
documentation_url: String::new(),
request_id: String::new(),
})
})?;
headers.insert(AUTHORIZATION, auth);
}
request = request.headers(headers);
if let Some(opts) = query {
request = request.query(opts);
}
if let Some(body) = body {
request = request.json(body);
}
let resp = request.send().await?;
Ok(resp)
}
}
pub(crate) async fn decode_response<T: DeserializeOwned>(resp: Response) -> Result<T, Error> {
let status = resp.status();
if status.is_client_error() || status.is_server_error() {
return Err(parse_api_error(resp).await);
}
if status == StatusCode::NO_CONTENT {
let text = resp.text().await.unwrap_or_default();
if text.is_empty() {
return serde_json::from_str("null").map_err(Error::from);
}
return serde_json::from_str(&text).map_err(Error::from);
}
resp.json::<T>().await.map_err(Error::from)
}
pub(crate) async fn handle_empty_response(resp: Response) -> Result<(), Error> {
let status = resp.status();
if status.is_client_error() || status.is_server_error() {
return Err(parse_api_error(resp).await);
}
Ok(())
}