ai-transform-runtime 0.1.0

Runtime library for AI-powered data transformations using OpenAI
Documentation
#![warn(clippy::pedantic)]

//! Runtime library, providing the core functionality for the [`ai_transform`] macro, including
//! the `OpenAI` client, error types, and the main transformation logic.
//!
//! ## Usage
//!
//! Typically used indirectly through the [`ai_transform`] macro, but can
//! also be used directly:
//!
//! ```rust,no_run
//! use ai_transform_runtime::{transform, error::TransformError};
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Serialize, Deserialize, Default)]
//! struct User { name: String, age: u32 }
//!
//! #[derive(Serialize, Deserialize, Default, Debug)]
//! struct Profile { full_name: String, years_old: u32, is_adult: bool }
//!
//! #[tokio::main]
//! async fn main() -> Result<(), TransformError> {
//!     let user = User { name: "Alice".to_string(), age: 28 };
//!     let profile: Profile = transform(user).await?;
//!     println!("{:?}", profile);
//!     Ok(())
//! }
//! ```
//!
//! ## How It Works
//!
//! 1. **Serialization**: Converts the source value to JSON
//! 2. **Schema Generation**: Creates example JSON for both source and target types
//! 3. **AI Request**: Sends a transformation prompt to `OpenAI`'s API
//! 4. **Response Processing**: Extracts and cleans the JSON response
//! 5. **Deserialization**: Converts the result back to the target type
//!
//! ## Configuration
//!
//! Environment variables:
//! - `OPENAI_API_KEY`: Your `OpenAI` API key (**required**)
//! - `OPENAI_MODEL`: Model to use (default: `"gpt-4o"`)
//! - `OPENAI_BASE_URL`: API base URL (default: `"https://api.openai.com/v1"`)
//!
//! ## Considerations
//!
//! - Each call makes a network request to `OpenAI`'s API
//! - Response time depends on data complexity and `OpenAI`'s response time
//! - API usage incurs costs based on `OpenAI`'s pricing
//!
//! ## Requirements
//!
//! Both source and target types must implement:
//! - `serde::Serialize` + `serde::Deserialize` + `Default`
//!
//! [`ai_transform`]: https://docs.rs/ai-transform

mod client;

pub mod error;

use crate::client::OpenAIClient;
use crate::error::TransformError;
use serde::{Deserialize, Serialize};

/// Transforms data from one type to another using AI-powered transformation.
///
/// # Arguments
///
/// * `val` - The source value to transform
///
/// # Returns
///
/// `Result<T, TransformError>` - Success contains transformed value, error contains failure details
///
/// # Examples
///
/// ```rust,no_run
/// use ai_transform_runtime::transform;
/// use serde::{Deserialize, Serialize};
///
/// #[derive(Serialize, Deserialize, Default)]
/// struct User { name: String, age: u32 }
///
/// #[derive(Serialize, Deserialize, Default)]
/// struct Profile { full_name: String, years_old: u32 }
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let user = User { name: "Alice".into(), age: 28 };
/// let profile: Profile = transform(user).await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// See [`TransformError`] for all possible failure modes
pub async fn transform<
    S: Serialize + for<'de> Deserialize<'de> + Default,
    T: Serialize + for<'de> Deserialize<'de> + Default,
>(
    val: S,
) -> Result<T, TransformError> {
    let source_json = serde_json::to_string(&val).map_err(TransformError::SerializationError)?;
    let client: OpenAIClient = OpenAIClient::new()?;

    let source_example = <S>::default();
    let target_example = <T>::default();

    let source_schema =
        serde_json::to_string(&source_example).map_err(TransformError::SerializationError)?;
    let target_schema =
        serde_json::to_string(&target_example).map_err(TransformError::SerializationError)?;

    let target_json = client
        .transform_json(
            &source_json,
            stringify!(S),
            stringify!(T),
            &source_schema,
            &target_schema,
        )
        .await?;

    let result: T =
        serde_json::from_str(&target_json).map_err(TransformError::DeserializationError)?;

    Ok(result)
}

#[cfg(test)]
mod tests {
    // Verify `OPENAI_API_KEY` environment variable is set
    // in order to use the macro
    #[test]
    #[ignore]
    fn test_api_key_in_env() {
        use std::env;

        let api_key = env::var("OPENAI_API_KEY").unwrap_or_else(|_| String::new());
        assert_ne!(
            api_key, "",
            "You must supple `OPENAI_API_KEY` via environment variable"
        );
    }
}