inf_circle_sdk/
helper.rs

1//! Shared helper functions and types used across the SDK
2//!
3//! This module provides common utilities, error types, HTTP client functionality,
4//! and cryptographic helpers used throughout the Circle SDK.
5//!
6//! # Main Components
7//!
8//! - [`CircleError`]: Comprehensive error type for all SDK operations
9//! - [`CircleResult`]: Type alias for `Result<T, CircleError>`
10//! - [`HttpClient`]: Configured HTTP client for Circle API requests
11//! - [`encrypt_entity_secret`]: RSA-OAEP encryption for entity secrets
12//! - Serialization helpers for API compatibility
13//!
14//! # Error Handling
15//!
16//! All SDK operations return `CircleResult<T>` which provides detailed error information:
17//!
18//! ```rust
19//! use inf_circle_sdk::helper::{CircleError, CircleResult};
20//!
21//! fn example_function() -> CircleResult<String> {
22//!     // Environment variable errors
23//!     // HTTP request errors  
24//!     // JSON parsing errors
25//!     // API errors with status codes
26//!     // etc.
27//!     Ok("Success".to_string())
28//! }
29//! ```
30
31use 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
38// Cryptography imports
39use anyhow::{anyhow, Result as AnyhowResult};
40use base64::{engine::general_purpose, Engine};
41use rsa::{pkcs1::DecodeRsaPublicKey, pkcs8::DecodePublicKey, Oaep, RsaPublicKey};
42use sha2::Sha256;
43
44/// Result type alias for Circle SDK operations
45pub type CircleResult<T> = Result<T, CircleError>;
46
47/// Comprehensive error type for Circle SDK operations
48///
49/// This enum covers all possible errors that can occur when using the Circle SDK,
50/// including environment configuration errors, HTTP request failures, JSON parsing issues,
51/// and API-specific errors with status codes.
52///
53/// # Variants
54///
55/// - `EnvVar`: Missing or invalid environment variables
56/// - `Http`: HTTP request failures (network errors, timeouts, etc.)
57/// - `Json`: JSON serialization/deserialization errors
58/// - `Url`: URL parsing errors
59/// - `Api`: Circle API errors with HTTP status code and message
60/// - `Config`: Invalid SDK configuration
61/// - `Uuid`: UUID parsing or generation errors
62#[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/// Standard Circle API response wrapper
87#[derive(Debug, Deserialize, Serialize)]
88pub struct CircleResponse<T> {
89    pub data: T,
90}
91
92/// Standard Circle API error response
93#[derive(Debug, Deserialize, Serialize)]
94pub struct CircleErrorResponse {
95    pub code: Option<i32>,
96    pub message: String,
97}
98
99/// Helper function to serialize u32 as string
100pub 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
110/// Helper function to serialize DateTime as string
111pub 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/// Common query parameters for pagination
135#[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/// HTTP client wrapper with common functionality
152///
153/// Handles HTTP requests to the Circle API with automatic header management,
154/// authentication, and response parsing.
155#[derive(Clone)]
156pub struct HttpClient {
157    client: Client,
158    base_url: Url,
159    api_key: Option<String>,
160}
161
162impl HttpClient {
163    /// Create a new HTTP client with base URL
164    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    /// Create a new HTTP client with base URL and API key
176    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    /// Build a request with common headers
183    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        // Add common headers
188        request = request.header("Content-Type", "application/json");
189
190        // Add authorization header if API key is available
191        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    /// Execute a request and handle the response
199    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    /// Handle HTTP response and convert to typed result
208    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            // Try to parse error response
220            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
233/// Helper function to read environment variable
234pub 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
239/// Helper function to build query string from parameters
240pub 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
251/// Helper function to generate UUID v4
252pub fn generate_uuid() -> String {
253    uuid::Uuid::new_v4().to_string()
254}
255
256/// Encrypts entity secret using RSA-OAEP with SHA-256
257///
258/// This function takes a hex-encoded entity secret and encrypts it using the provided
259/// RSA public key in PEM format. The result is base64-encoded.
260///
261/// # Arguments
262/// * `entity_secret_hex` - The entity secret as a hex string
263/// * `public_key_pem` - The RSA public key in PEM format (PKCS#1 or PKCS#8)
264///
265/// # Returns
266/// * `Result<String>` - Base64-encoded encrypted data on success
267pub fn encrypt_entity_secret(
268    entity_secret_hex: &str,
269    public_key_pem: &str,
270) -> AnyhowResult<String> {
271    // Convert hex string to bytes
272    let entity_secret_bytes = hex::decode(entity_secret_hex)
273        .map_err(|e| anyhow!("Failed to decode hex entity secret: {}", e))?;
274
275    // Try PKCS#1 format first, then fall back to PKCS#8 format
276    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    // Encrypt using RSA-OAEP with SHA-256
290    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    // Encode to base64
297    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); // Standard UUID length
310        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(&params).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        // Test that multiple encryptions of the same data produce different results
328        // This is expected behavior due to RSA-OAEP padding with random values
329
330        // Use a simple test key pair (in practice, this would come from environment)
331        let entity_secret = "deadbeef"; // Simple hex string
332        let test_public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3VoPN9PKUjKFLMwOge9+\nG852nMEQAiMpm8FZ8VpJx5yXHdVXyMqTDwGJstidHy5htGKsyEArvIHxBzgWhMfL\nKLFZjvnxWx+rm/d5fk/5UjpjGFI7KABlxEBOAArBuLoi8TJb9BF3MjEqtlHOHUj6\nKG2n4sRRqeWpyFxTJU2v8fhJgHR1HhYkdHw8JdJ6J1lNNJGE7JfGtKDHI4mEo8ZN\nKF8TlW4wIJLQ4CJtEZJH2vKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFwJNJG2v\nKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFwJNJG2vKJJFNyFw\nQIDAQAB\n-----END PUBLIC KEY-----";
333
334        // Note: This test would require a valid key pair to actually run
335        // For now, we'll just test the function signature and error handling
336        let result = encrypt_entity_secret(entity_secret, test_public_key);
337
338        // We expect this to fail with an invalid key, but that's ok for testing the interface
339        assert!(result.is_err());
340
341        // The important thing is that the function exists and has the right signature
342        // In real usage with valid keys, multiple calls would produce different encrypted values
343    }
344}