Skip to main content

parley/services/
review_service.rs

1use std::{
2    path::PathBuf,
3    time::{SystemTime, UNIX_EPOCH},
4};
5
6use anyhow::{Context, Result, anyhow};
7
8use crate::{
9    domain::{
10        config::AppConfig,
11        review::{
12            Author, CommentStatus, DiffSide, LineAnchorSnapshot, NewLineComment,
13            ReanchorLineComment, ReviewSession, ReviewState,
14        },
15    },
16    persistence::store::Store,
17};
18
19#[derive(Debug, Clone)]
20pub struct ReviewService {
21    store: Store,
22}
23
24#[derive(Debug, Clone)]
25pub struct AddCommentInput {
26    pub file_path: String,
27    pub old_line: Option<u32>,
28    pub new_line: Option<u32>,
29    pub side: DiffSide,
30    pub line_anchor: Option<LineAnchorSnapshot>,
31    pub body: String,
32    pub author: Author,
33}
34
35#[derive(Debug, Clone)]
36pub struct AddReplyInput {
37    pub comment_id: u64,
38    pub author: Author,
39    pub body: String,
40}
41
42#[derive(Debug, Clone)]
43pub struct ReanchorCommentInput {
44    pub comment_id: u64,
45    pub file_path: String,
46    pub old_line: Option<u32>,
47    pub new_line: Option<u32>,
48    pub side: DiffSide,
49    pub line_anchor: Option<LineAnchorSnapshot>,
50}
51
52impl ReviewService {
53    pub fn new(store: Store) -> Self {
54        Self { store }
55    }
56
57    pub async fn create_review(&self, name: &str) -> Result<ReviewSession> {
58        let session = ReviewSession::new(name.to_string(), now_ms()?);
59        self.store
60            .create_review(&session)
61            .await
62            .with_context(|| format!("failed to create review {name}"))?;
63        Ok(session)
64    }
65
66    pub async fn load_review(&self, name: &str) -> Result<ReviewSession> {
67        self.store
68            .load_review(name)
69            .await
70            .with_context(|| format!("failed to load review {name}"))
71    }
72
73    pub async fn list_reviews(&self) -> Result<Vec<String>> {
74        self.store
75            .list_reviews()
76            .await
77            .context("failed to list reviews")
78    }
79
80    pub async fn load_config(&self) -> Result<AppConfig> {
81        self.store
82            .load_config()
83            .await
84            .context("failed to load parler config")
85    }
86
87    pub async fn save_config(&self, config: &AppConfig) -> Result<()> {
88        self.store
89            .save_config(config)
90            .await
91            .context("failed to save parler config")
92    }
93
94    pub fn review_log_path(&self, review_name: &str) -> Result<PathBuf> {
95        self.store
96            .review_log_path(review_name)
97            .with_context(|| format!("failed to resolve log path for review {review_name}"))
98    }
99
100    pub async fn set_state(&self, name: &str, next: ReviewState) -> Result<ReviewSession> {
101        let mut session = self.load_review(name).await?;
102        session
103            .set_state(next, now_ms()?)
104            .map_err(|error| anyhow!(error))?;
105        self.store
106            .save_review(&session)
107            .await
108            .context("failed to save state change")?;
109        Ok(session)
110    }
111
112    pub async fn set_state_force(&self, name: &str, next: ReviewState) -> Result<ReviewSession> {
113        let mut session = self.load_review(name).await?;
114        session
115            .set_state_force(next, now_ms()?)
116            .map_err(|error| anyhow!(error))?;
117        self.store
118            .save_review(&session)
119            .await
120            .context("failed to save forced state change")?;
121        Ok(session)
122    }
123
124    pub async fn add_comment(&self, name: &str, input: AddCommentInput) -> Result<ReviewSession> {
125        let mut session = self.load_review(name).await?;
126        session.add_comment(
127            NewLineComment {
128                file_path: input.file_path,
129                old_line: input.old_line,
130                new_line: input.new_line,
131                side: input.side,
132                line_anchor: input.line_anchor,
133                body: input.body,
134                author: input.author,
135            },
136            now_ms()?,
137        );
138        self.store
139            .save_review(&session)
140            .await
141            .context("failed to persist new comment")?;
142        Ok(session)
143    }
144
145    pub async fn add_reply(&self, name: &str, input: AddReplyInput) -> Result<ReviewSession> {
146        let mut session = self.load_review(name).await?;
147        session
148            .add_reply(input.comment_id, input.author, input.body, now_ms()?)
149            .map_err(|error| anyhow!(error))?;
150        self.store
151            .save_review(&session)
152            .await
153            .context("failed to persist new reply")?;
154        Ok(session)
155    }
156
157    pub async fn mark_addressed(
158        &self,
159        name: &str,
160        comment_id: u64,
161        actor: Author,
162    ) -> Result<ReviewSession> {
163        self.set_comment_status(name, comment_id, CommentStatus::Addressed, actor)
164            .await
165    }
166
167    pub async fn mark_open(
168        &self,
169        name: &str,
170        comment_id: u64,
171        actor: Author,
172    ) -> Result<ReviewSession> {
173        self.set_comment_status(name, comment_id, CommentStatus::Open, actor)
174            .await
175    }
176
177    pub async fn force_mark_addressed(&self, name: &str, comment_id: u64) -> Result<ReviewSession> {
178        let mut session = self.load_review(name).await?;
179        session
180            .set_comment_status_force(comment_id, CommentStatus::Addressed, now_ms()?)
181            .map_err(|error| anyhow!(error))?;
182        self.store
183            .save_review(&session)
184            .await
185            .context("failed to persist forced comment status")?;
186        Ok(session)
187    }
188
189    pub async fn reanchor_comment(
190        &self,
191        name: &str,
192        input: ReanchorCommentInput,
193    ) -> Result<ReviewSession> {
194        let mut session = self.load_review(name).await?;
195        session
196            .reanchor_comment(
197                input.comment_id,
198                ReanchorLineComment {
199                    file_path: input.file_path,
200                    old_line: input.old_line,
201                    new_line: input.new_line,
202                    side: input.side,
203                    line_anchor: input.line_anchor,
204                },
205                now_ms()?,
206            )
207            .map_err(|error| anyhow!(error))?;
208        self.store
209            .save_review(&session)
210            .await
211            .context("failed to persist comment re-anchor")?;
212        Ok(session)
213    }
214
215    pub async fn save_review(&self, session: &ReviewSession) -> Result<()> {
216        self.store
217            .save_review(session)
218            .await
219            .context("failed to save review session")
220    }
221
222    async fn set_comment_status(
223        &self,
224        name: &str,
225        comment_id: u64,
226        status: CommentStatus,
227        actor: Author,
228    ) -> Result<ReviewSession> {
229        let mut session = self.load_review(name).await?;
230        session
231            .set_comment_status(comment_id, status, actor, now_ms()?)
232            .map_err(|error| anyhow!(error))?;
233        self.store
234            .save_review(&session)
235            .await
236            .context("failed to persist comment status")?;
237        Ok(session)
238    }
239}
240
241fn now_ms() -> Result<u64> {
242    let elapsed = SystemTime::now()
243        .duration_since(UNIX_EPOCH)
244        .context("system clock is before unix epoch")?;
245    Ok(elapsed.as_millis() as u64)
246}