ig_client/transport/
http_client.rs1use 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#[async_trait]
11pub trait IgHttpClient: Send + Sync {
12 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 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
38pub struct IgHttpClientImpl {
40 config: Arc<Config>,
41 client: Client,
42}
43
44impl IgHttpClientImpl {
45 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 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 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 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 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}