Skip to main content

sdaas_rs/
client.rs

1use crate::types::{DeltaRequest, DeltaResponse, ValidationResponse};
2use crate::{Error, Result};
3use reqwest::{Client as HttpClient, StatusCode};
4use uuid::Uuid;
5
6/// Async client for SDaaS API
7///
8/// The [`Client`] provides methods for computing text deltas and validating API keys.
9/// It uses async/await patterns with tokio for high-efficiency concurrent operations.
10///
11/// # Example
12///
13/// ```no_run
14/// use sdaas_rs::Client;
15///
16/// #[tokio::main]
17/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
18///     let client = Client::new(
19///         "your-api-key",
20///         "https://saas-core-production.up.railway.app"
21///     );
22///
23///     let delta = client.compute_delta("Hello", "Hello World").await?;
24///     println!("Delta: {:?}", delta.delta);
25///     Ok(())
26/// }
27/// ```
28pub struct Client {
29    http_client: HttpClient,
30    api_key: String,
31    base_url: String,
32}
33
34impl Client {
35    /// Create a new SDaaS client
36    ///
37    /// # Arguments
38    ///
39    /// * `api_key` - Your SDaaS API key (can be obtained from https://saas-core-production.up.railway.app)
40    /// * `base_url` - Base URL for the SDaaS API (defaults to `https://saas-core-production.up.railway.app`)
41    ///
42    /// # Example
43    ///
44    /// ```no_run
45    /// use sdaas_rs::Client;
46    ///
47    /// let client = Client::new(
48    ///     "your-api-key",
49    ///     "https://saas-core-production.up.railway.app"
50    /// );
51    /// ```
52    pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
53        Self {
54            http_client: HttpClient::new(),
55            api_key: api_key.into(),
56            base_url: base_url.into(),
57        }
58    }
59
60    /// Compute delta between source and target text
61    ///
62    /// Sends a request to the SDaaS API to compute the delta (diff) between two texts.
63    /// The response includes delta operations and compression metrics.
64    ///
65    /// # Arguments
66    ///
67    /// * `source` - The original text
68    /// * `target` - The target text to transform source into
69    ///
70    /// # Returns
71    ///
72    /// Returns a [`DeltaResponse`] containing:
73    /// - `delta`: Vector of insert/delete operations
74    /// - `size_bytes`: Compressed delta size
75    /// - `compression_ratio`: Efficiency metric (0.0-1.0)
76    ///
77    /// # Errors
78    ///
79    /// Returns [`Error::Unauthorized`] if API key is invalid.
80    /// Returns [`Error::RateLimitExceeded`] if rate limit is hit.
81    /// Returns [`Error::QuotaExceeded`] if quota is exhausted.
82    ///
83    /// # Example
84    ///
85    /// ```no_run
86    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
87    /// use sdaas_rs::Client;
88    ///
89    /// let client = Client::new("your-api-key", "https://saas-core-production.up.railway.app");
90    /// let delta = client.compute_delta("Hello", "Hello World").await?;
91    /// println!("Operations: {}", delta.delta.len());
92    /// println!("Compression: {:.1}%", delta.compression_ratio * 100.0);
93    /// # Ok(())
94    /// # }
95    /// ```
96    pub async fn compute_delta(&self, source: &str, target: &str) -> Result<DeltaResponse> {
97        let request = DeltaRequest {
98            source: source.to_string(),
99            target: target.to_string(),
100        };
101
102        let url = format!("{}/api/delta", self.base_url);
103        let request_id = Uuid::new_v4().to_string();
104
105        let response = self
106            .http_client
107            .post(&url)
108            .header("X-API-Key", &self.api_key)
109            .header("X-Request-ID", &request_id)
110            .header("Content-Type", "application/json")
111            .json(&request)
112            .send()
113            .await?;
114
115        match response.status() {
116            StatusCode::OK => response
117                .json::<DeltaResponse>()
118                .await
119                .map_err(|e| Error::InvalidResponse(e.to_string())),
120            StatusCode::UNAUTHORIZED => Err(Error::Unauthorized),
121            StatusCode::TOO_MANY_REQUESTS => Err(Error::RateLimitExceeded),
122            StatusCode::PAYMENT_REQUIRED => Err(Error::QuotaExceeded),
123            status => Err(Error::Api(format!("API returned: {}", status))),
124        }
125    }
126
127    /// Validate API key and get current quota/rate limit info
128    ///
129    /// Checks if the API key is valid and retrieves the current account details.
130    ///
131    /// # Returns
132    ///
133    /// Returns a [`ValidationResponse`] containing:
134    /// - `valid`: Whether the key is valid
135    /// - `key`: [`KeyValidation`] with tier, quota, and rate limit info
136    ///
137    /// # Errors
138    ///
139    /// Returns [`Error::InvalidApiKey`] if the API key is invalid.
140    ///
141    /// # Example
142    ///
143    /// ```no_run
144    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
145    /// use sdaas_rs::Client;
146    ///
147    /// let client = Client::new("your-api-key", "https://saas-core-production.up.railway.app");
148    /// let validation = client.validate_key().await?;
149    /// println!("Tier: {}", validation.key.tier);
150    /// println!("Quota remaining: {}", validation.key.quota_remaining);
151    /// println!("Rate limit: {}/sec", validation.key.rate_limit);
152    /// # Ok(())
153    /// # }
154    /// ```
155    pub async fn validate_key(&self) -> Result<ValidationResponse> {
156        let url = format!("{}/api/validate", self.base_url);
157
158        let response = self
159            .http_client
160            .get(&url)
161            .header("X-API-Key", &self.api_key)
162            .send()
163            .await?;
164
165        match response.status() {
166            StatusCode::OK => response
167                .json::<ValidationResponse>()
168                .await
169                .map_err(|e| Error::InvalidResponse(e.to_string())),
170            StatusCode::UNAUTHORIZED => Err(Error::InvalidApiKey),
171            status => Err(Error::Api(format!("API returned: {}", status))),
172        }
173    }
174
175    /// Set a new API key
176    ///
177    /// Updates the API key used for subsequent requests.
178    ///
179    /// # Example
180    ///
181    /// ```no_run
182    /// use sdaas_rs::Client;
183    ///
184    /// let mut client = Client::new("old-key", "https://saas-core-production.up.railway.app");
185    /// client.set_api_key("new-key");
186    /// ```
187    pub fn set_api_key(&mut self, api_key: impl Into<String>) {
188        self.api_key = api_key.into();
189    }
190
191    /// Get the current base URL
192    ///
193    /// Returns a reference to the base URL being used for API requests.
194    pub fn base_url(&self) -> &str {
195        &self.base_url
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_client_creation() {
205        let client = Client::new("test-key", "https://example.com");
206        assert_eq!(client.base_url(), "https://example.com");
207    }
208
209    #[test]
210    fn test_set_api_key() {
211        let mut client = Client::new("old-key", "https://example.com");
212        client.set_api_key("new-key");
213        assert_eq!(client.api_key, "new-key");
214    }
215
216    #[test]
217    fn test_client_url_normalization() {
218        let client = Client::new("key", "https://example.com/");
219        assert_eq!(client.base_url(), "https://example.com/");
220    }
221}