client.rs
1 use dotenv::dotenv; 2 use log::debug; 3 use reqwest::header::{HeaderMap, HeaderValue}; 4 use std::env; 5 6 use crate::csfloat::error::CSFloatError; 7 use crate::csfloat::endpoints::listings::ListingsHandler; 8 9 pub const API_BASE_URL: &str = "https://csfloat.com/api/v1"; 10 11 /// The main CSFloat API client. 12 pub struct CSFloatClient { 13 pub(crate) api_key: String, 14 pub(crate) http_client: reqwest::Client, 15 } 16 17 impl CSFloatClient { 18 /// Creates a new CSFloatClient instance. 19 /// 20 /// Reads `CSFLOAT_API_KEY` from environment variables. 21 pub fn new() -> Result<Self, CSFloatError> { 22 dotenv().ok(); 23 let api_key = env::var("CSFLOAT_API_KEY")?; 24 25 if api_key.is_empty() { 26 return Err(CSFloatError::ApiError( 27 "API key cannot be empty".to_string(), 28 )); 29 } 30 31 debug!( 32 "Initializing CSFloat client with API key (first 8 chars): {}...", 33 api_key.chars().take(8).collect::<String>() 34 ); 35 36 Ok(CSFloatClient { 37 api_key, 38 http_client: reqwest::Client::builder() 39 .user_agent("CSFloat-API-Client/1.0") 40 .build()?, 41 }) 42 } 43 44 /// Creates the required HTTP headers for an API request. 45 pub(crate) fn create_headers(&self) -> Result<HeaderMap, CSFloatError> { 46 let mut headers = HeaderMap::new(); 47 headers.insert("Authorization", HeaderValue::from_str(&self.api_key)?); 48 headers.insert("Content-Type", HeaderValue::from_static("application/json")); 49 headers.insert("Accept", HeaderValue::from_static("application/json")); 50 Ok(headers) 51 } 52 53 /// Makes a GET request to the CSFloat API. 54 pub(crate) async fn get<T: serde::de::DeserializeOwned>( 55 &self, 56 endpoint: &str, 57 ) -> Result<T, CSFloatError> { 58 let url = format!("{}{}", API_BASE_URL, endpoint); 59 debug!("GET request to URL: {}", url); 60 61 let response = self.http_client 62 .get(&url) 63 .headers(self.create_headers()?) 64 .send() 65 .await?; 66 67 let status = response.status(); 68 debug!("Response status: {}", status); 69 70 if !status.is_success() { 71 let error_text = response.text().await?; 72 debug!("Error response: {}", error_text); 73 return Err(CSFloatError::ApiError(format!( 74 "API request failed with status {}: {}", 75 status, error_text 76 ))); 77 } 78 79 // Debug the response body before trying to deserialize it 80 let text = response.text().await?; 81 debug!("Response body: {}", text); 82 83 match serde_json::from_str::<T>(&text) { 84 Ok(parsed) => Ok(parsed), 85 Err(e) => { 86 debug!("Failed to deserialize response: {}", e); 87 Err(CSFloatError::SerializationError(e)) 88 } 89 } 90 } 91 92 /// Makes a POST request to the CSFloat API. 93 pub(crate) async fn post<T: serde::de::DeserializeOwned, B: serde::Serialize>( 94 &self, 95 endpoint: &str, 96 body: &B, 97 ) -> Result<T, CSFloatError> { 98 let url = format!("{}{}", API_BASE_URL, endpoint); 99 100 let response = self.http_client 101 .post(&url) 102 .headers(self.create_headers()?) 103 .json(body) 104 .send() 105 .await?; 106 107 let status = response.status(); 108 109 if !status.is_success() { 110 let error_text = response.text().await?; 111 return Err(CSFloatError::ApiError(format!( 112 "API request failed with status {}: {}", 113 status, 114 error_text 115 ))); 116 } 117 118 // Debug the response body before trying to deserialize it 119 let text = response.text().await?; 120 debug!("Response body: {}", text); 121 122 match serde_json::from_str::<T>(&text) { 123 Ok(parsed) => Ok(parsed), 124 Err(e) => { 125 debug!("Failed to deserialize response: {}", e); 126 Err(CSFloatError::SerializationError(e)) 127 } 128 } 129 } 130 131 /// Provides access to listings-related API endpoints. 132 pub fn listings(&self) -> ListingsHandler<'_> { 133 ListingsHandler::new(self) 134 } 135 }