1use crate::domain::config::AppConfig;
2use crate::domain::review::{
3 Author, CommentLineRange, CommentStatus, DiffSide, LineAnchorSnapshot, NewLineComment,
4 ReanchorLineComment, ReviewSession, ReviewState, StoredAnchorSnapshot,
5};
6use crate::persistence::store::{Store, StoreError};
7use crate::utils::time::now_ms;
8use anyhow::{Context, Result};
9use std::path::PathBuf;
10
11#[derive(Debug, Clone)]
12pub struct ReviewService {
13 store: Store,
14}
15
16#[derive(Debug, Clone)]
17pub struct AddCommentInput {
18 pub file_path: String,
19 pub old_line: Option<u32>,
20 pub new_line: Option<u32>,
21 pub line_range: Option<CommentLineRange>,
22 pub side: DiffSide,
23 pub line_anchor: Option<LineAnchorSnapshot>,
24 pub original_anchor: Option<StoredAnchorSnapshot>,
25 pub body: String,
26 pub author: Author,
27}
28
29#[derive(Debug, Clone)]
30pub struct AddReplyInput {
31 pub comment_id: u64,
32 pub author: Author,
33 pub body: String,
34}
35
36#[derive(Debug, Clone)]
37pub struct ReanchorCommentInput {
38 pub comment_id: u64,
39 pub file_path: String,
40 pub old_line: Option<u32>,
41 pub new_line: Option<u32>,
42 pub line_range: Option<CommentLineRange>,
43 pub side: DiffSide,
44 pub line_anchor: Option<LineAnchorSnapshot>,
45}
46
47impl ReviewService {
48 #[must_use]
49 pub fn new(store: Store) -> Self {
50 Self { store }
51 }
52
53 pub async fn create_review(&self, name: &str) -> Result<ReviewSession> {
57 let session = ReviewSession::new(name.to_string(), now_ms()?);
58 self.store
59 .create_review(&session)
60 .await
61 .with_context(|| format!("failed to create review {name}"))?;
62 Ok(session)
63 }
64
65 pub async fn load_review(&self, name: &str) -> Result<ReviewSession> {
69 self.store
70 .load_review(name)
71 .await
72 .with_context(|| format!("failed to load review {name}"))
73 }
74
75 pub async fn load_or_create_review(&self, name: &str) -> Result<ReviewSession> {
80 match self.store.load_review(name).await {
81 Ok(session) => Ok(session),
82 Err(StoreError::ReviewNotFound(_)) => self.create_review(name).await,
83 Err(error) => Err(error).with_context(|| format!("failed to load review {name}")),
84 }
85 }
86
87 pub async fn list_reviews(&self) -> Result<Vec<String>> {
91 self.store
92 .list_reviews()
93 .await
94 .context("failed to list reviews")
95 }
96
97 pub async fn load_config(&self) -> Result<AppConfig> {
101 self.store
102 .load_config()
103 .await
104 .context("failed to load parler config")
105 }
106
107 pub async fn save_config(&self, config: &AppConfig) -> Result<()> {
111 self.store
112 .save_config(config)
113 .await
114 .context("failed to save parler config")
115 }
116
117 pub fn review_log_path(&self, review_name: &str) -> Result<PathBuf> {
121 self.store
122 .review_log_path(review_name)
123 .with_context(|| format!("failed to resolve log path for review {review_name}"))
124 }
125
126 pub async fn set_state(&self, name: &str, next: ReviewState) -> Result<ReviewSession> {
131 self.mutate_review(name, "failed to save state change", |session, now_ms| {
132 session.set_state(next, now_ms).map_err(Into::into)
133 })
134 .await
135 }
136
137 pub async fn add_comment(&self, name: &str, input: AddCommentInput) -> Result<ReviewSession> {
142 let new_comment = NewLineComment {
143 file_path: input.file_path,
144 old_line: input.old_line,
145 new_line: input.new_line,
146 line_range: input.line_range,
147 side: input.side,
148 line_anchor: input.line_anchor,
149 original_anchor: input.original_anchor,
150 body: input.body,
151 author: input.author,
152 };
153
154 self.mutate_review(name, "failed to persist new comment", |session, now_ms| {
155 session.add_comment(new_comment, now_ms);
156 Ok(())
157 })
158 .await
159 }
160
161 pub async fn add_reply(&self, name: &str, input: AddReplyInput) -> Result<ReviewSession> {
166 self.mutate_review(name, "failed to persist new reply", |session, now_ms| {
167 session
168 .add_reply(input.comment_id, input.author, input.body, now_ms)
169 .map(|_| ())
170 .map_err(Into::into)
171 })
172 .await
173 }
174
175 pub async fn mark_addressed(
180 &self,
181 name: &str,
182 comment_id: u64,
183 actor: Author,
184 ) -> Result<ReviewSession> {
185 self.set_comment_status(name, comment_id, CommentStatus::Addressed, actor)
186 .await
187 }
188
189 pub async fn mark_open(
194 &self,
195 name: &str,
196 comment_id: u64,
197 actor: Author,
198 ) -> Result<ReviewSession> {
199 self.set_comment_status(name, comment_id, CommentStatus::Open, actor)
200 .await
201 }
202
203 pub async fn force_mark_addressed(&self, name: &str, comment_id: u64) -> Result<ReviewSession> {
208 self.mutate_review(
209 name,
210 "failed to persist forced comment status",
211 |session, now_ms| {
212 session
213 .set_comment_status_force(comment_id, CommentStatus::Addressed, now_ms)
214 .map_err(Into::into)
215 },
216 )
217 .await
218 }
219
220 pub async fn reanchor_comment(
225 &self,
226 name: &str,
227 input: ReanchorCommentInput,
228 ) -> Result<ReviewSession> {
229 let target = ReanchorLineComment {
230 file_path: input.file_path,
231 old_line: input.old_line,
232 new_line: input.new_line,
233 line_range: input.line_range,
234 side: input.side,
235 line_anchor: input.line_anchor,
236 };
237
238 self.mutate_review(
239 name,
240 "failed to persist comment re-anchor",
241 |session, now_ms| {
242 session
243 .reanchor_comment(input.comment_id, target, now_ms)
244 .map_err(Into::into)
245 },
246 )
247 .await
248 }
249
250 pub async fn save_review(&self, session: &ReviewSession) -> Result<()> {
254 self.store
255 .save_review(session)
256 .await
257 .context("failed to save review session")
258 }
259
260 async fn set_comment_status(
261 &self,
262 name: &str,
263 comment_id: u64,
264 status: CommentStatus,
265 actor: Author,
266 ) -> Result<ReviewSession> {
267 self.mutate_review(
268 name,
269 "failed to persist comment status",
270 |session, now_ms| {
271 session
272 .set_comment_status(comment_id, status, actor, now_ms)
273 .map_err(Into::into)
274 },
275 )
276 .await
277 }
278
279 async fn mutate_review(
280 &self,
281 name: &str,
282 save_context: &'static str,
283 mutate: impl FnOnce(&mut ReviewSession, u64) -> Result<()>,
284 ) -> Result<ReviewSession> {
285 let mut session = self.load_review(name).await?;
286 mutate(&mut session, now_ms()?)?;
287 self.store
288 .save_review(&session)
289 .await
290 .context(save_context)?;
291 Ok(session)
292 }
293}