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}