#[cfg(test)]
mod openai_validation_tests {
use rstructor::{Instructor, LLMClient, OpenAIClient, OpenAIModel, RStructorError};
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(description = "Information about a recipe")]
struct RecipeInfo {
#[llm(description = "Name of the recipe")]
name: String,
#[llm(description = "Cooking time in minutes", example = 30)]
cooking_time: u32,
#[llm(description = "Preparation time in minutes", example = 15)]
prep_time: u32,
#[llm(description = "Difficulty level (1-5)", example = 3)]
difficulty: u8,
#[llm(description = "Ingredients list", example = ["flour", "sugar", "eggs"])]
ingredients: Vec<String>,
}
impl RecipeInfo {
fn validate(&self) -> rstructor::Result<()> {
if self.name.trim().is_empty() {
return Err(RStructorError::ValidationError(
"Recipe name cannot be empty".to_string(),
));
}
if self.cooking_time > 24 * 60 {
return Err(RStructorError::ValidationError(format!(
"Cooking time should be less than 24 hours, got {} minutes",
self.cooking_time
)));
}
if self.difficulty < 1 || self.difficulty > 5 {
return Err(RStructorError::ValidationError(format!(
"Difficulty must be between 1 and 5, got {}",
self.difficulty
)));
}
if self.ingredients.is_empty() {
return Err(RStructorError::ValidationError(
"Recipe must have at least one ingredient".to_string(),
));
}
Ok(())
}
}
#[tokio::test]
async fn test_openai_validation_fails_with_long_cooking_time() {
let invalid_recipe = RecipeInfo {
name: "Slow Cooked Stew".to_string(),
cooking_time: 10000, prep_time: 30,
difficulty: 3,
ingredients: vec![
"beef".to_string(),
"vegetables".to_string(),
"broth".to_string(),
],
};
let validation_result = invalid_recipe.validate();
assert!(
validation_result.is_err(),
"Validation should fail with extreme cooking time"
);
if let Err(RStructorError::ValidationError(msg)) = validation_result {
assert!(
msg.contains("Cooking time") || msg.contains("cooking time"),
"Error should mention cooking time: {}",
msg
);
} else if let Err(e) = validation_result {
panic!("Expected ValidationError about cooking time, got: {:?}", e);
}
}
#[tokio::test]
async fn test_openai_validation_fails_with_invalid_difficulty() {
let invalid_recipe = RecipeInfo {
name: "Extremely Difficult Soufflé".to_string(),
cooking_time: 45,
prep_time: 60,
difficulty: 10, ingredients: vec![
"eggs".to_string(),
"cheese".to_string(),
"flour".to_string(),
],
};
let validation_result = invalid_recipe.validate();
assert!(
validation_result.is_err(),
"Validation should fail with extreme difficulty"
);
if let Err(RStructorError::ValidationError(msg)) = validation_result {
assert!(
msg.contains("Difficulty") || msg.contains("difficulty"),
"Error should mention difficulty: {}",
msg
);
} else if let Err(e) = validation_result {
panic!("Expected ValidationError about difficulty, got: {:?}", e);
}
}
#[tokio::test]
async fn test_openai_validation_succeeds_with_valid_data() {
let api_key = env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY env var not set");
let client = OpenAIClient::new(api_key)
.expect("Failed to create OpenAI client")
.model(OpenAIModel::Gpt55)
.temperature(0.0);
let prompt = "Give me a recipe for chocolate chip cookies.";
let result = client.materialize::<RecipeInfo>(prompt).await;
assert!(result.is_ok(), "Validation failed: {:?}", result.err());
let recipe = result.unwrap();
assert!(!recipe.name.is_empty());
assert!(recipe.cooking_time > 0 && recipe.cooking_time < 24 * 60);
assert!(recipe.difficulty >= 1 && recipe.difficulty <= 5);
assert!(!recipe.ingredients.is_empty());
println!("Recipe: {:?}", recipe);
}
}