gitverse-sdk 1.2.0

Rust SDK for GitVerse API
Documentation
// Code generated by gitverse-codegen. DO NOT EDIT.

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,
    /// Время сброса лимита (Unix timestamp)
    pub reset: i64,
    /// Через сколько секунд можно повторить запрос
    pub retry_after: i64,
}

/// Информация о версии API
#[derive(Debug, Clone)]
pub struct ApiVersionInfo {
    /// Текущая версия API
    pub version: String,
    /// Последняя доступная версия
    pub latest_version: String,
    /// Признак устаревшей версии
    pub deprecated: bool,
    /// Дата вывода из эксплуатации
    pub decommissioning: String,
}

/// Конфигурация клиента
#[derive(Default)]
pub struct ClientConfig {
    /// Базовый URL API (по умолчанию https://api.gitverse.ru)
    pub base_url: Option<String>,
    /// Токен авторизации
    pub token: String,
    /// Версия API (по умолчанию "1")
    pub api_version: Option<String>,
    /// Таймаут запроса в секундах
    pub timeout_secs: Option<u64>,
    /// User-Agent заголовок
    pub user_agent: Option<String>,
}

/// HTTP клиент для GitVerse API
pub struct Client {
    base_url: String,
    token: String,
    api_version: String,
    http_client: HttpClient,
    user_agent: String,
}

impl Client {
    /// Создаёт новый клиент GitVerse API
    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();
    }

    /// Выполняет GET запрос
    pub(crate) async fn send_get(
        &self,
        path: &str,
        query: Option<&QueryOptions>,
    ) -> Result<Response, Error> {
        self.request(Method::GET, path, query, Option::<&()>::None)
            .await
    }

    /// Выполняет POST запрос
    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
    }

    /// Выполняет PUT запрос
    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
    }

    /// Выполняет PATCH запрос
    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
    }

    /// Выполняет DELETE запрос
    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
    }

    /// Выполняет HTTP запрос
    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);

        // Query параметры
        if let Some(opts) = query {
            request = request.query(opts);
        }

        // Body
        if let Some(body) = body {
            request = request.json(body);
        }

        let resp = request.send().await?;
        Ok(resp)
    }
}

/// Декодирует JSON ответ
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);
    }

    // Для пустых ответов (204 No Content)
    if status == StatusCode::NO_CONTENT {
        // Пытаемся десериализовать пустой JSON
        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)
}

/// Обрабатывает ответ без тела (void)
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(())
}