1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum DocState {
13 Fresh,
14 Noted,
15 Stale,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum StateReason {
22 AnchorChanged,
23 AnchorGone,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Drift {
29 #[serde(rename = "ref")]
30 pub reference: String,
31 #[serde(alias = "digest")]
33 pub observed: Option<String>,
34}
35
36pub type Cover = Drift;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct NoteCovers {
42 pub open: bool,
43 pub covers: Vec<Cover>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Verdict {
49 pub state: DocState,
50 pub reason: Option<StateReason>,
51 pub uncovered: Vec<String>,
52}
53
54pub fn of(
58 anchor_state: Option<&str>,
59 derived: &[&str],
60 drift: &[Drift],
61 notes: &[NoteCovers],
62) -> Verdict {
63 let fresh = Verdict {
64 state: DocState::Fresh,
65 reason: None,
66 uncovered: Vec::new(),
67 };
68 if matches!(anchor_state, None | Some("fresh")) {
69 return fresh;
70 }
71 let moved: Vec<&Drift> = drift
72 .iter()
73 .filter(|d| derived.contains(&d.reference.as_str()))
74 .collect();
75 if moved.is_empty() && !drift.is_empty() {
76 return fresh;
78 }
79 let gone = anchor_state == Some("gone") || moved.iter().any(|d| d.observed.is_none());
80 let reason = Some(if gone {
81 StateReason::AnchorGone
82 } else {
83 StateReason::AnchorChanged
84 });
85 let mut uncovered: Vec<String> = moved
86 .iter()
87 .filter(|d| {
88 !notes.iter().filter(|n| n.open).any(|n| {
89 n.covers
90 .iter()
91 .any(|c| c.reference == d.reference && c.observed == d.observed)
92 })
93 })
94 .map(|d| d.reference.clone())
95 .collect();
96 uncovered.sort();
97 let state = if moved.is_empty() || !uncovered.is_empty() {
99 DocState::Stale
100 } else {
101 DocState::Noted
102 };
103 Verdict {
104 state,
105 reason,
106 uncovered,
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 const D7: &str = "7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c";
115 const D5: &str = "5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e";
116 const DERIVED: &[&str] = &["file:a.rs"];
117
118 fn drift(r: &str, d: Option<&str>) -> Drift {
119 Drift {
120 reference: r.into(),
121 observed: d.map(Into::into),
122 }
123 }
124 fn note(open: bool, d: Option<&str>) -> NoteCovers {
125 NoteCovers {
126 open,
127 covers: vec![drift("file:a.rs", d)],
128 }
129 }
130 fn check(
131 name: &str,
132 st: Option<&str>,
133 dr: &[Drift],
134 notes: &[NoteCovers],
135 want: (DocState, Option<StateReason>, &[&str]),
136 ) {
137 let v = of(st, DERIVED, dr, notes);
138 assert_eq!((v.state, v.reason), (want.0, want.1), "{name}");
139 assert_eq!(v.uncovered, want.2, "{name}");
140 }
141
142 #[test]
144 fn doc_state_vectors() {
145 use DocState::{Fresh, Noted, Stale};
146 use StateReason::{AnchorChanged as Changed, AnchorGone as Gone};
147 let moved = [drift("file:a.rs", Some(D7))];
148 check("no_state", None, &[], &[], (Fresh, None, &[]));
149 check("fresh", Some("fresh"), &[], &[], (Fresh, None, &[]));
150 check(
151 "changed_uncovered",
152 Some("to_verify"),
153 &moved,
154 &[],
155 (Stale, Some(Changed), &["file:a.rs"]),
156 );
157 check(
158 "changed_covered",
159 Some("to_verify"),
160 &moved,
161 &[note(true, Some(D7))],
162 (Noted, Some(Changed), &[]),
163 );
164 check(
165 "covered_an_older_move",
166 Some("to_verify"),
167 &moved,
168 &[note(true, Some(D5))],
169 (Stale, Some(Changed), &["file:a.rs"]),
170 );
171 check(
172 "resolved_note_does_not_cover",
173 Some("to_verify"),
174 &moved,
175 &[note(false, Some(D7))],
176 (Stale, Some(Changed), &["file:a.rs"]),
177 );
178 check(
179 "migrated_null_cover_does_not_cover",
180 Some("to_verify"),
181 &moved,
182 &[note(true, None)],
183 (Stale, Some(Changed), &["file:a.rs"]),
184 );
185 check(
186 "gone_covered",
187 Some("gone"),
188 &[drift("file:a.rs", None)],
189 &[note(true, None)],
190 (Noted, Some(Gone), &[]),
191 );
192 check(
193 "cites_drift_only",
194 Some("to_verify"),
195 &[drift("file:docs/b.md", Some(D7))],
196 &[],
197 (Fresh, None, &[]),
198 );
199 check(
200 "state_without_drift",
201 Some("to_verify"),
202 &[],
203 &[],
204 (Stale, Some(Changed), &[]),
205 );
206 }
207}