sdaas-rs 0.1.0

Official Rust SDK for SDaaS — Semantic Delta as a Service
Documentation
use crate::types::{DeltaRequest, DeltaResponse, ValidationResponse};
use crate::{Error, Result};
use reqwest::{Client as HttpClient, StatusCode};
use uuid::Uuid;

/// Async client for SDaaS API
///
/// The [`Client`] provides methods for computing text deltas and validating API keys.
/// It uses async/await patterns with tokio for high-efficiency concurrent operations.
///
/// # Example
///
/// ```no_run
/// use sdaas_rs::Client;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let client = Client::new(
///         "your-api-key",
///         "https://saas-core-production.up.railway.app"
///     );
///
///     let delta = client.compute_delta("Hello", "Hello World").await?;
///     println!("Delta: {:?}", delta.delta);
///     Ok(())
/// }
/// ```
pub struct Client {
    http_client: HttpClient,
    api_key: String,
    base_url: String,
}

impl Client {
    /// Create a new SDaaS client
    ///
    /// # Arguments
    ///
    /// * `api_key` - Your SDaaS API key (can be obtained from https://saas-core-production.up.railway.app)
    /// * `base_url` - Base URL for the SDaaS API (defaults to `https://saas-core-production.up.railway.app`)
    ///
    /// # Example
    ///
    /// ```no_run
    /// use sdaas_rs::Client;
    ///
    /// let client = Client::new(
    ///     "your-api-key",
    ///     "https://saas-core-production.up.railway.app"
    /// );
    /// ```
    pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
        Self {
            http_client: HttpClient::new(),
            api_key: api_key.into(),
            base_url: base_url.into(),
        }
    }

    /// Compute delta between source and target text
    ///
    /// Sends a request to the SDaaS API to compute the delta (diff) between two texts.
    /// The response includes delta operations and compression metrics.
    ///
    /// # Arguments
    ///
    /// * `source` - The original text
    /// * `target` - The target text to transform source into
    ///
    /// # Returns
    ///
    /// Returns a [`DeltaResponse`] containing:
    /// - `delta`: Vector of insert/delete operations
    /// - `size_bytes`: Compressed delta size
    /// - `compression_ratio`: Efficiency metric (0.0-1.0)
    ///
    /// # Errors
    ///
    /// Returns [`Error::Unauthorized`] if API key is invalid.
    /// Returns [`Error::RateLimitExceeded`] if rate limit is hit.
    /// Returns [`Error::QuotaExceeded`] if quota is exhausted.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// use sdaas_rs::Client;
    ///
    /// let client = Client::new("your-api-key", "https://saas-core-production.up.railway.app");
    /// let delta = client.compute_delta("Hello", "Hello World").await?;
    /// println!("Operations: {}", delta.delta.len());
    /// println!("Compression: {:.1}%", delta.compression_ratio * 100.0);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn compute_delta(&self, source: &str, target: &str) -> Result<DeltaResponse> {
        let request = DeltaRequest {
            source: source.to_string(),
            target: target.to_string(),
        };

        let url = format!("{}/api/delta", self.base_url);
        let request_id = Uuid::new_v4().to_string();

        let response = self
            .http_client
            .post(&url)
            .header("X-API-Key", &self.api_key)
            .header("X-Request-ID", &request_id)
            .header("Content-Type", "application/json")
            .json(&request)
            .send()
            .await?;

        match response.status() {
            StatusCode::OK => response
                .json::<DeltaResponse>()
                .await
                .map_err(|e| Error::InvalidResponse(e.to_string())),
            StatusCode::UNAUTHORIZED => Err(Error::Unauthorized),
            StatusCode::TOO_MANY_REQUESTS => Err(Error::RateLimitExceeded),
            StatusCode::PAYMENT_REQUIRED => Err(Error::QuotaExceeded),
            status => Err(Error::Api(format!("API returned: {}", status))),
        }
    }

    /// Validate API key and get current quota/rate limit info
    ///
    /// Checks if the API key is valid and retrieves the current account details.
    ///
    /// # Returns
    ///
    /// Returns a [`ValidationResponse`] containing:
    /// - `valid`: Whether the key is valid
    /// - `key`: [`KeyValidation`] with tier, quota, and rate limit info
    ///
    /// # Errors
    ///
    /// Returns [`Error::InvalidApiKey`] if the API key is invalid.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// use sdaas_rs::Client;
    ///
    /// let client = Client::new("your-api-key", "https://saas-core-production.up.railway.app");
    /// let validation = client.validate_key().await?;
    /// println!("Tier: {}", validation.key.tier);
    /// println!("Quota remaining: {}", validation.key.quota_remaining);
    /// println!("Rate limit: {}/sec", validation.key.rate_limit);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn validate_key(&self) -> Result<ValidationResponse> {
        let url = format!("{}/api/validate", self.base_url);

        let response = self
            .http_client
            .get(&url)
            .header("X-API-Key", &self.api_key)
            .send()
            .await?;

        match response.status() {
            StatusCode::OK => response
                .json::<ValidationResponse>()
                .await
                .map_err(|e| Error::InvalidResponse(e.to_string())),
            StatusCode::UNAUTHORIZED => Err(Error::InvalidApiKey),
            status => Err(Error::Api(format!("API returned: {}", status))),
        }
    }

    /// Set a new API key
    ///
    /// Updates the API key used for subsequent requests.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use sdaas_rs::Client;
    ///
    /// let mut client = Client::new("old-key", "https://saas-core-production.up.railway.app");
    /// client.set_api_key("new-key");
    /// ```
    pub fn set_api_key(&mut self, api_key: impl Into<String>) {
        self.api_key = api_key.into();
    }

    /// Get the current base URL
    ///
    /// Returns a reference to the base URL being used for API requests.
    pub fn base_url(&self) -> &str {
        &self.base_url
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_client_creation() {
        let client = Client::new("test-key", "https://example.com");
        assert_eq!(client.base_url(), "https://example.com");
    }

    #[test]
    fn test_set_api_key() {
        let mut client = Client::new("old-key", "https://example.com");
        client.set_api_key("new-key");
        assert_eq!(client.api_key, "new-key");
    }

    #[test]
    fn test_client_url_normalization() {
        let client = Client::new("key", "https://example.com/");
        assert_eq!(client.base_url(), "https://example.com/");
    }
}