use json_eval_rs::JSONEval;
use serde_json::json;
#[test]
fn test_validation_error_has_all_fields() {
let schema = json!({
"type": "object",
"properties": {
"email": {
"type": "string",
"title": "Email",
"rules": {
"pattern": {
"value": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"message": "Invalid email format",
"code": "email.invalid_format"
}
}
},
"age": {
"type": "number",
"title": "Age",
"rules": {
"minValue": {
"value": 1,
"message": "Age must be at least 1",
"code": "age.too_young"
}
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data = json!({
"email": "invalid-email",
"age": 0
});
let data_str = serde_json::to_string(&data).unwrap();
eval.evaluate(&data_str, None, None, None).unwrap();
let validation = eval.validate(&data_str, None, None, None, None, None).unwrap();
assert!(validation.has_error, "Should have validation errors");
assert_eq!(validation.errors.len(), 2, "Should have 2 errors");
let email_error = validation
.errors
.get("email")
.expect("Should have email error");
assert_eq!(email_error.rule_type, "pattern");
assert_eq!(email_error.message, "Invalid email format");
assert_eq!(email_error.code, Some("email.invalid_format".to_string()));
assert!(
email_error.pattern.is_some(),
"Pattern error should have pattern field"
);
assert!(
email_error.field_value.is_some(),
"Pattern error should have field_value"
);
assert_eq!(email_error.field_value.as_ref().unwrap(), "invalid-email");
assert!(
email_error.data.is_none(),
"Pattern error should not have data field"
);
let age_error = validation.errors.get("age").expect("Should have age error");
assert_eq!(age_error.rule_type, "minValue");
assert_eq!(age_error.message, "Age must be at least 1");
assert_eq!(age_error.code, Some("age.too_young".to_string()));
assert!(
age_error.pattern.is_none(),
"minValue error should not have pattern"
);
assert!(
age_error.field_value.is_none(),
"minValue error should not have field_value"
);
assert!(
age_error.data.is_none(),
"minValue error should not have data"
);
}
#[test]
fn test_validation_error_default_code() {
let schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"rules": {
"required": {
"value": true,
"message": "Name is required"
}
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data = json!({});
let data_str = serde_json::to_string(&data).unwrap();
eval.evaluate(&data_str, None, None, None).unwrap();
let validation = eval.validate(&data_str, None, None, None, None, None).unwrap();
assert!(validation.has_error);
let error = validation
.errors
.get("name")
.expect("Should have name error");
assert_eq!(error.code, Some("name.required".to_string()));
}
#[test]
fn test_validation_error_serialization() {
let schema = json!({
"type": "object",
"properties": {
"test": {
"type": "string",
"rules": {
"required": {
"value": true,
"message": "Required"
}
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data = json!({});
let data_str = serde_json::to_string(&data).unwrap();
eval.evaluate(&data_str, None, None, None).unwrap();
let validation = eval.validate(&data_str, None, None, None, None, None).unwrap();
let json_str = serde_json::to_string(&validation).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["has_error"], true);
assert!(parsed["errors"].is_object());
let error = &parsed["errors"]["test"];
assert_eq!(error["type"], "required");
assert_eq!(error["message"], "Required");
assert_eq!(error["code"], "test.required");
assert!(!error.as_object().unwrap().contains_key("pattern"));
assert!(!error.as_object().unwrap().contains_key("fieldValue"));
assert!(!error.as_object().unwrap().contains_key("data"));
let schema_pattern = json!({
"type": "object",
"properties": {
"code": {
"type": "string",
"rules": {
"pattern": {
"value": "^[0-9]+$",
"message": "Must be digits"
}
}
}
}
});
let schema_pattern_str = serde_json::to_string(&schema_pattern).unwrap();
let mut eval_pattern = JSONEval::new(&schema_pattern_str, None, None).unwrap();
let data_pattern = json!({ "code": "abc" });
let data_pattern_str = serde_json::to_string(&data_pattern).unwrap();
eval_pattern
.evaluate(&data_pattern_str, None, None, None)
.unwrap();
let validation_pattern = eval_pattern
.validate(&data_pattern_str, None, None, None, None, None)
.unwrap();
let json_str_pattern = serde_json::to_string(&validation_pattern).unwrap();
let parsed_pattern: serde_json::Value = serde_json::from_str(&json_str_pattern).unwrap();
let error_pattern = &parsed_pattern["errors"]["code"];
assert_eq!(error_pattern["type"], "pattern");
assert_eq!(error_pattern["fieldValue"], "abc");
}
#[test]
fn test_validate_cache_identical_data() {
let schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Name is required" }
}
},
"score": {
"type": "number",
"rules": {
"minValue": { "value": 50, "message": "Minimum score is 50" }
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data = json!({ "name": "Alice", "score": 40 });
let data_str = serde_json::to_string(&data).unwrap();
let res1 = eval.validate(&data_str, None, None, None, None, None).unwrap();
assert!(res1.has_error);
assert_eq!(res1.errors.len(), 1);
assert!(res1.errors.contains_key("score"));
let res2 = eval.validate(&data_str, None, None, None, None, None).unwrap();
assert_eq!(res1.has_error, res2.has_error);
assert_eq!(res1.errors.len(), res2.errors.len());
assert_eq!(
res1.errors.get("score").unwrap().message,
res2.errors.get("score").unwrap().message
);
}
#[test]
fn test_validate_cache_incremental_field_change() {
let schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Name is required" }
}
},
"age": {
"type": "number",
"rules": {
"minValue": { "value": 18, "message": "Must be at least 18" }
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let d1 = serde_json::to_string(&json!({ "name": "Bob", "age": 16 })).unwrap();
let r1 = eval.validate(&d1, None, None, None, None, None).unwrap();
assert!(r1.has_error);
assert_eq!(r1.errors.len(), 1);
assert!(r1.errors.contains_key("age"));
let d2 = serde_json::to_string(&json!({ "name": "Bob", "age": 20 })).unwrap();
let r2 = eval.validate(&d2, None, None, None, None, None).unwrap();
assert!(!r2.has_error);
assert_eq!(r2.errors.len(), 0);
let d3 = serde_json::to_string(&json!({ "name": "", "age": 20 })).unwrap();
let r3 = eval.validate(&d3, None, None, None, None, None).unwrap();
assert!(r3.has_error);
assert_eq!(r3.errors.len(), 1);
assert!(r3.errors.contains_key("name"));
}
#[test]
fn test_validate_disabled_field_with_rules() {
let schema = json!({
"type": "object",
"properties": {
"fixed_id": {
"type": "string",
"disabled": true,
"rules": {
"required": { "value": true, "message": "ID is required" },
"minLength": { "value": 3, "message": "ID must have 3+ characters" }
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let d1 = serde_json::to_string(&json!({})).unwrap();
let r1_default = eval.validate(&d1, None, None, None, None, None).unwrap();
assert!(!r1_default.has_error, "Disabled field must be skipped by default");
assert!(!r1_default.errors.contains_key("fixed_id"));
let r1_false = eval.validate(&d1, None, None, None, Some(false), None).unwrap();
assert!(!r1_false.has_error, "Disabled field must be skipped when validate_readonly=false");
let r1_true = eval.validate(&d1, None, None, None, Some(true), None).unwrap();
assert!(r1_true.has_error, "Disabled field with required rule must be validated when validate_readonly=true");
assert!(r1_true.errors.contains_key("fixed_id"));
assert_eq!(r1_true.errors["fixed_id"].rule_type, "required");
let d2 = serde_json::to_string(&json!({ "fixed_id": "AB" })).unwrap();
let r2_true = eval.validate(&d2, None, None, None, Some(true), None).unwrap();
assert!(r2_true.has_error);
assert!(r2_true.errors.contains_key("fixed_id"));
assert_eq!(r2_true.errors["fixed_id"].rule_type, "minLength");
let r2_false = eval.validate(&d2, None, None, None, Some(false), None).unwrap();
assert!(!r2_false.has_error);
let d3 = serde_json::to_string(&json!({ "fixed_id": "ABC" })).unwrap();
let r3_default = eval.validate(&d3, None, None, None, None, None).unwrap();
assert!(!r3_default.has_error);
let r3_true = eval.validate(&d3, None, None, None, Some(true), None).unwrap();
assert!(!r3_true.has_error);
}
#[test]
fn test_validate_unmapped_field_missing_from_data_and_eval_data() {
let schema = json!({
"type": "object",
"properties": {
"unmapped_required_code": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Code is required" }
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data_str = "{}";
let res = eval.validate(data_str, None, None, None, None, None).unwrap();
assert!(res.has_error);
assert!(res.errors.contains_key("unmapped_required_code"));
assert_eq!(res.errors["unmapped_required_code"].rule_type, "required");
}
#[test]
fn test_validate_cache_invalidation_on_reload() {
let schema1 = json!({
"type": "object",
"properties": {
"val": {
"type": "number",
"rules": {
"minValue": { "value": 10, "message": "Min 10" }
}
}
}
});
let schema2 = json!({
"type": "object",
"properties": {
"val": {
"type": "number",
"rules": {
"minValue": { "value": 100, "message": "Min 100" }
}
}
}
});
let mut eval = JSONEval::new(&schema1.to_string(), None, None).unwrap();
let d = serde_json::to_string(&json!({ "val": 50 })).unwrap();
let r1 = eval.validate(&d, None, None, None, None, None).unwrap();
assert!(!r1.has_error, "50 >= 10");
eval.reload_schema(&schema2.to_string(), None, None).unwrap();
let r2 = eval.validate(&d, None, None, None, None, None).unwrap();
assert!(r2.has_error, "50 < 100, cache should have been invalidated on reload");
assert_eq!(r2.errors["val"].message, "Min 100");
}
#[test]
fn test_validate_readonly_field_direct_and_conditional() {
let schema = json!({
"type": "object",
"properties": {
"ro_field": {
"type": "string",
"readonly": true,
"rules": {
"required": { "value": true, "message": "Readonly field is required" }
}
},
"cond_ro_field": {
"type": "string",
"condition": {
"readonly": true
},
"rules": {
"required": { "value": true, "message": "Conditional readonly is required" }
}
},
"active_field": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Active field is required" }
}
}
}
});
let mut eval = JSONEval::new(&schema.to_string(), None, None).unwrap();
let data_empty = "{}";
let res_default = eval.validate(data_empty, None, None, None, None, None).unwrap();
assert!(res_default.has_error);
assert_eq!(res_default.errors.len(), 1);
assert!(res_default.errors.contains_key("active_field"));
assert!(!res_default.errors.contains_key("ro_field"));
assert!(!res_default.errors.contains_key("cond_ro_field"));
let res_true = eval.validate(data_empty, None, None, None, Some(true), None).unwrap();
assert!(res_true.has_error);
assert_eq!(res_true.errors.len(), 3);
assert!(res_true.errors.contains_key("active_field"));
assert!(res_true.errors.contains_key("ro_field"));
assert!(res_true.errors.contains_key("cond_ro_field"));
}
#[test]
fn test_validate_layout_disabled_ref() {
let schema = json!({
"type": "object",
"properties": {
"elem": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Elem is required" }
}
}
},
"$layout": {
"elements": [
{
"type": "Control",
"$ref": "#/properties/elem",
"disabled": true
}
]
}
});
let mut eval = JSONEval::new(&schema.to_string(), None, None).unwrap();
let data_empty = "{}";
eval.resolve_layout(false).unwrap();
let res_default = eval.validate(data_empty, None, None, None, None, None).unwrap();
assert!(!res_default.has_error, "Layout disabled ref should be skipped by default");
let res_true = eval.validate(data_empty, None, None, None, Some(true), None).unwrap();
assert!(res_true.has_error, "Layout disabled ref should be validated when validate_readonly=true");
assert!(res_true.errors.contains_key("elem"));
}
#[test]
fn test_validate_with_include_subforms_flag() {
let schema = json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Title is required" }
}
},
"contacts": {
"type": "array",
"items": {
"properties": {
"name": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Name is required" }
}
},
"phone": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Phone is required" }
}
}
}
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data = json!({
"title": "",
"contacts": [
{ "name": "Alice", "phone": "" },
{ "name": "", "phone": "12345" }
]
});
let data_str = serde_json::to_string(&data).unwrap();
let res_default = eval.validate(&data_str, None, None, None, None, None).unwrap();
assert!(res_default.has_error);
assert_eq!(res_default.errors.len(), 1);
assert!(res_default.errors.contains_key("title"));
assert!(!res_default.errors.contains_key("contacts.0.phone"));
assert!(!res_default.errors.contains_key("contacts.1.name"));
let res_false = eval.validate(&data_str, None, None, None, None, Some(false)).unwrap();
assert!(res_false.has_error);
assert_eq!(res_false.errors.len(), 1);
assert!(res_false.errors.contains_key("title"));
let res_true = eval.validate(&data_str, None, None, None, None, Some(true)).unwrap();
assert!(res_true.has_error);
assert_eq!(res_true.errors.len(), 3);
assert!(res_true.errors.contains_key("title"));
assert!(res_true.errors.contains_key("contacts.0.phone"));
assert!(res_true.errors.contains_key("contacts.1.name"));
assert!(!res_true.errors.contains_key("contacts.0.name"));
assert!(!res_true.errors.contains_key("contacts.1.phone"));
let err_c0_phone = &res_true.errors["contacts.0.phone"];
assert_eq!(err_c0_phone.message, "Phone is required");
assert_eq!(err_c0_phone.code, Some("contacts.0.phone.required".to_string()));
let err_c1_name = &res_true.errors["contacts.1.name"];
assert_eq!(err_c1_name.message, "Name is required");
assert_eq!(err_c1_name.code, Some("contacts.1.name.required".to_string()));
}
#[test]
fn test_validate_with_include_subforms_paths_filter_and_cache() {
let schema = json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Title is required" }
}
},
"contacts": {
"type": "array",
"items": {
"properties": {
"name": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Name is required" }
}
},
"phone": {
"type": "string",
"rules": {
"required": { "value": true, "message": "Phone is required" }
}
}
}
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data = json!({
"title": "",
"contacts": [
{ "name": "Alice", "phone": "" },
{ "name": "", "phone": "12345" }
]
});
let data_str = serde_json::to_string(&data).unwrap();
let paths_item0 = vec!["contacts.0.phone".to_string()];
let res_item0 = eval.validate(&data_str, None, Some(&paths_item0), None, None, Some(true)).unwrap();
assert!(res_item0.has_error);
assert_eq!(res_item0.errors.len(), 1);
assert!(res_item0.errors.contains_key("contacts.0.phone"));
let paths_whole_item0 = vec!["contacts.0".to_string()];
let res_whole0 = eval.validate(&data_str, None, Some(&paths_whole_item0), None, None, Some(true)).unwrap();
assert!(res_whole0.has_error);
assert_eq!(res_whole0.errors.len(), 1);
assert!(res_whole0.errors.contains_key("contacts.0.phone"));
assert!(!res_whole0.errors.contains_key("contacts.1.name"));
let paths_title = vec!["title".to_string()];
let res_title = eval.validate(&data_str, None, Some(&paths_title), None, None, Some(true)).unwrap();
assert!(res_title.has_error);
assert_eq!(res_title.errors.len(), 1);
assert!(res_title.errors.contains_key("title"));
let r_full_true = eval.validate(&data_str, None, None, None, None, Some(true)).unwrap();
assert_eq!(r_full_true.errors.len(), 3);
let r_full_false = eval.validate(&data_str, None, None, None, None, Some(false)).unwrap();
assert_eq!(r_full_false.errors.len(), 1, "Cache hit must not leak subform errors when include_subforms=false");
let r_full_true_again = eval.validate(&data_str, None, None, None, None, Some(true)).unwrap();
assert_eq!(r_full_true_again.errors.len(), 3, "Cached subforms result returned when include_subforms=true");
}
#[test]
fn test_validate_with_include_subforms_nested_and_items_root_key() {
let schema = json!({
"type": "object",
"properties": {
"form": {
"type": "object",
"properties": {
"items": {
"type": "array",
"itemsRootKey": "items",
"items": {
"properties": {
"amount": {
"type": "number",
"rules": {
"minValue": { "value": 1000, "message": "Min amount is 1000" }
}
}
}
}
}
}
}
}
});
let schema_str = serde_json::to_string(&schema).unwrap();
let mut eval = JSONEval::new(&schema_str, None, None).unwrap();
let data = json!({
"form": {
"items": [
{ "amount": 500 },
{ "amount": 2000 }
]
}
});
let data_str = serde_json::to_string(&data).unwrap();
let res = eval.validate(&data_str, None, None, None, None, Some(true)).unwrap();
assert!(res.has_error);
assert_eq!(res.errors.len(), 1);
let err = res.errors.get("form.items.0.amount").expect("Should map to form.items.0.amount");
assert_eq!(err.message, "Min amount is 1000");
assert_eq!(err.code, Some("form.items.0.amount.minValue".to_string()));
}