1use std::collections::{BTreeMap, BTreeSet};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum DocEvent {
14 NodeAdd {
16 seq: u64,
17 node: String,
18 node_type: String,
19 slug: Option<String>,
20 },
21 SlugSet {
23 seq: u64,
24 node: String,
25 slug: String,
26 },
27 EdgeAdd {
29 seq: u64,
30 edge: String,
31 kind: String,
32 from: String,
33 to: String,
34 },
35 Retract { seq: u64, target: String },
37 Restore { seq: u64, target: String },
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
43pub struct Membership {
44 pub section_page: BTreeMap<String, Option<String>>,
46 pub note_about: BTreeMap<String, Option<String>>,
48 pub slug_owner: BTreeMap<String, String>,
50 pub slug_conflict: Vec<String>,
52}
53
54struct Edge<'a> {
55 seq: u64,
56 kind: &'a str,
57 from: &'a str,
58 to: &'a str,
59}
60
61pub fn fold(events: &[DocEvent]) -> Membership {
63 let mut ordered: Vec<&DocEvent> = events.iter().collect();
64 ordered.sort_by_key(|e| seq(e));
65
66 let mut types: BTreeMap<&str, (&str, u64)> = BTreeMap::new();
67 let mut slugs: BTreeMap<&str, &str> = BTreeMap::new();
68 let mut edges: BTreeMap<&str, Edge<'_>> = BTreeMap::new();
69 let mut retracted: BTreeSet<&str> = BTreeSet::new();
70 for e in ordered {
71 match e {
72 DocEvent::NodeAdd {
73 seq,
74 node,
75 node_type,
76 slug,
77 } => {
78 types.entry(node).or_insert((node_type, *seq));
79 if let Some(s) = slug {
80 slugs.insert(node, s);
81 }
82 }
83 DocEvent::SlugSet { node, slug, .. } => {
84 slugs.insert(node, slug);
85 }
86 DocEvent::EdgeAdd {
87 seq,
88 edge,
89 kind,
90 from,
91 to,
92 } => {
93 edges.entry(edge).or_insert(Edge {
94 seq: *seq,
95 kind,
96 from,
97 to,
98 });
99 }
100 DocEvent::Retract { target, .. } => {
101 retracted.insert(target);
102 }
103 DocEvent::Restore { target, .. } => {
104 retracted.remove(target.as_str());
105 }
106 }
107 }
108
109 let mut m = Membership::default();
110 let live = |id: &str| !retracted.contains(id);
111 for e in edges.values() {
113 let map = match e.kind {
114 "part_of" => &mut m.section_page,
115 "about" => &mut m.note_about,
116 _ => continue,
117 };
118 map.entry(e.from.to_owned()).or_insert(None);
119 }
120 let mut best: BTreeMap<(&str, &str), (u64, &str)> = BTreeMap::new();
121 for (id, e) in &edges {
122 if !live(id) {
123 continue;
124 }
125 let slot = best.entry((e.kind, e.from)).or_insert((e.seq, e.to));
126 if e.seq < slot.0 {
127 *slot = (e.seq, e.to);
128 }
129 }
130 for ((kind, from), (_, to)) in best {
131 let map = if kind == "part_of" {
132 &mut m.section_page
133 } else {
134 &mut m.note_about
135 };
136 map.insert(from.to_owned(), Some(to.to_owned()));
137 }
138
139 let mut holders: BTreeMap<&str, Vec<(u64, &str)>> = BTreeMap::new();
140 for (node, slug) in &slugs {
141 if let Some(("page", first)) = types.get(node) {
143 if live(node) {
144 holders.entry(slug).or_default().push((*first, node));
145 }
146 }
147 }
148 for (slug, mut pages) in holders {
149 pages.sort_unstable();
150 m.slug_owner.insert(slug.to_owned(), pages[0].1.to_owned());
151 m.slug_conflict
152 .extend(pages[1..].iter().map(|(_, p)| (*p).to_owned()));
153 }
154 m.slug_conflict.sort();
155 m
156}
157
158fn seq(e: &DocEvent) -> u64 {
159 match e {
160 DocEvent::NodeAdd { seq, .. }
161 | DocEvent::SlugSet { seq, .. }
162 | DocEvent::EdgeAdd { seq, .. }
163 | DocEvent::Retract { seq, .. }
164 | DocEvent::Restore { seq, .. } => *seq,
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 const SEC: &str = "n_aaaaaaaaaaaaaaaaaaaaaadsea";
173 const PA: &str = "n_aaaaaaaaaaaaaaaaaaaaaadpga";
174 const PB: &str = "n_aaaaaaaaaaaaaaaaaaaaaadpgb";
175 const NOTE: &str = "n_aaaaaaaaaaaaaaaaaaaaaadnta";
176
177 fn edge(seq: u64, edge: &str, kind: &str, from: &str, to: &str) -> DocEvent {
178 DocEvent::EdgeAdd {
179 seq,
180 edge: edge.into(),
181 kind: kind.into(),
182 from: from.into(),
183 to: to.into(),
184 }
185 }
186 fn page(seq: u64, node: &str, slug: &str) -> DocEvent {
187 DocEvent::NodeAdd {
188 seq,
189 node: node.into(),
190 node_type: "page".into(),
191 slug: Some(slug.into()),
192 }
193 }
194 fn retract(seq: u64, target: &str) -> DocEvent {
195 DocEvent::Retract {
196 seq,
197 target: target.into(),
198 }
199 }
200
201 #[test]
203 fn part_of_and_about_vectors() {
204 let two = [
205 edge(5, "e_a", "part_of", SEC, PA),
206 edge(7, "e_b", "part_of", SEC, PB),
207 ];
208 assert_eq!(
209 fold(&two).section_page[SEC].as_deref(),
210 Some(PA),
211 "part_of_lowest_seq"
212 );
213 let mut after = two.to_vec();
214 after.push(retract(9, "e_a"));
215 assert_eq!(
216 fold(&after).section_page[SEC].as_deref(),
217 Some(PB),
218 "part_of_after_retract"
219 );
220 let gone = [edge(5, "e_a", "part_of", SEC, PA), retract(6, "e_a")];
221 assert_eq!(fold(&gone).section_page[SEC], None, "unattached");
222 let about = [
223 edge(4, "e_c", "about", NOTE, SEC),
224 edge(8, "e_d", "about", NOTE, PA),
225 ];
226 assert_eq!(
227 fold(&about).note_about[NOTE].as_deref(),
228 Some(SEC),
229 "about_lowest_seq"
230 );
231 }
232
233 #[test]
234 fn slug_vectors() {
235 let both = [page(3, PA, "a"), page(4, PB, "a")];
236 let m = fold(&both);
237 assert_eq!(m.slug_owner["a"], PA);
238 assert_eq!(m.slug_conflict, [PB]);
239
240 let mut freed = both.to_vec();
241 freed.push(retract(6, PA));
242 let m = fold(&freed);
243 assert_eq!(m.slug_owner["a"], PB);
244 assert!(m.slug_conflict.is_empty());
245
246 let mut renamed = both.to_vec();
247 renamed.push(DocEvent::SlugSet {
248 seq: 6,
249 node: PA.into(),
250 slug: "b".into(),
251 });
252 let m = fold(&renamed);
253 assert_eq!(
254 (m.slug_owner["a"].as_str(), m.slug_owner["b"].as_str()),
255 (PB, PA)
256 );
257 assert!(m.slug_conflict.is_empty());
258 }
259}