ig_client/transport/
http_client.rs

1use async_trait::async_trait;
2use reqwest::{Client, Method, RequestBuilder, Response, StatusCode};
3use serde::{Serialize, de::DeserializeOwned};
4use std::sync::Arc;
5use tracing::{debug, error, info};
6
7use crate::{config::Config, error::AppError, session::interface::IgSession};
8
9/// Interface for the IG HTTP client
10#[async_trait]
11pub trait IgHttpClient: Send + Sync {
12    /// Makes an HTTP request to the IG API
13    async fn request<T, R>(
14        &self,
15        method: Method,
16        path: &str,
17        session: &IgSession,
18        body: Option<&T>,
19        version: &str,
20    ) -> Result<R, AppError>
21    where
22        for<'de> R: DeserializeOwned + 'static,
23        T: Serialize + Send + Sync + 'static;
24
25    /// Makes an unauthenticated HTTP request (for login)
26    async fn request_no_auth<T, R>(
27        &self,
28        method: Method,
29        path: &str,
30        body: Option<&T>,
31        version: &str,
32    ) -> Result<R, AppError>
33    where
34        for<'de> R: DeserializeOwned + 'static,
35        T: Serialize + Send + Sync + 'static;
36}
37
38/// Implementación del cliente HTTP para IG
39pub struct IgHttpClientImpl {
40    config: Arc<Config>,
41    client: Client,
42}
43
44impl IgHttpClientImpl {
45    /// Crea una nueva instancia del cliente HTTP
46    pub fn new(config: Arc<Config>) -> Self {
47        let client = Client::builder()
48            .user_agent("ig-client/0.1.0")
49            .timeout(std::time::Duration::from_secs(config.rest_api.timeout))
50            .build()
51            .expect("Failed to create HTTP client");
52
53        Self { config, client }
54    }
55
56    /// Construye la URL completa para una petición
57    fn build_url(&self, path: &str) -> String {
58        format!(
59            "{}/{}",
60            self.config.rest_api.base_url.trim_end_matches('/'),
61            path.trim_start_matches('/')
62        )
63    }
64
65    /// Añade los headers comunes a todas las peticiones
66    fn add_common_headers(&self, builder: RequestBuilder, version: &str) -> RequestBuilder {
67        builder
68            .header("X-IG-API-KEY", &self.config.credentials.api_key)
69            .header("Content-Type", "application/json; charset=UTF-8")
70            .header("Accept", "application/json; charset=UTF-8")
71            .header("Version", version)
72    }
73
74    /// Añade los headers de autenticación a una petición
75    fn add_auth_headers(&self, builder: RequestBuilder, session: &IgSession) -> RequestBuilder {
76        builder
77            .header("CST", &session.cst)
78            .header("X-SECURITY-TOKEN", &session.token)
79    }
80
81    /// Procesa la respuesta HTTP
82    async fn process_response<R>(&self, response: Response) -> Result<R, AppError>
83    where
84        R: DeserializeOwned,
85    {
86        let status = response.status();
87        let url = response.url().to_string();
88
89        match status {
90            StatusCode::OK | StatusCode::CREATED | StatusCode::ACCEPTED => {
91                let json = response.json::<R>().await?;
92                debug!("Request to {} successful", url);
93                Ok(json)
94            }
95            StatusCode::UNAUTHORIZED => {
96                error!("Unauthorized request to {}", url);
97                Err(AppError::Unauthorized)
98            }
99            StatusCode::NOT_FOUND => {
100                error!("Resource not found at {}", url);
101                Err(AppError::NotFound)
102            }
103            StatusCode::TOO_MANY_REQUESTS => {
104                error!("Rate limit exceeded for {}", url);
105                Err(AppError::RateLimitExceeded)
106            }
107            _ => {
108                let error_text = response
109                    .text()
110                    .await
111                    .unwrap_or_else(|_| "Unknown error".to_string());
112                error!(
113                    "Request to {} failed with status {}: {}",
114                    url, status, error_text
115                );
116                Err(AppError::Unexpected(status))
117            }
118        }
119    }
120}
121
122#[async_trait]
123impl IgHttpClient for IgHttpClientImpl {
124    async fn request<T, R>(
125        &self,
126        method: Method,
127        path: &str,
128        session: &IgSession,
129        body: Option<&T>,
130        version: &str,
131    ) -> Result<R, AppError>
132    where
133        for<'de> R: DeserializeOwned + 'static,
134        T: Serialize + Send + Sync + 'static,
135    {
136        let url = self.build_url(path);
137        info!("Making {} request to {}", method, url);
138
139        let mut builder = self.client.request(method, &url);
140        builder = self.add_common_headers(builder, version);
141        builder = self.add_auth_headers(builder, session);
142
143        if let Some(data) = body {
144            builder = builder.json(data);
145        }
146
147        let response = builder.send().await?;
148        self.process_response::<R>(response).await
149    }
150
151    async fn request_no_auth<T, R>(
152        &self,
153        method: Method,
154        path: &str,
155        body: Option<&T>,
156        version: &str,
157    ) -> Result<R, AppError>
158    where
159        for<'de> R: DeserializeOwned + 'static,
160        T: Serialize + Send + Sync + 'static,
161    {
162        let url = self.build_url(path);
163        info!("Making unauthenticated {} request to {}", method, url);
164
165        let mut builder = self.client.request(method, &url);
166        builder = self.add_common_headers(builder, version);
167
168        if let Some(data) = body {
169            builder = builder.json(data);
170        }
171
172        let response = builder.send().await?;
173        self.process_response::<R>(response).await
174    }
175}