use crate::error::{Error, Result};
pub fn require<T>(opt: Option<T>, field: &str, message: &str) -> Result<T> {
opt.ok_or_else(|| Error::validation_invalid_argument(field, message, None, None))
}
pub fn require_with_hints<T>(
opt: Option<T>,
field: &str,
message: &str,
hints: Vec<String>,
) -> Result<T> {
opt.ok_or_else(|| Error::validation_invalid_argument(field, message, None, Some(hints)))
}
pub struct ValidationCollector {
errors: Vec<crate::error::ValidationErrorItem>,
}
impl ValidationCollector {
pub fn new() -> Self {
Self { errors: Vec::new() }
}
pub fn capture<T>(&mut self, result: Result<T>, field: &str) -> Option<T> {
match result {
Ok(value) => Some(value),
Err(err) => {
let problem = err
.details
.get("problem")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| err.message.clone());
self.errors.push(crate::error::ValidationErrorItem {
field: field.to_string(),
problem,
context: if err.details.as_object().is_some_and(|o| !o.is_empty()) {
Some(err.details)
} else {
None
},
});
None
}
}
}
pub fn push(&mut self, field: &str, problem: &str, context: Option<serde_json::Value>) {
self.errors.push(crate::error::ValidationErrorItem {
field: field.to_string(),
problem: problem.to_string(),
context,
});
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn finish(self) -> Result<()> {
match self.errors.len() {
0 => Ok(()),
1 => {
let err = &self.errors[0];
Err(Error::validation_invalid_argument(
&err.field,
&err.problem,
None,
None,
))
}
_ => Err(Error::validation_multiple_errors(self.errors)),
}
}
}
impl Default for ValidationCollector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn require_returns_value_when_some() {
let result = require(Some("value"), "field", "msg");
assert_eq!(result.unwrap(), "value");
}
#[test]
fn require_returns_error_when_none() {
let result: Result<&str> = require(None, "field", "Missing field");
assert!(result.is_err());
}
#[test]
fn collector_finish_returns_ok_when_no_errors() {
let v = ValidationCollector::new();
assert!(v.finish().is_ok());
}
#[test]
fn collector_finish_returns_single_error() {
use crate::error::ErrorCode;
let mut v = ValidationCollector::new();
v.push("field1", "Problem 1", None);
let err = v.finish().unwrap_err();
assert_eq!(err.code, ErrorCode::ValidationInvalidArgument);
}
#[test]
fn collector_finish_returns_multiple_errors() {
use crate::error::ErrorCode;
let mut v = ValidationCollector::new();
v.push("field1", "Problem 1", None);
v.push("field2", "Problem 2", None);
let err = v.finish().unwrap_err();
assert_eq!(err.code, ErrorCode::ValidationMultipleErrors);
assert!(err.message.contains("2 validation issue"));
}
#[test]
fn collector_capture_stores_error_and_returns_none() {
let mut v = ValidationCollector::new();
let result: Option<i32> = v.capture(
Err(Error::validation_invalid_argument(
"test", "msg", None, None,
)),
"test",
);
assert!(result.is_none());
assert!(v.has_errors());
}
#[test]
fn collector_capture_returns_value_on_success() {
let mut v = ValidationCollector::new();
let result: Option<i32> = v.capture(Ok(42), "test");
assert_eq!(result, Some(42));
assert!(!v.has_errors());
}
#[test]
fn collector_capture_extracts_problem_from_details() {
let mut v = ValidationCollector::new();
let err = Error::validation_invalid_argument(
"changelog",
"Changelog has no finalized versions",
None,
Some(vec![
"Add at least one finalized version section like '## [0.1.0] - YYYY-MM-DD'"
.to_string(),
]),
);
let result: Option<i32> = v.capture(Err(err), "changelog_sync");
assert!(result.is_none());
assert!(v.has_errors());
let final_err = v.finish().unwrap_err();
assert!(final_err
.details
.get("problem")
.and_then(|v| v.as_str())
.unwrap()
.contains("Changelog has no finalized versions"));
}
}