1use chrono::{DateTime, Utc};
32use reqwest::{Client, Method, RequestBuilder, Response};
33use serde::{Deserialize, Serialize, Serializer};
34use std::collections::HashMap;
35use thiserror::Error;
36use url::Url;
37
38use anyhow::{anyhow, Result as AnyhowResult};
40use base64::{engine::general_purpose, Engine};
41use rsa::{pkcs1::DecodeRsaPublicKey, pkcs8::DecodePublicKey, Oaep, RsaPublicKey};
42use sha2::Sha256;
43
44pub type CircleResult<T> = Result<T, CircleError>;
46
47#[derive(Error, Debug)]
63pub enum CircleError {
64 #[error("Environment variable error: {0}")]
65 EnvVar(String),
66
67 #[error("HTTP request error: {0}")]
68 Http(#[from] reqwest::Error),
69
70 #[error("JSON serialization/deserialization error: {0}")]
71 Json(#[from] serde_json::Error),
72
73 #[error("URL parsing error: {0}")]
74 Url(#[from] url::ParseError),
75
76 #[error("API error: {status} - {message}")]
77 Api { status: u16, message: String },
78
79 #[error("Invalid configuration: {0}")]
80 Config(String),
81
82 #[error("UUID error: {0}")]
83 Uuid(#[from] uuid::Error),
84}
85
86#[derive(Debug, Deserialize, Serialize)]
88pub struct CircleResponse<T> {
89 pub data: T,
90}
91
92#[derive(Debug, Deserialize, Serialize)]
94pub struct CircleErrorResponse {
95 pub code: Option<i32>,
96 pub message: String,
97}
98
99pub fn serialize_u32_as_string<S>(value: &Option<u32>, serializer: S) -> Result<S::Ok, S::Error>
101where
102 S: Serializer,
103{
104 match value {
105 Some(val) => serializer.serialize_str(&val.to_string()),
106 None => serializer.serialize_none(),
107 }
108}
109
110pub fn serialize_datetime_as_string<S>(
112 dt: &Option<DateTime<Utc>>,
113 serializer: S,
114) -> Result<S::Ok, S::Error>
115where
116 S: Serializer,
117{
118 match dt {
119 Some(dt) => serializer.serialize_str(&dt.to_rfc3339()),
120 None => serializer.serialize_none(),
121 }
122}
123
124pub fn serialize_bool_as_string<S>(value: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error>
125where
126 S: Serializer,
127{
128 match value {
129 Some(val) => serializer.serialize_str(&val.to_string()),
130 None => serializer.serialize_none(),
131 }
132}
133
134#[derive(Debug, Serialize, Default, Clone)]
136pub struct PaginationParams {
137 #[serde(rename = "pageAfter", skip_serializing_if = "Option::is_none")]
138 pub page_after: Option<String>,
139
140 #[serde(rename = "pageBefore", skip_serializing_if = "Option::is_none")]
141 pub page_before: Option<String>,
142
143 #[serde(
144 rename = "pageSize",
145 skip_serializing_if = "Option::is_none",
146 serialize_with = "serialize_u32_as_string"
147 )]
148 pub page_size: Option<u32>,
149}
150
151#[derive(Clone)]
156pub struct HttpClient {
157 client: Client,
158 base_url: Url,
159 api_key: Option<String>,
160}
161
162impl HttpClient {
163 pub fn new(base_url: &str) -> CircleResult<Self> {
165 let client = Client::new();
166 let base_url = Url::parse(base_url)?;
167
168 Ok(Self {
169 client,
170 base_url,
171 api_key: None,
172 })
173 }
174
175 pub fn with_api_key(base_url: &str, api_key: String) -> CircleResult<Self> {
177 let mut client = Self::new(base_url)?;
178 client.api_key = Some(api_key);
179 Ok(client)
180 }
181
182 pub fn request(&self, method: Method, path: &str) -> CircleResult<RequestBuilder> {
184 let url = self.base_url.join(path)?;
185 let mut request = self.client.request(method, url);
186
187 request = request.header("Content-Type", "application/json");
189
190 if let Some(ref api_key) = self.api_key {
192 request = request.header("Authorization", format!("Bearer {}", api_key));
193 }
194
195 Ok(request)
196 }
197
198 pub async fn execute<T>(&self, request: RequestBuilder) -> CircleResult<T>
200 where
201 T: for<'de> Deserialize<'de>,
202 {
203 let response = request.send().await?;
204 self.handle_response(response).await
205 }
206
207 async fn handle_response<T>(&self, response: Response) -> CircleResult<T>
209 where
210 T: for<'de> Deserialize<'de>,
211 {
212 let status = response.status();
213 let response_text = response.text().await?;
214
215 if status.is_success() {
216 let circle_response: CircleResponse<T> = serde_json::from_str(&response_text)?;
217 Ok(circle_response.data)
218 } else {
219 let error_message = match serde_json::from_str::<CircleErrorResponse>(&response_text) {
221 Ok(error_resp) => error_resp.message,
222 Err(_) => response_text,
223 };
224
225 Err(CircleError::Api {
226 status: status.as_u16(),
227 message: error_message,
228 })
229 }
230 }
231}
232
233pub fn get_env_var(name: &str) -> CircleResult<String> {
235 std::env::var(name)
236 .map_err(|_| CircleError::EnvVar(format!("Missing environment variable: {}", name)))
237}
238
239pub fn build_query_params<T: Serialize>(params: &T) -> CircleResult<String> {
241 let query_map: HashMap<String, String> = serde_json::from_value(serde_json::to_value(params)?)?;
242 let query_pairs: Vec<String> = query_map
243 .into_iter()
244 .filter(|(_, v)| !v.is_empty())
245 .map(|(k, v)| format!("{}={}", urlencoding::encode(&k), urlencoding::encode(&v)))
246 .collect();
247
248 Ok(query_pairs.join("&"))
249}
250
251pub fn generate_uuid() -> String {
253 uuid::Uuid::new_v4().to_string()
254}
255
256pub fn encrypt_entity_secret(
268 entity_secret_hex: &str,
269 public_key_pem: &str,
270) -> AnyhowResult<String> {
271 let entity_secret_bytes = hex::decode(entity_secret_hex)
273 .map_err(|e| anyhow!("Failed to decode hex entity secret: {}", e))?;
274
275 let public_key = match RsaPublicKey::from_pkcs1_pem(public_key_pem) {
277 Ok(key) => key,
278 Err(e1) => match RsaPublicKey::from_public_key_pem(public_key_pem) {
279 Ok(key) => key,
280 Err(e2) => {
281 return Err(anyhow!(
282 "Failed to parse public key from PEM (tried both PKCS#1 and PKCS#8): PKCS#1 error: {}, PKCS#8 error: {}",
283 e1, e2
284 ));
285 }
286 },
287 };
288
289 let mut rng = rand::thread_rng();
291 let padding = Oaep::new::<Sha256>();
292 let encrypted_data = public_key
293 .encrypt(&mut rng, padding, &entity_secret_bytes)
294 .map_err(|e| anyhow!("Failed to encrypt data: {}", e))?;
295
296 let base64_encoded = general_purpose::STANDARD.encode(&encrypted_data);
298
299 Ok(base64_encoded)
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_generate_uuid() {
308 let uuid = generate_uuid();
309 assert_eq!(uuid.len(), 36); assert!(uuid.contains('-'));
311 }
312
313 #[test]
314 fn test_pagination_params_serialization() {
315 let params = PaginationParams {
316 page_size: Some(10),
317 ..Default::default()
318 };
319
320 let serialized = serde_json::to_string(¶ms).unwrap();
321 assert!(serialized.contains("pageSize"));
322 assert!(!serialized.contains("pageAfter"));
323 }
324
325 #[test]
326 fn test_encrypt_entity_secret_generates_different_values() {
327 let entity_secret = "deadbeef"; let test_public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3VoPN9PKUjKFLMwOge9+\nG852nMEQAiMpm8FZ8VpJx5yXHdVXyMqTDwGJstidHy5htGKsyEArvIHxBzgWhMfL\nKLFZjvnxWx+rm/d5fk/5UjpjGFI7KABlxEBOAArBuLoi8TJb9BF3MjEqtlHOHUj6\nKG2n4sRRqeWpyFxTJU2v8fhJgHR1HhYkdHw8JdJ6J1lNNJGE7JfGtKDHI4mEo8ZN\nKF8TlW4wIJLQ4CJtEZJH2vKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFwJNJG2v\nKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFw\nQIDAQAB\n-----END PUBLIC KEY-----";
333
334 let result = encrypt_entity_secret(entity_secret, test_public_key);
337
338 assert!(result.is_err());
340
341 }
344}