use postmortem::{JsonPath, SchemaError, SchemaErrors, ValidationResult};
use stillwater::prelude::*;
use stillwater::Validation;
#[test]
fn test_schema_error_full_context() {
let error = SchemaError::new(JsonPath::root().push_field("email"), "invalid email format")
.with_code("invalid_email")
.with_got("not-an-email")
.with_expected("valid email address");
assert_eq!(error.path.to_string(), "email");
assert_eq!(error.message, "invalid email format");
assert_eq!(error.code, "invalid_email");
assert_eq!(error.got, Some("not-an-email".to_string()));
assert_eq!(error.expected, Some("valid email address".to_string()));
}
#[test]
fn test_schema_errors_never_empty() {
let error = SchemaError::new(JsonPath::root(), "test error");
let errors = SchemaErrors::single(error);
assert!(!errors.is_empty());
assert_eq!(errors.len(), 1);
}
#[test]
fn test_errors_combine_via_semigroup() {
let e1 = SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("name"),
"name is required",
));
let e2 = SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("email"),
"email is invalid",
));
let e3 = SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("age"),
"age must be positive",
));
let combined = e1.combine(e2).combine(e3);
assert_eq!(combined.len(), 3);
let messages: Vec<&str> = combined.iter().map(|e| e.message.as_str()).collect();
assert!(messages.contains(&"name is required"));
assert!(messages.contains(&"email is invalid"));
assert!(messages.contains(&"age must be positive"));
}
#[test]
fn test_validation_success() {
let result: ValidationResult<i32> = Validation::Success(42);
match result {
Validation::Success(v) => assert_eq!(v, 42),
Validation::Failure(_) => panic!("Expected success"),
}
}
#[test]
fn test_validation_failure() {
let errors = SchemaErrors::single(SchemaError::new(JsonPath::root(), "invalid"));
let result: ValidationResult<i32> = Validation::Failure(errors);
match result {
Validation::Success(_) => panic!("Expected failure"),
Validation::Failure(e) => assert_eq!(e.len(), 1),
}
}
#[test]
fn test_validation_and_accumulates_errors() {
let v1: ValidationResult<i32> = Validation::Failure(SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("a"),
"error a",
)));
let v2: ValidationResult<i32> = Validation::Failure(SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("b"),
"error b",
)));
let combined = v1.and(v2);
match combined {
Validation::Failure(errors) => {
assert_eq!(errors.len(), 2);
let paths: Vec<String> = errors.iter().map(|e| e.path.to_string()).collect();
assert!(paths.contains(&"a".to_string()));
assert!(paths.contains(&"b".to_string()));
}
Validation::Success(_) => panic!("Expected failure"),
}
}
#[test]
fn test_validation_map() {
let result: ValidationResult<i32> = Validation::Success(10);
let mapped = result.map(|x| x * 2);
match mapped {
Validation::Success(v) => assert_eq!(v, 20),
Validation::Failure(_) => panic!("Expected success"),
}
}
#[test]
fn test_validation_map_on_failure() {
let errors = SchemaErrors::single(SchemaError::new(JsonPath::root(), "error"));
let result: ValidationResult<i32> = Validation::Failure(errors);
let mapped = result.map(|x| x * 2);
match mapped {
Validation::Success(_) => panic!("Expected failure"),
Validation::Failure(e) => assert_eq!(e.len(), 1),
}
}
#[test]
fn test_validation_and_then_short_circuits() {
let v1: ValidationResult<i32> = Validation::Failure(SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("first"),
"first error",
)));
let result = v1.and_then(|_| -> ValidationResult<i32> {
Validation::Failure(SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("second"),
"second error",
)))
});
match result {
Validation::Failure(errors) => {
assert_eq!(errors.len(), 1);
assert_eq!(errors.first().path.to_string(), "first");
}
Validation::Success(_) => panic!("Expected failure"),
}
}
#[test]
fn test_query_errors_by_path() {
let path_email = JsonPath::root().push_field("email");
let path_name = JsonPath::root().push_field("name");
let errors = SchemaErrors::single(
SchemaError::new(path_email.clone(), "invalid format").with_code("format"),
)
.combine(SchemaErrors::single(
SchemaError::new(path_email.clone(), "domain blocked").with_code("blocked"),
))
.combine(SchemaErrors::single(
SchemaError::new(path_name.clone(), "required").with_code("required"),
));
let email_errors = errors.at_path(&path_email);
assert_eq!(email_errors.len(), 2);
let name_errors = errors.at_path(&path_name);
assert_eq!(name_errors.len(), 1);
}
#[test]
fn test_query_errors_by_code() {
let errors = SchemaErrors::single(
SchemaError::new(JsonPath::root().push_field("a"), "error").with_code("required"),
)
.combine(SchemaErrors::single(
SchemaError::new(JsonPath::root().push_field("b"), "error").with_code("format"),
))
.combine(SchemaErrors::single(
SchemaError::new(JsonPath::root().push_field("c"), "error").with_code("required"),
));
let required = errors.with_code("required");
assert_eq!(required.len(), 2);
let format = errors.with_code("format");
assert_eq!(format.len(), 1);
let nonexistent = errors.with_code("nonexistent");
assert_eq!(nonexistent.len(), 0);
}
#[test]
fn test_errors_into_vec() {
let e1 = SchemaError::new(JsonPath::root().push_field("a"), "error a");
let e2 = SchemaError::new(JsonPath::root().push_field("b"), "error b");
let errors = SchemaErrors::single(e1).combine(SchemaErrors::single(e2));
let vec = errors.into_vec();
assert_eq!(vec.len(), 2);
}
#[test]
fn test_schema_error_display() {
let error = SchemaError::new(
JsonPath::root()
.push_field("users")
.push_index(0)
.push_field("age"),
"must be positive",
)
.with_expected("positive integer")
.with_got("-5");
let display = error.to_string();
assert!(display.contains("users[0].age"));
assert!(display.contains("must be positive"));
assert!(display.contains("expected: positive integer"));
assert!(display.contains("got: -5"));
}
#[test]
fn test_schema_errors_display() {
let errors = SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("name"),
"required",
))
.combine(SchemaErrors::single(SchemaError::new(
JsonPath::root().push_field("email"),
"invalid",
)));
let display = errors.to_string();
assert!(display.contains("2 error(s)"));
assert!(display.contains("1. name: required"));
assert!(display.contains("2. email: invalid"));
}
#[test]
fn test_complex_validation_scenario() {
fn validate_name(name: &str) -> ValidationResult<String> {
if name.is_empty() {
Validation::Failure(SchemaErrors::single(
SchemaError::new(JsonPath::root().push_field("name"), "name is required")
.with_code("required"),
))
} else {
Validation::Success(name.to_string())
}
}
fn validate_email(email: &str) -> ValidationResult<String> {
if !email.contains('@') {
Validation::Failure(SchemaErrors::single(
SchemaError::new(JsonPath::root().push_field("email"), "invalid email format")
.with_code("invalid_email")
.with_got(email)
.with_expected("valid email address"),
))
} else {
Validation::Success(email.to_string())
}
}
fn validate_age(age: i32) -> ValidationResult<i32> {
if age < 0 {
Validation::Failure(SchemaErrors::single(
SchemaError::new(
JsonPath::root().push_field("age"),
"age must be non-negative",
)
.with_code("min_value")
.with_got(age.to_string())
.with_expected("value >= 0"),
))
} else if age > 150 {
Validation::Failure(SchemaErrors::single(
SchemaError::new(JsonPath::root().push_field("age"), "age must be realistic")
.with_code("max_value")
.with_got(age.to_string())
.with_expected("value <= 150"),
))
} else {
Validation::Success(age)
}
}
let name_result = validate_name("");
let email_result = validate_email("not-an-email");
let age_result = validate_age(-5);
let combined = name_result
.and(email_result)
.and(age_result)
.map(|_| "valid user");
match combined {
Validation::Failure(errors) => {
assert_eq!(errors.len(), 3);
assert_eq!(errors.with_code("required").len(), 1);
assert_eq!(errors.with_code("invalid_email").len(), 1);
assert_eq!(errors.with_code("min_value").len(), 1);
}
Validation::Success(_) => panic!("Expected validation to fail"),
}
}