use super::model::{CommentStore, Thread};
use crate::diff::model::Side;
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Action {
AddComment {
thread_id: String,
file: PathBuf,
side: Side,
#[serde(skip_serializing_if = "Option::is_none")]
start_line: Option<u32>,
line: u32,
body: String,
#[serde(skip_serializing_if = "Option::is_none")]
author: Option<String>,
},
Reply {
thread_id: String,
body: String,
#[serde(skip_serializing_if = "Option::is_none")]
author: Option<String>,
},
Resolve { thread_id: String },
Unresolve { thread_id: String },
}
fn added_thread_actions(t: &Thread, out: &mut Vec<Action>) {
let mut comments = t.comments.iter();
if let Some(root) = comments.next() {
let start_line = (t.range.start != t.range.end).then_some(t.range.start);
out.push(Action::AddComment {
thread_id: t.id.clone(),
file: t.file.clone(),
side: t.side,
start_line,
line: t.range.end,
body: root.body.clone(),
author: root.author.clone(),
});
}
for c in comments {
out.push(Action::Reply {
thread_id: t.id.clone(),
body: c.body.clone(),
author: c.author.clone(),
});
}
if t.resolved {
out.push(Action::Resolve {
thread_id: t.id.clone(),
});
}
}
pub fn diff(base: &CommentStore, cur: &CommentStore) -> Vec<Action> {
let base_by_id: HashMap<&str, &Thread> =
base.threads.iter().map(|t| (t.id.as_str(), t)).collect();
let mut out = Vec::new();
for t in &cur.threads {
match base_by_id.get(t.id.as_str()) {
None => added_thread_actions(t, &mut out),
Some(base_t) => {
let base_cids: HashSet<&str> =
base_t.comments.iter().map(|c| c.id.as_str()).collect();
for c in t
.comments
.iter()
.filter(|c| !base_cids.contains(c.id.as_str()))
{
out.push(Action::Reply {
thread_id: t.id.clone(),
body: c.body.clone(),
author: c.author.clone(),
});
}
if t.resolved != base_t.resolved {
out.push(if t.resolved {
Action::Resolve {
thread_id: t.id.clone(),
}
} else {
Action::Unresolve {
thread_id: t.id.clone(),
}
});
}
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::comments::model::LineRange;
fn store() -> CommentStore {
CommentStore::default()
}
#[test]
fn added_thread_with_reply() {
let base = store();
let mut cur = store();
let id = cur.add_thread(
"a.rs".into(),
Side::New,
LineRange { start: 5, end: 5 },
Some("you".into()),
"root".into(),
);
cur.reply(&id, Some("you".into()), "more".into());
let actions = diff(&base, &cur);
assert_eq!(actions.len(), 2);
assert!(matches!(
&actions[0],
Action::AddComment {
start_line: None,
line: 5,
..
}
));
assert!(matches!(&actions[1], Action::Reply { .. }));
}
#[test]
fn multi_line_add_carries_start_line() {
let base = store();
let mut cur = store();
cur.add_thread(
"a.rs".into(),
Side::New,
LineRange { start: 10, end: 14 },
None,
"spans".into(),
);
let actions = diff(&base, &cur);
assert!(matches!(
&actions[0],
Action::AddComment {
start_line: Some(10),
line: 14,
..
}
));
}
#[test]
fn add_then_delete_cancels() {
let base = store();
let mut cur = store();
let id = cur.add_thread(
"a.rs".into(),
Side::New,
LineRange { start: 1, end: 1 },
None,
"x".into(),
);
let cid = cur.threads[0].comments[0].id.clone();
cur.remove_comment(&id, &cid);
assert!(diff(&base, &cur).is_empty());
}
#[test]
fn resolve_toggle_cancels_but_single_resolve_shows() {
let mut base = store();
let id = base.add_thread(
"a.rs".into(),
Side::Old,
LineRange { start: 2, end: 2 },
None,
"x".into(),
);
let mut cur = base.clone();
cur.toggle_resolved(&id);
cur.toggle_resolved(&id);
assert!(diff(&base, &cur).is_empty());
let mut cur = base.clone();
cur.toggle_resolved(&id);
let actions = diff(&base, &cur);
assert_eq!(actions, vec![Action::Resolve { thread_id: id }]);
}
#[test]
fn reply_to_base_thread() {
let mut base = store();
let id = base.add_thread(
"a.rs".into(),
Side::New,
LineRange { start: 4, end: 4 },
None,
"root".into(),
);
let mut cur = base.clone();
cur.reply(&id, Some("you".into()), "reply".into());
let actions = diff(&base, &cur);
assert_eq!(actions.len(), 1);
assert!(matches!(&actions[0], Action::Reply { body, .. } if body == "reply"));
}
}