#[cfg(test)]
mod example_validation_tests {
use rstructor::{Instructor, SchemaType};
use serde::{Deserialize, Serialize};
#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(description = "A cooking ingredient with amount and unit")]
struct TestIngredient {
#[llm(description = "Name of the ingredient", example = "flour")]
name: String,
#[llm(description = "Amount of the ingredient", example = 2.5)]
amount: f32,
#[llm(description = "Unit of measurement", example = "cups")]
unit: String,
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(
description = "Nutritional information per serving. All values are numbers, not strings."
)]
struct TestNutrition {
#[llm(
description = "Calories per serving (must be a number, not a string)",
example = 350
)]
calories: u16,
#[llm(
description = "Protein in grams (must be a number, field name is 'protein_g')",
example = 7.5
)]
protein_g: f32,
#[llm(
description = "Carbohydrates in grams (must be a number, field name is 'carbs_g', not 'carbohydrates')",
example = 45.0
)]
carbs_g: f32,
#[llm(
description = "Fat in grams (must be a number, field name is 'fat_g')",
example = 15.2
)]
fat_g: f32,
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct TestRecipe {
#[llm(description = "Name of the recipe", example = "Chocolate Chip Cookies")]
name: String,
#[llm(
description = "List of ingredients needed. MUST be an array of objects, not strings. Each object must have 'name', 'amount', and 'unit' fields."
)]
ingredients: Vec<TestIngredient>,
#[llm(
description = "Nutritional information per serving. MUST be an object with exactly these fields: calories (number), protein_g (number), carbs_g (number), fat_g (number). Field names must match exactly."
)]
nutrition: TestNutrition,
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(
description = "An entity mentioned in the article. This must be a complete object with all three fields: name, entity_type, and relevance."
)]
struct TestEntity {
#[llm(
description = "Name of the entity (must be a string)",
example = "Microsoft"
)]
name: String,
#[llm(
description = "Type of the entity (person, organization, location, etc.) (must be a string)",
example = "organization"
)]
entity_type: String,
#[llm(
description = "How important this entity is to the article (1-10 scale, must be a number between 1 and 10)",
example = 8
)]
relevance: u8,
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct TestArticleAnalysis {
#[llm(description = "Title of the article", example = "Tech Stocks Tumble")]
title: String,
#[llm(
description = "Main entities mentioned in the article. MUST be an array of objects, not strings. Each object must have 'name' (string), 'entity_type' (string), and 'relevance' (number 1-10) fields."
)]
entities: Vec<TestEntity>,
}
#[test]
fn test_nested_objects_schema_generation() {
let schema = TestRecipe::schema();
let schema_json = schema.to_json();
assert_eq!(schema_json["type"], "object");
assert_eq!(schema_json["title"], "TestRecipe");
let ingredients_prop = &schema_json["properties"]["ingredients"];
assert_eq!(ingredients_prop["type"], "array");
let ingredients_items = &ingredients_prop["items"];
eprintln!(
"DEBUG: Ingredients items schema: {}",
serde_json::to_string_pretty(ingredients_items).unwrap()
);
let has_properties = ingredients_items["properties"].is_object();
let is_object_type = ingredients_items["type"] == "object";
assert!(
is_object_type || has_properties,
"Ingredients items should be objects or have properties. Type: {:?}, Has properties: {:?}",
ingredients_items.get("type"),
has_properties
);
if has_properties {
let props = ingredients_items["properties"].as_object().unwrap();
assert!(
props.contains_key("name")
|| props.contains_key("amount")
|| props.contains_key("unit"),
"Ingredients items properties should include name, amount, or unit. Got: {:?}",
props.keys().collect::<Vec<_>>()
);
} else {
assert_eq!(
ingredients_items["type"], "object",
"If no properties, type must be object. Got: {:?}",
ingredients_items
);
}
let nutrition_prop = &schema_json["properties"]["nutrition"];
assert!(nutrition_prop.get("type").is_some());
if nutrition_prop["type"] == "object" {
if nutrition_prop["properties"].is_object() {
let nutrition_props = nutrition_prop["properties"].as_object().unwrap();
assert!(nutrition_props.contains_key("calories"));
assert!(nutrition_props.contains_key("protein_g"));
assert!(nutrition_props.contains_key("carbs_g"));
assert!(nutrition_props.contains_key("fat_g"));
}
}
}
#[test]
fn test_entities_schema_generation() {
let schema = TestArticleAnalysis::schema();
let schema_json = schema.to_json();
assert_eq!(schema_json["type"], "object");
assert_eq!(schema_json["title"], "TestArticleAnalysis");
let entities_prop = &schema_json["properties"]["entities"];
assert_eq!(entities_prop["type"], "array");
let entities_items = &entities_prop["items"];
assert!(
entities_items["type"] == "object" || entities_items["properties"].is_object(),
"Entities items should be objects or have properties. Got: {:?}",
entities_items
);
assert!(
entities_items["properties"].is_object(),
"Entities items should have properties. Got: {:?}",
entities_items
);
let entity_props = entities_items["properties"].as_object().unwrap();
assert!(
entity_props.contains_key("name")
|| entity_props.contains_key("entity_type")
|| entity_props.contains_key("relevance"),
"Entity properties should include name, entity_type, or relevance. Got: {:?}",
entity_props
);
}
#[test]
fn test_nutrition_schema_has_correct_field_names() {
let schema = TestNutrition::schema();
let schema_json = schema.to_json();
let props = schema_json["properties"].as_object().unwrap();
assert!(
props.contains_key("protein_g"),
"Must have 'protein_g' field"
);
assert!(props.contains_key("carbs_g"), "Must have 'carbs_g' field");
assert!(props.contains_key("fat_g"), "Must have 'fat_g' field");
assert_eq!(props["calories"]["type"], "integer");
assert_eq!(props["protein_g"]["type"], "number");
assert_eq!(props["carbs_g"]["type"], "number");
assert_eq!(props["fat_g"]["type"], "number");
}
#[test]
fn test_ingredient_schema_structure() {
let schema = TestIngredient::schema();
let schema_json = schema.to_json();
assert_eq!(schema_json["type"], "object");
let props = schema_json["properties"].as_object().unwrap();
assert!(props.contains_key("name"));
assert!(props.contains_key("amount"));
assert!(props.contains_key("unit"));
assert_eq!(props["name"]["type"], "string");
assert_eq!(props["amount"]["type"], "number");
assert_eq!(props["unit"]["type"], "string");
}
#[test]
fn test_example_deserialization() {
let valid_recipe_json = serde_json::json!({
"name": "Test Recipe",
"ingredients": [
{"name": "flour", "amount": 2.5, "unit": "cups"},
{"name": "sugar", "amount": 1.0, "unit": "cup"}
],
"nutrition": {
"calories": 350,
"protein_g": 7.5,
"carbs_g": 45.0,
"fat_g": 15.2
}
});
let recipe: TestRecipe = serde_json::from_value(valid_recipe_json).unwrap();
assert_eq!(recipe.name, "Test Recipe");
assert_eq!(recipe.ingredients.len(), 2);
assert_eq!(recipe.nutrition.calories, 350);
assert_eq!(recipe.nutrition.protein_g, 7.5);
}
#[test]
fn test_entity_deserialization() {
let valid_entity_json = serde_json::json!({
"title": "Test Article",
"entities": [
{"name": "Microsoft", "entity_type": "organization", "relevance": 8},
{"name": "John Doe", "entity_type": "person", "relevance": 5}
]
});
let analysis: TestArticleAnalysis = serde_json::from_value(valid_entity_json).unwrap();
assert_eq!(analysis.title, "Test Article");
assert_eq!(analysis.entities.len(), 2);
assert_eq!(analysis.entities[0].name, "Microsoft");
assert_eq!(analysis.entities[0].relevance, 8);
}
}