1use std::time::Duration;
2
3use reqwest::{Client, ClientBuilder};
4use tracing::debug;
5
6use crate::config::Config;
7use crate::context::RequestContext;
8use crate::errors::{TogglrError, TogglrResult};
9use crate::track_event::TrackEvent;
10use crate::types::{EvaluateResponse, HealthResponse, FeatureHealth, FeatureErrorReport};
11
12use crate::generated::apis::default_api::{
13 get_feature_health, report_feature_error, sdk_v1_features_feature_key_evaluate_post,
14 sdk_v1_health_get, track_feature_event
15};
16use crate::generated::apis::configuration::Configuration;
17use crate::generated::models::{
18 TrackRequest, FeatureErrorReport as ApiFeatureErrorReport
19};
20
21pub struct TogglrClient {
22 config: Config,
23 http_client: Client,
24 configuration: Configuration,
25}
26
27impl TogglrClient {
28 pub async fn new(config: Config) -> TogglrResult<Self> {
29 let http_client = Self::build_http_client(&config).await?;
30 let mut configuration = Configuration::default();
31 configuration.base_path = config.base_url.clone();
32 configuration.client = http_client.clone();
33 configuration.api_key = Some(crate::generated::apis::configuration::ApiKey {
34 prefix: None,
35 key: config.api_key.clone(),
36 });
37
38 Ok(Self {
39 config,
40 http_client,
41 configuration,
42 })
43 }
44
45 async fn build_http_client(config: &Config) -> TogglrResult<Client> {
46 let mut client_builder = ClientBuilder::new()
47 .timeout(config.timeout)
48 .pool_max_idle_per_host(config.max_connections);
49
50 if config.insecure {
51 client_builder = client_builder.danger_accept_invalid_certs(true);
52 }
53
54 Ok(client_builder.build()?)
55 }
56
57
58 pub async fn health_check(&self) -> TogglrResult<HealthResponse> {
59 let response = sdk_v1_health_get(&self.configuration).await
60 .map_err(|e| TogglrError::Unknown(format!("Health check failed: {:?}", e)))?;
61 Ok(HealthResponse {
62 status: match response.status {
63 crate::generated::models::health_response::Status::Ok => "ok".to_string(),
64 },
65 server_time: response.server_time,
66 })
67 }
68
69 pub async fn evaluate_feature(
70 &self,
71 feature_key: &str,
72 context: RequestContext,
73 ) -> TogglrResult<EvaluateResponse> {
74 let response = sdk_v1_features_feature_key_evaluate_post(
75 &self.configuration,
76 feature_key,
77 context.to_map(),
78 ).await
79 .map_err(|e| TogglrError::Unknown(format!("Feature evaluation failed: {:?}", e)))?;
80
81 Ok(EvaluateResponse {
82 feature_key: response.feature_key,
83 enabled: response.enabled,
84 value: response.value,
85 })
86 }
87
88 pub async fn track_event(
89 &self,
90 feature_key: &str,
91 event: TrackEvent,
92 ) -> TogglrResult<()> {
93 let api_request = event.to_api_request();
94
95 self.track_event_with_retries(feature_key, api_request).await
96 }
97
98 async fn track_event_with_retries(
99 &self,
100 feature_key: &str,
101 request: TrackRequest,
102 ) -> TogglrResult<()> {
103 let mut last_error = None;
104
105 for attempt in 0..=self.config.retries {
106 if attempt > 0 {
107 let delay = self.calculate_backoff_delay(attempt);
108 debug!("Retrying track event after delay: {:?} (attempt {})", delay, attempt);
109 tokio::time::sleep(delay).await;
110 }
111
112 match self.track_event_single(feature_key, &request).await {
113 Ok(()) => return Ok(()),
114 Err(e) => {
115 last_error = Some(e);
116 if !self.should_retry(&last_error.as_ref().unwrap()) {
117 debug!("Not retrying track event due to error type: {:?}", last_error);
118 break;
119 }
120 debug!("Retrying track event due to error: {:?} (attempt {})", last_error, attempt);
121 }
122 }
123 }
124
125 Err(last_error.unwrap())
126 }
127
128 async fn track_event_single(
129 &self,
130 feature_key: &str,
131 request: &TrackRequest,
132 ) -> TogglrResult<()> {
133 track_feature_event(&self.configuration, feature_key, request.clone())
134 .await
135 .map_err(|e| match e {
136 crate::generated::apis::Error::Reqwest(reqwest_err) => {
137 if reqwest_err.is_timeout() {
138 TogglrError::Timeout(reqwest_err.to_string())
139 } else if reqwest_err.is_connect() {
140 TogglrError::NetworkError(reqwest_err.to_string())
141 } else {
142 TogglrError::HttpError(reqwest_err)
143 }
144 }
145 crate::generated::apis::Error::Serde(serde_err) => {
146 TogglrError::SerializationError(serde_err)
147 }
148 crate::generated::apis::Error::ResponseError(response) => {
149 TogglrError::Unknown(format!("Response error: {:?}", response))
150 }
151 _ => TogglrError::Unknown(format!("Unknown error: {:?}", e)),
152 })?;
153
154 Ok(())
155 }
156
157 pub async fn report_feature_error(
158 &self,
159 feature_key: &str,
160 error_report: FeatureErrorReport,
161 ) -> TogglrResult<()> {
162 let api_request = ApiFeatureErrorReport {
163 error_type: error_report.error_type,
164 error_message: error_report.error_message,
165 context: error_report.context,
166 };
167
168 report_feature_error(&self.configuration, feature_key, api_request)
169 .await
170 .map_err(|e| TogglrError::Unknown(format!("Failed to report error: {:?}", e)))?;
171
172 Ok(())
173 }
174
175 pub async fn get_feature_health(&self, feature_key: &str) -> TogglrResult<FeatureHealth> {
176 let response = get_feature_health(&self.configuration, feature_key)
177 .await
178 .map_err(|e| TogglrError::Unknown(format!("Failed to get feature health: {:?}", e)))?;
179
180 Ok(FeatureHealth {
181 feature_key: response.feature_key,
182 environment_key: response.environment_key,
183 enabled: response.enabled,
184 auto_disabled: response.auto_disabled,
185 error_rate: response.error_rate,
186 threshold: response.threshold,
187 last_error_at: response.last_error_at,
188 })
189 }
190
191 fn calculate_backoff_delay(&self, attempt: u32) -> Duration {
192 let delay_ms = (self.config.backoff.base_delay.as_millis() as f64
193 * self.config.backoff.factor.powi(attempt as i32 - 1))
194 .min(self.config.backoff.max_delay.as_millis() as f64) as u64;
195
196 Duration::from_millis(delay_ms)
197 }
198
199 fn should_retry(&self, error: &TogglrError) -> bool {
200 match error {
201 TogglrError::Timeout(_) | TogglrError::NetworkError(_) | TogglrError::HttpError(_) => true,
202 TogglrError::InternalServerError(_) => true,
203 _ => false,
204 }
205 }
206}
207