use std::collections::HashMap;
use rstructor::{Instructor, RStructorError, Result};
use serde::{Deserialize, Serialize};
#[derive(Instructor, Serialize, Deserialize, Debug, Clone)]
#[llm(validate = "validate_child")]
struct Child {
value: i32,
}
fn validate_child(c: &Child) -> Result<()> {
if c.value < 0 {
return Err(RStructorError::ValidationError(format!(
"child.value must be >= 0, got {}",
c.value
)));
}
Ok(())
}
fn child(v: i32) -> Child {
Child { value: v }
}
fn validation_message(err: RStructorError) -> String {
match err {
RStructorError::ValidationError(msg) => msg,
other => panic!("expected ValidationError, got {other:?}"),
}
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct BoxParent {
boxed: Box<Child>,
}
#[test]
fn box_field_recurses_into_child() {
let bad = BoxParent {
boxed: Box::new(child(-1)),
};
let err = bad.validate().expect_err("boxed child -1 must fail");
assert_eq!(validation_message(err), "child.value must be >= 0, got -1");
let good = BoxParent {
boxed: Box::new(child(1)),
};
assert!(good.validate().is_ok());
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct MapParent {
map: HashMap<String, Child>,
}
#[test]
fn hashmap_value_recursion_one_bad_errors() {
let mut map = HashMap::new();
map.insert("a".to_string(), child(0));
map.insert("b".to_string(), child(-3));
map.insert("c".to_string(), child(5));
let err = MapParent { map }
.validate()
.expect_err("map with a -3 value must fail");
assert_eq!(validation_message(err), "child.value must be >= 0, got -3");
}
#[test]
fn hashmap_value_recursion_all_good_ok() {
let mut map = HashMap::new();
map.insert("a".to_string(), child(0));
map.insert("b".to_string(), child(7));
assert!(MapParent { map }.validate().is_ok());
}
#[test]
fn hashmap_value_recursion_empty_ok() {
let map: HashMap<String, Child> = HashMap::new();
assert!(MapParent { map }.validate().is_ok());
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct ComboParent {
vec_opt: Vec<Option<Child>>,
opt_vec: Option<Vec<Child>>,
map_vec: HashMap<String, Vec<Child>>,
}
fn empty_combo() -> ComboParent {
ComboParent {
vec_opt: vec![],
opt_vec: None,
map_vec: HashMap::new(),
}
}
#[test]
fn vec_of_option_reaches_innermost_validator() {
let mut p = empty_combo();
p.vec_opt = vec![Some(child(1)), None, Some(child(-2))];
let err = p
.validate()
.expect_err("Vec<Option<Child>> with -2 must fail");
assert_eq!(validation_message(err), "child.value must be >= 0, got -2");
let mut ok = empty_combo();
ok.vec_opt = vec![Some(child(1)), None, Some(child(3))];
assert!(ok.validate().is_ok());
}
#[test]
fn option_of_vec_recurses() {
let mut bad = empty_combo();
bad.opt_vec = Some(vec![child(-1)]);
assert!(bad.validate().is_err());
let mut empty = empty_combo();
empty.opt_vec = Some(vec![]);
assert!(empty.validate().is_ok());
let mut none = empty_combo();
none.opt_vec = None;
assert!(none.validate().is_ok());
}
#[test]
fn map_of_vec_recurses() {
let mut bad = empty_combo();
let mut m = HashMap::new();
m.insert("k".to_string(), vec![child(2), child(-1)]);
bad.map_vec = m;
assert!(bad.validate().is_err());
let mut good = empty_combo();
let mut m = HashMap::new();
m.insert("k".to_string(), vec![child(2), child(4)]);
good.map_vec = m;
assert!(good.validate().is_ok());
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(validate = "validate_order_parent")]
struct OrderParent {
child: Child,
name: String,
}
fn validate_order_parent(p: &OrderParent) -> Result<()> {
if p.name == "bad" {
return Err(RStructorError::ValidationError(
"parent name must not be 'bad'".to_string(),
));
}
Ok(())
}
#[test]
fn child_failure_surfaces_before_parent_failure() {
let p = OrderParent {
child: child(-1),
name: "bad".to_string(),
};
let err = p.validate().expect_err("invalid child must fail");
assert_eq!(validation_message(err), "child.value must be >= 0, got -1");
}
#[test]
fn parent_failure_surfaces_when_children_valid() {
let p = OrderParent {
child: child(1),
name: "bad".to_string(),
};
let err = p.validate().expect_err("invalid parent must fail");
assert_eq!(validation_message(err), "parent name must not be 'bad'");
}
#[test]
fn order_parent_all_valid_ok() {
let p = OrderParent {
child: child(1),
name: "ok".to_string(),
};
assert!(p.validate().is_ok());
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct IdentityParent {
direct: Child,
maybe: Option<Child>,
list: Vec<Child>,
}
#[test]
fn nested_error_message_is_unmodified_child_message() {
let p = IdentityParent {
direct: child(0),
maybe: None,
list: vec![child(1), child(2), child(-9)],
};
let err = p.validate().expect_err("a -9 in the list must fail");
assert_eq!(validation_message(err), "child.value must be >= 0, got -9");
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct ListParent {
list: Vec<Child>,
}
#[test]
fn vec_short_circuits_on_first_failure() {
let p = ListParent {
list: vec![child(-1), child(-2)],
};
let msg = validation_message(p.validate().expect_err("first element must fail"));
assert!(
msg.contains("got -1"),
"expected first failure (-1), got message: {msg}"
);
assert!(
!msg.contains("got -2"),
"second element must not be reached, got message: {msg}"
);
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct OptVecParent {
items: Option<Vec<Child>>,
}
#[test]
fn option_none_short_circuits_inner_validation() {
assert!(
OptVecParent { items: None }.validate().is_ok(),
"None must short-circuit and pass"
);
assert!(
OptVecParent {
items: Some(vec![child(-1)])
}
.validate()
.is_err(),
"Some([Child{{-1}}]) must reach the inner validator and fail"
);
}
#[derive(Instructor, Serialize, Deserialize, Debug)]
struct Prim {
label: String,
count: u32,
tags: Vec<String>,
}
#[test]
fn primitive_probe_fallback_is_noop() {
let p = Prim {
label: "x".to_string(),
count: 3,
tags: vec!["a".to_string(), "b".to_string()],
};
assert!(p.validate().is_ok());
}