use crate::domain::config::AppConfig;
use crate::domain::review::{
Author, CommentLineRange, CommentStatus, DiffSide, LineAnchorSnapshot, NewLineComment,
ReanchorLineComment, ReviewSession, ReviewState, StoredAnchorSnapshot,
};
use crate::persistence::store::{Store, StoreError};
use crate::utils::time::now_ms;
use anyhow::{Context, Result};
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct ReviewService {
store: Store,
}
#[derive(Debug, Clone)]
pub struct AddCommentInput {
pub file_path: String,
pub old_line: Option<u32>,
pub new_line: Option<u32>,
pub line_range: Option<CommentLineRange>,
pub side: DiffSide,
pub line_anchor: Option<LineAnchorSnapshot>,
pub original_anchor: Option<StoredAnchorSnapshot>,
pub body: String,
pub author: Author,
}
#[derive(Debug, Clone)]
pub struct AddReplyInput {
pub comment_id: u64,
pub author: Author,
pub body: String,
}
#[derive(Debug, Clone)]
pub struct ReanchorCommentInput {
pub comment_id: u64,
pub file_path: String,
pub old_line: Option<u32>,
pub new_line: Option<u32>,
pub line_range: Option<CommentLineRange>,
pub side: DiffSide,
pub line_anchor: Option<LineAnchorSnapshot>,
}
impl ReviewService {
#[must_use]
pub fn new(store: Store) -> Self {
Self { store }
}
pub async fn create_review(&self, name: &str) -> Result<ReviewSession> {
let session = ReviewSession::new(name.to_string(), now_ms()?);
self.store
.create_review(&session)
.await
.with_context(|| format!("failed to create review {name}"))?;
Ok(session)
}
pub async fn load_review(&self, name: &str) -> Result<ReviewSession> {
self.store
.load_review(name)
.await
.with_context(|| format!("failed to load review {name}"))
}
pub async fn load_or_create_review(&self, name: &str) -> Result<ReviewSession> {
match self.store.load_review(name).await {
Ok(session) => Ok(session),
Err(StoreError::ReviewNotFound(_)) => self.create_review(name).await,
Err(error) => Err(error).with_context(|| format!("failed to load review {name}")),
}
}
pub async fn list_reviews(&self) -> Result<Vec<String>> {
self.store
.list_reviews()
.await
.context("failed to list reviews")
}
pub async fn load_config(&self) -> Result<AppConfig> {
self.store
.load_config()
.await
.context("failed to load parler config")
}
pub async fn save_config(&self, config: &AppConfig) -> Result<()> {
self.store
.save_config(config)
.await
.context("failed to save parler config")
}
pub fn review_log_path(&self, review_name: &str) -> Result<PathBuf> {
self.store
.review_log_path(review_name)
.with_context(|| format!("failed to resolve log path for review {review_name}"))
}
pub async fn set_state(&self, name: &str, next: ReviewState) -> Result<ReviewSession> {
self.mutate_review(name, "failed to save state change", |session, now_ms| {
session.set_state(next, now_ms).map_err(Into::into)
})
.await
}
pub async fn add_comment(&self, name: &str, input: AddCommentInput) -> Result<ReviewSession> {
let new_comment = NewLineComment {
file_path: input.file_path,
old_line: input.old_line,
new_line: input.new_line,
line_range: input.line_range,
side: input.side,
line_anchor: input.line_anchor,
original_anchor: input.original_anchor,
body: input.body,
author: input.author,
};
self.mutate_review(name, "failed to persist new comment", |session, now_ms| {
session.add_comment(new_comment, now_ms);
Ok(())
})
.await
}
pub async fn add_reply(&self, name: &str, input: AddReplyInput) -> Result<ReviewSession> {
self.mutate_review(name, "failed to persist new reply", |session, now_ms| {
session
.add_reply(input.comment_id, input.author, input.body, now_ms)
.map(|_| ())
.map_err(Into::into)
})
.await
}
pub async fn mark_addressed(
&self,
name: &str,
comment_id: u64,
actor: Author,
) -> Result<ReviewSession> {
self.set_comment_status(name, comment_id, CommentStatus::Addressed, actor)
.await
}
pub async fn mark_open(
&self,
name: &str,
comment_id: u64,
actor: Author,
) -> Result<ReviewSession> {
self.set_comment_status(name, comment_id, CommentStatus::Open, actor)
.await
}
pub async fn force_mark_addressed(&self, name: &str, comment_id: u64) -> Result<ReviewSession> {
self.mutate_review(
name,
"failed to persist forced comment status",
|session, now_ms| {
session
.set_comment_status_force(comment_id, CommentStatus::Addressed, now_ms)
.map_err(Into::into)
},
)
.await
}
pub async fn reanchor_comment(
&self,
name: &str,
input: ReanchorCommentInput,
) -> Result<ReviewSession> {
let target = ReanchorLineComment {
file_path: input.file_path,
old_line: input.old_line,
new_line: input.new_line,
line_range: input.line_range,
side: input.side,
line_anchor: input.line_anchor,
};
self.mutate_review(
name,
"failed to persist comment re-anchor",
|session, now_ms| {
session
.reanchor_comment(input.comment_id, target, now_ms)
.map_err(Into::into)
},
)
.await
}
pub async fn save_review(&self, session: &ReviewSession) -> Result<()> {
self.store
.save_review(session)
.await
.context("failed to save review session")
}
async fn set_comment_status(
&self,
name: &str,
comment_id: u64,
status: CommentStatus,
actor: Author,
) -> Result<ReviewSession> {
self.mutate_review(
name,
"failed to persist comment status",
|session, now_ms| {
session
.set_comment_status(comment_id, status, actor, now_ms)
.map_err(Into::into)
},
)
.await
}
async fn mutate_review(
&self,
name: &str,
save_context: &'static str,
mutate: impl FnOnce(&mut ReviewSession, u64) -> Result<()>,
) -> Result<ReviewSession> {
let mut session = self.load_review(name).await?;
mutate(&mut session, now_ms()?)?;
self.store
.save_review(&session)
.await
.context(save_context)?;
Ok(session)
}
}