refactor: typed errors in rss/import ports
This commit is contained in:
@@ -44,7 +44,8 @@ pub async fn execute(
|
||||
let mut failed = Vec::new();
|
||||
|
||||
let semaphore = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY_LIMIT));
|
||||
let mut tasks: tokio::task::JoinSet<(usize, Result<(), String>)> = tokio::task::JoinSet::new();
|
||||
let mut tasks: tokio::task::JoinSet<(usize, Result<(), DomainError>)> =
|
||||
tokio::task::JoinSet::new();
|
||||
|
||||
for (idx, annotated) in row_results.into_iter().enumerate() {
|
||||
if !confirmed_set.contains(&idx) {
|
||||
@@ -56,12 +57,12 @@ pub async fn execute(
|
||||
failed.push((idx, errors.join("; ")));
|
||||
}
|
||||
RowResult::Valid(row) => match row_to_command(&row, user_id.value()) {
|
||||
Err(e) => failed.push((idx, e)),
|
||||
Err(e) => failed.push((idx, import_error_message(e))),
|
||||
Ok(log_cmd) => {
|
||||
let permit = Arc::clone(&semaphore).acquire_owned().await.unwrap();
|
||||
let logger = deps.review_logger.clone();
|
||||
tasks.spawn(async move {
|
||||
let result = logger.log_review(log_cmd).await.map_err(|e| e.to_string());
|
||||
let result = logger.log_review(log_cmd).await;
|
||||
drop(permit);
|
||||
(idx, result)
|
||||
});
|
||||
@@ -74,7 +75,7 @@ pub async fn execute(
|
||||
let (idx, outcome) = res.expect("import task panicked");
|
||||
match outcome {
|
||||
Ok(()) => imported += 1,
|
||||
Err(e) => failed.push((idx, e)),
|
||||
Err(e) => failed.push((idx, e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,20 +92,36 @@ pub async fn execute(
|
||||
#[path = "tests/execute.rs"]
|
||||
mod tests;
|
||||
|
||||
fn row_to_command(row: &ImportRow, user_id: Uuid) -> Result<LogReviewCommand, String> {
|
||||
/// Row-level validation failures keep their bare message in the summary.
|
||||
fn import_error_message(e: DomainError) -> String {
|
||||
match e {
|
||||
DomainError::ValidationError(msg) => msg,
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_command(row: &ImportRow, user_id: Uuid) -> Result<LogReviewCommand, DomainError> {
|
||||
let rating = row
|
||||
.rating
|
||||
.as_deref()
|
||||
.ok_or("missing rating")?
|
||||
.ok_or_else(|| DomainError::ValidationError("missing rating".into()))?
|
||||
.parse::<u8>()
|
||||
.map_err(|_| "rating is not a valid u8".to_string())?;
|
||||
.map_err(|_| DomainError::ValidationError("rating is not a valid u8".into()))?;
|
||||
|
||||
let watched_at_str = row.watched_at.as_deref().ok_or("missing watched_at")?;
|
||||
let watched_at_str = row
|
||||
.watched_at
|
||||
.as_deref()
|
||||
.ok_or_else(|| DomainError::ValidationError("missing watched_at".into()))?;
|
||||
let watched_at =
|
||||
NaiveDateTime::parse_from_str(&format!("{} 00:00:00", watched_at_str), "%Y-%m-%d %H:%M:%S")
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%d %H:%M:%S"))
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%dT%H:%M:%S"))
|
||||
.map_err(|_| format!("cannot parse watched_at: '{}'", watched_at_str))?;
|
||||
.map_err(|_| {
|
||||
DomainError::ValidationError(format!(
|
||||
"cannot parse watched_at: '{}'",
|
||||
watched_at_str
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(LogReviewCommand {
|
||||
user_id,
|
||||
|
||||
@@ -38,43 +38,5 @@ pub(super) fn paged_user_filter(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn feed_sort_to_direction_all_variants() {
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::Date),
|
||||
ReviewSortBy::Descending
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::DateAsc),
|
||||
ReviewSortBy::Ascending
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::Rating),
|
||||
ReviewSortBy::ByRatingDesc
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::RatingAsc),
|
||||
ReviewSortBy::ByRatingAsc
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paged_user_filter_builds_correctly() {
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let filter = paged_user_filter(
|
||||
uid.clone(),
|
||||
ReviewSortBy::Descending,
|
||||
Some(20),
|
||||
Some(5),
|
||||
Some("blade".into()),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(filter.user_id.unwrap().value(), uid.value());
|
||||
assert_eq!(filter.search.as_deref(), Some("blade"));
|
||||
}
|
||||
}
|
||||
#[path = "tests/diary_filter.rs"]
|
||||
mod tests;
|
||||
|
||||
38
crates/application/src/users/tests/diary_filter.rs
Normal file
38
crates/application/src/users/tests/diary_filter.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn feed_sort_to_direction_all_variants() {
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::Date),
|
||||
ReviewSortBy::Descending
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::DateAsc),
|
||||
ReviewSortBy::Ascending
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::Rating),
|
||||
ReviewSortBy::ByRatingDesc
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::RatingAsc),
|
||||
ReviewSortBy::ByRatingAsc
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paged_user_filter_builds_correctly() {
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let filter = paged_user_filter(
|
||||
uid.clone(),
|
||||
ReviewSortBy::Descending,
|
||||
Some(20),
|
||||
Some(5),
|
||||
Some("blade".into()),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(filter.user_id.unwrap().value(), uid.value());
|
||||
assert_eq!(filter.search.as_deref(), Some("blade"));
|
||||
}
|
||||
Reference in New Issue
Block a user